feat(daemon): Persist desired process states and add daemon restart command
This commit is contained in:
		@@ -1,5 +1,14 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## 2025-08-29 - 4.4.0 - feat(daemon)
 | 
			
		||||
Persist desired process states and add daemon restart command
 | 
			
		||||
 | 
			
		||||
- Persist desired process states: ProcessManager now stores desiredStates to user storage (desiredStates key) and reloads them on startup.
 | 
			
		||||
- Start/stop operations update desired state: IPC handlers in the daemon now set desired state when processes are started, stopped, restarted or when batch start/stop is invoked.
 | 
			
		||||
- Resume desired state on daemon start: Daemon loads desired states and calls startDesired() to bring processes to their desired 'online' state after startup.
 | 
			
		||||
- Remove desired state on deletion/reset: Deleting a process or resetting clears its desired state; reset clears all desired states as well.
 | 
			
		||||
- CLI: Added 'tspm daemon restart' — stops the daemon (gracefully) and restarts it in the foreground for the current session, with checks and informative output.
 | 
			
		||||
 | 
			
		||||
## 2025-08-29 - 4.3.1 - fix(daemon)
 | 
			
		||||
Fix daemon describe handler to return correct process info and config; bump @push.rocks/smartipc to ^2.2.2
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,6 @@
 | 
			
		||||
 */
 | 
			
		||||
export const commitinfo = {
 | 
			
		||||
  name: '@git.zone/tspm',
 | 
			
		||||
  version: '4.3.1',
 | 
			
		||||
  version: '4.4.0',
 | 
			
		||||
  description: 'a no fuzz process manager'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -80,6 +80,48 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
 | 
			
		||||
        case 'restart':
 | 
			
		||||
          try {
 | 
			
		||||
            console.log('Restarting TSPM daemon...');
 | 
			
		||||
            await tspmIpcClient.stopDaemon(true);
 | 
			
		||||
 | 
			
		||||
            // Reuse the manual start logic from 'start'
 | 
			
		||||
            const statusAfterStop = await tspmIpcClient.getDaemonStatus();
 | 
			
		||||
            if (statusAfterStop) {
 | 
			
		||||
              console.warn('Daemon still appears to be running; proceeding to start anyway.');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            console.log('Starting TSPM daemon manually...');
 | 
			
		||||
            const { spawn } = await import('child_process');
 | 
			
		||||
            const daemonScript = plugins.path.join(
 | 
			
		||||
              paths.packageDir,
 | 
			
		||||
              'dist_ts',
 | 
			
		||||
              'daemon.js',
 | 
			
		||||
            );
 | 
			
		||||
            const daemonProcess = spawn(process.execPath, [daemonScript], {
 | 
			
		||||
              detached: true,
 | 
			
		||||
              stdio: process.env.TSPM_DEBUG === 'true' ? 'inherit' : 'ignore',
 | 
			
		||||
              env: { ...process.env, TSPM_DAEMON_MODE: 'true' },
 | 
			
		||||
            });
 | 
			
		||||
            daemonProcess.unref();
 | 
			
		||||
            console.log(`Started daemon with PID: ${daemonProcess.pid}`);
 | 
			
		||||
 | 
			
		||||
            await new Promise((resolve) => setTimeout(resolve, 2000));
 | 
			
		||||
            const newStatus = await tspmIpcClient.getDaemonStatus();
 | 
			
		||||
            if (newStatus) {
 | 
			
		||||
              console.log('✓ TSPM daemon restarted successfully');
 | 
			
		||||
              console.log(`  PID: ${newStatus.pid}`);
 | 
			
		||||
            } else {
 | 
			
		||||
              console.warn('\n⚠️  Warning: Daemon restart attempted but status is unavailable.');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await tspmIpcClient.disconnect();
 | 
			
		||||
          } catch (error) {
 | 
			
		||||
            console.error('Error restarting daemon:', (error as any).message || String(error));
 | 
			
		||||
            process.exit(1);
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
 | 
			
		||||
        case 'start-service':
 | 
			
		||||
          // This is called by systemd - start the daemon directly
 | 
			
		||||
          console.log('Starting TSPM daemon for systemd service...');
 | 
			
		||||
@@ -135,6 +177,7 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
          console.log('Usage: tspm daemon <command>');
 | 
			
		||||
          console.log('\nCommands:');
 | 
			
		||||
          console.log('  start   Start the TSPM daemon');
 | 
			
		||||
          console.log('  restart Restart the TSPM daemon');
 | 
			
		||||
          console.log('  stop    Stop the TSPM daemon');
 | 
			
		||||
          console.log('  status  Show daemon status');
 | 
			
		||||
          break;
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,8 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
  public processInfo: Map<string, IProcessInfo> = new Map();
 | 
			
		||||
  private config: TspmConfig;
 | 
			
		||||
  private configStorageKey = 'processes';
 | 
			
		||||
  private desiredStateStorageKey = 'desiredStates';
 | 
			
		||||
  private desiredStates: Map<string, IProcessInfo['status']> = new Map();
 | 
			
		||||
  private logger: Logger;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
@@ -32,6 +34,7 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
    this.logger = new Logger('Tspm');
 | 
			
		||||
    this.config = new TspmConfig();
 | 
			
		||||
    this.loadProcessConfigs();
 | 
			
		||||
    this.loadDesiredStates();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -67,6 +70,7 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await this.saveProcessConfigs();
 | 
			
		||||
    await this.setDesiredState(config.id, 'stopped');
 | 
			
		||||
    return config.id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -279,6 +283,7 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
 | 
			
		||||
      // Save updated configs
 | 
			
		||||
      await this.saveProcessConfigs();
 | 
			
		||||
      await this.removeDesiredState(id);
 | 
			
		||||
 | 
			
		||||
      this.logger.info(`Successfully deleted process with id '${id}'`);
 | 
			
		||||
    } catch (error: Error | unknown) {
 | 
			
		||||
@@ -288,6 +293,7 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
        this.processConfigs.delete(id);
 | 
			
		||||
        this.processInfo.delete(id);
 | 
			
		||||
        await this.saveProcessConfigs();
 | 
			
		||||
        await this.removeDesiredState(id);
 | 
			
		||||
 | 
			
		||||
        this.logger.info(
 | 
			
		||||
          `Successfully deleted process with id '${id}' after stopping failure`,
 | 
			
		||||
@@ -415,6 +421,80 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // === Desired state persistence ===
 | 
			
		||||
  private async saveDesiredStates(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const obj: Record<string, IProcessInfo['status']> = {};
 | 
			
		||||
      for (const [id, state] of this.desiredStates.entries()) {
 | 
			
		||||
        obj[id] = state;
 | 
			
		||||
      }
 | 
			
		||||
      await this.config.writeKey(
 | 
			
		||||
        this.desiredStateStorageKey,
 | 
			
		||||
        JSON.stringify(obj),
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      this.logger.warn(
 | 
			
		||||
        `Failed to save desired states: ${error?.message || String(error)}`,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async loadDesiredStates(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const raw = await this.config.readKey(this.desiredStateStorageKey);
 | 
			
		||||
      if (raw) {
 | 
			
		||||
        const obj = JSON.parse(raw) as Record<string, IProcessInfo['status']>;
 | 
			
		||||
        this.desiredStates = new Map(Object.entries(obj));
 | 
			
		||||
        this.logger.debug(
 | 
			
		||||
          `Loaded desired states for ${this.desiredStates.size} processes`,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      this.logger.warn(
 | 
			
		||||
        `Failed to load desired states: ${error?.message || String(error)}`,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async setDesiredState(
 | 
			
		||||
    id: string,
 | 
			
		||||
    state: IProcessInfo['status'],
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    this.desiredStates.set(id, state);
 | 
			
		||||
    await this.saveDesiredStates();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async removeDesiredState(id: string): Promise<void> {
 | 
			
		||||
    this.desiredStates.delete(id);
 | 
			
		||||
    await this.saveDesiredStates();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async setDesiredStateForAll(
 | 
			
		||||
    state: IProcessInfo['status'],
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    for (const id of this.processConfigs.keys()) {
 | 
			
		||||
      this.desiredStates.set(id, state);
 | 
			
		||||
    }
 | 
			
		||||
    await this.saveDesiredStates();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async startDesired(): Promise<void> {
 | 
			
		||||
    for (const [id, config] of this.processConfigs.entries()) {
 | 
			
		||||
      const desired = this.desiredStates.get(id);
 | 
			
		||||
      if (desired === 'online' && !this.processes.has(id)) {
 | 
			
		||||
        try {
 | 
			
		||||
          await this.start(config);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          this.logger.warn(
 | 
			
		||||
            `Failed to start desired process ${id}: ${
 | 
			
		||||
              (e as Error)?.message || String(e)
 | 
			
		||||
            }`,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Load process configurations from config storage
 | 
			
		||||
   */
 | 
			
		||||
@@ -499,10 +579,12 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
    this.processes.clear();
 | 
			
		||||
    this.processInfo.clear();
 | 
			
		||||
    this.processConfigs.clear();
 | 
			
		||||
    this.desiredStates.clear();
 | 
			
		||||
 | 
			
		||||
    // Remove persisted configs
 | 
			
		||||
    try {
 | 
			
		||||
      await this.config.deleteKey(this.configStorageKey);
 | 
			
		||||
      await this.config.deleteKey(this.desiredStateStorageKey).catch(() => {});
 | 
			
		||||
      this.logger.debug('Cleared persisted process configurations');
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // Fallback: write empty list if deleteKey fails for any reason
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,7 @@ export class TspmDaemon {
 | 
			
		||||
 | 
			
		||||
    // Load existing process configurations
 | 
			
		||||
    await this.tspmInstance.loadProcessConfigs();
 | 
			
		||||
    await this.tspmInstance.loadDesiredStates();
 | 
			
		||||
 | 
			
		||||
    // Set up log publishing
 | 
			
		||||
    this.tspmInstance.on('process:log', ({ processId, log }) => {
 | 
			
		||||
@@ -95,6 +96,9 @@ export class TspmDaemon {
 | 
			
		||||
    // Set up graceful shutdown handlers
 | 
			
		||||
    this.setupShutdownHandlers();
 | 
			
		||||
 | 
			
		||||
    // Start processes that should be online per desired state
 | 
			
		||||
    await this.tspmInstance.startDesired();
 | 
			
		||||
 | 
			
		||||
    console.log(`TSPM daemon started successfully on ${this.socketPath}`);
 | 
			
		||||
    console.log(`PID: ${process.pid}`);
 | 
			
		||||
  }
 | 
			
		||||
@@ -108,6 +112,7 @@ export class TspmDaemon {
 | 
			
		||||
      'start',
 | 
			
		||||
      async (request: RequestForMethod<'start'>) => {
 | 
			
		||||
        try {
 | 
			
		||||
          await this.tspmInstance.setDesiredState(request.config.id, 'online');
 | 
			
		||||
          await this.tspmInstance.start(request.config);
 | 
			
		||||
          const processInfo = this.tspmInstance.processInfo.get(
 | 
			
		||||
            request.config.id,
 | 
			
		||||
@@ -127,6 +132,7 @@ export class TspmDaemon {
 | 
			
		||||
      'stop',
 | 
			
		||||
      async (request: RequestForMethod<'stop'>) => {
 | 
			
		||||
        try {
 | 
			
		||||
          await this.tspmInstance.setDesiredState(request.id, 'stopped');
 | 
			
		||||
          await this.tspmInstance.stop(request.id);
 | 
			
		||||
          return {
 | 
			
		||||
            success: true,
 | 
			
		||||
@@ -142,6 +148,7 @@ export class TspmDaemon {
 | 
			
		||||
      'restart',
 | 
			
		||||
      async (request: RequestForMethod<'restart'>) => {
 | 
			
		||||
        try {
 | 
			
		||||
          await this.tspmInstance.setDesiredState(request.id, 'online');
 | 
			
		||||
          await this.tspmInstance.restart(request.id);
 | 
			
		||||
          const processInfo = this.tspmInstance.processInfo.get(request.id);
 | 
			
		||||
          return {
 | 
			
		||||
@@ -234,6 +241,7 @@ export class TspmDaemon {
 | 
			
		||||
        const started: string[] = [];
 | 
			
		||||
        const failed: Array<{ id: string; error: string }> = [];
 | 
			
		||||
 | 
			
		||||
        await this.tspmInstance.setDesiredStateForAll('online');
 | 
			
		||||
        await this.tspmInstance.startAll();
 | 
			
		||||
 | 
			
		||||
        // Get status of all processes
 | 
			
		||||
@@ -255,6 +263,7 @@ export class TspmDaemon {
 | 
			
		||||
        const stopped: string[] = [];
 | 
			
		||||
        const failed: Array<{ id: string; error: string }> = [];
 | 
			
		||||
 | 
			
		||||
        await this.tspmInstance.setDesiredStateForAll('stopped');
 | 
			
		||||
        await this.tspmInstance.stopAll();
 | 
			
		||||
 | 
			
		||||
        // Get status of all processes
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user