Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
1c2310c185 | |||
d33a001edc | |||
35b6a6a8d0 | |||
50c5fdb0ea |
23
changelog.md
23
changelog.md
@@ -1,5 +1,28 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-28 - 2.0.0 - BREAKING CHANGE(daemon)
|
||||||
|
Refactor daemon lifecycle and service management: remove IPC auto-spawn, add TspmServiceManager and CLI enable/disable
|
||||||
|
|
||||||
|
- Do not auto-spawn the daemon from the IPC client anymore — attempts to connect will now error with instructions to start the daemon manually or enable the system service (breaking change).
|
||||||
|
- Add TspmServiceManager to manage the daemon as a systemd service via smartdaemon (enable/disable/reload/status helpers).
|
||||||
|
- CLI: add 'enable' and 'disable' commands to install/uninstall the daemon as a system service and add 'daemon start-service' entrypoint used by systemd.
|
||||||
|
- CLI: improve error handling and user hints when the daemon is not running (suggests `tspm daemon start` or `tspm enable`).
|
||||||
|
- IPC client: removed startDaemon() and related auto-reconnect/start logic; request() no longer auto-reconnects or implicitly start the daemon.
|
||||||
|
- Export TspmServiceManager from the package index so service management is part of the public API.
|
||||||
|
- Updated development plan/readme (readme.plan.md) to reflect the refactor toward proper SmartDaemon integration and migration notes.
|
||||||
|
|
||||||
|
## 2025-08-26 - 1.8.0 - feat(daemon)
|
||||||
|
Add real-time log streaming and pub/sub: daemon publishes per-process logs, IPC client subscribe/unsubscribe, CLI --follow streaming, and sequencing for logs
|
||||||
|
|
||||||
|
- Upgrade @push.rocks/smartipc dependency to ^2.1.2
|
||||||
|
- Daemon: initialize SmartIpc server with heartbeat and publish process logs to topic `logs.<processId>`; write PID file and start heartbeat monitoring
|
||||||
|
- Tspm: re-emit monitor log events as 'process:log' so daemon can broadcast logs
|
||||||
|
- ProcessWrapper: include seq and runId on IProcessLog entries and maintain nextSeq/runId (adds sequencing to logs); default log buffer size applied
|
||||||
|
- TspmIpcClient: improved connect options (retries, timeouts, heartbeat handling), add subscribe/unsubscribe for real-time logs, and use SmartIpc.waitForServer when starting daemon
|
||||||
|
- CLI: add --follow flag to `logs` command to stream live logs, detect sequence gaps/duplicates, and handle graceful cleanup on Ctrl+C
|
||||||
|
- ProcessMonitor: now extends EventEmitter and re-emits process logs for upstream consumption
|
||||||
|
- Standardized heartbeat and IPC timing defaults (heartbeatInterval: 5000ms, heartbeatTimeout: 20000ms, heartbeatInitialGracePeriodMs: 10000ms)
|
||||||
|
|
||||||
## 2025-08-25 - 1.7.0 - feat(readme)
|
## 2025-08-25 - 1.7.0 - feat(readme)
|
||||||
Add comprehensive README with detailed usage, command reference, daemon management, architecture and development instructions
|
Add comprehensive README with detailed usage, command reference, daemon management, architecture and development instructions
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tspm",
|
"name": "@git.zone/tspm",
|
||||||
"version": "1.7.0",
|
"version": "2.0.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "a no fuzz process manager",
|
"description": "a no fuzz process manager",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"@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/smartipc": "^2.0.3",
|
"@push.rocks/smartipc": "^2.1.2",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"pidusage": "^4.0.1",
|
"pidusage": "^4.0.1",
|
||||||
"ps-tree": "^1.2.0"
|
"ps-tree": "^1.2.0"
|
||||||
|
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -21,8 +21,8 @@ importers:
|
|||||||
specifier: ^2.0.8
|
specifier: ^2.0.8
|
||||||
version: 2.0.8
|
version: 2.0.8
|
||||||
'@push.rocks/smartipc':
|
'@push.rocks/smartipc':
|
||||||
specifier: ^2.0.3
|
specifier: ^2.1.2
|
||||||
version: 2.0.3
|
version: 2.1.2
|
||||||
'@push.rocks/smartpath':
|
'@push.rocks/smartpath':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
@@ -950,8 +950,8 @@ packages:
|
|||||||
'@push.rocks/smarthash@3.2.3':
|
'@push.rocks/smarthash@3.2.3':
|
||||||
resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==}
|
resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==}
|
||||||
|
|
||||||
'@push.rocks/smartipc@2.0.3':
|
'@push.rocks/smartipc@2.1.2':
|
||||||
resolution: {integrity: sha512-Yty+craFj9lYp6dL1dxHwrF1ykeu02o78D9kNGb5XR+4c53Cci7puqgK9+zbSakaHlNMqKHUWICi50ziGuq5xQ==}
|
resolution: {integrity: sha512-QyFrohq9jq4ISl6DUyeS1uuWgKxQiTrWZAzIqsGZW/BT36FGoqMpGufgjjkVuBvZtYW8e3hl+lcmT+DHfVMfmg==}
|
||||||
|
|
||||||
'@push.rocks/smartjson@5.0.20':
|
'@push.rocks/smartjson@5.0.20':
|
||||||
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
|
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
|
||||||
@@ -6425,7 +6425,7 @@ snapshots:
|
|||||||
'@types/through2': 2.0.41
|
'@types/through2': 2.0.41
|
||||||
through2: 4.0.2
|
through2: 4.0.2
|
||||||
|
|
||||||
'@push.rocks/smartipc@2.0.3':
|
'@push.rocks/smartipc@2.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
|
239
readme.plan.md
239
readme.plan.md
@@ -1,209 +1,48 @@
|
|||||||
# TSPM Refactoring Plan: Central Daemon Architecture
|
# TSPM SmartDaemon Service Management Refactor
|
||||||
|
|
||||||
## Problem Analysis
|
## Problem
|
||||||
|
Currently TSPM auto-spawns the daemon as a detached child process, which is improper daemon management. It should use smartdaemon for all lifecycle management and never spawn processes directly.
|
||||||
|
|
||||||
Currently, each `startAsDaemon` creates an isolated tspm instance with no coordination:
|
## Solution
|
||||||
|
Refactor to use SmartDaemon for proper systemd service integration.
|
||||||
|
|
||||||
- Multiple daemons reading/writing same config file
|
## Implementation Tasks
|
||||||
- No communication between instances
|
|
||||||
- Inconsistent process management
|
|
||||||
- `tspm list` shows all processes but each daemon only manages its own
|
|
||||||
|
|
||||||
## Proposed Architecture
|
### Phase 1: Remove Auto-Spawn Behavior
|
||||||
|
- [x] Remove spawn import from ts/classes.ipcclient.ts
|
||||||
|
- [x] Delete startDaemon() method from IpcClient
|
||||||
|
- [x] Update connect() to throw error when daemon not running
|
||||||
|
- [x] Remove auto-reconnect logic from request() method
|
||||||
|
|
||||||
### 1. Central Daemon Manager (`ts/classes.daemon.ts`)
|
### Phase 2: Create Service Manager
|
||||||
|
- [x] Create new file ts/classes.servicemanager.ts
|
||||||
|
- [x] Implement TspmServiceManager class
|
||||||
|
- [x] Add getOrCreateService() method
|
||||||
|
- [x] Add enableService() method
|
||||||
|
- [x] Add disableService() method
|
||||||
|
- [x] Add getServiceStatus() method
|
||||||
|
|
||||||
- Single daemon instance managing ALL processes
|
### Phase 3: Update CLI Commands
|
||||||
- Runs continuously in background
|
- [x] Add 'enable' command to CLI
|
||||||
- Uses Unix socket for IPC at `~/.tspm/tspm.sock`
|
- [x] Add 'disable' command to CLI
|
||||||
- Maintains single source of truth for process state
|
- [x] Update 'daemon start' to work without systemd
|
||||||
|
- [x] Add 'daemon start-service' internal command for systemd
|
||||||
|
- [x] Update all commands to handle missing daemon gracefully
|
||||||
|
- [x] Add proper error messages with hints
|
||||||
|
|
||||||
### 2. IPC Communication Layer (`ts/classes.ipc.ts`)
|
### Phase 4: Update Documentation
|
||||||
|
- [x] Update help text in CLI
|
||||||
|
- [ ] Update command descriptions
|
||||||
|
- [x] Add service management section
|
||||||
|
|
||||||
- **Framework**: Use `@push.rocks/smartipc` v2.0.1
|
### Phase 5: Testing
|
||||||
- **Server**: SmartIpc server in daemon using Unix Domain Socket
|
- [x] Test enable command
|
||||||
- **Client**: SmartIpc client in CLI for all operations
|
- [x] Test disable command
|
||||||
- **Socket Path**: `~/.tspm/tspm.sock` (Unix) or named pipe (Windows)
|
- [x] Test daemon commands
|
||||||
- **Protocol**: Type-safe request/response with SmartIpc's built-in patterns
|
- [x] Test error handling when daemon not running
|
||||||
- **Features**:
|
- [x] Build and verify TypeScript compilation
|
||||||
- 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
|
## Migration Notes
|
||||||
|
- Users will need to run `tspm enable` once after update
|
||||||
- `tspm enable` - Start central daemon using systemd/launchd
|
- Existing daemon instances will stop working
|
||||||
- `tspm disable` - Stop and disable central daemon
|
- Documentation needs updating to explain new behavior
|
||||||
- `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
|
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tspm',
|
name: '@git.zone/tspm',
|
||||||
version: '1.7.0',
|
version: '2.0.0',
|
||||||
description: 'a no fuzz process manager'
|
description: 'a no fuzz process manager'
|
||||||
}
|
}
|
||||||
|
@@ -43,14 +43,19 @@ export class TspmDaemon {
|
|||||||
this.ipcServer = plugins.smartipc.SmartIpc.createServer({
|
this.ipcServer = plugins.smartipc.SmartIpc.createServer({
|
||||||
id: 'tspm-daemon',
|
id: 'tspm-daemon',
|
||||||
socketPath: this.socketPath,
|
socketPath: this.socketPath,
|
||||||
heartbeat: false, // Disable heartbeat for now
|
autoCleanupSocketFile: true, // Clean up stale sockets
|
||||||
|
socketMode: 0o600, // Set proper permissions
|
||||||
|
heartbeat: true,
|
||||||
|
heartbeatInterval: 5000,
|
||||||
|
heartbeatTimeout: 20000,
|
||||||
|
heartbeatInitialGracePeriodMs: 10000 // Grace period for startup
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register message handlers
|
// Register message handlers
|
||||||
this.registerHandlers();
|
this.registerHandlers();
|
||||||
|
|
||||||
// Start the IPC server
|
// Start the IPC server and wait until ready to accept connections
|
||||||
await this.ipcServer.start();
|
await this.ipcServer.start({ readyWhen: 'accepting' });
|
||||||
|
|
||||||
// Write PID file
|
// Write PID file
|
||||||
await this.writePidFile();
|
await this.writePidFile();
|
||||||
@@ -61,6 +66,16 @@ export class TspmDaemon {
|
|||||||
// Load existing process configurations
|
// Load existing process configurations
|
||||||
await this.tspmInstance.loadProcessConfigs();
|
await this.tspmInstance.loadProcessConfigs();
|
||||||
|
|
||||||
|
// Set up log publishing
|
||||||
|
this.tspmInstance.on('process:log', ({ processId, log }) => {
|
||||||
|
// Publish to topic for this process
|
||||||
|
const topic = `logs.${processId}`;
|
||||||
|
// Broadcast to all connected clients subscribed to this topic
|
||||||
|
if (this.ipcServer) {
|
||||||
|
this.ipcServer.broadcast(`topic:${topic}`, log);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Set up graceful shutdown handlers
|
// Set up graceful shutdown handlers
|
||||||
this.setupShutdownHandlers();
|
this.setupShutdownHandlers();
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
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 { spawn } from 'child_process';
|
|
||||||
import type {
|
import type {
|
||||||
IpcMethodMap,
|
IpcMethodMap,
|
||||||
RequestForMethod,
|
RequestForMethod,
|
||||||
@@ -34,28 +34,50 @@ export class TspmIpcClient {
|
|||||||
const daemonRunning = await this.isDaemonRunning();
|
const daemonRunning = await this.isDaemonRunning();
|
||||||
|
|
||||||
if (!daemonRunning) {
|
if (!daemonRunning) {
|
||||||
console.log('Daemon not running, starting it...');
|
throw new Error(
|
||||||
await this.startDaemon();
|
'TSPM daemon is not running.\n\n' +
|
||||||
// Wait a bit for daemon to initialize
|
'To start the daemon, run one of:\n' +
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
' tspm daemon start - Start daemon for this session\n' +
|
||||||
|
' tspm enable - Enable daemon as system service (recommended)\n'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create IPC client
|
// Create IPC client
|
||||||
this.ipcClient = plugins.smartipc.SmartIpc.createClient({
|
this.ipcClient = plugins.smartipc.SmartIpc.createClient({
|
||||||
id: 'tspm-cli',
|
id: 'tspm-cli',
|
||||||
socketPath: this.socketPath,
|
socketPath: this.socketPath,
|
||||||
heartbeat: false, // Disable heartbeat for now
|
clientId: `cli-${process.pid}`,
|
||||||
|
connectRetry: {
|
||||||
|
enabled: true,
|
||||||
|
initialDelay: 100,
|
||||||
|
maxDelay: 2000,
|
||||||
|
maxAttempts: 30,
|
||||||
|
totalTimeout: 15000,
|
||||||
|
},
|
||||||
|
registerTimeoutMs: 8000,
|
||||||
|
heartbeat: true,
|
||||||
|
heartbeatInterval: 5000,
|
||||||
|
heartbeatTimeout: 20000,
|
||||||
|
heartbeatInitialGracePeriodMs: 10000,
|
||||||
|
heartbeatThrowOnTimeout: false // Don't throw, emit events instead
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect to the daemon
|
// Connect to the daemon
|
||||||
try {
|
try {
|
||||||
await this.ipcClient.connect();
|
await this.ipcClient.connect({ waitForReady: true });
|
||||||
this.isConnected = true;
|
this.isConnected = true;
|
||||||
|
|
||||||
|
// Handle heartbeat timeouts gracefully
|
||||||
|
this.ipcClient.on('heartbeatTimeout', () => {
|
||||||
|
console.warn('Heartbeat timeout detected, connection may be degraded');
|
||||||
|
this.isConnected = false;
|
||||||
|
});
|
||||||
|
|
||||||
console.log('Connected to TSPM daemon');
|
console.log('Connected to TSPM daemon');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to connect to daemon:', error);
|
console.error('Failed to connect to daemon:', error);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.',
|
'Could not connect to TSPM daemon. Please try running "tspm daemon start" or "tspm enable".',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +101,10 @@ export class TspmIpcClient {
|
|||||||
params: RequestForMethod<M>,
|
params: RequestForMethod<M>,
|
||||||
): Promise<ResponseForMethod<M>> {
|
): Promise<ResponseForMethod<M>> {
|
||||||
if (!this.isConnected || !this.ipcClient) {
|
if (!this.isConnected || !this.ipcClient) {
|
||||||
await this.connect();
|
throw new Error(
|
||||||
|
'Not connected to TSPM daemon.\n' +
|
||||||
|
'Run "tspm daemon start" or "tspm enable" first.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -90,26 +115,35 @@ export class TspmIpcClient {
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle connection errors by trying to reconnect once
|
// Don't try to auto-reconnect, just throw the error
|
||||||
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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to log updates for a specific process
|
||||||
|
*/
|
||||||
|
public async subscribe(processId: string, handler: (log: any) => void): Promise<void> {
|
||||||
|
if (!this.ipcClient || !this.isConnected) {
|
||||||
|
throw new Error('Not connected to daemon');
|
||||||
|
}
|
||||||
|
|
||||||
|
const topic = `logs.${processId}`;
|
||||||
|
await this.ipcClient.subscribe(`topic:${topic}`, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from log updates for a specific process
|
||||||
|
*/
|
||||||
|
public async unsubscribe(processId: string): Promise<void> {
|
||||||
|
if (!this.ipcClient || !this.isConnected) {
|
||||||
|
throw new Error('Not connected to daemon');
|
||||||
|
}
|
||||||
|
|
||||||
|
const topic = `logs.${processId}`;
|
||||||
|
await this.ipcClient.unsubscribe(`topic:${topic}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the daemon is running
|
* Check if the daemon is running
|
||||||
*/
|
*/
|
||||||
@@ -151,44 +185,7 @@ export class TspmIpcClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
* Stop the daemon
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
import { ProcessWrapper, type IProcessLog } from './classes.processwrapper.js';
|
import { ProcessWrapper, type IProcessLog } from './classes.processwrapper.js';
|
||||||
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
|
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export interface IMonitorConfig {
|
|||||||
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 extends EventEmitter {
|
||||||
private processWrapper: ProcessWrapper | null = null;
|
private processWrapper: ProcessWrapper | null = null;
|
||||||
private config: IMonitorConfig;
|
private config: IMonitorConfig;
|
||||||
private intervalId: NodeJS.Timeout | null = null;
|
private intervalId: NodeJS.Timeout | null = null;
|
||||||
@@ -22,6 +23,7 @@ export class ProcessMonitor {
|
|||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(config: IMonitorConfig) {
|
constructor(config: IMonitorConfig) {
|
||||||
|
super();
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
|
this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
|
||||||
}
|
}
|
||||||
@@ -65,8 +67,10 @@ export class ProcessMonitor {
|
|||||||
|
|
||||||
// Set up event handlers
|
// Set up event handlers
|
||||||
this.processWrapper.on('log', (log: IProcessLog): void => {
|
this.processWrapper.on('log', (log: IProcessLog): void => {
|
||||||
// Here we could add handlers to send logs somewhere
|
// Re-emit the log event for upstream handlers
|
||||||
// For now, we just log system messages to the console
|
this.emit('log', log);
|
||||||
|
|
||||||
|
// Log system messages to the console
|
||||||
if (log.type === 'system') {
|
if (log.type === 'system') {
|
||||||
this.log(log.message);
|
this.log(log.message);
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,8 @@ export interface IProcessLog {
|
|||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
type: 'stdout' | 'stderr' | 'system';
|
type: 'stdout' | 'stderr' | 'system';
|
||||||
message: string;
|
message: string;
|
||||||
|
seq: number;
|
||||||
|
runId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProcessWrapper extends EventEmitter {
|
export class ProcessWrapper extends EventEmitter {
|
||||||
@@ -24,12 +26,15 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
private logBufferSize: number;
|
private logBufferSize: number;
|
||||||
private startTime: Date | null = null;
|
private startTime: Date | null = null;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private nextSeq: number = 0;
|
||||||
|
private runId: string = '';
|
||||||
|
|
||||||
constructor(options: IProcessWrapperOptions) {
|
constructor(options: IProcessWrapperOptions) {
|
||||||
super();
|
super();
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.logBufferSize = options.logBuffer || 100;
|
this.logBufferSize = options.logBuffer || 100;
|
||||||
this.logger = new Logger(`ProcessWrapper:${options.name}`);
|
this.logger = new Logger(`ProcessWrapper:${options.name}`);
|
||||||
|
this.runId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,6 +222,8 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
type,
|
type,
|
||||||
message,
|
message,
|
||||||
|
seq: this.nextSeq++,
|
||||||
|
runId: this.runId,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logs.push(log);
|
this.logs.push(log);
|
||||||
@@ -238,6 +245,8 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
type: 'system',
|
type: 'system',
|
||||||
message,
|
message,
|
||||||
|
seq: this.nextSeq++,
|
||||||
|
runId: this.runId,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logs.push(log);
|
this.logs.push(log);
|
||||||
|
103
ts/classes.servicemanager.ts
Normal file
103
ts/classes.servicemanager.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import * as paths from './paths.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages TSPM daemon as a systemd service via smartdaemon
|
||||||
|
*/
|
||||||
|
export class TspmServiceManager {
|
||||||
|
private smartDaemon: plugins.smartdaemon.SmartDaemon;
|
||||||
|
private service: any = null; // SmartDaemonService type is not exported
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.smartDaemon = new plugins.smartdaemon.SmartDaemon();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create the TSPM daemon service configuration
|
||||||
|
*/
|
||||||
|
private async getOrCreateService(): Promise<any> {
|
||||||
|
if (!this.service) {
|
||||||
|
const cliPath = plugins.path.join(paths.packageDir, 'cli.js');
|
||||||
|
|
||||||
|
// Create service configuration
|
||||||
|
this.service = await this.smartDaemon.addService({
|
||||||
|
name: 'tspm-daemon',
|
||||||
|
description: 'TSPM Process Manager Daemon',
|
||||||
|
command: `${process.execPath} ${cliPath} daemon start-service`,
|
||||||
|
workingDir: process.env.HOME || process.cwd(),
|
||||||
|
version: '1.0.0'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the TSPM daemon as a system service
|
||||||
|
*/
|
||||||
|
public async enableService(): Promise<void> {
|
||||||
|
const service = await this.getOrCreateService();
|
||||||
|
|
||||||
|
// Save service configuration
|
||||||
|
await service.save();
|
||||||
|
|
||||||
|
// Enable service to start on boot
|
||||||
|
await service.enable();
|
||||||
|
|
||||||
|
// Start the service immediately
|
||||||
|
await service.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable the TSPM daemon service
|
||||||
|
*/
|
||||||
|
public async disableService(): Promise<void> {
|
||||||
|
const service = await this.getOrCreateService();
|
||||||
|
|
||||||
|
// Stop the service if running
|
||||||
|
try {
|
||||||
|
await service.stop();
|
||||||
|
} catch (error) {
|
||||||
|
// Service might not be running
|
||||||
|
console.log('Service was not running');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable service from starting on boot
|
||||||
|
await service.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current status of the systemd service
|
||||||
|
*/
|
||||||
|
public async getServiceStatus(): Promise<{
|
||||||
|
enabled: boolean;
|
||||||
|
running: boolean;
|
||||||
|
status: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
await this.getOrCreateService();
|
||||||
|
|
||||||
|
// Note: SmartDaemon doesn't provide direct status methods,
|
||||||
|
// so we'll need to check via systemctl commands
|
||||||
|
// This is a simplified implementation
|
||||||
|
return {
|
||||||
|
enabled: true, // Would need to check systemctl is-enabled
|
||||||
|
running: true, // Would need to check systemctl is-active
|
||||||
|
status: 'active'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
running: false,
|
||||||
|
status: 'inactive'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the systemd service configuration
|
||||||
|
*/
|
||||||
|
public async reloadService(): Promise<void> {
|
||||||
|
const service = await this.getOrCreateService();
|
||||||
|
await service.reload();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,9 +1,11 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
import * as paths from './paths.js';
|
import * as paths from './paths.js';
|
||||||
import {
|
import {
|
||||||
ProcessMonitor,
|
ProcessMonitor,
|
||||||
type IMonitorConfig,
|
type IMonitorConfig,
|
||||||
} from './classes.processmonitor.js';
|
} from './classes.processmonitor.js';
|
||||||
|
import { type IProcessLog } from './classes.processwrapper.js';
|
||||||
import { TspmConfig } from './classes.config.js';
|
import { TspmConfig } from './classes.config.js';
|
||||||
import {
|
import {
|
||||||
Logger,
|
Logger,
|
||||||
@@ -30,13 +32,9 @@ export interface IProcessInfo {
|
|||||||
restarts: number;
|
restarts: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProcessLog {
|
|
||||||
timestamp: Date;
|
|
||||||
type: 'stdout' | 'stderr' | 'system';
|
|
||||||
message: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Tspm {
|
|
||||||
|
export class Tspm extends EventEmitter {
|
||||||
public processes: Map<string, ProcessMonitor> = new Map();
|
public processes: Map<string, ProcessMonitor> = new Map();
|
||||||
public processConfigs: Map<string, IProcessConfig> = new Map();
|
public processConfigs: Map<string, IProcessConfig> = new Map();
|
||||||
public processInfo: Map<string, IProcessInfo> = new Map();
|
public processInfo: Map<string, IProcessInfo> = new Map();
|
||||||
@@ -45,6 +43,7 @@ export class Tspm {
|
|||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
super();
|
||||||
this.logger = new Logger('Tspm');
|
this.logger = new Logger('Tspm');
|
||||||
this.config = new TspmConfig();
|
this.config = new TspmConfig();
|
||||||
this.loadProcessConfigs();
|
this.loadProcessConfigs();
|
||||||
@@ -98,6 +97,12 @@ export class Tspm {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.processes.set(config.id, monitor);
|
this.processes.set(config.id, monitor);
|
||||||
|
|
||||||
|
// Set up log event handler to re-emit for pub/sub
|
||||||
|
monitor.on('log', (log: IProcessLog) => {
|
||||||
|
this.emit('process:log', { processId: config.id, log });
|
||||||
|
});
|
||||||
|
|
||||||
monitor.start();
|
monitor.start();
|
||||||
|
|
||||||
// Update process info
|
// Update process info
|
||||||
|
221
ts/cli.ts
221
ts/cli.ts
@@ -1,6 +1,7 @@
|
|||||||
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 { tspmIpcClient } from './classes.ipcclient.js';
|
import { tspmIpcClient } from './classes.ipcclient.js';
|
||||||
|
import { TspmServiceManager } from './classes.servicemanager.js';
|
||||||
import { Logger, LogLevel } from './utils.errorhandler.js';
|
import { Logger, LogLevel } from './utils.errorhandler.js';
|
||||||
import type { IProcessConfig } from './classes.tspm.js';
|
import type { IProcessConfig } from './classes.tspm.js';
|
||||||
|
|
||||||
@@ -51,6 +52,21 @@ function formatMemory(bytes: number): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to handle daemon connection errors
|
||||||
|
function handleDaemonError(error: any, action: string): void {
|
||||||
|
if (error.message?.includes('daemon is not running') ||
|
||||||
|
error.message?.includes('Not connected') ||
|
||||||
|
error.message?.includes('ECONNREFUSED')) {
|
||||||
|
console.error(`Error: Cannot ${action} - TSPM daemon is not running.`);
|
||||||
|
console.log('\nTo start the daemon, run one of:');
|
||||||
|
console.log(' tspm daemon start - Start for this session only');
|
||||||
|
console.log(' tspm enable - Enable as system service (recommended)');
|
||||||
|
} else {
|
||||||
|
console.error(`Error ${action}:`, error.message);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function for padding strings
|
// Helper function for padding strings
|
||||||
function pad(str: string, length: number): string {
|
function pad(str: string, length: number): string {
|
||||||
return str.length > length
|
return str.length > length
|
||||||
@@ -79,7 +95,10 @@ export const run = async (): Promise<void> => {
|
|||||||
`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`,
|
`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('\nService Management:');
|
||||||
|
console.log(' enable Enable TSPM as system service (systemd)');
|
||||||
|
console.log(' disable Disable TSPM system service');
|
||||||
|
console.log('\nProcess Commands:');
|
||||||
console.log(' start <script> Start a process');
|
console.log(' start <script> Start a process');
|
||||||
console.log(' list List all processes');
|
console.log(' list List all processes');
|
||||||
console.log(' stop <id> Stop a process');
|
console.log(' stop <id> Stop a process');
|
||||||
@@ -91,8 +110,8 @@ export const run = async (): Promise<void> => {
|
|||||||
console.log(' stop-all Stop all processes');
|
console.log(' stop-all Stop all processes');
|
||||||
console.log(' restart-all Restart all processes');
|
console.log(' restart-all Restart all processes');
|
||||||
console.log('\nDaemon Commands:');
|
console.log('\nDaemon Commands:');
|
||||||
console.log(' daemon start Start the TSPM daemon');
|
console.log(' daemon start Start daemon manually (current session)');
|
||||||
console.log(' daemon stop Stop the TSPM daemon');
|
console.log(' daemon stop Stop the daemon');
|
||||||
console.log(' daemon status Show daemon status');
|
console.log(' daemon status Show daemon status');
|
||||||
console.log(
|
console.log(
|
||||||
'\nUse tspm [command] --help for more information about a command.',
|
'\nUse tspm [command] --help for more information about a command.',
|
||||||
@@ -139,9 +158,10 @@ export const run = async (): Promise<void> => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error('Error: TSPM daemon is not running.');
|
||||||
'Error: Could not connect to TSPM daemon. Use "tspm daemon start" to start it.',
|
console.log('\nTo start the daemon, run one of:');
|
||||||
);
|
console.log(' tspm daemon start - Start for this session only');
|
||||||
|
console.log(' tspm enable - Enable as system service (recommended)');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -217,8 +237,7 @@ export const run = async (): Promise<void> => {
|
|||||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||||
console.log(` Status: ${response.status}`);
|
console.log(` Status: ${response.status}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error starting process:', error.message);
|
handleDaemonError(error, 'start process');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -247,8 +266,7 @@ export const run = async (): Promise<void> => {
|
|||||||
console.error(`✗ Failed to stop process: ${response.message}`);
|
console.error(`✗ Failed to stop process: ${response.message}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error stopping process:', error.message);
|
handleDaemonError(error, 'stop process');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -276,8 +294,7 @@ export const run = async (): Promise<void> => {
|
|||||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||||
console.log(` Status: ${response.status}`);
|
console.log(` Status: ${response.status}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error restarting process:', error.message);
|
handleDaemonError(error, 'restart process');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -306,8 +323,7 @@ export const run = async (): Promise<void> => {
|
|||||||
console.error(`✗ Failed to delete process: ${response.message}`);
|
console.error(`✗ Failed to delete process: ${response.message}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting process:', error.message);
|
handleDaemonError(error, 'delete process');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -356,8 +372,7 @@ export const run = async (): Promise<void> => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error listing processes:', error.message);
|
handleDaemonError(error, 'list processes');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -409,8 +424,7 @@ export const run = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error describing process:', error.message);
|
handleDaemonError(error, 'describe process');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -426,24 +440,87 @@ export const run = async (): Promise<void> => {
|
|||||||
const id = argvArg._[1];
|
const id = argvArg._[1];
|
||||||
if (!id) {
|
if (!id) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process ID');
|
||||||
console.log('Usage: tspm logs <id>');
|
console.log('Usage: tspm logs <id> [options]');
|
||||||
|
console.log('\nOptions:');
|
||||||
|
console.log(' --lines <n> Number of lines to show (default: 50)');
|
||||||
|
console.log(' --follow Stream logs in real-time (like tail -f)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = argvArg.lines || 50;
|
const lines = argvArg.lines || 50;
|
||||||
|
const follow = argvArg.follow || argvArg.f || false;
|
||||||
|
|
||||||
|
// Get initial logs
|
||||||
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
||||||
|
|
||||||
|
if (!follow) {
|
||||||
|
// Static log output
|
||||||
console.log(`Logs for process: ${id} (last ${lines} lines)`);
|
console.log(`Logs for process: ${id} (last ${lines} lines)`);
|
||||||
console.log('─'.repeat(60));
|
console.log('─'.repeat(60));
|
||||||
|
|
||||||
for (const log of response.logs) {
|
for (const log of response.logs) {
|
||||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||||
const prefix = log.type === 'stdout' ? '[OUT]' : '[ERR]';
|
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
|
||||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Streaming log output
|
||||||
|
console.log(`Logs for process: ${id} (streaming...)`);
|
||||||
|
console.log('─'.repeat(60));
|
||||||
|
|
||||||
|
// Display initial logs
|
||||||
|
let lastSeq = 0;
|
||||||
|
for (const log of response.logs) {
|
||||||
|
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||||
|
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
|
||||||
|
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||||
|
if (log.seq !== undefined) {
|
||||||
|
lastSeq = Math.max(lastSeq, log.seq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to real-time updates
|
||||||
|
await tspmIpcClient.subscribe(id, (log: any) => {
|
||||||
|
// Check for sequence gap or duplicate
|
||||||
|
if (log.seq !== undefined && log.seq <= lastSeq) {
|
||||||
|
return; // Skip duplicate
|
||||||
|
}
|
||||||
|
if (log.seq !== undefined && log.seq > lastSeq + 1) {
|
||||||
|
console.log(`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||||
|
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
|
||||||
|
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||||
|
|
||||||
|
if (log.seq !== undefined) {
|
||||||
|
lastSeq = log.seq;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Ctrl+C gracefully
|
||||||
|
let isCleaningUp = false;
|
||||||
|
const cleanup = async () => {
|
||||||
|
if (isCleaningUp) return;
|
||||||
|
isCleaningUp = true;
|
||||||
|
console.log('\n\nStopping log stream...');
|
||||||
|
try {
|
||||||
|
await tspmIpcClient.unsubscribe(id);
|
||||||
|
await tspmIpcClient.disconnect();
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', cleanup);
|
||||||
|
process.on('SIGTERM', cleanup);
|
||||||
|
|
||||||
|
// Keep the process alive
|
||||||
|
await new Promise(() => {}); // Block forever until interrupted
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting logs:', error.message);
|
handleDaemonError(error, 'get logs');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -473,8 +550,7 @@ export const run = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error starting all processes:', error.message);
|
handleDaemonError(error, 'start all processes');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -504,8 +580,7 @@ export const run = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error stopping all processes:', error.message);
|
handleDaemonError(error, 'stop all processes');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -537,8 +612,7 @@ export const run = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error restarting all processes:', error.message);
|
handleDaemonError(error, 'restart all processes');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -566,13 +640,36 @@ export const run = async (): Promise<void> => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Starting TSPM daemon...');
|
console.log('Starting TSPM daemon manually...');
|
||||||
await tspmIpcClient.connect();
|
|
||||||
console.log('✓ TSPM daemon started successfully');
|
// Import spawn to start daemon process
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
const daemonScript = plugins.path.join(
|
||||||
|
paths.packageDir,
|
||||||
|
'dist_ts',
|
||||||
|
'daemon.js',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start daemon as a regular background process (not detached)
|
||||||
|
const daemonProcess = spawn(process.execPath, [daemonScript], {
|
||||||
|
stdio: 'ignore',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
TSPM_DAEMON_MODE: 'true',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Started daemon with PID: ${daemonProcess.pid}`);
|
||||||
|
|
||||||
|
// Wait for daemon to be ready
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
const newStatus = await tspmIpcClient.getDaemonStatus();
|
const newStatus = await tspmIpcClient.getDaemonStatus();
|
||||||
if (newStatus) {
|
if (newStatus) {
|
||||||
|
console.log('✓ TSPM daemon started successfully');
|
||||||
console.log(` PID: ${newStatus.pid}`);
|
console.log(` PID: ${newStatus.pid}`);
|
||||||
|
console.log('\nNote: This daemon will run until you stop it or logout.');
|
||||||
|
console.log('For automatic startup, use "tspm enable" instead.');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error starting daemon:', error.message);
|
console.error('Error starting daemon:', error.message);
|
||||||
@@ -580,6 +677,13 @@ export const run = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'start-service':
|
||||||
|
// This is called by systemd - start the daemon directly
|
||||||
|
console.log('Starting TSPM daemon for systemd service...');
|
||||||
|
const { startDaemon } = await import('./classes.daemon.js');
|
||||||
|
await startDaemon();
|
||||||
|
break;
|
||||||
|
|
||||||
case 'stop':
|
case 'stop':
|
||||||
try {
|
try {
|
||||||
console.log('Stopping TSPM daemon...');
|
console.log('Stopping TSPM daemon...');
|
||||||
@@ -612,6 +716,9 @@ export const run = async (): Promise<void> => {
|
|||||||
`Memory: ${formatMemory(status.memoryUsage || 0)}`,
|
`Memory: ${formatMemory(status.memoryUsage || 0)}`,
|
||||||
);
|
);
|
||||||
console.log(`CPU: ${status.cpuUsage?.toFixed(1) || 0}s`);
|
console.log(`CPU: ${status.cpuUsage?.toFixed(1) || 0}s`);
|
||||||
|
|
||||||
|
// Disconnect from daemon after getting status
|
||||||
|
await tspmIpcClient.disconnect();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting daemon status:', error.message);
|
console.error('Error getting daemon status:', error.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -633,6 +740,58 @@ export const run = async (): Promise<void> => {
|
|||||||
complete: () => {},
|
complete: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enable command - Enable TSPM daemon as systemd service
|
||||||
|
smartcliInstance.addCommand('enable').subscribe({
|
||||||
|
next: async (argvArg: CliArguments) => {
|
||||||
|
try {
|
||||||
|
const serviceManager = new TspmServiceManager();
|
||||||
|
console.log('Enabling TSPM daemon as system service...');
|
||||||
|
|
||||||
|
await serviceManager.enableService();
|
||||||
|
|
||||||
|
console.log('✓ TSPM daemon enabled and started as system service');
|
||||||
|
console.log(' The daemon will now start automatically on system boot');
|
||||||
|
console.log(' Use "tspm disable" to remove the service');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error enabling service:', error.message);
|
||||||
|
if (error.message.includes('permission') || error.message.includes('denied')) {
|
||||||
|
console.log('\nNote: You may need to run this command with sudo');
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable command - Disable TSPM daemon systemd service
|
||||||
|
smartcliInstance.addCommand('disable').subscribe({
|
||||||
|
next: async (argvArg: CliArguments) => {
|
||||||
|
try {
|
||||||
|
const serviceManager = new TspmServiceManager();
|
||||||
|
console.log('Disabling TSPM daemon service...');
|
||||||
|
|
||||||
|
await serviceManager.disableService();
|
||||||
|
|
||||||
|
console.log('✓ TSPM daemon service disabled');
|
||||||
|
console.log(' The daemon will no longer start on system boot');
|
||||||
|
console.log(' Use "tspm enable" to re-enable the service');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error disabling service:', error.message);
|
||||||
|
if (error.message.includes('permission') || error.message.includes('denied')) {
|
||||||
|
console.log('\nNote: You may need to run this command with sudo');
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
cliLogger.error(err);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
// Start parsing commands
|
// Start parsing commands
|
||||||
smartcliInstance.startParse();
|
smartcliInstance.startParse();
|
||||||
};
|
};
|
||||||
|
@@ -2,6 +2,7 @@ export * from './classes.tspm.js';
|
|||||||
export * from './classes.processmonitor.js';
|
export * from './classes.processmonitor.js';
|
||||||
export * from './classes.daemon.js';
|
export * from './classes.daemon.js';
|
||||||
export * from './classes.ipcclient.js';
|
export * from './classes.ipcclient.js';
|
||||||
|
export * from './classes.servicemanager.js';
|
||||||
export * from './ipc.types.js';
|
export * from './ipc.types.js';
|
||||||
|
|
||||||
import * as cli from './cli.js';
|
import * as cli from './cli.js';
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
IProcessConfig,
|
IProcessConfig,
|
||||||
IProcessInfo,
|
IProcessInfo,
|
||||||
IProcessLog,
|
|
||||||
} from './classes.tspm.js';
|
} from './classes.tspm.js';
|
||||||
|
import type { IProcessLog } from './classes.processwrapper.js';
|
||||||
|
|
||||||
// Base message types
|
// Base message types
|
||||||
export interface IpcRequest<T = any> {
|
export interface IpcRequest<T = any> {
|
||||||
|
Reference in New Issue
Block a user