Compare commits

..

6 Commits

Author SHA1 Message Date
e507b75c40 4.4.2
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 3m58s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 21:22:03 +00:00
97a8377a75 fix(daemon): Fix daemon IPC id handling, reload configs on demand and correct CLI daemon start path 2025-08-29 21:22:03 +00:00
3676bff04c 4.4.1
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Failing after 3m58s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 21:10:01 +00:00
dfe0677cab fix(cli): Use server-side start-by-id flow for starting processes 2025-08-29 21:10:01 +00:00
611b756670 4.4.0
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 3m59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 17:27:32 +00:00
2291348774 feat(daemon): Persist desired process states and add daemon restart command 2025-08-29 17:27:32 +00:00
10 changed files with 251 additions and 22 deletions

View File

@@ -1,5 +1,30 @@
# Changelog # Changelog
## 2025-08-29 - 4.4.2 - fix(daemon)
Fix daemon IPC id handling, reload configs on demand and correct CLI daemon start path
- Normalize process IDs in daemon IPC handlers (trim strings) to avoid lookup mismatches
- Attempt to reload saved process configurations when a startById request cannot find a config (handles races/stale state)
- Use normalized IDs in responses and messages for stop/restart/delete/remove/describe handlers
- Fix CLI daemon start path to point at dist_ts/daemon/tspm.daemon.js when launching the background daemon
- Ensure the IPC client disconnects after showing CLI version/status to avoid leaked connections
## 2025-08-29 - 4.4.1 - fix(cli)
Use server-side start-by-id flow for starting processes
- CLI: 'tspm start <id>' now calls a new 'startById' IPC method instead of fetching the full config via 'describe' and submitting it back to 'start'.
- Daemon: Added server-side handler for 'startById' which resolves the stored process config and starts the process on the daemon.
- Protocol: Added StartByIdRequest/StartByIdResponse types and registered 'startById' in the IPC method map.
## 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) ## 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 Fix daemon describe handler to return correct process info and config; bump @push.rocks/smartipc to ^2.2.2

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tspm", "name": "@git.zone/tspm",
"version": "4.3.1", "version": "4.4.2",
"private": false, "private": false,
"description": "a no fuzz process manager", "description": "a no fuzz process manager",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tspm', name: '@git.zone/tspm',
version: '4.3.1', version: '4.4.2',
description: 'a no fuzz process manager' description: 'a no fuzz process manager'
} }

View File

@@ -33,7 +33,8 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
const daemonScript = plugins.path.join( const daemonScript = plugins.path.join(
paths.packageDir, paths.packageDir,
'dist_ts', 'dist_ts',
'daemon.js', 'daemon',
'tspm.daemon.js',
); );
// Start daemon as a detached background process // Start daemon as a detached background process
@@ -80,6 +81,48 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
} }
break; 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': case 'start-service':
// This is called by systemd - start the daemon directly // This is called by systemd - start the daemon directly
console.log('Starting TSPM daemon for systemd service...'); console.log('Starting TSPM daemon for systemd service...');
@@ -135,6 +178,7 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
console.log('Usage: tspm daemon <command>'); console.log('Usage: tspm daemon <command>');
console.log('\nCommands:'); console.log('\nCommands:');
console.log(' start Start the TSPM daemon'); console.log(' start Start the TSPM daemon');
console.log(' restart Restart the TSPM daemon');
console.log(' stop Stop the TSPM daemon'); console.log(' stop Stop the TSPM daemon');
console.log(' status Show daemon status'); console.log(' status Show daemon status');
break; break;

View File

@@ -17,14 +17,8 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
return; return;
} }
const desc = await tspmIpcClient.request('describe', { id }).catch(() => null); console.log(`Starting process id ${id}...`);
if (!desc) { const response = await tspmIpcClient.request('startById', { id });
console.error(`Process with id '${id}' not found. Use 'tspm add' first.`);
return;
}
console.log(`Starting process id ${id} (${desc.config.name || id})...`);
const response = await tspmIpcClient.request('start', { config: desc.config });
console.log('✓ Process started'); console.log('✓ Process started');
console.log(` ID: ${response.processId}`); console.log(` ID: ${response.processId}`);
console.log(` PID: ${response.pid || 'N/A'}`); console.log(` PID: ${response.pid || 'N/A'}`);

View File

@@ -1,4 +1,5 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import { tspmIpcClient } from '../client/tspm.ipcclient.js';
import * as paths from '../paths.js'; import * as paths from '../paths.js';
import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js'; import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js';
@@ -38,6 +39,24 @@ export const run = async (): Promise<void> => {
} }
const smartcliInstance = new plugins.smartcli.Smartcli(); const smartcliInstance = new plugins.smartcli.Smartcli();
// Intercept -v/--version to show CLI and daemon versions
const args = process.argv.slice(2);
if (args.includes('-v') || args.includes('--version')) {
const cliVersion = tspmProjectinfo.npm.version;
console.log(`tspm CLI: ${cliVersion}`);
const status = await tspmIpcClient.getDaemonStatus();
if (status) {
console.log(
`Daemon: running v${status.version || 'unknown'} (pid ${status.pid})`,
);
} else {
console.log('Daemon: not running');
}
// Ensure we disconnect any IPC client connection used for status
try { await tspmIpcClient.disconnect(); } catch {}
return; // do not start parser
}
// Keep Smartcli version info for help output but not used for -v now
smartcliInstance.addVersion(tspmProjectinfo.npm.version); smartcliInstance.addVersion(tspmProjectinfo.npm.version);
// Register all commands // Register all commands

View File

@@ -25,6 +25,8 @@ export class ProcessManager extends EventEmitter {
public processInfo: Map<string, IProcessInfo> = new Map(); public processInfo: Map<string, IProcessInfo> = new Map();
private config: TspmConfig; private config: TspmConfig;
private configStorageKey = 'processes'; private configStorageKey = 'processes';
private desiredStateStorageKey = 'desiredStates';
private desiredStates: Map<string, IProcessInfo['status']> = new Map();
private logger: Logger; private logger: Logger;
constructor() { constructor() {
@@ -32,6 +34,7 @@ export class ProcessManager extends EventEmitter {
this.logger = new Logger('Tspm'); this.logger = new Logger('Tspm');
this.config = new TspmConfig(); this.config = new TspmConfig();
this.loadProcessConfigs(); this.loadProcessConfigs();
this.loadDesiredStates();
} }
/** /**
@@ -67,6 +70,7 @@ export class ProcessManager extends EventEmitter {
}); });
await this.saveProcessConfigs(); await this.saveProcessConfigs();
await this.setDesiredState(config.id, 'stopped');
return config.id; return config.id;
} }
@@ -279,6 +283,7 @@ export class ProcessManager extends EventEmitter {
// Save updated configs // Save updated configs
await this.saveProcessConfigs(); await this.saveProcessConfigs();
await this.removeDesiredState(id);
this.logger.info(`Successfully deleted process with id '${id}'`); this.logger.info(`Successfully deleted process with id '${id}'`);
} catch (error: Error | unknown) { } catch (error: Error | unknown) {
@@ -288,6 +293,7 @@ export class ProcessManager extends EventEmitter {
this.processConfigs.delete(id); this.processConfigs.delete(id);
this.processInfo.delete(id); this.processInfo.delete(id);
await this.saveProcessConfigs(); await this.saveProcessConfigs();
await this.removeDesiredState(id);
this.logger.info( this.logger.info(
`Successfully deleted process with id '${id}' after stopping failure`, `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 * Load process configurations from config storage
*/ */
@@ -499,10 +579,12 @@ export class ProcessManager extends EventEmitter {
this.processes.clear(); this.processes.clear();
this.processInfo.clear(); this.processInfo.clear();
this.processConfigs.clear(); this.processConfigs.clear();
this.desiredStates.clear();
// Remove persisted configs // Remove persisted configs
try { try {
await this.config.deleteKey(this.configStorageKey); await this.config.deleteKey(this.configStorageKey);
await this.config.deleteKey(this.desiredStateStorageKey).catch(() => {});
this.logger.debug('Cleared persisted process configurations'); this.logger.debug('Cleared persisted process configurations');
} catch (error) { } catch (error) {
// Fallback: write empty list if deleteKey fails for any reason // Fallback: write empty list if deleteKey fails for any reason

View File

@@ -20,12 +20,20 @@ export class TspmDaemon {
private socketPath: string; private socketPath: string;
private heartbeatInterval: NodeJS.Timeout | null = null; private heartbeatInterval: NodeJS.Timeout | null = null;
private daemonPidFile: string; private daemonPidFile: string;
private version: string;
constructor() { constructor() {
this.tspmInstance = new ProcessManager(); this.tspmInstance = new ProcessManager();
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock'); this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid'); this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
this.startTime = Date.now(); this.startTime = Date.now();
// Determine daemon version from package metadata
try {
const proj = new plugins.projectinfo.ProjectInfo(paths.packageDir);
this.version = proj.npm.version || 'unknown';
} catch {
this.version = 'unknown';
}
} }
/** /**
@@ -81,6 +89,7 @@ export class TspmDaemon {
// Load existing process configurations // Load existing process configurations
await this.tspmInstance.loadProcessConfigs(); await this.tspmInstance.loadProcessConfigs();
await this.tspmInstance.loadDesiredStates();
// Set up log publishing // Set up log publishing
this.tspmInstance.on('process:log', ({ processId, log }) => { this.tspmInstance.on('process:log', ({ processId, log }) => {
@@ -95,6 +104,9 @@ export class TspmDaemon {
// Set up graceful shutdown handlers // Set up graceful shutdown handlers
this.setupShutdownHandlers(); 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(`TSPM daemon started successfully on ${this.socketPath}`);
console.log(`PID: ${process.pid}`); console.log(`PID: ${process.pid}`);
} }
@@ -108,6 +120,7 @@ export class TspmDaemon {
'start', 'start',
async (request: RequestForMethod<'start'>) => { async (request: RequestForMethod<'start'>) => {
try { try {
await this.tspmInstance.setDesiredState(request.config.id, 'online');
await this.tspmInstance.start(request.config); await this.tspmInstance.start(request.config);
const processInfo = this.tspmInstance.processInfo.get( const processInfo = this.tspmInstance.processInfo.get(
request.config.id, request.config.id,
@@ -123,14 +136,45 @@ export class TspmDaemon {
}, },
); );
// Start by id (resolve config on server)
this.ipcServer.onMessage(
'startById',
async (request: RequestForMethod<'startById'>) => {
try {
const id = String(request.id).trim();
let config = this.tspmInstance.processConfigs.get(id);
if (!config) {
// Try to reload configs if not found (handles races or stale state)
await this.tspmInstance.loadProcessConfigs();
config = this.tspmInstance.processConfigs.get(id) || null as any;
}
if (!config) {
throw new Error(`Process ${id} not found`);
}
await this.tspmInstance.setDesiredState(id, 'online');
await this.tspmInstance.start(config);
const processInfo = this.tspmInstance.processInfo.get(id);
return {
processId: id,
pid: processInfo?.pid,
status: processInfo?.status || 'stopped',
};
} catch (error) {
throw new Error(`Failed to start process: ${error.message}`);
}
},
);
this.ipcServer.onMessage( this.ipcServer.onMessage(
'stop', 'stop',
async (request: RequestForMethod<'stop'>) => { async (request: RequestForMethod<'stop'>) => {
try { try {
await this.tspmInstance.stop(request.id); const id = String(request.id).trim();
await this.tspmInstance.setDesiredState(id, 'stopped');
await this.tspmInstance.stop(id);
return { return {
success: true, success: true,
message: `Process ${request.id} stopped successfully`, message: `Process ${id} stopped successfully`,
}; };
} catch (error) { } catch (error) {
throw new Error(`Failed to stop process: ${error.message}`); throw new Error(`Failed to stop process: ${error.message}`);
@@ -142,10 +186,12 @@ export class TspmDaemon {
'restart', 'restart',
async (request: RequestForMethod<'restart'>) => { async (request: RequestForMethod<'restart'>) => {
try { try {
await this.tspmInstance.restart(request.id); const id = String(request.id).trim();
const processInfo = this.tspmInstance.processInfo.get(request.id); await this.tspmInstance.setDesiredState(id, 'online');
await this.tspmInstance.restart(id);
const processInfo = this.tspmInstance.processInfo.get(id);
return { return {
processId: request.id, processId: id,
pid: processInfo?.pid, pid: processInfo?.pid,
status: processInfo?.status || 'stopped', status: processInfo?.status || 'stopped',
}; };
@@ -159,10 +205,11 @@ export class TspmDaemon {
'delete', 'delete',
async (request: RequestForMethod<'delete'>) => { async (request: RequestForMethod<'delete'>) => {
try { try {
await this.tspmInstance.delete(request.id); const id = String(request.id).trim();
await this.tspmInstance.delete(id);
return { return {
success: true, success: true,
message: `Process ${request.id} deleted successfully`, message: `Process ${id} deleted successfully`,
}; };
} catch (error) { } catch (error) {
throw new Error(`Failed to delete process: ${error.message}`); throw new Error(`Failed to delete process: ${error.message}`);
@@ -188,8 +235,9 @@ export class TspmDaemon {
'remove', 'remove',
async (request: RequestForMethod<'remove'>) => { async (request: RequestForMethod<'remove'>) => {
try { try {
await this.tspmInstance.delete(request.id); const id = String(request.id).trim();
return { success: true, message: `Process ${request.id} deleted successfully` }; await this.tspmInstance.delete(id);
return { success: true, message: `Process ${id} deleted successfully` };
} catch (error) { } catch (error) {
throw new Error(`Failed to remove process: ${error.message}`); throw new Error(`Failed to remove process: ${error.message}`);
} }
@@ -207,9 +255,10 @@ export class TspmDaemon {
this.ipcServer.onMessage( this.ipcServer.onMessage(
'describe', 'describe',
async (request: RequestForMethod<'describe'>) => { async (request: RequestForMethod<'describe'>) => {
const result = await this.tspmInstance.describe(request.id); const id = String(request.id).trim();
const result = await this.tspmInstance.describe(id);
if (!result) { if (!result) {
throw new Error(`Process ${request.id} not found`); throw new Error(`Process ${id} not found`);
} }
// Return correctly shaped response // Return correctly shaped response
return { return {
@@ -234,6 +283,7 @@ export class TspmDaemon {
const started: string[] = []; const started: string[] = [];
const failed: Array<{ id: string; error: string }> = []; const failed: Array<{ id: string; error: string }> = [];
await this.tspmInstance.setDesiredStateForAll('online');
await this.tspmInstance.startAll(); await this.tspmInstance.startAll();
// Get status of all processes // Get status of all processes
@@ -255,6 +305,7 @@ export class TspmDaemon {
const stopped: string[] = []; const stopped: string[] = [];
const failed: Array<{ id: string; error: string }> = []; const failed: Array<{ id: string; error: string }> = [];
await this.tspmInstance.setDesiredStateForAll('stopped');
await this.tspmInstance.stopAll(); await this.tspmInstance.stopAll();
// Get status of all processes // Get status of all processes
@@ -312,6 +363,7 @@ export class TspmDaemon {
processCount: this.tspmInstance.processes.size, processCount: this.tspmInstance.processes.size,
memoryUsage: memUsage.heapUsed, memoryUsage: memUsage.heapUsed,
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
version: this.version,
}; };
}, },
); );

View File

@@ -66,6 +66,17 @@ export interface StartResponse {
status: 'online' | 'stopped' | 'errored'; status: 'online' | 'stopped' | 'errored';
} }
// Start by id (server resolves config)
export interface StartByIdRequest {
id: string;
}
export interface StartByIdResponse {
processId: string;
pid?: number;
status: 'online' | 'stopped' | 'errored';
}
// Stop command // Stop command
export interface StopRequest { export interface StopRequest {
id: string; id: string;
@@ -191,6 +202,7 @@ export interface DaemonStatusResponse {
processCount: number; processCount: number;
memoryUsage?: number; memoryUsage?: number;
cpuUsage?: number; cpuUsage?: number;
version?: string;
} }
// Daemon shutdown command // Daemon shutdown command
@@ -238,6 +250,7 @@ export interface RemoveResponse {
// Type mappings for methods // Type mappings for methods
export type IpcMethodMap = { export type IpcMethodMap = {
start: { request: StartRequest; response: StartResponse }; start: { request: StartRequest; response: StartResponse };
startById: { request: StartByIdRequest; response: StartByIdResponse };
stop: { request: StopRequest; response: StopResponse }; stop: { request: StopRequest; response: StopResponse };
restart: { request: RestartRequest; response: RestartResponse }; restart: { request: RestartRequest; response: RestartResponse };
delete: { request: DeleteRequest; response: DeleteResponse }; delete: { request: DeleteRequest; response: DeleteResponse };