Files
David Garcia b36ab704ff Server: reliable output flush, DB mutex + upsert, persistent log mirror
Fixes intermittent "command completes with no terminal output" and
"session table shows Unknown/N/A" bugs, plus adds a couple of
diagnostic knobs so future issues are easier to root-cause.

- sendTo() returns bool and now calls sock->flush() after write, so the
  Qt write buffer never sits on stdout under event-loop starvation. All
  broadcast call sites migrated to broadcastToSession(), which prunes
  dead sockets on write failure.
- SessionDBManager: updateSessionUser / updateSessionTenant /
  setSessionAlive / setSessionStatus wrap their SQL in
  QMutexLocker(&m_mutex) to fix intermittent SQLITE_BUSY under
  concurrent writes.
- addSessionToMainDB uses INSERT ... ON CONFLICT DO UPDATE SET so
  re-adding a session preserves CreatedBy instead of clobbering it.
- setSessionAlive() / setSessionStatus() wired into the session
  lifecycle (ensureProcess, session_exited, LOGIN_OK/LOGIN_FAIL) so the
  Alive/Status columns reflect real state.
- Nested-mutex audit: removed a nested QMutexLocker(&g_stateMutex)
  inside readyReadStandardOutput's pending-reinject block. Qt's QMutex
  is non-recursive by default; the inner lock was the "run one command
  and that's it" deadlock.
- Pre-login output allowlist filter (microsoft.com/devicelogin,
  [Animo], WARNING, ...) so the terminal no longer shows pwsh's stdin
  echo fragments during login.
- update_session_meta RPC handler so a client can heal a stale DB row
  it detected on its side.
- server/main.cpp installs a QMutex-guarded qInstallMessageHandler that
  mirrors qInfo/qWarning to /tmp/animo-srv-dbg.log across restarts
  (see DEBUGGING in the repo docs).
2026-07-28 13:16:55 -06:00

75 lines
2.2 KiB
C++
Executable File

#pragma once
#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <QSet>
#include <QHash>
#include <QJsonObject>
#include <QString>
#include <QDir>
#include <QRegularExpression>
// Server owns all session lifecycle, PowerShell processes, and DB state.
// Clients send JSON actions; server replies + pushes async events.
class Server : public QObject {
Q_OBJECT
public:
explicit Server(const QString &bindIp,
quint16 port,
const QString &user,
const QString &pass,
QObject *parent = nullptr);
// Start listening; also used to perform one-time DB init in .cpp
bool start();
// Update auth credential (simple shared-secret login)
void setLoginCredential(const QString &user, const QString &pass);
signals:
// Append human-friendly log lines to a UI/console
void log(const QString &line);
private slots:
// TCP plumbing
void onNewConnection();
void onClientReady();
void onClientDisconnected();
private:
// Route a single JSON line; returns true if handled/formatted
bool handleLine(QTcpSocket *sock, const QByteArray &line);
// Simple password auth gate for this connection
bool handleLogin(QTcpSocket *sock, const QJsonObject &obj);
// Constant-time string comparison to prevent timing attacks
static bool constantTimeCompare(const QString &a, const QString &b);
// Listener + connected sockets
QTcpServer tcp_;
QSet<QTcpSocket*> clients_;
QSet<QTcpSocket*> authed_; // sockets that passed login
QHash<QTcpSocket*, QString> operatorBySocket_; // authed socket -> operator handle (for attribution)
// Bind + auth config
QString bindIp_;
quint16 port_;
QString allowedUser_;
QString allowedPass_;
// Login rate limiting per IP
struct LoginAttempt {
int failCount = 0;
qint64 lastAttemptMs = 0;
qint64 lockoutUntilMs = 0;
};
QHash<QString, LoginAttempt> loginAttempts_;
// Server limits
static constexpr int MAX_SESSIONS = 100;
static constexpr int MAX_CMD_LENGTH = 100000; // 100KB
static constexpr int MAX_LOGIN_FAILURES = 5;
static constexpr qint64 LOGIN_LOCKOUT_MS = 60000; // 1 minute lockout
};