feat(daemon): Add central TSPM daemon and IPC client; refactor CLI to use daemon and improve monitoring/error handling
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -16,4 +16,8 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
dist_*/
|
dist_*/
|
||||||
|
|
||||||
|
# AI
|
||||||
|
.claude/
|
||||||
|
.serena/
|
||||||
|
|
||||||
#------# custom
|
#------# custom
|
25
changelog.md
25
changelog.md
@@ -1,6 +1,20 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-25 - 1.6.0 - feat(daemon)
|
||||||
|
Add central TSPM daemon and IPC client; refactor CLI to use daemon and improve monitoring/error handling
|
||||||
|
|
||||||
|
- Add central daemon implementation (ts/classes.daemon.ts) to manage all processes via a single background service and Unix socket.
|
||||||
|
- Introduce IPC client and typed IPC contracts (ts/classes.ipcclient.ts, ts/ipc.types.ts) so CLI communicates with the daemon.
|
||||||
|
- Refactor CLI to use the daemon for commands (ts/cli.ts): start/stop/restart/delete/list/describe/logs/start-all/stop-all/restart-all and new daemon start/stop/status commands.
|
||||||
|
- Enhance process monitoring and wrapping: ProcessMonitor and ProcessWrapper improvements (ts/classes.processmonitor.ts, ts/classes.processwrapper.ts) with better logging, memory checks, and restart behavior.
|
||||||
|
- Improve centralized error handling and Logger behavior (ts/utils.errorhandler.ts).
|
||||||
|
- Persist and load process configurations via TspmConfig and config storage changes (ts/classes.config.ts, ts/classes.tspm.ts).
|
||||||
|
- Bump dependency and devDependency versions and add packageManager entry in package.json.
|
||||||
|
- Add ts/daemon entrypoint and export daemon/ipc types from ts/index.ts; add paths for tspm runtime dir (ts/paths.ts).
|
||||||
|
- Update tests and test tooling imports (test/test.ts) and adjust commitinfo and readme hints.
|
||||||
|
|
||||||
## 2025-03-10 - 1.5.1 - fix(core)
|
## 2025-03-10 - 1.5.1 - fix(core)
|
||||||
|
|
||||||
Improve error handling, logging, and test suite; update dependency versions
|
Improve error handling, logging, and test suite; update dependency versions
|
||||||
|
|
||||||
- Updated devDependencies versions in package.json (@git.zone/tsbuild, @push.rocks/tapbundle, and @push.rocks/smartdaemon)
|
- Updated devDependencies versions in package.json (@git.zone/tsbuild, @push.rocks/tapbundle, and @push.rocks/smartdaemon)
|
||||||
@@ -8,6 +22,7 @@ Improve error handling, logging, and test suite; update dependency versions
|
|||||||
- Improved test structure by adding clear module import tests and usage examples in test files
|
- Improved test structure by adding clear module import tests and usage examples in test files
|
||||||
|
|
||||||
## 2025-03-04 - 1.5.0 - feat(cli)
|
## 2025-03-04 - 1.5.0 - feat(cli)
|
||||||
|
|
||||||
Enhance CLI with new process management commands
|
Enhance CLI with new process management commands
|
||||||
|
|
||||||
- Added comprehensive CLI commands for process management including start, stop, restart, list, describe and logs.
|
- Added comprehensive CLI commands for process management including start, stop, restart, list, describe and logs.
|
||||||
@@ -15,6 +30,7 @@ Enhance CLI with new process management commands
|
|||||||
- Enhanced CLI output with formatted table listings for active processes.
|
- Enhanced CLI output with formatted table listings for active processes.
|
||||||
|
|
||||||
## 2025-03-03 - 1.4.0 - feat(core)
|
## 2025-03-03 - 1.4.0 - feat(core)
|
||||||
|
|
||||||
Introduced process management features using ProcessWrapper and enhanced configuration.
|
Introduced process management features using ProcessWrapper and enhanced configuration.
|
||||||
|
|
||||||
- Added ProcessWrapper for wrapping and managing child processes.
|
- Added ProcessWrapper for wrapping and managing child processes.
|
||||||
@@ -23,12 +39,14 @@ Introduced process management features using ProcessWrapper and enhanced configu
|
|||||||
- Enhanced CLI to support new process management commands like 'startAsDaemon'.
|
- Enhanced CLI to support new process management commands like 'startAsDaemon'.
|
||||||
|
|
||||||
## 2025-03-01 - 1.3.1 - fix(test)
|
## 2025-03-01 - 1.3.1 - fix(test)
|
||||||
|
|
||||||
Update test script to fix type references and remove private method call
|
Update test script to fix type references and remove private method call
|
||||||
|
|
||||||
- Corrected type references in test script for IMonitorConfig.
|
- Corrected type references in test script for IMonitorConfig.
|
||||||
- Fixed test script to use console.log instead of private method monitor.log.
|
- Fixed test script to use console.log instead of private method monitor.log.
|
||||||
|
|
||||||
## 2025-03-01 - 1.3.0 - feat(cli)
|
## 2025-03-01 - 1.3.0 - feat(cli)
|
||||||
|
|
||||||
Add CLI support with command parsing and version display
|
Add CLI support with command parsing and version display
|
||||||
|
|
||||||
- Added a basic CLI interface using smartcli.
|
- Added a basic CLI interface using smartcli.
|
||||||
@@ -36,6 +54,7 @@ Add CLI support with command parsing and version display
|
|||||||
- Integrated project version display in the CLI.
|
- Integrated project version display in the CLI.
|
||||||
|
|
||||||
## 2025-03-01 - 1.2.0 - feat(core)
|
## 2025-03-01 - 1.2.0 - feat(core)
|
||||||
|
|
||||||
Introduce ProcessMonitor with memory management and spawning features
|
Introduce ProcessMonitor with memory management and spawning features
|
||||||
|
|
||||||
- Added ProcessMonitor class with functionality to manage process execution and memory usage.
|
- Added ProcessMonitor class with functionality to manage process execution and memory usage.
|
||||||
@@ -45,12 +64,14 @@ Introduce ProcessMonitor with memory management and spawning features
|
|||||||
- Updated test file to include example usage of ProcessMonitor.
|
- Updated test file to include example usage of ProcessMonitor.
|
||||||
|
|
||||||
## 2025-03-01 - 1.1.1 - fix(package)
|
## 2025-03-01 - 1.1.1 - fix(package)
|
||||||
|
|
||||||
Update dependencies and pnpm configuration
|
Update dependencies and pnpm configuration
|
||||||
|
|
||||||
- Updated @types/node to 22.13.8
|
- Updated @types/node to 22.13.8
|
||||||
- Updated pnpm configuration to include onlyBuiltDependencies with esbuild, mongodb-memory-server, and puppeteer
|
- Updated pnpm configuration to include onlyBuiltDependencies with esbuild, mongodb-memory-server, and puppeteer
|
||||||
|
|
||||||
## 2025-03-01 - 1.1.0 - feat(core)
|
## 2025-03-01 - 1.1.0 - feat(core)
|
||||||
|
|
||||||
Introduce ProcessMonitor class and integrate native and external plugins
|
Introduce ProcessMonitor class and integrate native and external plugins
|
||||||
|
|
||||||
- Added a new ProcessMonitor class to manage and monitor child processes with memory constraints.
|
- Added a new ProcessMonitor class to manage and monitor child processes with memory constraints.
|
||||||
@@ -58,14 +79,16 @@ Introduce ProcessMonitor class and integrate native and external plugins
|
|||||||
- Adjusted index and related files for improved modular structure.
|
- Adjusted index and related files for improved modular structure.
|
||||||
|
|
||||||
## 2025-02-24 - 1.0.3 - fix(core)
|
## 2025-02-24 - 1.0.3 - fix(core)
|
||||||
|
|
||||||
Corrected description in package.json and readme.md from 'task manager' to 'process manager'.
|
Corrected description in package.json and readme.md from 'task manager' to 'process manager'.
|
||||||
|
|
||||||
- Updated the project description in package.json.
|
- Updated the project description in package.json.
|
||||||
- Aligned the description in readme.md with package.json.
|
- Aligned the description in readme.md with package.json.
|
||||||
|
|
||||||
## 2025-02-24 - 1.0.2 - fix(core)
|
## 2025-02-24 - 1.0.2 - fix(core)
|
||||||
|
|
||||||
Internal changes with no functional impact.
|
Internal changes with no functional impact.
|
||||||
|
|
||||||
|
|
||||||
## 2025-02-24 - 1.0.1 - initial release
|
## 2025-02-24 - 1.0.1 - initial release
|
||||||
|
|
||||||
Initial release with baseline functionality.
|
Initial release with baseline functionality.
|
||||||
|
18
package.json
18
package.json
@@ -18,20 +18,21 @@
|
|||||||
"tspm": "./cli.js"
|
"tspm": "./cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.2.6",
|
"@git.zone/tsbuild": "^2.6.7",
|
||||||
"@git.zone/tsbundle": "^2.0.5",
|
"@git.zone/tsbundle": "^2.5.1",
|
||||||
"@git.zone/tsrun": "^1.2.46",
|
"@git.zone/tsrun": "^1.2.46",
|
||||||
"@git.zone/tstest": "^1.0.44",
|
"@git.zone/tstest": "^2.3.5",
|
||||||
"@push.rocks/tapbundle": "^5.5.9",
|
"@push.rocks/tapbundle": "^6.0.3",
|
||||||
"@types/node": "^22.13.10"
|
"@types/node": "^22.13.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/npmextra": "^5.1.2",
|
"@push.rocks/npmextra": "^5.3.3",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/smartcli": "^4.0.11",
|
"@push.rocks/smartcli": "^4.0.11",
|
||||||
"@push.rocks/smartdaemon": "^2.0.8",
|
"@push.rocks/smartdaemon": "^2.0.8",
|
||||||
"@push.rocks/smartpath": "^5.0.18",
|
"@push.rocks/smartipc": "^2.0.3",
|
||||||
"pidusage": "^4.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
|
"pidusage": "^4.0.1",
|
||||||
"ps-tree": "^1.2.0"
|
"ps-tree": "^1.2.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -61,5 +62,6 @@
|
|||||||
"mongodb-memory-server",
|
"mongodb-memory-server",
|
||||||
"puppeteer"
|
"puppeteer"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
|
||||||
}
|
}
|
||||||
|
5443
pnpm-lock.yaml
generated
5443
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
209
readme.plan.md
Normal file
209
readme.plan.md
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
# TSPM Refactoring Plan: Central Daemon Architecture
|
||||||
|
|
||||||
|
## Problem Analysis
|
||||||
|
|
||||||
|
Currently, each `startAsDaemon` creates an isolated tspm instance with no coordination:
|
||||||
|
|
||||||
|
- Multiple daemons reading/writing same config file
|
||||||
|
- No communication between instances
|
||||||
|
- Inconsistent process management
|
||||||
|
- `tspm list` shows all processes but each daemon only manages its own
|
||||||
|
|
||||||
|
## Proposed Architecture
|
||||||
|
|
||||||
|
### 1. Central Daemon Manager (`ts/classes.daemon.ts`)
|
||||||
|
|
||||||
|
- Single daemon instance managing ALL processes
|
||||||
|
- Runs continuously in background
|
||||||
|
- Uses Unix socket for IPC at `~/.tspm/tspm.sock`
|
||||||
|
- Maintains single source of truth for process state
|
||||||
|
|
||||||
|
### 2. IPC Communication Layer (`ts/classes.ipc.ts`)
|
||||||
|
|
||||||
|
- **Framework**: Use `@push.rocks/smartipc` v2.0.1
|
||||||
|
- **Server**: SmartIpc server in daemon using Unix Domain Socket
|
||||||
|
- **Client**: SmartIpc client in CLI for all operations
|
||||||
|
- **Socket Path**: `~/.tspm/tspm.sock` (Unix) or named pipe (Windows)
|
||||||
|
- **Protocol**: Type-safe request/response with SmartIpc's built-in patterns
|
||||||
|
- **Features**:
|
||||||
|
- Automatic reconnection with exponential backoff
|
||||||
|
- Heartbeat monitoring for daemon health
|
||||||
|
- Type-safe message contracts
|
||||||
|
- **Auto-start**: CLI starts daemon if connection fails
|
||||||
|
|
||||||
|
### 3. New CLI Commands
|
||||||
|
|
||||||
|
- `tspm enable` - Start central daemon using systemd/launchd
|
||||||
|
- `tspm disable` - Stop and disable central daemon
|
||||||
|
- `tspm status` - Show daemon status
|
||||||
|
- Remove `startAsDaemon` (replaced by daemon + `tspm start`)
|
||||||
|
|
||||||
|
### 4. Refactored CLI (`ts/cli.ts`)
|
||||||
|
|
||||||
|
All commands become daemon clients:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Before: Direct process management
|
||||||
|
await tspm.start(config);
|
||||||
|
|
||||||
|
// After: Send to daemon
|
||||||
|
await ipcClient.request('start', config);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. File Structure Changes
|
||||||
|
|
||||||
|
```
|
||||||
|
ts/
|
||||||
|
├── classes.daemon.ts # New: Central daemon server
|
||||||
|
├── classes.ipc.ts # New: IPC client/server
|
||||||
|
├── classes.tspm.ts # Modified: Used by daemon only
|
||||||
|
├── cli.ts # Modified: Becomes thin client
|
||||||
|
└── classes.daemonmanager.ts # New: Systemd/launchd integration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### Phase 1: Core Infrastructure
|
||||||
|
|
||||||
|
- [ ] Add `@push.rocks/smartipc` dependency (v2.0.1)
|
||||||
|
- [ ] Create IPC message type definitions for all operations
|
||||||
|
- [ ] Implement daemon server with SmartIpc server
|
||||||
|
- [ ] Create IPC client wrapper for CLI
|
||||||
|
- [ ] Add daemon lifecycle management (enable/disable)
|
||||||
|
|
||||||
|
### Phase 2: CLI Refactoring
|
||||||
|
|
||||||
|
- [ ] Convert all CLI commands to SmartIpc client requests
|
||||||
|
- [ ] Add daemon auto-start logic with connection monitoring
|
||||||
|
- [ ] Leverage SmartIpc's built-in reconnection and error handling
|
||||||
|
- [ ] Implement type-safe message contracts for all commands
|
||||||
|
|
||||||
|
### Phase 3: Migration & Cleanup
|
||||||
|
|
||||||
|
- [ ] Migrate existing config to daemon-compatible format
|
||||||
|
- [ ] Remove `startAsDaemon` command
|
||||||
|
- [ ] Add migration guide for users
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### IPC Implementation with SmartIpc
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Daemon server setup
|
||||||
|
import { SmartIpc } from '@push.rocks/smartipc';
|
||||||
|
|
||||||
|
const ipcServer = SmartIpc.createServer({
|
||||||
|
id: 'tspm-daemon',
|
||||||
|
socketPath: '~/.tspm/tspm.sock', // Unix socket
|
||||||
|
});
|
||||||
|
|
||||||
|
// Message handlers with type safety
|
||||||
|
ipcServer.onMessage<StartRequest, StartResponse>(
|
||||||
|
'start',
|
||||||
|
async (data, clientId) => {
|
||||||
|
const result = await tspmManager.start(data.config);
|
||||||
|
return { success: true, processId: result.pid };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// CLI client setup
|
||||||
|
const ipcClient = SmartIpc.createClient({
|
||||||
|
id: 'tspm-daemon',
|
||||||
|
socketPath: '~/.tspm/tspm.sock',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Type-safe requests
|
||||||
|
const response = await ipcClient.request<StartRequest, StartResponse>('start', {
|
||||||
|
config: processConfig,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface StartRequest {
|
||||||
|
config: ProcessConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StartResponse {
|
||||||
|
success: boolean;
|
||||||
|
processId?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Daemon State File
|
||||||
|
|
||||||
|
`~/.tspm/daemon.state` - PID, socket path, version
|
||||||
|
|
||||||
|
### Process Management
|
||||||
|
|
||||||
|
Daemon maintains all ProcessMonitor instances internally, CLI never directly manages processes.
|
||||||
|
|
||||||
|
## Key Benefits
|
||||||
|
|
||||||
|
### Architecture Benefits
|
||||||
|
|
||||||
|
- Single daemon manages all processes
|
||||||
|
- Consistent state management
|
||||||
|
- Efficient resource usage
|
||||||
|
- Better process coordination
|
||||||
|
- Proper service integration with OS
|
||||||
|
|
||||||
|
### SmartIpc Advantages
|
||||||
|
|
||||||
|
- **Cross-platform**: Unix sockets on Linux/macOS, named pipes on Windows
|
||||||
|
- **Type-safe**: Full TypeScript support with generic message types
|
||||||
|
- **Resilient**: Automatic reconnection with exponential backoff
|
||||||
|
- **Observable**: Built-in metrics and heartbeat monitoring
|
||||||
|
- **Performant**: Low-latency messaging with zero external dependencies
|
||||||
|
- **Secure**: Connection limits and message size restrictions
|
||||||
|
|
||||||
|
## Backwards Compatibility
|
||||||
|
|
||||||
|
- Keep existing config format
|
||||||
|
- Auto-migrate on first run
|
||||||
|
- Provide clear upgrade instructions
|
||||||
|
|
||||||
|
## Architecture Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ IPC ┌──────────────┐
|
||||||
|
│ CLI │◄────────────►│ Daemon │
|
||||||
|
│ (thin client)│ Socket │ (server) │
|
||||||
|
└─────────────┘ └──────────────┘
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ ┌──────────────┐
|
||||||
|
│ │ Tspm │
|
||||||
|
│ │ Manager │
|
||||||
|
│ └──────────────┘
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────┐ ┌──────────────┐
|
||||||
|
│ User │ │ProcessMonitor│
|
||||||
|
│ Commands │ │ Instances │
|
||||||
|
└─────────────┘ └──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
|
||||||
|
1. **Version 2.0.0-alpha**: Implement daemon with backwards compatibility
|
||||||
|
2. **Version 2.0.0-beta**: Deprecate `startAsDaemon`, encourage daemon mode
|
||||||
|
3. **Version 2.0.0**: Remove legacy code, daemon-only operation
|
||||||
|
4. **Documentation**: Update all examples and guides
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Unix socket permissions (user-only access)
|
||||||
|
- Validate all IPC messages
|
||||||
|
- Rate limiting for IPC requests
|
||||||
|
- Secure daemon shutdown mechanism
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
|
||||||
|
- Unit tests for IPC layer
|
||||||
|
- Integration tests for daemon lifecycle
|
||||||
|
- Migration tests from current architecture
|
||||||
|
- Performance tests for multiple processes
|
||||||
|
- Stress tests for IPC communication
|
@@ -1,4 +1,4 @@
|
|||||||
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as tspm from '../ts/index.js';
|
import * as tspm from '../ts/index.js';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tspm',
|
name: '@git.zone/tspm',
|
||||||
version: '1.5.1',
|
version: '1.6.0',
|
||||||
description: 'a no fuzz process manager'
|
description: 'a no fuzz process manager'
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ export class TspmConfig {
|
|||||||
public npmextraInstance = new plugins.npmextra.KeyValueStore({
|
public npmextraInstance = new plugins.npmextra.KeyValueStore({
|
||||||
identityArg: '@git.zone__tspm',
|
identityArg: '@git.zone__tspm',
|
||||||
typeArg: 'userHomeDir',
|
typeArg: 'userHomeDir',
|
||||||
})
|
});
|
||||||
|
|
||||||
public async readKey(keyArg: string): Promise<string> {
|
public async readKey(keyArg: string): Promise<string> {
|
||||||
return await this.npmextraInstance.readKey(keyArg);
|
return await this.npmextraInstance.readKey(keyArg);
|
||||||
|
415
ts/classes.daemon.ts
Normal file
415
ts/classes.daemon.ts
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import * as paths from './paths.js';
|
||||||
|
import { Tspm } from './classes.tspm.js';
|
||||||
|
import type {
|
||||||
|
IpcMethodMap,
|
||||||
|
RequestForMethod,
|
||||||
|
ResponseForMethod,
|
||||||
|
DaemonStatusResponse,
|
||||||
|
HeartbeatResponse,
|
||||||
|
} from './ipc.types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central daemon server that manages all TSPM processes
|
||||||
|
*/
|
||||||
|
export class TspmDaemon {
|
||||||
|
private tspmInstance: Tspm;
|
||||||
|
private ipcServer: plugins.smartipc.IpcServer;
|
||||||
|
private startTime: number;
|
||||||
|
private isShuttingDown: boolean = false;
|
||||||
|
private socketPath: string;
|
||||||
|
private heartbeatInterval: NodeJS.Timeout | null = null;
|
||||||
|
private daemonPidFile: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.tspmInstance = new Tspm();
|
||||||
|
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
|
||||||
|
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
|
||||||
|
this.startTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the daemon server
|
||||||
|
*/
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
console.log('Starting TSPM daemon...');
|
||||||
|
|
||||||
|
// Check if another daemon is already running
|
||||||
|
if (await this.isDaemonRunning()) {
|
||||||
|
throw new Error('Another TSPM daemon instance is already running');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize IPC server
|
||||||
|
this.ipcServer = new plugins.smartipc.IpcServer({
|
||||||
|
id: 'tspm-daemon',
|
||||||
|
socketPath: this.socketPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register message handlers
|
||||||
|
this.registerHandlers();
|
||||||
|
|
||||||
|
// Start the IPC server
|
||||||
|
await this.ipcServer.start();
|
||||||
|
|
||||||
|
// Write PID file
|
||||||
|
await this.writePidFile();
|
||||||
|
|
||||||
|
// Start heartbeat monitoring
|
||||||
|
this.startHeartbeatMonitoring();
|
||||||
|
|
||||||
|
// Load existing process configurations
|
||||||
|
await this.tspmInstance.loadProcessConfigs();
|
||||||
|
|
||||||
|
// Set up graceful shutdown handlers
|
||||||
|
this.setupShutdownHandlers();
|
||||||
|
|
||||||
|
console.log(`TSPM daemon started successfully on ${this.socketPath}`);
|
||||||
|
console.log(`PID: ${process.pid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all IPC message handlers
|
||||||
|
*/
|
||||||
|
private registerHandlers(): void {
|
||||||
|
// Process management handlers
|
||||||
|
this.ipcServer.on<RequestForMethod<'start'>>(
|
||||||
|
'start',
|
||||||
|
async (request) => {
|
||||||
|
try {
|
||||||
|
await this.tspmInstance.start(request.config);
|
||||||
|
const processInfo = this.tspmInstance.processInfo.get(
|
||||||
|
request.config.id,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
processId: request.config.id,
|
||||||
|
pid: processInfo?.pid,
|
||||||
|
status: processInfo?.status || 'stopped',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to start process: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.ipcServer.on<RequestForMethod<'stop'>>(
|
||||||
|
'stop',
|
||||||
|
async (request) => {
|
||||||
|
try {
|
||||||
|
await this.tspmInstance.stop(request.id);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Process ${request.id} stopped successfully`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to stop process: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.ipcServer.on<RequestForMethod<'restart'>>('restart', async (request) => {
|
||||||
|
try {
|
||||||
|
await this.tspmInstance.restart(request.id);
|
||||||
|
const processInfo = this.tspmInstance.processInfo.get(request.id);
|
||||||
|
return {
|
||||||
|
processId: request.id,
|
||||||
|
pid: processInfo?.pid,
|
||||||
|
status: processInfo?.status || 'stopped',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to restart process: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ipcServer.on<RequestForMethod<'delete'>>(
|
||||||
|
'delete',
|
||||||
|
async (request) => {
|
||||||
|
try {
|
||||||
|
await this.tspmInstance.delete(request.id);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Process ${request.id} deleted successfully`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to delete process: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Query handlers
|
||||||
|
this.ipcServer.on<RequestForMethod<'list'>>(
|
||||||
|
'list',
|
||||||
|
async () => {
|
||||||
|
const processes = await this.tspmInstance.list();
|
||||||
|
return { processes };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.ipcServer.on<RequestForMethod<'describe'>>('describe', async (request) => {
|
||||||
|
const processInfo = await this.tspmInstance.describe(request.id);
|
||||||
|
const config = this.tspmInstance.processConfigs.get(request.id);
|
||||||
|
|
||||||
|
if (!processInfo || !config) {
|
||||||
|
throw new Error(`Process ${request.id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
processInfo,
|
||||||
|
config,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ipcServer.on<RequestForMethod<'getLogs'>>('getLogs', async (request) => {
|
||||||
|
const logs = await this.tspmInstance.getLogs(request.id);
|
||||||
|
return { logs };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Batch operations handlers
|
||||||
|
this.ipcServer.on<RequestForMethod<'startAll'>>('startAll', async () => {
|
||||||
|
const started: string[] = [];
|
||||||
|
const failed: Array<{ id: string; error: string }> = [];
|
||||||
|
|
||||||
|
await this.tspmInstance.startAll();
|
||||||
|
|
||||||
|
// Get status of all processes
|
||||||
|
for (const [id, processInfo] of this.tspmInstance.processInfo) {
|
||||||
|
if (processInfo.status === 'online') {
|
||||||
|
started.push(id);
|
||||||
|
} else {
|
||||||
|
failed.push({ id, error: 'Failed to start' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { started, failed };
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ipcServer.on<RequestForMethod<'stopAll'>>('stopAll', async () => {
|
||||||
|
const stopped: string[] = [];
|
||||||
|
const failed: Array<{ id: string; error: string }> = [];
|
||||||
|
|
||||||
|
await this.tspmInstance.stopAll();
|
||||||
|
|
||||||
|
// Get status of all processes
|
||||||
|
for (const [id, processInfo] of this.tspmInstance.processInfo) {
|
||||||
|
if (processInfo.status === 'stopped') {
|
||||||
|
stopped.push(id);
|
||||||
|
} else {
|
||||||
|
failed.push({ id, error: 'Failed to stop' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stopped, failed };
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ipcServer.on<RequestForMethod<'restartAll'>>('restartAll', async () => {
|
||||||
|
const restarted: string[] = [];
|
||||||
|
const failed: Array<{ id: string; error: string }> = [];
|
||||||
|
|
||||||
|
await this.tspmInstance.restartAll();
|
||||||
|
|
||||||
|
// Get status of all processes
|
||||||
|
for (const [id, processInfo] of this.tspmInstance.processInfo) {
|
||||||
|
if (processInfo.status === 'online') {
|
||||||
|
restarted.push(id);
|
||||||
|
} else {
|
||||||
|
failed.push({ id, error: 'Failed to restart' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { restarted, failed };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Daemon management handlers
|
||||||
|
this.ipcServer.on<RequestForMethod<'daemon:status'>>('daemon:status', async () => {
|
||||||
|
const memUsage = process.memoryUsage();
|
||||||
|
return {
|
||||||
|
status: 'running',
|
||||||
|
pid: process.pid,
|
||||||
|
uptime: Date.now() - this.startTime,
|
||||||
|
processCount: this.tspmInstance.processes.size,
|
||||||
|
memoryUsage: memUsage.heapUsed,
|
||||||
|
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ipcServer.on<RequestForMethod<'daemon:shutdown'>>('daemon:shutdown', async (request) => {
|
||||||
|
if (this.isShuttingDown) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Daemon is already shutting down',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule shutdown
|
||||||
|
const graceful = request.graceful !== false;
|
||||||
|
const timeout = request.timeout || 10000;
|
||||||
|
|
||||||
|
if (graceful) {
|
||||||
|
setTimeout(() => this.shutdown(true), 100);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => this.shutdown(false), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Daemon will shutdown ${graceful ? 'gracefully' : 'immediately'} in ${timeout}ms`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Heartbeat handler
|
||||||
|
this.ipcServer.on<RequestForMethod<'heartbeat'>>('heartbeat', async () => {
|
||||||
|
return {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
status: this.isShuttingDown ? 'degraded' : 'healthy',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start heartbeat monitoring
|
||||||
|
*/
|
||||||
|
private startHeartbeatMonitoring(): void {
|
||||||
|
// Send heartbeat every 30 seconds
|
||||||
|
this.heartbeatInterval = setInterval(() => {
|
||||||
|
// This is where we could implement health checks
|
||||||
|
// For now, just log that the daemon is alive
|
||||||
|
const uptime = Math.floor((Date.now() - this.startTime) / 1000);
|
||||||
|
console.log(
|
||||||
|
`[Heartbeat] Daemon alive - Uptime: ${uptime}s, Processes: ${this.tspmInstance.processes.size}`,
|
||||||
|
);
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up graceful shutdown handlers
|
||||||
|
*/
|
||||||
|
private setupShutdownHandlers(): void {
|
||||||
|
const shutdownHandler = async (signal: string) => {
|
||||||
|
console.log(`\nReceived ${signal}, initiating graceful shutdown...`);
|
||||||
|
await this.shutdown(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => shutdownHandler('SIGTERM'));
|
||||||
|
process.on('SIGINT', () => shutdownHandler('SIGINT'));
|
||||||
|
process.on('SIGHUP', () => shutdownHandler('SIGHUP'));
|
||||||
|
|
||||||
|
// Handle uncaught errors
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.error('Uncaught exception:', error);
|
||||||
|
this.shutdown(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('Unhandled rejection at:', promise, 'reason:', reason);
|
||||||
|
// Don't exit on unhandled rejection, just log it
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown the daemon
|
||||||
|
*/
|
||||||
|
public async shutdown(graceful: boolean = true): Promise<void> {
|
||||||
|
if (this.isShuttingDown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isShuttingDown = true;
|
||||||
|
console.log('Shutting down TSPM daemon...');
|
||||||
|
|
||||||
|
// Clear heartbeat interval
|
||||||
|
if (this.heartbeatInterval) {
|
||||||
|
clearInterval(this.heartbeatInterval);
|
||||||
|
this.heartbeatInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graceful) {
|
||||||
|
// Stop all processes gracefully
|
||||||
|
try {
|
||||||
|
console.log('Stopping all managed processes...');
|
||||||
|
await this.tspmInstance.stopAll();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping processes:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop IPC server
|
||||||
|
if (this.ipcServer) {
|
||||||
|
try {
|
||||||
|
await this.ipcServer.stop();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping IPC server:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove PID file
|
||||||
|
await this.removePidFile();
|
||||||
|
|
||||||
|
// Remove socket file if it exists
|
||||||
|
try {
|
||||||
|
const fs = await import('fs');
|
||||||
|
await fs.promises.unlink(this.socketPath).catch(() => {});
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('TSPM daemon shutdown complete');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if another daemon instance is running
|
||||||
|
*/
|
||||||
|
private async isDaemonRunning(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const fs = await import('fs');
|
||||||
|
const pidContent = await fs.promises.readFile(
|
||||||
|
this.daemonPidFile,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
const pid = parseInt(pidContent.trim(), 10);
|
||||||
|
|
||||||
|
// Check if process is running
|
||||||
|
try {
|
||||||
|
process.kill(pid, 0);
|
||||||
|
return true; // Process exists
|
||||||
|
} catch {
|
||||||
|
// Process doesn't exist, clean up stale PID file
|
||||||
|
await this.removePidFile();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// PID file doesn't exist
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the daemon PID to a file
|
||||||
|
*/
|
||||||
|
private async writePidFile(): Promise<void> {
|
||||||
|
const fs = await import('fs');
|
||||||
|
await fs.promises.writeFile(this.daemonPidFile, process.pid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the daemon PID file
|
||||||
|
*/
|
||||||
|
private async removePidFile(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const fs = await import('fs');
|
||||||
|
await fs.promises.unlink(this.daemonPidFile);
|
||||||
|
} catch {
|
||||||
|
// Ignore if file doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for the daemon
|
||||||
|
*/
|
||||||
|
export const startDaemon = async (): Promise<void> => {
|
||||||
|
const daemon = new TspmDaemon();
|
||||||
|
await daemon.start();
|
||||||
|
|
||||||
|
// Keep the process alive
|
||||||
|
await new Promise(() => {});
|
||||||
|
};
|
263
ts/classes.ipcclient.ts
Normal file
263
ts/classes.ipcclient.ts
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import * as paths from './paths.js';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import type {
|
||||||
|
IpcMethodMap,
|
||||||
|
RequestForMethod,
|
||||||
|
ResponseForMethod,
|
||||||
|
} from './ipc.types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IPC client for communicating with the TSPM daemon
|
||||||
|
*/
|
||||||
|
export class TspmIpcClient {
|
||||||
|
private ipcClient: plugins.smartipc.IpcClient | null = null;
|
||||||
|
private socketPath: string;
|
||||||
|
private daemonPidFile: string;
|
||||||
|
private isConnected: boolean = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
|
||||||
|
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to the daemon, starting it if necessary
|
||||||
|
*/
|
||||||
|
public async connect(): Promise<void> {
|
||||||
|
// Check if already connected
|
||||||
|
if (this.isConnected && this.ipcClient) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if daemon is running
|
||||||
|
const daemonRunning = await this.isDaemonRunning();
|
||||||
|
|
||||||
|
if (!daemonRunning) {
|
||||||
|
console.log('Daemon not running, starting it...');
|
||||||
|
await this.startDaemon();
|
||||||
|
// Wait a bit for daemon to initialize
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create IPC client
|
||||||
|
this.ipcClient = new plugins.smartipc.IpcClient({
|
||||||
|
id: 'tspm-cli',
|
||||||
|
socketPath: this.socketPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect to the daemon
|
||||||
|
try {
|
||||||
|
await this.ipcClient.connect();
|
||||||
|
this.isConnected = true;
|
||||||
|
console.log('Connected to TSPM daemon');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect to daemon:', error);
|
||||||
|
throw new Error(
|
||||||
|
'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the daemon
|
||||||
|
*/
|
||||||
|
public async disconnect(): Promise<void> {
|
||||||
|
if (this.ipcClient) {
|
||||||
|
await this.ipcClient.disconnect();
|
||||||
|
this.ipcClient = null;
|
||||||
|
this.isConnected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a request to the daemon
|
||||||
|
*/
|
||||||
|
public async request<M extends keyof IpcMethodMap>(
|
||||||
|
method: M,
|
||||||
|
params: RequestForMethod<M>,
|
||||||
|
): Promise<ResponseForMethod<M>> {
|
||||||
|
if (!this.isConnected || !this.ipcClient) {
|
||||||
|
await this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.ipcClient!.request<
|
||||||
|
RequestForMethod<M>,
|
||||||
|
ResponseForMethod<M>
|
||||||
|
>(method, params);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
// Handle connection errors by trying to reconnect once
|
||||||
|
if (
|
||||||
|
error.message?.includes('ECONNREFUSED') ||
|
||||||
|
error.message?.includes('ENOENT')
|
||||||
|
) {
|
||||||
|
console.log('Connection lost, attempting to reconnect...');
|
||||||
|
this.isConnected = false;
|
||||||
|
await this.connect();
|
||||||
|
|
||||||
|
// Retry the request
|
||||||
|
return await this.ipcClient!.request<
|
||||||
|
RequestForMethod<M>,
|
||||||
|
ResponseForMethod<M>
|
||||||
|
>(method, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the daemon is running
|
||||||
|
*/
|
||||||
|
private async isDaemonRunning(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const fs = await import('fs');
|
||||||
|
|
||||||
|
// Check if PID file exists
|
||||||
|
try {
|
||||||
|
const pidContent = await fs.promises.readFile(
|
||||||
|
this.daemonPidFile,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
const pid = parseInt(pidContent.trim(), 10);
|
||||||
|
|
||||||
|
// Check if process is running
|
||||||
|
try {
|
||||||
|
process.kill(pid, 0);
|
||||||
|
|
||||||
|
// Also check if socket exists and is accessible
|
||||||
|
try {
|
||||||
|
await fs.promises.access(this.socketPath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
// Socket doesn't exist, daemon might be starting
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Process doesn't exist, clean up stale PID file
|
||||||
|
await fs.promises.unlink(this.daemonPidFile).catch(() => {});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// PID file doesn't exist
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the daemon process
|
||||||
|
*/
|
||||||
|
private async startDaemon(): Promise<void> {
|
||||||
|
const daemonScript = plugins.path.join(
|
||||||
|
paths.packageDir,
|
||||||
|
'dist_ts',
|
||||||
|
'daemon.js',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Spawn the daemon as a detached process
|
||||||
|
const daemonProcess = spawn(process.execPath, [daemonScript], {
|
||||||
|
detached: true,
|
||||||
|
stdio: ['ignore', 'ignore', 'ignore'],
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
TSPM_DAEMON_MODE: 'true',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unref the process so the parent can exit
|
||||||
|
daemonProcess.unref();
|
||||||
|
|
||||||
|
console.log(`Started daemon process with PID: ${daemonProcess.pid}`);
|
||||||
|
|
||||||
|
// Wait for daemon to be ready (check for socket file)
|
||||||
|
const maxWaitTime = 10000; // 10 seconds
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < maxWaitTime) {
|
||||||
|
if (await this.isDaemonRunning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Daemon failed to start within timeout period');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the daemon
|
||||||
|
*/
|
||||||
|
public async stopDaemon(graceful: boolean = true): Promise<void> {
|
||||||
|
if (!(await this.isDaemonRunning())) {
|
||||||
|
console.log('Daemon is not running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.connect();
|
||||||
|
await this.request('daemon:shutdown', {
|
||||||
|
graceful,
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Daemon shutdown initiated');
|
||||||
|
|
||||||
|
// Wait for daemon to actually stop
|
||||||
|
const maxWaitTime = 15000; // 15 seconds
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < maxWaitTime) {
|
||||||
|
if (!(await this.isDaemonRunning())) {
|
||||||
|
console.log('Daemon stopped successfully');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
'Daemon did not stop within timeout, it may still be running',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping daemon:', error);
|
||||||
|
|
||||||
|
// Try to kill the process directly if graceful shutdown failed
|
||||||
|
try {
|
||||||
|
const fs = await import('fs');
|
||||||
|
const pidContent = await fs.promises.readFile(
|
||||||
|
this.daemonPidFile,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
const pid = parseInt(pidContent.trim(), 10);
|
||||||
|
process.kill(pid, 'SIGKILL');
|
||||||
|
console.log('Force killed daemon process');
|
||||||
|
} catch {
|
||||||
|
console.error('Could not force kill daemon');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get daemon status
|
||||||
|
*/
|
||||||
|
public async getDaemonStatus(): Promise<ResponseForMethod<'daemon:status'> | null> {
|
||||||
|
try {
|
||||||
|
if (!(await this.isDaemonRunning())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.connect();
|
||||||
|
return await this.request('daemon:status', {});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting daemon status:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
export const tspmIpcClient = new TspmIpcClient();
|
@@ -3,14 +3,14 @@ import { ProcessWrapper, type IProcessLog } from './classes.processwrapper.js';
|
|||||||
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
|
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
|
||||||
|
|
||||||
export interface IMonitorConfig {
|
export interface IMonitorConfig {
|
||||||
name?: string; // Optional name to identify the instance
|
name?: string; // Optional name to identify the instance
|
||||||
projectDir: string; // Directory where the command will run
|
projectDir: string; // Directory where the command will run
|
||||||
command: string; // Full command to run (e.g., "npm run xyz")
|
command: string; // Full command to run (e.g., "npm run xyz")
|
||||||
args?: string[]; // Optional: arguments for the command
|
args?: string[]; // Optional: arguments for the command
|
||||||
memoryLimitBytes: number; // Maximum allowed memory (in bytes) for the process group
|
memoryLimitBytes: number; // Maximum allowed memory (in bytes) for the process group
|
||||||
monitorIntervalMs?: number; // Interval (in ms) at which memory is checked (default: 5000)
|
monitorIntervalMs?: number; // Interval (in ms) at which memory is checked (default: 5000)
|
||||||
env?: NodeJS.ProcessEnv; // Optional: custom environment variables
|
env?: NodeJS.ProcessEnv; // Optional: custom environment variables
|
||||||
logBufferSize?: number; // Optional: number of log lines to keep (default: 100)
|
logBufferSize?: number; // Optional: number of log lines to keep (default: 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProcessMonitor {
|
export class ProcessMonitor {
|
||||||
@@ -36,7 +36,10 @@ export class ProcessMonitor {
|
|||||||
const interval = this.config.monitorIntervalMs || 5000;
|
const interval = this.config.monitorIntervalMs || 5000;
|
||||||
this.intervalId = setInterval((): void => {
|
this.intervalId = setInterval((): void => {
|
||||||
if (this.processWrapper && this.processWrapper.getPid()) {
|
if (this.processWrapper && this.processWrapper.getPid()) {
|
||||||
this.monitorProcessGroup(this.processWrapper.getPid()!, this.config.memoryLimitBytes);
|
this.monitorProcessGroup(
|
||||||
|
this.processWrapper.getPid()!,
|
||||||
|
this.config.memoryLimitBytes,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, interval);
|
}, interval);
|
||||||
}
|
}
|
||||||
@@ -69,25 +72,31 @@ export class ProcessMonitor {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.processWrapper.on('exit', (code: number | null, signal: string | null): void => {
|
this.processWrapper.on(
|
||||||
const exitMsg = `Process exited with code ${code}, signal ${signal}.`;
|
'exit',
|
||||||
this.logger.info(exitMsg);
|
(code: number | null, signal: string | null): void => {
|
||||||
this.log(exitMsg);
|
const exitMsg = `Process exited with code ${code}, signal ${signal}.`;
|
||||||
|
this.logger.info(exitMsg);
|
||||||
|
this.log(exitMsg);
|
||||||
|
|
||||||
if (!this.stopped) {
|
if (!this.stopped) {
|
||||||
this.logger.info('Restarting process...');
|
this.logger.info('Restarting process...');
|
||||||
this.log('Restarting process...');
|
this.log('Restarting process...');
|
||||||
this.restartCount++;
|
this.restartCount++;
|
||||||
this.spawnProcess();
|
this.spawnProcess();
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug('Not restarting process because monitor is stopped');
|
this.logger.debug(
|
||||||
}
|
'Not restarting process because monitor is stopped',
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.processWrapper.on('error', (error: Error | ProcessError): void => {
|
this.processWrapper.on('error', (error: Error | ProcessError): void => {
|
||||||
const errorMsg = error instanceof ProcessError
|
const errorMsg =
|
||||||
? `Process error: ${error.toString()}`
|
error instanceof ProcessError
|
||||||
: `Process error: ${error.message}`;
|
? `Process error: ${error.toString()}`
|
||||||
|
: `Process error: ${error.message}`;
|
||||||
|
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
this.log(errorMsg);
|
this.log(errorMsg);
|
||||||
@@ -108,7 +117,9 @@ export class ProcessMonitor {
|
|||||||
} catch (error: Error | unknown) {
|
} catch (error: Error | unknown) {
|
||||||
// The process wrapper will handle logging the error
|
// The process wrapper will handle logging the error
|
||||||
// Just prevent it from bubbling up further
|
// Just prevent it from bubbling up further
|
||||||
this.logger.error(`Failed to start process: ${error instanceof Error ? error.message : String(error)}`);
|
this.logger.error(
|
||||||
|
`Failed to start process: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,24 +127,27 @@ export class ProcessMonitor {
|
|||||||
* 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.
|
||||||
*/
|
*/
|
||||||
private async monitorProcessGroup(pid: number, memoryLimit: number): Promise<void> {
|
private async monitorProcessGroup(
|
||||||
|
pid: number,
|
||||||
|
memoryLimit: number,
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const memoryUsage = await this.getProcessGroupMemory(pid);
|
const memoryUsage = await this.getProcessGroupMemory(pid);
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Memory usage for PID ${pid}: ${this.humanReadableBytes(memoryUsage)} (${memoryUsage} bytes)`
|
`Memory usage for PID ${pid}: ${this.humanReadableBytes(memoryUsage)} (${memoryUsage} bytes)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only log to the process log at longer intervals to avoid spamming
|
// Only log to the process log at longer intervals to avoid spamming
|
||||||
this.log(
|
this.log(
|
||||||
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
|
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
|
||||||
memoryUsage
|
memoryUsage,
|
||||||
)} (${memoryUsage} bytes)`
|
)} (${memoryUsage} bytes)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (memoryUsage > memoryLimit) {
|
if (memoryUsage > memoryLimit) {
|
||||||
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
|
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
|
||||||
memoryUsage
|
memoryUsage,
|
||||||
)} exceeds limit of ${this.humanReadableBytes(memoryLimit)}. Restarting process.`;
|
)} exceeds limit of ${this.humanReadableBytes(memoryLimit)}. Restarting process.`;
|
||||||
|
|
||||||
this.logger.warn(memoryLimitMsg);
|
this.logger.warn(memoryLimitMsg);
|
||||||
@@ -148,7 +162,7 @@ export class ProcessMonitor {
|
|||||||
const processError = new ProcessError(
|
const processError = new ProcessError(
|
||||||
error instanceof Error ? error.message : String(error),
|
error instanceof Error ? error.message : String(error),
|
||||||
'ERR_MEMORY_MONITORING_FAILED',
|
'ERR_MEMORY_MONITORING_FAILED',
|
||||||
{ pid }
|
{ pid },
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.error(processError);
|
this.logger.error(processError);
|
||||||
@@ -161,43 +175,58 @@ export class ProcessMonitor {
|
|||||||
*/
|
*/
|
||||||
private getProcessGroupMemory(pid: number): Promise<number> {
|
private getProcessGroupMemory(pid: number): Promise<number> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.logger.debug(`Getting memory usage for process group with PID ${pid}`);
|
this.logger.debug(
|
||||||
|
`Getting memory usage for process group with PID ${pid}`,
|
||||||
|
);
|
||||||
|
|
||||||
plugins.psTree(pid, (err: Error | null, children: Array<{ PID: string }>) => {
|
plugins.psTree(
|
||||||
if (err) {
|
pid,
|
||||||
const processError = new ProcessError(
|
(err: Error | null, children: Array<{ PID: string }>) => {
|
||||||
`Failed to get process tree: ${err.message}`,
|
|
||||||
'ERR_PSTREE_FAILED',
|
|
||||||
{ pid }
|
|
||||||
);
|
|
||||||
this.logger.debug(`psTree error: ${err.message}`);
|
|
||||||
return reject(processError);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include the main process and its children.
|
|
||||||
const pids: number[] = [pid, ...children.map(child => Number(child.PID))];
|
|
||||||
this.logger.debug(`Found ${pids.length} processes in group with parent PID ${pid}`);
|
|
||||||
|
|
||||||
plugins.pidusage(pids, (err: Error | null, stats: Record<string, { memory: number }>) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
const processError = new ProcessError(
|
const processError = new ProcessError(
|
||||||
`Failed to get process usage stats: ${err.message}`,
|
`Failed to get process tree: ${err.message}`,
|
||||||
'ERR_PIDUSAGE_FAILED',
|
'ERR_PSTREE_FAILED',
|
||||||
{ pids }
|
{ pid },
|
||||||
);
|
);
|
||||||
this.logger.debug(`pidusage error: ${err.message}`);
|
this.logger.debug(`psTree error: ${err.message}`);
|
||||||
return reject(processError);
|
return reject(processError);
|
||||||
}
|
}
|
||||||
|
|
||||||
let totalMemory = 0;
|
// Include the main process and its children.
|
||||||
for (const key in stats) {
|
const pids: number[] = [
|
||||||
totalMemory += stats[key].memory;
|
pid,
|
||||||
}
|
...children.map((child) => Number(child.PID)),
|
||||||
|
];
|
||||||
|
this.logger.debug(
|
||||||
|
`Found ${pids.length} processes in group with parent PID ${pid}`,
|
||||||
|
);
|
||||||
|
|
||||||
this.logger.debug(`Total memory for process group: ${this.humanReadableBytes(totalMemory)}`);
|
plugins.pidusage(
|
||||||
resolve(totalMemory);
|
pids,
|
||||||
});
|
(err: Error | null, stats: Record<string, { memory: number }>) => {
|
||||||
});
|
if (err) {
|
||||||
|
const processError = new ProcessError(
|
||||||
|
`Failed to get process usage stats: ${err.message}`,
|
||||||
|
'ERR_PIDUSAGE_FAILED',
|
||||||
|
{ pids },
|
||||||
|
);
|
||||||
|
this.logger.debug(`pidusage error: ${err.message}`);
|
||||||
|
return reject(processError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalMemory = 0;
|
||||||
|
for (const key in stats) {
|
||||||
|
totalMemory += stats[key].memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Total memory for process group: ${this.humanReadableBytes(totalMemory)}`,
|
||||||
|
);
|
||||||
|
resolve(totalMemory);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -42,11 +42,15 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
this.logger.debug(`Starting process: ${this.options.command}`);
|
this.logger.debug(`Starting process: ${this.options.command}`);
|
||||||
|
|
||||||
if (this.options.args && this.options.args.length > 0) {
|
if (this.options.args && this.options.args.length > 0) {
|
||||||
this.process = plugins.childProcess.spawn(this.options.command, this.options.args, {
|
this.process = plugins.childProcess.spawn(
|
||||||
cwd: this.options.cwd,
|
this.options.command,
|
||||||
env: this.options.env || process.env,
|
this.options.args,
|
||||||
stdio: ['ignore', 'pipe', 'pipe'], // We need to pipe stdout and stderr
|
{
|
||||||
});
|
cwd: this.options.cwd,
|
||||||
|
env: this.options.env || process.env,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'], // We need to pipe stdout and stderr
|
||||||
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Use shell mode to allow a full command string
|
// Use shell mode to allow a full command string
|
||||||
this.process = plugins.childProcess.spawn(this.options.command, {
|
this.process = plugins.childProcess.spawn(this.options.command, {
|
||||||
@@ -72,7 +76,7 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
const processError = new ProcessError(
|
const processError = new ProcessError(
|
||||||
error.message,
|
error.message,
|
||||||
'ERR_PROCESS_EXECUTION',
|
'ERR_PROCESS_EXECUTION',
|
||||||
{ command: this.options.command, pid: this.process?.pid }
|
{ command: this.options.command, pid: this.process?.pid },
|
||||||
);
|
);
|
||||||
this.logger.error(processError);
|
this.logger.error(processError);
|
||||||
this.addSystemLog(`Process error: ${processError.toString()}`);
|
this.addSystemLog(`Process error: ${processError.toString()}`);
|
||||||
@@ -106,15 +110,15 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
this.addSystemLog(`Process started with PID ${this.process.pid}`);
|
this.addSystemLog(`Process started with PID ${this.process.pid}`);
|
||||||
this.logger.info(`Process started with PID ${this.process.pid}`);
|
this.logger.info(`Process started with PID ${this.process.pid}`);
|
||||||
this.emit('start', this.process.pid);
|
this.emit('start', this.process.pid);
|
||||||
|
|
||||||
} catch (error: Error | unknown) {
|
} catch (error: Error | unknown) {
|
||||||
const processError = error instanceof ProcessError
|
const processError =
|
||||||
? error
|
error instanceof ProcessError
|
||||||
: new ProcessError(
|
? error
|
||||||
error instanceof Error ? error.message : String(error),
|
: new ProcessError(
|
||||||
'ERR_PROCESS_START_FAILED',
|
error instanceof Error ? error.message : String(error),
|
||||||
{ command: this.options.command }
|
'ERR_PROCESS_START_FAILED',
|
||||||
);
|
{ command: this.options.command },
|
||||||
|
);
|
||||||
|
|
||||||
this.logger.error(processError);
|
this.logger.error(processError);
|
||||||
this.addSystemLog(`Failed to start process: ${processError.toString()}`);
|
this.addSystemLog(`Failed to start process: ${processError.toString()}`);
|
||||||
@@ -145,15 +149,21 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
// Give it 5 seconds to shut down gracefully
|
// Give it 5 seconds to shut down gracefully
|
||||||
setTimeout((): void => {
|
setTimeout((): void => {
|
||||||
if (this.process && this.process.pid) {
|
if (this.process && this.process.pid) {
|
||||||
this.logger.warn(`Process ${this.process.pid} did not exit gracefully, force killing...`);
|
this.logger.warn(
|
||||||
this.addSystemLog('Process did not exit gracefully, force killing...');
|
`Process ${this.process.pid} did not exit gracefully, force killing...`,
|
||||||
|
);
|
||||||
|
this.addSystemLog(
|
||||||
|
'Process did not exit gracefully, force killing...',
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
process.kill(this.process.pid, 'SIGKILL');
|
process.kill(this.process.pid, 'SIGKILL');
|
||||||
} catch (error: Error | unknown) {
|
} catch (error: Error | unknown) {
|
||||||
// Process might have exited between checks
|
// Process might have exited between checks
|
||||||
this.logger.debug(`Failed to send SIGKILL, process probably already exited: ${
|
this.logger.debug(
|
||||||
error instanceof Error ? error.message : String(error)
|
`Failed to send SIGKILL, process probably already exited: ${
|
||||||
}`);
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
@@ -161,7 +171,7 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
const processError = new ProcessError(
|
const processError = new ProcessError(
|
||||||
error instanceof Error ? error.message : String(error),
|
error instanceof Error ? error.message : String(error),
|
||||||
'ERR_PROCESS_STOP_FAILED',
|
'ERR_PROCESS_STOP_FAILED',
|
||||||
{ pid: this.process.pid }
|
{ pid: this.process.pid },
|
||||||
);
|
);
|
||||||
this.logger.error(processError);
|
this.logger.error(processError);
|
||||||
this.addSystemLog(`Error stopping process: ${processError.toString()}`);
|
this.addSystemLog(`Error stopping process: ${processError.toString()}`);
|
||||||
|
@@ -1,19 +1,22 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as paths from './paths.js';
|
import * as paths from './paths.js';
|
||||||
import { ProcessMonitor, type IMonitorConfig } from './classes.processmonitor.js';
|
import {
|
||||||
|
ProcessMonitor,
|
||||||
|
type IMonitorConfig,
|
||||||
|
} from './classes.processmonitor.js';
|
||||||
import { TspmConfig } from './classes.config.js';
|
import { TspmConfig } from './classes.config.js';
|
||||||
import {
|
import {
|
||||||
Logger,
|
Logger,
|
||||||
ProcessError,
|
ProcessError,
|
||||||
ConfigError,
|
ConfigError,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
handleError
|
handleError,
|
||||||
} from './utils.errorhandler.js';
|
} from './utils.errorhandler.js';
|
||||||
|
|
||||||
export interface IProcessConfig extends IMonitorConfig {
|
export interface IProcessConfig extends IMonitorConfig {
|
||||||
id: string; // Unique identifier for the process
|
id: string; // Unique identifier for the process
|
||||||
autorestart: boolean; // Whether to restart the process automatically on crash
|
autorestart: boolean; // Whether to restart the process automatically on crash
|
||||||
watch?: boolean; // Whether to watch for file changes and restart
|
watch?: boolean; // Whether to watch for file changes and restart
|
||||||
watchPaths?: string[]; // Paths to watch for changes
|
watchPaths?: string[]; // Paths to watch for changes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,9 +37,9 @@ export interface IProcessLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Tspm {
|
export class Tspm {
|
||||||
private processes: Map<string, ProcessMonitor> = new Map();
|
public processes: Map<string, ProcessMonitor> = new Map();
|
||||||
private processConfigs: Map<string, IProcessConfig> = new Map();
|
public processConfigs: Map<string, IProcessConfig> = new Map();
|
||||||
private 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 logger: Logger;
|
private logger: Logger;
|
||||||
@@ -58,7 +61,7 @@ export class Tspm {
|
|||||||
throw new ValidationError(
|
throw new ValidationError(
|
||||||
'Invalid process configuration: missing required fields',
|
'Invalid process configuration: missing required fields',
|
||||||
'ERR_INVALID_CONFIG',
|
'ERR_INVALID_CONFIG',
|
||||||
{ config }
|
{ config },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +69,7 @@ export class Tspm {
|
|||||||
if (this.processes.has(config.id)) {
|
if (this.processes.has(config.id)) {
|
||||||
throw new ValidationError(
|
throw new ValidationError(
|
||||||
`Process with id '${config.id}' already exists`,
|
`Process with id '${config.id}' already exists`,
|
||||||
'ERR_DUPLICATE_PROCESS'
|
'ERR_DUPLICATE_PROCESS',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +82,7 @@ export class Tspm {
|
|||||||
id: config.id,
|
id: config.id,
|
||||||
status: 'stopped',
|
status: 'stopped',
|
||||||
memory: 0,
|
memory: 0,
|
||||||
restarts: 0
|
restarts: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create and start process monitor
|
// Create and start process monitor
|
||||||
@@ -91,7 +94,7 @@ export class Tspm {
|
|||||||
memoryLimitBytes: config.memoryLimitBytes,
|
memoryLimitBytes: config.memoryLimitBytes,
|
||||||
monitorIntervalMs: config.monitorIntervalMs,
|
monitorIntervalMs: config.monitorIntervalMs,
|
||||||
env: config.env,
|
env: config.env,
|
||||||
logBufferSize: config.logBufferSize
|
logBufferSize: config.logBufferSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.processes.set(config.id, monitor);
|
this.processes.set(config.id, monitor);
|
||||||
@@ -115,13 +118,13 @@ export class Tspm {
|
|||||||
throw new ProcessError(
|
throw new ProcessError(
|
||||||
`Failed to start process: ${error.message}`,
|
`Failed to start process: ${error.message}`,
|
||||||
'ERR_PROCESS_START_FAILED',
|
'ERR_PROCESS_START_FAILED',
|
||||||
{ id: config.id, command: config.command }
|
{ id: config.id, command: config.command },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const genericError = new ProcessError(
|
const genericError = new ProcessError(
|
||||||
`Failed to start process: ${String(error)}`,
|
`Failed to start process: ${String(error)}`,
|
||||||
'ERR_PROCESS_START_FAILED',
|
'ERR_PROCESS_START_FAILED',
|
||||||
{ id: config.id }
|
{ id: config.id },
|
||||||
);
|
);
|
||||||
this.logger.error(genericError);
|
this.logger.error(genericError);
|
||||||
throw genericError;
|
throw genericError;
|
||||||
@@ -139,7 +142,7 @@ export class Tspm {
|
|||||||
if (!monitor) {
|
if (!monitor) {
|
||||||
const error = new ValidationError(
|
const error = new ValidationError(
|
||||||
`Process with id '${id}' not found`,
|
`Process with id '${id}' not found`,
|
||||||
'ERR_PROCESS_NOT_FOUND'
|
'ERR_PROCESS_NOT_FOUND',
|
||||||
);
|
);
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -153,7 +156,7 @@ export class Tspm {
|
|||||||
const processError = new ProcessError(
|
const processError = new ProcessError(
|
||||||
`Failed to stop process: ${error instanceof Error ? error.message : String(error)}`,
|
`Failed to stop process: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
'ERR_PROCESS_STOP_FAILED',
|
'ERR_PROCESS_STOP_FAILED',
|
||||||
{ id }
|
{ id },
|
||||||
);
|
);
|
||||||
this.logger.error(processError);
|
this.logger.error(processError);
|
||||||
throw processError;
|
throw processError;
|
||||||
@@ -175,7 +178,7 @@ export class Tspm {
|
|||||||
if (!monitor || !config) {
|
if (!monitor || !config) {
|
||||||
const error = new ValidationError(
|
const error = new ValidationError(
|
||||||
`Process with id '${id}' not found`,
|
`Process with id '${id}' not found`,
|
||||||
'ERR_PROCESS_NOT_FOUND'
|
'ERR_PROCESS_NOT_FOUND',
|
||||||
);
|
);
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -194,7 +197,7 @@ export class Tspm {
|
|||||||
memoryLimitBytes: config.memoryLimitBytes,
|
memoryLimitBytes: config.memoryLimitBytes,
|
||||||
monitorIntervalMs: config.monitorIntervalMs,
|
monitorIntervalMs: config.monitorIntervalMs,
|
||||||
env: config.env,
|
env: config.env,
|
||||||
logBufferSize: config.logBufferSize
|
logBufferSize: config.logBufferSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.processes.set(id, newMonitor);
|
this.processes.set(id, newMonitor);
|
||||||
@@ -205,7 +208,7 @@ export class Tspm {
|
|||||||
if (info) {
|
if (info) {
|
||||||
this.updateProcessInfo(id, {
|
this.updateProcessInfo(id, {
|
||||||
status: 'online',
|
status: 'online',
|
||||||
restarts: info.restarts + 1
|
restarts: info.restarts + 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +217,7 @@ export class Tspm {
|
|||||||
const processError = new ProcessError(
|
const processError = new ProcessError(
|
||||||
`Failed to restart process: ${error instanceof Error ? error.message : String(error)}`,
|
`Failed to restart process: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
'ERR_PROCESS_RESTART_FAILED',
|
'ERR_PROCESS_RESTART_FAILED',
|
||||||
{ id }
|
{ id },
|
||||||
);
|
);
|
||||||
this.logger.error(processError);
|
this.logger.error(processError);
|
||||||
throw processError;
|
throw processError;
|
||||||
@@ -231,7 +234,7 @@ export class Tspm {
|
|||||||
if (!this.processConfigs.has(id)) {
|
if (!this.processConfigs.has(id)) {
|
||||||
const error = new ValidationError(
|
const error = new ValidationError(
|
||||||
`Process with id '${id}' not found`,
|
`Process with id '${id}' not found`,
|
||||||
'ERR_PROCESS_NOT_FOUND'
|
'ERR_PROCESS_NOT_FOUND',
|
||||||
);
|
);
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -260,12 +263,14 @@ export class Tspm {
|
|||||||
this.processInfo.delete(id);
|
this.processInfo.delete(id);
|
||||||
await this.saveProcessConfigs();
|
await this.saveProcessConfigs();
|
||||||
|
|
||||||
this.logger.info(`Successfully deleted process with id '${id}' after stopping failure`);
|
this.logger.info(
|
||||||
|
`Successfully deleted process with id '${id}' after stopping failure`,
|
||||||
|
);
|
||||||
} catch (deleteError: Error | unknown) {
|
} catch (deleteError: Error | unknown) {
|
||||||
const configError = new ConfigError(
|
const configError = new ConfigError(
|
||||||
`Failed to delete process configuration: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`,
|
`Failed to delete process configuration: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`,
|
||||||
'ERR_CONFIG_DELETE_FAILED',
|
'ERR_CONFIG_DELETE_FAILED',
|
||||||
{ id }
|
{ id },
|
||||||
);
|
);
|
||||||
this.logger.error(configError);
|
this.logger.error(configError);
|
||||||
throw configError;
|
throw configError;
|
||||||
@@ -283,7 +288,9 @@ export class Tspm {
|
|||||||
/**
|
/**
|
||||||
* Get detailed info for a specific process
|
* Get detailed info for a specific process
|
||||||
*/
|
*/
|
||||||
public describe(id: string): { config: IProcessConfig; info: IProcessInfo } | null {
|
public describe(
|
||||||
|
id: string,
|
||||||
|
): { config: IProcessConfig; info: IProcessInfo } | null {
|
||||||
const config = this.processConfigs.get(id);
|
const config = this.processConfigs.get(id);
|
||||||
const info = this.processInfo.get(id);
|
const info = this.processInfo.get(id);
|
||||||
|
|
||||||
@@ -353,12 +360,15 @@ export class Tspm {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const configs = Array.from(this.processConfigs.values());
|
const configs = Array.from(this.processConfigs.values());
|
||||||
await this.config.writeKey(this.configStorageKey, JSON.stringify(configs));
|
await this.config.writeKey(
|
||||||
|
this.configStorageKey,
|
||||||
|
JSON.stringify(configs),
|
||||||
|
);
|
||||||
this.logger.debug(`Saved ${configs.length} process configurations`);
|
this.logger.debug(`Saved ${configs.length} process configurations`);
|
||||||
} catch (error: Error | unknown) {
|
} catch (error: Error | unknown) {
|
||||||
const configError = new ConfigError(
|
const configError = new ConfigError(
|
||||||
`Failed to save process configurations: ${error instanceof Error ? error.message : String(error)}`,
|
`Failed to save process configurations: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
'ERR_CONFIG_SAVE_FAILED'
|
'ERR_CONFIG_SAVE_FAILED',
|
||||||
);
|
);
|
||||||
this.logger.error(configError);
|
this.logger.error(configError);
|
||||||
throw configError;
|
throw configError;
|
||||||
@@ -368,7 +378,7 @@ export class Tspm {
|
|||||||
/**
|
/**
|
||||||
* Load process configurations from config storage
|
* Load process configurations from config storage
|
||||||
*/
|
*/
|
||||||
private async loadProcessConfigs(): Promise<void> {
|
public async loadProcessConfigs(): Promise<void> {
|
||||||
this.logger.debug('Loading process configurations from storage');
|
this.logger.debug('Loading process configurations from storage');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -381,7 +391,9 @@ export class Tspm {
|
|||||||
for (const config of configs) {
|
for (const config of configs) {
|
||||||
// Validate config
|
// Validate config
|
||||||
if (!config.id || !config.command || !config.projectDir) {
|
if (!config.id || !config.command || !config.projectDir) {
|
||||||
this.logger.warn(`Skipping invalid process config for id '${config.id || 'unknown'}'`);
|
this.logger.warn(
|
||||||
|
`Skipping invalid process config for id '${config.id || 'unknown'}'`,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,13 +404,13 @@ export class Tspm {
|
|||||||
id: config.id,
|
id: config.id,
|
||||||
status: 'stopped',
|
status: 'stopped',
|
||||||
memory: 0,
|
memory: 0,
|
||||||
restarts: 0
|
restarts: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (parseError: Error | unknown) {
|
} catch (parseError: Error | unknown) {
|
||||||
const configError = new ConfigError(
|
const configError = new ConfigError(
|
||||||
`Failed to parse process configurations: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
`Failed to parse process configurations: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
||||||
'ERR_CONFIG_PARSE_FAILED'
|
'ERR_CONFIG_PARSE_FAILED',
|
||||||
);
|
);
|
||||||
this.logger.error(configError);
|
this.logger.error(configError);
|
||||||
throw configError;
|
throw configError;
|
||||||
@@ -413,7 +425,9 @@ export class Tspm {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If no configs found or error reading, just continue with empty configs
|
// If no configs found or error reading, just continue with empty configs
|
||||||
this.logger.info('No saved process configurations found or error reading them');
|
this.logger.info(
|
||||||
|
'No saved process configurations found or error reading them',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
811
ts/cli.ts
811
ts/cli.ts
@@ -1,26 +1,66 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as paths from './paths.js';
|
import * as paths from './paths.js';
|
||||||
import { Tspm, type IProcessConfig } from './classes.tspm.js';
|
import { tspmIpcClient } from './classes.ipcclient.js';
|
||||||
import {
|
import { Logger, LogLevel } from './utils.errorhandler.js';
|
||||||
Logger,
|
import type { IProcessConfig } from './classes.tspm.js';
|
||||||
LogLevel,
|
|
||||||
handleError,
|
|
||||||
TspmError,
|
|
||||||
ProcessError,
|
|
||||||
ConfigError,
|
|
||||||
ValidationError
|
|
||||||
} from './utils.errorhandler.js';
|
|
||||||
|
|
||||||
// Define interface for CLI arguments
|
export interface CliArguments {
|
||||||
interface CliArguments {
|
verbose?: boolean;
|
||||||
_: (string | number)[];
|
watch?: boolean;
|
||||||
|
memory?: string;
|
||||||
|
cwd?: string;
|
||||||
|
daemon?: boolean;
|
||||||
|
test?: boolean;
|
||||||
|
name?: string;
|
||||||
|
autorestart?: boolean;
|
||||||
|
watchPaths?: string[];
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to parse memory strings (e.g., "512MB", "2GB")
|
||||||
|
function parseMemoryString(memStr: string): number {
|
||||||
|
const units = {
|
||||||
|
KB: 1024,
|
||||||
|
MB: 1024 * 1024,
|
||||||
|
GB: 1024 * 1024 * 1024,
|
||||||
|
};
|
||||||
|
|
||||||
|
const match = memStr.toUpperCase().match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid memory format: ${memStr}. Use format like "512MB" or "2GB"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = parseFloat(match[1]);
|
||||||
|
const unit = (match[2] || 'MB') as keyof typeof units;
|
||||||
|
|
||||||
|
return Math.floor(value * units[unit]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format memory for display
|
||||||
|
function formatMemory(bytes: number): string {
|
||||||
|
if (bytes >= 1024 * 1024 * 1024) {
|
||||||
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||||
|
} else if (bytes >= 1024 * 1024) {
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
} else if (bytes >= 1024) {
|
||||||
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
} else {
|
||||||
|
return `${bytes} B`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for padding strings
|
||||||
|
function pad(str: string, length: number): string {
|
||||||
|
return str.length > length
|
||||||
|
? str.substring(0, length - 3) + '...'
|
||||||
|
: str.padEnd(length);
|
||||||
|
}
|
||||||
|
|
||||||
export const run = async (): Promise<void> => {
|
export const run = async (): Promise<void> => {
|
||||||
const cliLogger = new Logger('CLI');
|
const cliLogger = new Logger('CLI');
|
||||||
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||||
const tspm = new Tspm();
|
|
||||||
|
|
||||||
// Check if debug mode is enabled
|
// Check if debug mode is enabled
|
||||||
const debugMode = process.env.TSPM_DEBUG === 'true';
|
const debugMode = process.env.TSPM_DEBUG === 'true';
|
||||||
@@ -35,337 +75,564 @@ export const run = async (): Promise<void> => {
|
|||||||
// Default command - show help and list processes
|
// Default command - show help and list processes
|
||||||
smartcliInstance.standardCommand().subscribe({
|
smartcliInstance.standardCommand().subscribe({
|
||||||
next: async (argvArg: CliArguments) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
console.log(`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`);
|
console.log(
|
||||||
|
`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`,
|
||||||
|
);
|
||||||
console.log('Usage: tspm [command] [options]');
|
console.log('Usage: tspm [command] [options]');
|
||||||
console.log('\nCommands:');
|
console.log('\nCommands:');
|
||||||
console.log(' start <script> Start a process');
|
console.log(' start <script> Start a process');
|
||||||
console.log(' startAsDaemon <script> Start a process in daemon mode');
|
|
||||||
console.log(' list List all processes');
|
console.log(' list List all processes');
|
||||||
console.log(' stop <id> Stop a process');
|
console.log(' stop <id> Stop a process');
|
||||||
console.log(' restart <id> Restart a process');
|
console.log(' restart <id> Restart a process');
|
||||||
console.log(' delete <id> Delete a process');
|
console.log(' delete <id> Delete a process');
|
||||||
console.log(' describe <id> Show details for a process');
|
console.log(' describe <id> Show details for a process');
|
||||||
console.log('\nUse tspm [command] --help for more information about a command.');
|
console.log(' logs <id> Show logs for a process');
|
||||||
|
console.log(' start-all Start all saved processes');
|
||||||
|
console.log(' stop-all Stop all processes');
|
||||||
|
console.log(' restart-all Restart all processes');
|
||||||
|
console.log('\nDaemon Commands:');
|
||||||
|
console.log(' daemon start Start the TSPM daemon');
|
||||||
|
console.log(' daemon stop Stop the TSPM daemon');
|
||||||
|
console.log(' daemon status Show daemon status');
|
||||||
|
console.log(
|
||||||
|
'\nUse tspm [command] --help for more information about a command.',
|
||||||
|
);
|
||||||
|
|
||||||
// Show current process list
|
// Show current process list
|
||||||
console.log('\nProcess List:');
|
console.log('\nProcess List:');
|
||||||
const processes = tspm.list();
|
|
||||||
|
|
||||||
if (processes.length === 0) {
|
|
||||||
console.log(' No processes running. Use "tspm start" to start a process.');
|
|
||||||
} else {
|
|
||||||
console.log('┌─────────┬─────────────┬───────────┬───────────┬──────────┐');
|
|
||||||
console.log('│ ID │ Name │ Status │ Memory │ Restarts │');
|
|
||||||
console.log('├─────────┼─────────────┼───────────┼───────────┼──────────┤');
|
|
||||||
|
|
||||||
for (const proc of processes) {
|
|
||||||
console.log(`│ ${pad(proc.id, 8)} │ ${pad(proc.id, 12)} │ ${pad(proc.status, 10)} │ ${pad(formatMemory(proc.memory), 10)} │ ${pad(String(proc.restarts), 9)} │`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('└─────────┴─────────────┴───────────┴───────────┴──────────┘');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start command - start a new process
|
|
||||||
smartcliInstance.addCommand('start').subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
const script = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
|
||||||
if (!script) {
|
|
||||||
console.error('Error: Missing script argument. Usage: tspm start <script>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse additional options
|
|
||||||
const name = argvArg.name || script;
|
|
||||||
const cwd = argvArg.cwd || process.cwd();
|
|
||||||
const memLimit = parseMemoryString(argvArg.memory || '500MB');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cliLogger.debug(`Starting process with script: ${script}`);
|
const response = await tspmIpcClient.request('list', {});
|
||||||
|
const processes = response.processes;
|
||||||
|
|
||||||
const processConfig: IProcessConfig = {
|
if (processes.length === 0) {
|
||||||
id: argvArg.id || name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase(),
|
console.log(
|
||||||
name: name,
|
' No processes running. Use "tspm start" to start a process.',
|
||||||
projectDir: cwd,
|
);
|
||||||
command: script,
|
|
||||||
args: argvArg.args ? String(argvArg.args).split(' ') : undefined,
|
|
||||||
memoryLimitBytes: memLimit,
|
|
||||||
monitorIntervalMs: Number(argvArg.interval) || 5000,
|
|
||||||
autorestart: argvArg.autorestart !== 'false',
|
|
||||||
watch: Boolean(argvArg.watch)
|
|
||||||
};
|
|
||||||
|
|
||||||
cliLogger.debug(`Created process config: ${JSON.stringify(processConfig)}`);
|
|
||||||
|
|
||||||
await tspm.start(processConfig);
|
|
||||||
console.log(`Process ${processConfig.id} started successfully.`);
|
|
||||||
} catch (error: Error | unknown) {
|
|
||||||
const tspmError = handleError(error);
|
|
||||||
|
|
||||||
if (tspmError instanceof ValidationError) {
|
|
||||||
console.error(`Validation error: ${tspmError.message}`);
|
|
||||||
} else if (tspmError instanceof ProcessError) {
|
|
||||||
console.error(`Process error: ${tspmError.message}`);
|
|
||||||
if (debugMode) {
|
|
||||||
console.error(`Error details: ${JSON.stringify(tspmError.details)}`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error starting process: ${tspmError.message}`);
|
console.log(
|
||||||
}
|
'┌─────────┬─────────────┬───────────┬───────────┬──────────┐',
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'│ ID │ Name │ Status │ Memory │ Restarts │',
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'├─────────┼─────────────┼───────────┼───────────┼──────────┤',
|
||||||
|
);
|
||||||
|
|
||||||
cliLogger.error(tspmError);
|
for (const proc of processes) {
|
||||||
|
const statusColor =
|
||||||
|
proc.status === 'online'
|
||||||
|
? '\x1b[32m'
|
||||||
|
: proc.status === 'errored'
|
||||||
|
? '\x1b[31m'
|
||||||
|
: '\x1b[33m';
|
||||||
|
const resetColor = '\x1b[0m';
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`│ ${pad(proc.id, 7)} │ ${pad(proc.id, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad(formatMemory(proc.memory), 9)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'└─────────┴─────────────┴───────────┴───────────┴──────────┘',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'Error: Could not connect to TSPM daemon. Use "tspm daemon start" to start it.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start as daemon command
|
// Start command
|
||||||
smartcliInstance.addCommand('startAsDaemon').subscribe({
|
smartcliInstance.addCommand('start').subscribe({
|
||||||
next: async (argvArg: CliArguments) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const script = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
try {
|
||||||
if (!script) {
|
const script = argvArg._[1];
|
||||||
console.error('Error: Missing script argument. Usage: tspm startAsDaemon <script>');
|
if (!script) {
|
||||||
return;
|
console.error('Error: Please provide a script to run');
|
||||||
}
|
console.log('Usage: tspm start <script> [options]');
|
||||||
|
console.log('\nOptions:');
|
||||||
// For daemon mode, we'll detach from the console
|
console.log(' --name <name> Name for the process');
|
||||||
const daemonProcess = plugins.childProcess.spawn(
|
console.log(
|
||||||
process.execPath,
|
' --memory <size> Memory limit (e.g., "512MB", "2GB")',
|
||||||
[
|
);
|
||||||
...process.execArgv,
|
console.log(' --cwd <path> Working directory');
|
||||||
process.argv[1], // The tspm script path
|
console.log(
|
||||||
'start',
|
' --watch Watch for file changes and restart',
|
||||||
script,
|
);
|
||||||
...process.argv.slice(3) // Pass other arguments
|
console.log(' --watch-paths <paths> Comma-separated paths to watch');
|
||||||
],
|
console.log(' --autorestart Auto-restart on crash');
|
||||||
{
|
return;
|
||||||
detached: true,
|
|
||||||
stdio: 'ignore',
|
|
||||||
cwd: process.cwd()
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
// Unref to allow parent to exit
|
const memoryLimit = argvArg.memory
|
||||||
daemonProcess.unref();
|
? parseMemoryString(argvArg.memory)
|
||||||
|
: 512 * 1024 * 1024; // Default 512MB
|
||||||
|
const projectDir = argvArg.cwd || process.cwd();
|
||||||
|
const name = argvArg.name || script;
|
||||||
|
const watch = argvArg.watch || false;
|
||||||
|
const autorestart = argvArg.autorestart !== false; // Default true
|
||||||
|
const watchPaths = argvArg.watchPaths
|
||||||
|
? typeof argvArg.watchPaths === 'string'
|
||||||
|
? (argvArg.watchPaths as string).split(',')
|
||||||
|
: argvArg.watchPaths
|
||||||
|
: undefined;
|
||||||
|
|
||||||
console.log(`Started process ${script} as daemon.`);
|
const processConfig: IProcessConfig = {
|
||||||
}
|
id: name.replace(/[^a-zA-Z0-9-_]/g, '_'),
|
||||||
|
name,
|
||||||
|
command: script,
|
||||||
|
projectDir,
|
||||||
|
memoryLimitBytes: memoryLimit,
|
||||||
|
autorestart,
|
||||||
|
watch,
|
||||||
|
watchPaths,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`Starting process: ${name}`);
|
||||||
|
console.log(` Command: ${script}`);
|
||||||
|
console.log(` Directory: ${projectDir}`);
|
||||||
|
console.log(` Memory limit: ${formatMemory(memoryLimit)}`);
|
||||||
|
console.log(` Auto-restart: ${autorestart}`);
|
||||||
|
if (watch) {
|
||||||
|
console.log(` Watch mode: enabled`);
|
||||||
|
if (watchPaths) {
|
||||||
|
console.log(` Watch paths: ${watchPaths.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await tspmIpcClient.request('start', {
|
||||||
|
config: processConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✓ Process started successfully`);
|
||||||
|
console.log(` ID: ${response.processId}`);
|
||||||
|
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||||
|
console.log(` Status: ${response.status}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting process:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stop command
|
// Stop command
|
||||||
smartcliInstance.addCommand('stop').subscribe({
|
smartcliInstance.addCommand('stop').subscribe({
|
||||||
next: async (argvArg: CliArguments) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
console.error('Error: Missing process ID. Usage: tspm stop <id>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cliLogger.debug(`Stopping process: ${id}`);
|
const id = argvArg._[1];
|
||||||
await tspm.stop(id);
|
if (!id) {
|
||||||
console.log(`Process ${id} stopped.`);
|
console.error('Error: Please provide a process ID');
|
||||||
} catch (error: Error | unknown) {
|
console.log('Usage: tspm stop <id>');
|
||||||
const tspmError = handleError(error);
|
return;
|
||||||
|
|
||||||
if (tspmError instanceof ValidationError) {
|
|
||||||
console.error(`Validation error: ${tspmError.message}`);
|
|
||||||
} else {
|
|
||||||
console.error(`Error stopping process: ${tspmError.message}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cliLogger.error(tspmError);
|
console.log(`Stopping process: ${id}`);
|
||||||
|
const response = await tspmIpcClient.request('stop', { id });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
console.log(`✓ ${response.message}`);
|
||||||
|
} else {
|
||||||
|
console.error(`✗ Failed to stop process: ${response.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping process:', error.message);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restart command
|
// Restart command
|
||||||
smartcliInstance.addCommand('restart').subscribe({
|
smartcliInstance.addCommand('restart').subscribe({
|
||||||
next: async (argvArg: CliArguments) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
console.error('Error: Missing process ID. Usage: tspm restart <id>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cliLogger.debug(`Restarting process: ${id}`);
|
const id = argvArg._[1];
|
||||||
await tspm.restart(id);
|
if (!id) {
|
||||||
console.log(`Process ${id} restarted.`);
|
console.error('Error: Please provide a process ID');
|
||||||
} catch (error: Error | unknown) {
|
console.log('Usage: tspm restart <id>');
|
||||||
const tspmError = handleError(error);
|
return;
|
||||||
|
|
||||||
if (tspmError instanceof ValidationError) {
|
|
||||||
console.error(`Validation error: ${tspmError.message}`);
|
|
||||||
} else if (tspmError instanceof ProcessError) {
|
|
||||||
console.error(`Process error: ${tspmError.message}`);
|
|
||||||
} else {
|
|
||||||
console.error(`Error restarting process: ${tspmError.message}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cliLogger.error(tspmError);
|
console.log(`Restarting process: ${id}`);
|
||||||
|
const response = await tspmIpcClient.request('restart', { id });
|
||||||
|
|
||||||
|
console.log(`✓ Process restarted successfully`);
|
||||||
|
console.log(` ID: ${response.processId}`);
|
||||||
|
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||||
|
console.log(` Status: ${response.status}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error restarting process:', error.message);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete command
|
// Delete command
|
||||||
smartcliInstance.addCommand('delete').subscribe({
|
smartcliInstance.addCommand('delete').subscribe({
|
||||||
next: async (argvArg: CliArguments) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
console.error('Error: Missing process ID. Usage: tspm delete <id>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cliLogger.debug(`Deleting process: ${id}`);
|
const id = argvArg._[1];
|
||||||
await tspm.delete(id);
|
if (!id) {
|
||||||
console.log(`Process ${id} deleted.`);
|
console.error('Error: Please provide a process ID');
|
||||||
} catch (error: Error | unknown) {
|
console.log('Usage: tspm delete <id>');
|
||||||
const tspmError = handleError(error);
|
return;
|
||||||
|
|
||||||
if (tspmError instanceof ValidationError) {
|
|
||||||
console.error(`Validation error: ${tspmError.message}`);
|
|
||||||
} else if (tspmError instanceof ConfigError) {
|
|
||||||
console.error(`Configuration error: ${tspmError.message}`);
|
|
||||||
} else {
|
|
||||||
console.error(`Error deleting process: ${tspmError.message}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cliLogger.error(tspmError);
|
console.log(`Deleting process: ${id}`);
|
||||||
|
const response = await tspmIpcClient.request('delete', { id });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
console.log(`✓ ${response.message}`);
|
||||||
|
} else {
|
||||||
|
console.error(`✗ Failed to delete process: ${response.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting process:', error.message);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// List command
|
// List command
|
||||||
smartcliInstance.addCommand('list').subscribe({
|
smartcliInstance.addCommand('list').subscribe({
|
||||||
next: async (argvArg: CliArguments) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const processes = tspm.list();
|
try {
|
||||||
|
const response = await tspmIpcClient.request('list', {});
|
||||||
|
const processes = response.processes;
|
||||||
|
|
||||||
if (processes.length === 0) {
|
if (processes.length === 0) {
|
||||||
console.log('No processes running.');
|
console.log('No processes running.');
|
||||||
return;
|
} else {
|
||||||
|
console.log('Process List:');
|
||||||
|
console.log(
|
||||||
|
'┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐',
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'│ ID │ Name │ Status │ PID │ Memory │ Restarts │',
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const proc of processes) {
|
||||||
|
const statusColor =
|
||||||
|
proc.status === 'online'
|
||||||
|
? '\x1b[32m'
|
||||||
|
: proc.status === 'errored'
|
||||||
|
? '\x1b[31m'
|
||||||
|
: '\x1b[33m';
|
||||||
|
const resetColor = '\x1b[0m';
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`│ ${pad(proc.id, 7)} │ ${pad(proc.id, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad((proc.pid || '-').toString(), 9)} │ ${pad(formatMemory(proc.memory), 8)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error listing processes:', error.message);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
console.log('┌─────────┬─────────────┬───────────┬───────────┬──────────┐');
|
error: (err) => {
|
||||||
console.log('│ ID │ Name │ Status │ Memory │ Restarts │');
|
cliLogger.error(err);
|
||||||
console.log('├─────────┼─────────────┼───────────┼───────────┼──────────┤');
|
},
|
||||||
|
complete: () => {},
|
||||||
for (const proc of processes) {
|
|
||||||
console.log(`│ ${pad(proc.id, 8)} │ ${pad(proc.id, 12)} │ ${pad(proc.status, 10)} │ ${pad(formatMemory(proc.memory), 10)} │ ${pad(String(proc.restarts), 9)} │`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('└─────────┴─────────────┴───────────┴───────────┴──────────┘');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Describe command
|
// Describe command
|
||||||
smartcliInstance.addCommand('describe').subscribe({
|
smartcliInstance.addCommand('describe').subscribe({
|
||||||
next: async (argvArg: CliArguments) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
try {
|
||||||
|
const id = argvArg._[1];
|
||||||
|
if (!id) {
|
||||||
|
console.error('Error: Please provide a process ID');
|
||||||
|
console.log('Usage: tspm describe <id>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!id) {
|
const response = await tspmIpcClient.request('describe', { id });
|
||||||
console.error('Error: Missing process ID. Usage: tspm describe <id>');
|
|
||||||
return;
|
console.log(`Process Details: ${id}`);
|
||||||
|
console.log('─'.repeat(40));
|
||||||
|
console.log(`Status: ${response.processInfo.status}`);
|
||||||
|
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
|
||||||
|
console.log(
|
||||||
|
`Memory: ${formatMemory(response.processInfo.memory)}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`CPU: ${response.processInfo.cpu ? response.processInfo.cpu.toFixed(1) + '%' : 'N/A'}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Uptime: ${response.processInfo.uptime ? Math.floor(response.processInfo.uptime / 1000) + 's' : 'N/A'}`,
|
||||||
|
);
|
||||||
|
console.log(`Restarts: ${response.processInfo.restarts}`);
|
||||||
|
console.log('\nConfiguration:');
|
||||||
|
console.log(`Command: ${response.config.command}`);
|
||||||
|
console.log(`Directory: ${response.config.projectDir}`);
|
||||||
|
console.log(
|
||||||
|
`Memory Limit: ${formatMemory(response.config.memoryLimitBytes)}`,
|
||||||
|
);
|
||||||
|
console.log(`Auto-restart: ${response.config.autorestart}`);
|
||||||
|
if (response.config.watch) {
|
||||||
|
console.log(`Watch: enabled`);
|
||||||
|
if (response.config.watchPaths) {
|
||||||
|
console.log(
|
||||||
|
`Watch Paths: ${response.config.watchPaths.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error describing process:', error.message);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const details = tspm.describe(id);
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
if (!details) {
|
},
|
||||||
console.error(`Process with ID '${id}' not found.`);
|
complete: () => {},
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Details for process '${id}':`);
|
|
||||||
console.log(` Status: ${details.info.status}`);
|
|
||||||
console.log(` Memory: ${formatMemory(details.info.memory)}`);
|
|
||||||
console.log(` Restarts: ${details.info.restarts}`);
|
|
||||||
console.log(` Command: ${details.config.command}`);
|
|
||||||
console.log(` Directory: ${details.config.projectDir}`);
|
|
||||||
console.log(` Memory limit: ${formatMemory(details.config.memoryLimitBytes)}`);
|
|
||||||
|
|
||||||
if (details.config.args && details.config.args.length > 0) {
|
|
||||||
console.log(` Arguments: ${details.config.args.join(' ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Logs command
|
// Logs command
|
||||||
smartcliInstance.addCommand('logs').subscribe({
|
smartcliInstance.addCommand('logs').subscribe({
|
||||||
next: async (argvArg: CliArguments) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
try {
|
||||||
|
const id = argvArg._[1];
|
||||||
if (!id) {
|
if (!id) {
|
||||||
console.error('Error: Missing process ID. Usage: tspm logs <id>');
|
console.error('Error: Please provide a process ID');
|
||||||
return;
|
console.log('Usage: tspm logs <id>');
|
||||||
}
|
return;
|
||||||
|
|
||||||
const lines = Number(argvArg.lines || argvArg.n) || 20;
|
|
||||||
const logs = tspm.getLogs(id, lines);
|
|
||||||
|
|
||||||
if (logs.length === 0) {
|
|
||||||
console.log(`No logs found for process '${id}'.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display logs with colors for different log types
|
|
||||||
for (const log of logs) {
|
|
||||||
const timestamp = log.timestamp.toISOString();
|
|
||||||
const prefix = `[${timestamp}] `;
|
|
||||||
|
|
||||||
switch (log.type) {
|
|
||||||
case 'stdout':
|
|
||||||
console.log(`${prefix}${log.message}`);
|
|
||||||
break;
|
|
||||||
case 'stderr':
|
|
||||||
console.error(`${prefix}${log.message}`);
|
|
||||||
break;
|
|
||||||
case 'system':
|
|
||||||
console.log(`${prefix}[SYSTEM] ${log.message}`);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lines = argvArg.lines || 50;
|
||||||
|
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
||||||
|
|
||||||
|
console.log(`Logs for process: ${id} (last ${lines} lines)`);
|
||||||
|
console.log('─'.repeat(60));
|
||||||
|
|
||||||
|
for (const log of response.logs) {
|
||||||
|
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||||
|
const prefix = log.type === 'stdout' ? '[OUT]' : '[ERR]';
|
||||||
|
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting logs:', error.message);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start parsing
|
// Start-all command
|
||||||
|
smartcliInstance.addCommand('start-all').subscribe({
|
||||||
|
next: async (argvArg: CliArguments) => {
|
||||||
|
try {
|
||||||
|
console.log('Starting all processes...');
|
||||||
|
const response = await tspmIpcClient.request('startAll', {});
|
||||||
|
|
||||||
|
if (response.started.length > 0) {
|
||||||
|
console.log(`✓ Started ${response.started.length} processes:`);
|
||||||
|
for (const id of response.started) {
|
||||||
|
console.log(` - ${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.failed.length > 0) {
|
||||||
|
console.log(`✗ Failed to start ${response.failed.length} processes:`);
|
||||||
|
for (const failure of response.failed) {
|
||||||
|
console.log(` - ${failure.id}: ${failure.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting all processes:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop-all command
|
||||||
|
smartcliInstance.addCommand('stop-all').subscribe({
|
||||||
|
next: async (argvArg: CliArguments) => {
|
||||||
|
try {
|
||||||
|
console.log('Stopping all processes...');
|
||||||
|
const response = await tspmIpcClient.request('stopAll', {});
|
||||||
|
|
||||||
|
if (response.stopped.length > 0) {
|
||||||
|
console.log(`✓ Stopped ${response.stopped.length} processes:`);
|
||||||
|
for (const id of response.stopped) {
|
||||||
|
console.log(` - ${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.failed.length > 0) {
|
||||||
|
console.log(`✗ Failed to stop ${response.failed.length} processes:`);
|
||||||
|
for (const failure of response.failed) {
|
||||||
|
console.log(` - ${failure.id}: ${failure.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping all processes:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restart-all command
|
||||||
|
smartcliInstance.addCommand('restart-all').subscribe({
|
||||||
|
next: async (argvArg: CliArguments) => {
|
||||||
|
try {
|
||||||
|
console.log('Restarting all processes...');
|
||||||
|
const response = await tspmIpcClient.request('restartAll', {});
|
||||||
|
|
||||||
|
if (response.restarted.length > 0) {
|
||||||
|
console.log(`✓ Restarted ${response.restarted.length} processes:`);
|
||||||
|
for (const id of response.restarted) {
|
||||||
|
console.log(` - ${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.failed.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`✗ Failed to restart ${response.failed.length} processes:`,
|
||||||
|
);
|
||||||
|
for (const failure of response.failed) {
|
||||||
|
console.log(` - ${failure.id}: ${failure.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error restarting all processes:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Daemon commands
|
||||||
|
smartcliInstance.addCommand('daemon').subscribe({
|
||||||
|
next: async (argvArg: CliArguments) => {
|
||||||
|
const subCommand = argvArg._[1];
|
||||||
|
|
||||||
|
switch (subCommand) {
|
||||||
|
case 'start':
|
||||||
|
try {
|
||||||
|
const status = await tspmIpcClient.getDaemonStatus();
|
||||||
|
if (status) {
|
||||||
|
console.log('TSPM daemon is already running');
|
||||||
|
console.log(` PID: ${status.pid}`);
|
||||||
|
console.log(
|
||||||
|
` Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
|
||||||
|
);
|
||||||
|
console.log(` Processes: ${status.processCount}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Starting TSPM daemon...');
|
||||||
|
await tspmIpcClient.connect();
|
||||||
|
console.log('✓ TSPM daemon started successfully');
|
||||||
|
|
||||||
|
const newStatus = await tspmIpcClient.getDaemonStatus();
|
||||||
|
if (newStatus) {
|
||||||
|
console.log(` PID: ${newStatus.pid}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting daemon:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'stop':
|
||||||
|
try {
|
||||||
|
console.log('Stopping TSPM daemon...');
|
||||||
|
await tspmIpcClient.stopDaemon(true);
|
||||||
|
console.log('✓ TSPM daemon stopped successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping daemon:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
try {
|
||||||
|
const status = await tspmIpcClient.getDaemonStatus();
|
||||||
|
if (!status) {
|
||||||
|
console.log('TSPM daemon is not running');
|
||||||
|
console.log('Use "tspm daemon start" to start it');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('TSPM Daemon Status:');
|
||||||
|
console.log('─'.repeat(40));
|
||||||
|
console.log(`Status: ${status.status}`);
|
||||||
|
console.log(`PID: ${status.pid}`);
|
||||||
|
console.log(
|
||||||
|
`Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
|
||||||
|
);
|
||||||
|
console.log(`Processes: ${status.processCount}`);
|
||||||
|
console.log(
|
||||||
|
`Memory: ${formatMemory(status.memoryUsage || 0)}`,
|
||||||
|
);
|
||||||
|
console.log(`CPU: ${status.cpuUsage?.toFixed(1) || 0}s`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting daemon status:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log('Usage: tspm daemon <command>');
|
||||||
|
console.log('\nCommands:');
|
||||||
|
console.log(' start Start the TSPM daemon');
|
||||||
|
console.log(' stop Stop the TSPM daemon');
|
||||||
|
console.log(' status Show daemon status');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start parsing commands
|
||||||
smartcliInstance.startParse();
|
smartcliInstance.startParse();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to format memory usage
|
|
||||||
function formatMemory(bytes: number): string {
|
|
||||||
if (bytes === 0) return '0 B';
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to parse memory strings like "500MB"
|
|
||||||
function parseMemoryString(memString: string): number {
|
|
||||||
const units = {
|
|
||||||
'B': 1,
|
|
||||||
'KB': 1024,
|
|
||||||
'MB': 1024 * 1024,
|
|
||||||
'GB': 1024 * 1024 * 1024,
|
|
||||||
'TB': 1024 * 1024 * 1024 * 1024
|
|
||||||
};
|
|
||||||
|
|
||||||
const match = memString.match(/^(\d+(?:\.\d+)?)\s*([KMGT]?B)$/i);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error(`Invalid memory format: ${memString}. Use format like 500MB`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = parseFloat(match[1]);
|
|
||||||
const unit = match[2].toUpperCase();
|
|
||||||
|
|
||||||
return value * units[unit];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to pad strings for table display
|
|
||||||
function pad(str: string, length: number): string {
|
|
||||||
return str.padEnd(length);
|
|
||||||
}
|
|
9
ts/daemon.ts
Normal file
9
ts/daemon.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { startDaemon } from './classes.daemon.js';
|
||||||
|
|
||||||
|
// Start the daemon
|
||||||
|
startDaemon().catch((error) => {
|
||||||
|
console.error('Failed to start daemon:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
@@ -1,5 +1,8 @@
|
|||||||
export * from './classes.tspm.js';
|
export * from './classes.tspm.js';
|
||||||
export * from './classes.processmonitor.js';
|
export * from './classes.processmonitor.js';
|
||||||
|
export * from './classes.daemon.js';
|
||||||
|
export * from './classes.ipcclient.js';
|
||||||
|
export * from './ipc.types.js';
|
||||||
|
|
||||||
import * as cli from './cli.js';
|
import * as cli from './cli.js';
|
||||||
|
|
||||||
@@ -8,4 +11,4 @@ import * as cli from './cli.js';
|
|||||||
*/
|
*/
|
||||||
export const runCli = async () => {
|
export const runCli = async () => {
|
||||||
await cli.run();
|
await cli.run();
|
||||||
}
|
};
|
||||||
|
201
ts/ipc.types.ts
Normal file
201
ts/ipc.types.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import type {
|
||||||
|
IProcessConfig,
|
||||||
|
IProcessInfo,
|
||||||
|
IProcessLog,
|
||||||
|
} from './classes.tspm.js';
|
||||||
|
|
||||||
|
// Base message types
|
||||||
|
export interface IpcRequest<T = any> {
|
||||||
|
id: string;
|
||||||
|
method: string;
|
||||||
|
params: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IpcResponse<T = any> {
|
||||||
|
id: string;
|
||||||
|
success: boolean;
|
||||||
|
result?: T;
|
||||||
|
error?: {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data?: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request/Response pairs for each operation
|
||||||
|
|
||||||
|
// Start command
|
||||||
|
export interface StartRequest {
|
||||||
|
config: IProcessConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartResponse {
|
||||||
|
processId: string;
|
||||||
|
pid?: number;
|
||||||
|
status: 'online' | 'stopped' | 'errored';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop command
|
||||||
|
export interface StopRequest {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StopResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart command
|
||||||
|
export interface RestartRequest {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RestartResponse {
|
||||||
|
processId: string;
|
||||||
|
pid?: number;
|
||||||
|
status: 'online' | 'stopped' | 'errored';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete command
|
||||||
|
export interface DeleteRequest {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List command
|
||||||
|
export interface ListRequest {
|
||||||
|
// No parameters needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListResponse {
|
||||||
|
processes: IProcessInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Describe command
|
||||||
|
export interface DescribeRequest {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DescribeResponse {
|
||||||
|
processInfo: IProcessInfo;
|
||||||
|
config: IProcessConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get logs command
|
||||||
|
export interface GetLogsRequest {
|
||||||
|
id: string;
|
||||||
|
lines?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetLogsResponse {
|
||||||
|
logs: IProcessLog[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start all command
|
||||||
|
export interface StartAllRequest {
|
||||||
|
// No parameters needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartAllResponse {
|
||||||
|
started: string[];
|
||||||
|
failed: Array<{
|
||||||
|
id: string;
|
||||||
|
error: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop all command
|
||||||
|
export interface StopAllRequest {
|
||||||
|
// No parameters needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StopAllResponse {
|
||||||
|
stopped: string[];
|
||||||
|
failed: Array<{
|
||||||
|
id: string;
|
||||||
|
error: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart all command
|
||||||
|
export interface RestartAllRequest {
|
||||||
|
// No parameters needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RestartAllResponse {
|
||||||
|
restarted: string[];
|
||||||
|
failed: Array<{
|
||||||
|
id: string;
|
||||||
|
error: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daemon status command
|
||||||
|
export interface DaemonStatusRequest {
|
||||||
|
// No parameters needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DaemonStatusResponse {
|
||||||
|
status: 'running' | 'stopped';
|
||||||
|
pid?: number;
|
||||||
|
uptime?: number;
|
||||||
|
processCount: number;
|
||||||
|
memoryUsage?: number;
|
||||||
|
cpuUsage?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daemon shutdown command
|
||||||
|
export interface DaemonShutdownRequest {
|
||||||
|
graceful?: boolean;
|
||||||
|
timeout?: number; // milliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DaemonShutdownResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat command
|
||||||
|
export interface HeartbeatRequest {
|
||||||
|
// No parameters needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HeartbeatResponse {
|
||||||
|
timestamp: number;
|
||||||
|
status: 'healthy' | 'degraded';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type mappings for methods
|
||||||
|
export type IpcMethodMap = {
|
||||||
|
start: { request: StartRequest; response: StartResponse };
|
||||||
|
stop: { request: StopRequest; response: StopResponse };
|
||||||
|
restart: { request: RestartRequest; response: RestartResponse };
|
||||||
|
delete: { request: DeleteRequest; response: DeleteResponse };
|
||||||
|
list: { request: ListRequest; response: ListResponse };
|
||||||
|
describe: { request: DescribeRequest; response: DescribeResponse };
|
||||||
|
getLogs: { request: GetLogsRequest; response: GetLogsResponse };
|
||||||
|
startAll: { request: StartAllRequest; response: StartAllResponse };
|
||||||
|
stopAll: { request: StopAllRequest; response: StopAllResponse };
|
||||||
|
restartAll: { request: RestartAllRequest; response: RestartAllResponse };
|
||||||
|
'daemon:status': {
|
||||||
|
request: DaemonStatusRequest;
|
||||||
|
response: DaemonStatusResponse;
|
||||||
|
};
|
||||||
|
'daemon:shutdown': {
|
||||||
|
request: DaemonShutdownRequest;
|
||||||
|
response: DaemonShutdownResponse;
|
||||||
|
};
|
||||||
|
heartbeat: { request: HeartbeatRequest; response: HeartbeatResponse };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper type to extract request type for a method
|
||||||
|
export type RequestForMethod<M extends keyof IpcMethodMap> =
|
||||||
|
IpcMethodMap[M]['request'];
|
||||||
|
|
||||||
|
// Helper type to extract response type for a method
|
||||||
|
export type ResponseForMethod<M extends keyof IpcMethodMap> =
|
||||||
|
IpcMethodMap[M]['response'];
|
@@ -1,4 +1,10 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
export const packageDir: string = plugins.path.join(plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '..');
|
export const packageDir: string = plugins.path.join(
|
||||||
|
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||||
|
'..',
|
||||||
|
);
|
||||||
export const cwd: string = process.cwd();
|
export const cwd: string = process.cwd();
|
||||||
|
|
||||||
|
import * as os from 'os';
|
||||||
|
export const tspmDir: string = plugins.path.join(os.homedir(), '.tspm');
|
||||||
|
@@ -3,33 +3,22 @@ import * as childProcess from 'child_process';
|
|||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
|
||||||
// Export with explicit module types
|
// Export with explicit module types
|
||||||
export {
|
export { childProcess, path };
|
||||||
childProcess,
|
|
||||||
path,
|
|
||||||
}
|
|
||||||
|
|
||||||
// @push.rocks scope
|
// @push.rocks scope
|
||||||
import * as npmextra from '@push.rocks/npmextra';
|
import * as npmextra from '@push.rocks/npmextra';
|
||||||
import * as projectinfo from '@push.rocks/projectinfo';
|
import * as projectinfo from '@push.rocks/projectinfo';
|
||||||
import * as smartcli from '@push.rocks/smartcli';
|
import * as smartcli from '@push.rocks/smartcli';
|
||||||
import * as smartdaemon from '@push.rocks/smartdaemon';
|
import * as smartdaemon from '@push.rocks/smartdaemon';
|
||||||
|
import * as smartipc from '@push.rocks/smartipc';
|
||||||
import * as smartpath from '@push.rocks/smartpath';
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
|
|
||||||
// Export with explicit module types
|
// Export with explicit module types
|
||||||
export {
|
export { npmextra, projectinfo, smartcli, smartdaemon, smartipc, smartpath };
|
||||||
npmextra,
|
|
||||||
projectinfo,
|
|
||||||
smartcli,
|
|
||||||
smartdaemon,
|
|
||||||
smartpath,
|
|
||||||
}
|
|
||||||
|
|
||||||
// third-party scope
|
// third-party scope
|
||||||
import psTree from 'ps-tree';
|
import psTree from 'ps-tree';
|
||||||
import pidusage from 'pidusage';
|
import pidusage from 'pidusage';
|
||||||
|
|
||||||
// Add explicit types for third-party exports
|
// Add explicit types for third-party exports
|
||||||
export {
|
export { psTree, pidusage };
|
||||||
psTree,
|
|
||||||
pidusage,
|
|
||||||
}
|
|
||||||
|
@@ -8,7 +8,7 @@ export enum ErrorType {
|
|||||||
PROCESS = 'ProcessError',
|
PROCESS = 'ProcessError',
|
||||||
RUNTIME = 'RuntimeError',
|
RUNTIME = 'RuntimeError',
|
||||||
VALIDATION = 'ValidationError',
|
VALIDATION = 'ValidationError',
|
||||||
UNKNOWN = 'UnknownError'
|
UNKNOWN = 'UnknownError',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base error class with type and code support
|
// Base error class with type and code support
|
||||||
@@ -21,7 +21,7 @@ export class TspmError extends Error {
|
|||||||
message: string,
|
message: string,
|
||||||
type: ErrorType = ErrorType.UNKNOWN,
|
type: ErrorType = ErrorType.UNKNOWN,
|
||||||
code: string = 'ERR_UNKNOWN',
|
code: string = 'ERR_UNKNOWN',
|
||||||
details?: Record<string, any>
|
details?: Record<string, any>,
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = type;
|
this.name = type;
|
||||||
@@ -42,19 +42,31 @@ export class TspmError extends Error {
|
|||||||
|
|
||||||
// Specific error classes
|
// Specific error classes
|
||||||
export class ConfigError extends TspmError {
|
export class ConfigError extends TspmError {
|
||||||
constructor(message: string, code: string = 'ERR_CONFIG', details?: Record<string, any>) {
|
constructor(
|
||||||
|
message: string,
|
||||||
|
code: string = 'ERR_CONFIG',
|
||||||
|
details?: Record<string, any>,
|
||||||
|
) {
|
||||||
super(message, ErrorType.CONFIG, code, details);
|
super(message, ErrorType.CONFIG, code, details);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProcessError extends TspmError {
|
export class ProcessError extends TspmError {
|
||||||
constructor(message: string, code: string = 'ERR_PROCESS', details?: Record<string, any>) {
|
constructor(
|
||||||
|
message: string,
|
||||||
|
code: string = 'ERR_PROCESS',
|
||||||
|
details?: Record<string, any>,
|
||||||
|
) {
|
||||||
super(message, ErrorType.PROCESS, code, details);
|
super(message, ErrorType.PROCESS, code, details);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ValidationError extends TspmError {
|
export class ValidationError extends TspmError {
|
||||||
constructor(message: string, code: string = 'ERR_VALIDATION', details?: Record<string, any>) {
|
constructor(
|
||||||
|
message: string,
|
||||||
|
code: string = 'ERR_VALIDATION',
|
||||||
|
details?: Record<string, any>,
|
||||||
|
) {
|
||||||
super(message, ErrorType.VALIDATION, code, details);
|
super(message, ErrorType.VALIDATION, code, details);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +78,9 @@ export const handleError = (error: Error | unknown): TspmError => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return new TspmError(error.message, ErrorType.UNKNOWN, 'ERR_UNKNOWN', { originalError: error });
|
return new TspmError(error.message, ErrorType.UNKNOWN, 'ERR_UNKNOWN', {
|
||||||
|
originalError: error,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return new TspmError(String(error), ErrorType.UNKNOWN, 'ERR_UNKNOWN');
|
return new TspmError(String(error), ErrorType.UNKNOWN, 'ERR_UNKNOWN');
|
||||||
@@ -78,7 +92,7 @@ export enum LogLevel {
|
|||||||
INFO = 1,
|
INFO = 1,
|
||||||
WARN = 2,
|
WARN = 2,
|
||||||
ERROR = 3,
|
ERROR = 3,
|
||||||
NONE = 4
|
NONE = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Logger {
|
export class Logger {
|
||||||
|
@@ -11,7 +11,5 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {}
|
"paths": {}
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": ["dist_*/**/*.d.ts"]
|
||||||
"dist_*/**/*.d.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
Reference in New Issue
Block a user