feat(cli/daemon/processmonitor): Add flexible target resolution and search command; improve restart/backoff and error handling
This commit is contained in:
12
changelog.md
12
changelog.md
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.3.0 - feat(cli/daemon/processmonitor)
|
||||||
|
Add flexible target resolution and search command; improve restart/backoff and error handling
|
||||||
|
|
||||||
|
- Add new cli command `search` to find processes by id or name fragment.
|
||||||
|
- Allow flexible process targets in CLI commands (accepts numeric id, id:<n>, or name:<label>) for start/stop/restart/delete/describe/logs/edit commands.
|
||||||
|
- Introduce a new daemon IPC method `resolveTarget` to normalize user-provided targets to ProcessId (supports id:<n>, name:<label>, or bare numeric id).
|
||||||
|
- Keep `remove` as a CLI alias but daemon exposes `delete` only; CLI resolves targets and always calls daemon `delete`.
|
||||||
|
- Implement scheduled restart/backoff in ProcessMonitor with incremental debounce, max retries, and a 1-hour reset window.
|
||||||
|
- Emit a `failed` event from ProcessMonitor when max restart attempts are exceeded; ProcessManager listens and marks processes as `errored` and clears pid.
|
||||||
|
- Ensure desired state is set to `stopped` before deleting a process to avoid race conditions.
|
||||||
|
- Improve cli output messages to include resolved names alongside numeric ids where available.
|
||||||
|
|
||||||
## 2025-08-30 - 5.2.0 - feat(cli)
|
## 2025-08-30 - 5.2.0 - feat(cli)
|
||||||
Preserve CLI environment when adding processes, simplify edit flow, and refresh README docs
|
Preserve CLI environment when adding processes, simplify edit flow, and refresh README docs
|
||||||
|
|
||||||
|
84
readme.md
84
readme.md
@@ -38,21 +38,23 @@ npm install --save-dev @git.zone/tspm
|
|||||||
# Add a process (creates config without starting)
|
# Add a process (creates config without starting)
|
||||||
tspm add "node server.js" --name my-server --memory 1GB
|
tspm add "node server.js" --name my-server --memory 1GB
|
||||||
|
|
||||||
# Start the process
|
# Start the process (by name or id)
|
||||||
tspm start my-server
|
tspm start name:my-server
|
||||||
|
# or
|
||||||
|
tspm start id:1
|
||||||
|
|
||||||
# Or add and start in one go
|
# Or add and start in one go
|
||||||
tspm add "node app.js" --name my-app
|
tspm add "node app.js" --name my-app
|
||||||
tspm start my-app
|
tspm start name:my-app
|
||||||
|
|
||||||
# List all processes
|
# List all processes
|
||||||
tspm list
|
tspm list
|
||||||
|
|
||||||
# View logs
|
# View logs
|
||||||
tspm logs my-app
|
tspm logs name:my-app
|
||||||
|
|
||||||
# Stop a process
|
# Stop a process
|
||||||
tspm stop my-app
|
tspm stop name:my-app
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📋 Commands
|
## 📋 Commands
|
||||||
@@ -86,38 +88,38 @@ tspm add "tsx watch src/index.ts" --name dev-server --watch --watch-paths "src,c
|
|||||||
tspm add "node worker.js" --name one-time-job --autorestart false
|
tspm add "node worker.js" --name one-time-job --autorestart false
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm start <id>`
|
#### `tspm start <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Start a previously added process by its ID or name.
|
Start a previously added process by its ID or name.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm start my-server
|
tspm start name:my-server
|
||||||
tspm start 1 # Can also use numeric ID
|
tspm start id:1 # Or a bare numeric id: tspm start 1
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm stop <id>`
|
#### `tspm stop <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
|
Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm stop my-server
|
tspm stop name:my-server
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm restart <id>`
|
#### `tspm restart <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Stop and restart a process with the same configuration.
|
Stop and restart a process with the same configuration.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm restart my-server
|
tspm restart name:my-server
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm delete <id>` / `tspm remove <id>`
|
#### `tspm delete <id|id:N|name:LABEL>` / `tspm remove <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Stop and remove a process from TSPM management. Also deletes persisted logs.
|
Stop and remove a process from TSPM management. Also deletes persisted logs.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm delete old-server
|
tspm delete name:old-server
|
||||||
tspm remove old-server # Alias for delete
|
tspm remove name:old-server # Alias for delete (daemon handles delete)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm edit <id>`
|
#### `tspm edit <id>`
|
||||||
@@ -148,12 +150,12 @@ tspm list
|
|||||||
└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘
|
└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm describe <id>`
|
#### `tspm describe <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Get detailed information about a specific process.
|
Get detailed information about a specific process.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm describe my-server
|
tspm describe name:my-server
|
||||||
|
|
||||||
# Output:
|
# Output:
|
||||||
Process Details: my-server
|
Process Details: my-server
|
||||||
@@ -173,7 +175,7 @@ Auto-restart: true
|
|||||||
Watch: disabled
|
Watch: disabled
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm logs <id> [options]`
|
#### `tspm logs <id|id:N|name:LABEL> [options]`
|
||||||
|
|
||||||
View process logs (stdout and stderr combined).
|
View process logs (stdout and stderr combined).
|
||||||
|
|
||||||
@@ -183,13 +185,13 @@ View process logs (stdout and stderr combined).
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# View last 50 lines
|
# View last 50 lines
|
||||||
tspm logs my-server
|
tspm logs name:my-server
|
||||||
|
|
||||||
# View last 100 lines
|
# View last 100 lines
|
||||||
tspm logs my-server --lines 100
|
tspm logs name:my-server --lines 100
|
||||||
|
|
||||||
# Follow logs in real-time
|
# Follow logs in real-time
|
||||||
tspm logs my-server --follow
|
tspm logs name:my-server --follow
|
||||||
```
|
```
|
||||||
|
|
||||||
### Batch Operations
|
### Batch Operations
|
||||||
@@ -496,7 +498,31 @@ Common issues:
|
|||||||
|
|
||||||
- **"Daemon not running"**: Run `tspm daemon start` or `tspm enable`
|
- **"Daemon not running"**: Run `tspm daemon start` or `tspm enable`
|
||||||
- **"Permission denied"**: Check socket permissions in `~/.tspm/`
|
- **"Permission denied"**: Check socket permissions in `~/.tspm/`
|
||||||
- **"Process won't start"**: Check logs with `tspm logs <id>`
|
- **"Process won't start"**: Check logs with `tspm logs <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
|
## 🎯 Targeting Processes (IDs and Names)
|
||||||
|
|
||||||
|
Most process commands accept the following target formats:
|
||||||
|
|
||||||
|
- Numeric ID: `tspm start 1`
|
||||||
|
- Explicit ID: `tspm start id:1`
|
||||||
|
- Explicit name: `tspm start name:api-server`
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Names must be used with the `name:` prefix.
|
||||||
|
- If multiple processes share the same name, the CLI will report the ambiguous matches. Use `id:N` to disambiguate.
|
||||||
|
- Use `tspm search <query>` to discover IDs by name or ID fragments.
|
||||||
|
|
||||||
|
### `tspm search <query>`
|
||||||
|
|
||||||
|
Search processes by name or ID substring and print matching IDs (and names when available):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm search api
|
||||||
|
# Matches for "api":
|
||||||
|
# - id:3 name:api-server
|
||||||
|
```
|
||||||
|
|
||||||
- **"Memory limit exceeded"**: Increase limit with `tspm edit <id>`
|
- **"Memory limit exceeded"**: Increase limit with `tspm edit <id>`
|
||||||
|
|
||||||
## 🤝 Why Choose TSPM?
|
## 🤝 Why Choose TSPM?
|
||||||
@@ -515,6 +541,20 @@ Common issues:
|
|||||||
|
|
||||||
### Perfect For
|
### Perfect For
|
||||||
|
|
||||||
|
### Restart Backoff and Failure Handling
|
||||||
|
|
||||||
|
TSPM automatically restarts crashed processes with an incremental backoff:
|
||||||
|
|
||||||
|
- Debounce delay grows linearly from 1s up to 10s for consecutive retries.
|
||||||
|
- After the 10th retry, the process is marked as failed (status: "errored") and auto-restarts stop.
|
||||||
|
- The retry counter resets if no retry happens for 1 hour since the last attempt.
|
||||||
|
|
||||||
|
You can manually restart a failed process at any time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm restart id:1
|
||||||
|
```
|
||||||
|
|
||||||
- 🚀 **Production Node.js apps** - Reliable process management
|
- 🚀 **Production Node.js apps** - Reliable process management
|
||||||
- 🔧 **Microservices** - Manage multiple services easily
|
- 🔧 **Microservices** - Manage multiple services easily
|
||||||
- 👨💻 **Development** - File watching and auto-restart
|
- 👨💻 **Development** - File watching and auto-restart
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tspm',
|
name: '@git.zone/tspm',
|
||||||
version: '5.2.0',
|
version: '5.3.0',
|
||||||
description: 'a no fuzz process manager'
|
description: 'a no fuzz process manager'
|
||||||
}
|
}
|
||||||
|
@@ -22,13 +22,14 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
);
|
);
|
||||||
console.log(' disable Disable TSPM system service');
|
console.log(' disable Disable TSPM system service');
|
||||||
console.log('\nProcess Commands:');
|
console.log('\nProcess Commands:');
|
||||||
console.log(' start <script> Start a process');
|
console.log(' start <id|id:N|name:LBL> Start a process');
|
||||||
console.log(' list List all processes');
|
console.log(' list List all processes');
|
||||||
console.log(' stop <id> Stop a process');
|
console.log(' stop <id|id:N|name:LBL> Stop a process');
|
||||||
console.log(' restart <id> Restart a process');
|
console.log(' restart <id|id:N|name:LBL> Restart a process');
|
||||||
console.log(' delete <id> Delete a process');
|
console.log(' delete <id|id:N|name:LBL> Delete a process');
|
||||||
console.log(' describe <id> Show details for a process');
|
console.log(' describe <id|id:N|name:LBL> Show details for a process');
|
||||||
console.log(' logs <id> Show logs for a process');
|
console.log(' logs <id|id:N|name:LBL> Show logs for a process');
|
||||||
|
console.log(' search <query> Find processes by id/name');
|
||||||
console.log(' start-all Start all saved processes');
|
console.log(' start-all Start all saved processes');
|
||||||
console.log(' stop-all Stop all processes');
|
console.log(' stop-all Stop all processes');
|
||||||
console.log(' restart-all Restart all processes');
|
console.log(' restart-all Restart all processes');
|
||||||
|
@@ -8,23 +8,25 @@ export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
['delete', 'remove'],
|
['delete', 'remove'],
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm delete <id> | tspm remove <id>');
|
console.log('Usage: tspm delete <id|id:N|name:LABEL> | tspm remove <id|id:N|name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if command was 'remove' to use the new IPC route, otherwise 'delete'
|
// Determine if command was 'remove' to use the new IPC route, otherwise 'delete'
|
||||||
const cmd = String(argvArg._[0]);
|
const cmd = String(argvArg._[0]);
|
||||||
const useRemove = cmd === 'remove';
|
const isRemoveAlias = cmd === 'remove';
|
||||||
console.log(`${useRemove ? 'Removing' : 'Deleting'} process: ${id}`);
|
console.log(`${isRemoveAlias ? 'Removing' : 'Deleting'} process: ${target}`);
|
||||||
const response = await tspmIpcClient.request(useRemove ? 'remove' : 'delete', { id } as any);
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
// Always call daemon 'delete'; 'remove' is CLI alias only
|
||||||
|
const response = await tspmIpcClient.request('delete', { id: resolved.id } as any);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
console.log(`✓ ${response.message || (useRemove ? 'Removed successfully' : 'Deleted successfully')}`);
|
console.log(`✓ ${response.message || (isRemoveAlias ? 'Removed successfully' : 'Deleted successfully')}`);
|
||||||
} else {
|
} else {
|
||||||
console.error(`✗ Failed to ${useRemove ? 'remove' : 'delete'} process: ${response.message}`);
|
console.error(`✗ Failed to ${isRemoveAlias ? 'remove' : 'delete'} process: ${response.message}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ actionLabel: 'delete/remove process' },
|
{ actionLabel: 'delete/remove process' },
|
||||||
|
@@ -9,16 +9,17 @@ export function registerDescribeCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'describe',
|
'describe',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm describe <id>');
|
console.log('Usage: tspm describe <id | id:N | name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await tspmIpcClient.request('describe', { id });
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
const response = await tspmIpcClient.request('describe', { id: resolved.id });
|
||||||
|
|
||||||
console.log(`Process Details: ${id}`);
|
console.log(`Process Details: ${response.config.name || resolved.id}`);
|
||||||
console.log('─'.repeat(40));
|
console.log('─'.repeat(40));
|
||||||
console.log(`Status: ${response.processInfo.status}`);
|
console.log(`Status: ${response.processInfo.status}`);
|
||||||
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
|
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
|
||||||
|
@@ -9,17 +9,16 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'edit',
|
'edit',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const idRaw = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!idRaw) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID to edit');
|
console.error('Error: Please provide a process target to edit');
|
||||||
console.log('Usage: tspm edit <id>');
|
console.log('Usage: tspm edit <id | id:N | name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = idRaw;
|
// Resolve and load current config
|
||||||
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
// Load current config
|
const { config } = await tspmIpcClient.request('describe', { id: resolved.id });
|
||||||
const { config } = await tspmIpcClient.request('describe', { id });
|
|
||||||
|
|
||||||
// Interactive editing is temporarily disabled - needs smartinteract API update
|
// Interactive editing is temporarily disabled - needs smartinteract API update
|
||||||
console.log('Interactive editing is temporarily disabled.');
|
console.log('Interactive editing is temporarily disabled.');
|
||||||
@@ -63,7 +62,7 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateResponse = await tspmIpcClient.request('update', {
|
const updateResponse = await tspmIpcClient.request('update', {
|
||||||
id,
|
id: resolved.id,
|
||||||
updates,
|
updates,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,4 +72,3 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
{ actionLabel: 'edit process config' },
|
{ actionLabel: 'edit process config' },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,10 +11,10 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'logs',
|
'logs',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm logs <id> [options]');
|
console.log('Usage: tspm logs <id | id:N | name:LABEL> [options]');
|
||||||
console.log('\nOptions:');
|
console.log('\nOptions:');
|
||||||
console.log(' --lines <n> Number of lines to show (default: 50)');
|
console.log(' --lines <n> Number of lines to show (default: 50)');
|
||||||
console.log(' --follow Stream logs in real-time (like tail -f)');
|
console.log(' --follow Stream logs in real-time (like tail -f)');
|
||||||
@@ -24,6 +24,8 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
const lines = getNumber(argvArg, 'lines', 50);
|
const lines = getNumber(argvArg, 'lines', 50);
|
||||||
const follow = getBool(argvArg, 'follow', 'f');
|
const follow = getBool(argvArg, 'follow', 'f');
|
||||||
|
|
||||||
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
const id = resolved.id;
|
||||||
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
||||||
|
|
||||||
if (!follow) {
|
if (!follow) {
|
||||||
@@ -44,7 +46,7 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Streaming mode
|
// Streaming mode
|
||||||
console.log(`Logs for process: ${id} (streaming...)`);
|
console.log(`Logs for process: ${resolved.name || id} (streaming...)`);
|
||||||
console.log('─'.repeat(60));
|
console.log('─'.repeat(60));
|
||||||
|
|
||||||
let lastSeq = 0;
|
let lastSeq = 0;
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import { toProcessId } from '../../../shared/protocol/id.js';
|
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
|
||||||
@@ -11,9 +10,9 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const arg = argvArg._[1];
|
const arg = argvArg._[1];
|
||||||
if (!arg) {
|
if (!arg) {
|
||||||
console.error('Error: Please provide a process ID or "all"');
|
console.error('Error: Please provide a process target or "all"');
|
||||||
console.log('Usage:');
|
console.log('Usage:');
|
||||||
console.log(' tspm restart <id>');
|
console.log(' tspm restart <id | id:N | name:LABEL>');
|
||||||
console.log(' tspm restart all');
|
console.log(' tspm restart all');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -33,12 +32,13 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = String(arg);
|
const target = String(arg);
|
||||||
console.log(`Restarting process: ${id}`);
|
console.log(`Restarting process: ${target}`);
|
||||||
const response = await tspmIpcClient.request('restart', { id: toProcessId(id) });
|
const resolved = await tspmIpcClient.request('resolveTarget', { target });
|
||||||
|
const response = await tspmIpcClient.request('restart', { id: resolved.id });
|
||||||
|
|
||||||
console.log(`✓ Process restarted successfully`);
|
console.log(`✓ Process restarted successfully`);
|
||||||
console.log(` ID: ${response.processId}`);
|
console.log(` ID: ${response.processId}${resolved.name ? ` (name: ${resolved.name})` : ''}`);
|
||||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||||
console.log(` Status: ${response.status}`);
|
console.log(` Status: ${response.status}`);
|
||||||
},
|
},
|
||||||
|
62
ts/cli/commands/process/search.ts
Normal file
62
ts/cli/commands/process/search.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
|
import type { CliArguments } from '../../types.js';
|
||||||
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
|
||||||
|
export function registerSearchCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||||
|
registerIpcCommand(
|
||||||
|
smartcli,
|
||||||
|
'search',
|
||||||
|
async (argvArg: CliArguments) => {
|
||||||
|
const query = String(argvArg._[1] || '').trim();
|
||||||
|
if (!query) {
|
||||||
|
console.error('Error: Please provide a search query');
|
||||||
|
console.log('Usage: tspm search <name-fragment | id-fragment>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch list of processes, then enrich with names via describe
|
||||||
|
const listRes = await tspmIpcClient.request('list', {});
|
||||||
|
const processes = listRes.processes;
|
||||||
|
|
||||||
|
// If there are no processes, short-circuit
|
||||||
|
if (processes.length === 0) {
|
||||||
|
console.log('No processes found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerQ = query.toLowerCase();
|
||||||
|
const matches: Array<{ id: number; name?: string }> = [];
|
||||||
|
|
||||||
|
// Collect describe calls to obtain names
|
||||||
|
for (const proc of processes) {
|
||||||
|
try {
|
||||||
|
const desc = await tspmIpcClient.request('describe', { id: proc.id });
|
||||||
|
const name = desc.config.name || '';
|
||||||
|
const idStr = String(proc.id);
|
||||||
|
if (name.toLowerCase().includes(lowerQ) || idStr.includes(query)) {
|
||||||
|
matches.push({ id: proc.id, name });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore describe errors for individual processes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
console.log(`No matches for "${query}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Matches for "${query}":`);
|
||||||
|
for (const m of matches) {
|
||||||
|
if (m.name) {
|
||||||
|
console.log(`- id:${m.id}\tname:${m.name}`);
|
||||||
|
} else {
|
||||||
|
console.log(`- id:${m.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ actionLabel: 'search processes' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@@ -10,17 +10,18 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'start',
|
'start',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID to start');
|
console.error('Error: Please provide a process target to start');
|
||||||
console.log('Usage: tspm start <id>');
|
console.log('Usage: tspm start <id | id:N | name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Starting process id ${id}...`);
|
console.log(`Starting process: ${target}...`);
|
||||||
const response = await tspmIpcClient.request('startById', { id });
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
const response = await tspmIpcClient.request('startById', { id: resolved.id });
|
||||||
console.log('✓ Process started');
|
console.log('✓ Process started');
|
||||||
console.log(` ID: ${response.processId}`);
|
console.log(` ID: ${response.processId}${resolved.name ? ` (name: ${resolved.name})` : ''}`);
|
||||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||||
console.log(` Status: ${response.status}`);
|
console.log(` Status: ${response.status}`);
|
||||||
},
|
},
|
||||||
|
@@ -8,15 +8,16 @@ export function registerStopCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'stop',
|
'stop',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm stop <id>');
|
console.log('Usage: tspm stop <id | id:N | name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Stopping process: ${id}`);
|
console.log(`Stopping process: ${target}`);
|
||||||
const response = await tspmIpcClient.request('stop', { id });
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
const response = await tspmIpcClient.request('stop', { id: resolved.id });
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
console.log(`✓ ${response.message}`);
|
console.log(`✓ ${response.message}`);
|
||||||
|
@@ -10,6 +10,7 @@ import { registerAddCommand } from './commands/process/add.js';
|
|||||||
import { registerStopCommand } from './commands/process/stop.js';
|
import { registerStopCommand } from './commands/process/stop.js';
|
||||||
import { registerRestartCommand } from './commands/process/restart.js';
|
import { registerRestartCommand } from './commands/process/restart.js';
|
||||||
import { registerDeleteCommand } from './commands/process/delete.js';
|
import { registerDeleteCommand } from './commands/process/delete.js';
|
||||||
|
import { registerSearchCommand } from './commands/process/search.js';
|
||||||
import { registerListCommand } from './commands/process/list.js';
|
import { registerListCommand } from './commands/process/list.js';
|
||||||
import { registerDescribeCommand } from './commands/process/describe.js';
|
import { registerDescribeCommand } from './commands/process/describe.js';
|
||||||
import { registerLogsCommand } from './commands/process/logs.js';
|
import { registerLogsCommand } from './commands/process/logs.js';
|
||||||
@@ -74,6 +75,7 @@ export const run = async (): Promise<void> => {
|
|||||||
registerDescribeCommand(smartcliInstance);
|
registerDescribeCommand(smartcliInstance);
|
||||||
registerLogsCommand(smartcliInstance);
|
registerLogsCommand(smartcliInstance);
|
||||||
registerEditCommand(smartcliInstance);
|
registerEditCommand(smartcliInstance);
|
||||||
|
registerSearchCommand(smartcliInstance);
|
||||||
|
|
||||||
// Batch commands
|
// Batch commands
|
||||||
registerStartAllCommand(smartcliInstance);
|
registerStartAllCommand(smartcliInstance);
|
||||||
|
@@ -156,6 +156,11 @@ export class ProcessManager extends EventEmitter {
|
|||||||
this.updateProcessInfo(config.id, { pid: undefined });
|
this.updateProcessInfo(config.id, { pid: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up failure handler to mark process as errored
|
||||||
|
monitor.on('failed', () => {
|
||||||
|
this.updateProcessInfo(config.id, { status: 'errored', pid: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
await monitor.start();
|
await monitor.start();
|
||||||
|
|
||||||
// Wait a moment for the process to spawn and get its PID
|
// Wait a moment for the process to spawn and get its PID
|
||||||
@@ -327,6 +332,11 @@ export class ProcessManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark errored on failure events
|
||||||
|
newMonitor.on('failed', () => {
|
||||||
|
this.updateProcessInfo(id, { status: 'errored', pid: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
this.logger.info(`Successfully restarted process with id '${id}'`);
|
this.logger.info(`Successfully restarted process with id '${id}'`);
|
||||||
} catch (error: Error | unknown) {
|
} catch (error: Error | unknown) {
|
||||||
const processError = new ProcessError(
|
const processError = new ProcessError(
|
||||||
|
@@ -18,6 +18,10 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
private processId?: ProcessId;
|
private processId?: ProcessId;
|
||||||
private currentLogMemorySize: number = 0;
|
private currentLogMemorySize: number = 0;
|
||||||
private readonly MAX_LOG_MEMORY_SIZE = 10 * 1024 * 1024; // 10MB
|
private readonly MAX_LOG_MEMORY_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
private restartTimer: NodeJS.Timeout | null = null;
|
||||||
|
private lastRetryAt: number | null = null;
|
||||||
|
private readonly MAX_RETRIES = 10;
|
||||||
|
private readonly RESET_WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
||||||
|
|
||||||
constructor(config: IMonitorConfig & { id?: ProcessId }) {
|
constructor(config: IMonitorConfig & { id?: ProcessId }) {
|
||||||
super();
|
super();
|
||||||
@@ -132,10 +136,7 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
this.emit('exit', code, signal);
|
this.emit('exit', code, signal);
|
||||||
|
|
||||||
if (!this.stopped) {
|
if (!this.stopped) {
|
||||||
this.logger.info('Restarting process...');
|
this.scheduleRestart('exit');
|
||||||
this.log('Restarting process...');
|
|
||||||
this.restartCount++;
|
|
||||||
this.spawnProcess();
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Not restarting process because monitor is stopped',
|
'Not restarting process because monitor is stopped',
|
||||||
@@ -164,10 +165,7 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.stopped) {
|
if (!this.stopped) {
|
||||||
this.logger.info('Restarting process due to error...');
|
this.scheduleRestart('error');
|
||||||
this.log('Restarting process due to error...');
|
|
||||||
this.restartCount++;
|
|
||||||
this.spawnProcess();
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug('Not restarting process because monitor is stopped');
|
this.logger.debug('Not restarting process because monitor is stopped');
|
||||||
}
|
}
|
||||||
@@ -185,6 +183,49 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule a restart with incremental debounce and failure cutoff.
|
||||||
|
*/
|
||||||
|
private scheduleRestart(reason: 'exit' | 'error'): void {
|
||||||
|
const now = Date.now();
|
||||||
|
// Reset window: if last retry was more than 1 hour ago, reset counter
|
||||||
|
if (this.lastRetryAt && now - this.lastRetryAt >= this.RESET_WINDOW_MS) {
|
||||||
|
this.logger.info('Resetting retry counter after 1 hour window');
|
||||||
|
this.restartCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already at or above max retries?
|
||||||
|
if (this.restartCount >= this.MAX_RETRIES) {
|
||||||
|
const msg = 'Maximum restart attempts reached. Marking process as failed.';
|
||||||
|
this.logger.warn(msg);
|
||||||
|
this.log(msg);
|
||||||
|
this.stopped = true;
|
||||||
|
// Emit a specific event so manager can set status to errored
|
||||||
|
this.emit('failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment and compute delay (1..10 seconds)
|
||||||
|
this.restartCount++;
|
||||||
|
const delaySec = Math.min(this.restartCount, 10);
|
||||||
|
const msg = `Restarting process in ${delaySec}s (attempt ${this.restartCount}/${this.MAX_RETRIES}) due to ${reason}...`;
|
||||||
|
this.logger.info(msg);
|
||||||
|
this.log(msg);
|
||||||
|
|
||||||
|
// Clear existing timer if any, then schedule
|
||||||
|
if (this.restartTimer) {
|
||||||
|
clearTimeout(this.restartTimer);
|
||||||
|
}
|
||||||
|
this.lastRetryAt = now;
|
||||||
|
this.restartTimer = setTimeout(() => {
|
||||||
|
// If stopped in the meantime, do not spawn
|
||||||
|
if (this.stopped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.spawnProcess();
|
||||||
|
}, delaySec * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monitor the process group's memory usage. If the total memory exceeds the limit,
|
* Monitor the process group's memory usage. If the total memory exceeds the limit,
|
||||||
* kill the process group so that the 'exit' handler can restart it.
|
* kill the process group so that the 'exit' handler can restart it.
|
||||||
|
@@ -208,6 +208,8 @@ export class TspmDaemon {
|
|||||||
async (request: RequestForMethod<'delete'>) => {
|
async (request: RequestForMethod<'delete'>) => {
|
||||||
try {
|
try {
|
||||||
const id = toProcessId(request.id);
|
const id = toProcessId(request.id);
|
||||||
|
// Ensure desired state reflects stopped before deletion
|
||||||
|
await this.tspmInstance.setDesiredState(id, 'stopped');
|
||||||
await this.tspmInstance.delete(id);
|
await this.tspmInstance.delete(id);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -246,18 +248,7 @@ export class TspmDaemon {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.ipcServer.onMessage(
|
// Note: 'remove' is only a CLI alias. Daemon exposes 'delete' only.
|
||||||
'remove',
|
|
||||||
async (request: RequestForMethod<'remove'>) => {
|
|
||||||
try {
|
|
||||||
const id = toProcessId(request.id);
|
|
||||||
await this.tspmInstance.delete(id);
|
|
||||||
return { success: true, message: `Process ${id} deleted successfully` };
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to remove process: ${error.message}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'list',
|
'list',
|
||||||
@@ -291,6 +282,58 @@ export class TspmDaemon {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Resolve target (id:n | name:foo | numeric string) to ProcessId
|
||||||
|
this.ipcServer.onMessage(
|
||||||
|
'resolveTarget',
|
||||||
|
async (request: RequestForMethod<'resolveTarget'>) => {
|
||||||
|
const raw = String(request.target || '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
throw new Error('Empty target');
|
||||||
|
}
|
||||||
|
|
||||||
|
// id:<n>
|
||||||
|
if (/^id:\s*\d+$/i.test(raw)) {
|
||||||
|
const idNum = raw.split(':')[1].trim();
|
||||||
|
const id = toProcessId(idNum);
|
||||||
|
const config = this.tspmInstance.processConfigs.get(id);
|
||||||
|
if (!config) throw new Error(`Process ${id} not found`);
|
||||||
|
return { id, name: config.name } as ResponseForMethod<'resolveTarget'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// name:<label>
|
||||||
|
if (/^name:/i.test(raw)) {
|
||||||
|
const name = raw.slice(raw.indexOf(':') + 1).trim();
|
||||||
|
if (!name) throw new Error('Missing name after name:');
|
||||||
|
const matches = Array.from(this.tspmInstance.processConfigs.values()).filter(
|
||||||
|
(c) => (c.name || '').trim() === name,
|
||||||
|
);
|
||||||
|
if (matches.length === 0) {
|
||||||
|
throw new Error(`No process found with name "${name}"`);
|
||||||
|
}
|
||||||
|
if (matches.length > 1) {
|
||||||
|
const ids = matches.map((c) => String(c.id)).join(', ');
|
||||||
|
throw new Error(
|
||||||
|
`Multiple processes found with name "${name}": ids [${ids}]. Please use id:<n>.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { id: matches[0].id, name } as ResponseForMethod<'resolveTarget'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bare numeric id
|
||||||
|
if (/^\d+$/.test(raw)) {
|
||||||
|
const id = toProcessId(raw);
|
||||||
|
const config = this.tspmInstance.processConfigs.get(id);
|
||||||
|
if (!config) throw new Error(`Process ${id} not found`);
|
||||||
|
return { id, name: config.name } as ResponseForMethod<'resolveTarget'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown format
|
||||||
|
throw new Error(
|
||||||
|
'Unsupported target format. Use numeric id (e.g. 1), id:<n> (e.g. id:1), or name:<label> (e.g. name:api).',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Batch operations handlers
|
// Batch operations handlers
|
||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'startAll',
|
'startAll',
|
||||||
|
@@ -240,14 +240,6 @@ export interface AddResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove (delete config and stop if running)
|
// Remove (delete config and stop if running)
|
||||||
export interface RemoveRequest {
|
|
||||||
id: ProcessId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RemoveResponse {
|
|
||||||
success: boolean;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update (modify existing config)
|
// Update (modify existing config)
|
||||||
export interface UpdateRequest {
|
export interface UpdateRequest {
|
||||||
@@ -260,6 +252,16 @@ export interface UpdateResponse {
|
|||||||
config: IProcessConfig;
|
config: IProcessConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve a user-provided target (id:n or name:foo or numeric string) to a ProcessId
|
||||||
|
export interface ResolveTargetRequest {
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolveTargetResponse {
|
||||||
|
id: ProcessId;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Type mappings for methods
|
// Type mappings for methods
|
||||||
export type IpcMethodMap = {
|
export type IpcMethodMap = {
|
||||||
start: { request: StartRequest; response: StartResponse };
|
start: { request: StartRequest; response: StartResponse };
|
||||||
@@ -269,7 +271,6 @@ export type IpcMethodMap = {
|
|||||||
delete: { request: DeleteRequest; response: DeleteResponse };
|
delete: { request: DeleteRequest; response: DeleteResponse };
|
||||||
add: { request: AddRequest; response: AddResponse };
|
add: { request: AddRequest; response: AddResponse };
|
||||||
update: { request: UpdateRequest; response: UpdateResponse };
|
update: { request: UpdateRequest; response: UpdateResponse };
|
||||||
remove: { request: RemoveRequest; response: RemoveResponse };
|
|
||||||
list: { request: ListRequest; response: ListResponse };
|
list: { request: ListRequest; response: ListResponse };
|
||||||
describe: { request: DescribeRequest; response: DescribeResponse };
|
describe: { request: DescribeRequest; response: DescribeResponse };
|
||||||
getLogs: { request: GetLogsRequest; response: GetLogsResponse };
|
getLogs: { request: GetLogsRequest; response: GetLogsResponse };
|
||||||
@@ -286,6 +287,7 @@ export type IpcMethodMap = {
|
|||||||
response: DaemonShutdownResponse;
|
response: DaemonShutdownResponse;
|
||||||
};
|
};
|
||||||
heartbeat: { request: HeartbeatRequest; response: HeartbeatResponse };
|
heartbeat: { request: HeartbeatRequest; response: HeartbeatResponse };
|
||||||
|
resolveTarget: { request: ResolveTargetRequest; response: ResolveTargetResponse };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper type to extract request type for a method
|
// Helper type to extract request type for a method
|
||||||
|
Reference in New Issue
Block a user