Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
1c2310c185 | |||
d33a001edc | |||
35b6a6a8d0 | |||
50c5fdb0ea | |||
4e0944034b | |||
ca0dfa6432 |
31
changelog.md
31
changelog.md
@@ -1,5 +1,36 @@
|
|||||||
# 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)
|
||||||
|
Add comprehensive README with detailed usage, command reference, daemon management, architecture and development instructions
|
||||||
|
|
||||||
|
- Expanded README from a short placeholder to a full documentation covering: Quick Start, Installation, Command Reference, Daemon Management, Monitoring & Information, Batch Operations, Architecture, Programmatic Usage, Advanced Features, Development, Debugging, Performance, and Legal information
|
||||||
|
- Included usage examples and CLI command reference for start/stop/restart/delete/list/describe/logs and batch/daemon commands
|
||||||
|
- Added human-friendly memory formatting and examples, process and daemon status outputs, and programmatic TypeScript usage snippet
|
||||||
|
- Improved onboarding instructions: cloning, installing, testing, building, and running the project
|
||||||
|
|
||||||
## 2025-08-25 - 1.6.1 - fix(daemon)
|
## 2025-08-25 - 1.6.1 - fix(daemon)
|
||||||
Fix smartipc integration and add daemon/ipc integration tests
|
Fix smartipc integration and add daemon/ipc integration tests
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tspm",
|
"name": "@git.zone/tspm",
|
||||||
"version": "1.6.1",
|
"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
|
||||||
|
341
readme.md
341
readme.md
@@ -1,7 +1,340 @@
|
|||||||
# @git.zone/tspm
|
# @git.zone/tspm 🚀
|
||||||
|
|
||||||
a no fuzz process manager
|
**TypeScript Process Manager** - A robust, no-fuss process manager designed specifically for TypeScript and Node.js applications. Built for developers who need reliable process management without the complexity.
|
||||||
|
|
||||||
## How to create the docs
|
## 🎯 What TSPM Does
|
||||||
|
|
||||||
To create docs run gitzone aidoc.
|
TSPM is your production-ready process manager that handles the hard parts of running Node.js applications:
|
||||||
|
|
||||||
|
- **Automatic Memory Management** - Set memory limits and let TSPM handle the rest
|
||||||
|
- **Smart Auto-Restart** - Crashed processes come back automatically (when you want them to)
|
||||||
|
- **File Watching** - Auto-restart on file changes during development
|
||||||
|
- **Process Groups** - Track parent and child processes together
|
||||||
|
- **Daemon Architecture** - Survives terminal sessions with a persistent background daemon
|
||||||
|
- **Beautiful CLI** - Clean, informative terminal output with real-time status
|
||||||
|
- **Structured Logging** - Capture and manage stdout/stderr with intelligent buffering
|
||||||
|
- **Zero Config** - Works out of the box, customize when you need to
|
||||||
|
|
||||||
|
## 📦 Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install globally
|
||||||
|
npm install -g @git.zone/tspm
|
||||||
|
|
||||||
|
# Or with pnpm (recommended)
|
||||||
|
pnpm add -g @git.zone/tspm
|
||||||
|
|
||||||
|
# Or use in your project
|
||||||
|
npm install --save-dev @git.zone/tspm
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the daemon (happens automatically on first use)
|
||||||
|
tspm daemon start
|
||||||
|
|
||||||
|
# Start a process
|
||||||
|
tspm start server.js --name my-server
|
||||||
|
|
||||||
|
# Start with memory limit
|
||||||
|
tspm start app.js --memory 512MB --name my-app
|
||||||
|
|
||||||
|
# Start with file watching (great for development)
|
||||||
|
tspm start dev.js --watch --name dev-server
|
||||||
|
|
||||||
|
# List all processes
|
||||||
|
tspm list
|
||||||
|
|
||||||
|
# Check process details
|
||||||
|
tspm describe my-server
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
tspm logs my-server --lines 100
|
||||||
|
|
||||||
|
# Stop a process
|
||||||
|
tspm stop my-server
|
||||||
|
|
||||||
|
# Restart a process
|
||||||
|
tspm restart my-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Command Reference
|
||||||
|
|
||||||
|
### Process Management
|
||||||
|
|
||||||
|
#### `tspm start <script> [options]`
|
||||||
|
Start a new process with automatic monitoring and management.
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--name <name>` - Custom name for the process (default: script name)
|
||||||
|
- `--memory <size>` - Memory limit (e.g., "512MB", "2GB", default: 512MB)
|
||||||
|
- `--cwd <path>` - Working directory (default: current directory)
|
||||||
|
- `--watch` - Enable file watching for auto-restart
|
||||||
|
- `--watch-paths <paths>` - Comma-separated paths to watch (with --watch)
|
||||||
|
- `--autorestart` - Auto-restart on crash (default: true)
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
# Simple start
|
||||||
|
tspm start server.js
|
||||||
|
|
||||||
|
# Production setup with 2GB memory
|
||||||
|
tspm start app.js --name production-api --memory 2GB
|
||||||
|
|
||||||
|
# Development with watching
|
||||||
|
tspm start dev-server.js --watch --watch-paths "src,config" --name dev
|
||||||
|
|
||||||
|
# Custom working directory
|
||||||
|
tspm start ../other-project/index.js --cwd ../other-project --name other
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tspm stop <id>`
|
||||||
|
Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm stop my-server
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tspm restart <id>`
|
||||||
|
Stop and restart a process with the same configuration.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm restart my-server
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tspm delete <id>`
|
||||||
|
Stop and remove a process from TSPM management.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm delete old-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring & Information
|
||||||
|
|
||||||
|
#### `tspm list`
|
||||||
|
Display all managed processes in a beautiful table.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm list
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
┌─────────┬─────────────┬───────────┬───────────┬──────────┐
|
||||||
|
│ ID │ Name │ Status │ Memory │ Restarts │
|
||||||
|
├─────────┼─────────────┼───────────┼───────────┼──────────┤
|
||||||
|
│ my-app │ my-app │ online │ 245.3 MB │ 0 │
|
||||||
|
│ worker │ worker │ online │ 128.7 MB │ 2 │
|
||||||
|
└─────────┴─────────────┴───────────┴───────────┴──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tspm describe <id>`
|
||||||
|
Get detailed information about a specific process.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm describe my-server
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
Process Details: my-server
|
||||||
|
────────────────────────────────────────
|
||||||
|
Status: online
|
||||||
|
PID: 45123
|
||||||
|
Memory: 245.3 MB
|
||||||
|
CPU: 2.3%
|
||||||
|
Uptime: 3600s
|
||||||
|
Restarts: 0
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
Command: server.js
|
||||||
|
Directory: /home/user/project
|
||||||
|
Memory Limit: 2 GB
|
||||||
|
Auto-restart: true
|
||||||
|
Watch: enabled
|
||||||
|
Watch Paths: src, config
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tspm logs <id> [options]`
|
||||||
|
View process logs (stdout and stderr).
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `--lines <n>` - Number of lines to display (default: 50)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm logs my-server --lines 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Operations
|
||||||
|
|
||||||
|
#### `tspm start-all`
|
||||||
|
Start all saved processes at once.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm start-all
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tspm stop-all`
|
||||||
|
Stop all running processes.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm stop-all
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tspm restart-all`
|
||||||
|
Restart all running processes.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm restart-all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Daemon Management
|
||||||
|
|
||||||
|
#### `tspm daemon start`
|
||||||
|
Start the TSPM daemon (happens automatically on first command).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm daemon start
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tspm daemon stop`
|
||||||
|
Stop the TSPM daemon and all managed processes.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm daemon stop
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tspm daemon status`
|
||||||
|
Check daemon health and statistics.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm daemon status
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
TSPM Daemon Status:
|
||||||
|
────────────────────────────────────────
|
||||||
|
Status: running
|
||||||
|
PID: 12345
|
||||||
|
Uptime: 86400s
|
||||||
|
Processes: 5
|
||||||
|
Memory: 45.2 MB
|
||||||
|
CPU: 0.1%
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
TSPM uses a three-tier architecture for maximum reliability:
|
||||||
|
|
||||||
|
1. **ProcessWrapper** - Low-level process management with stream handling
|
||||||
|
2. **ProcessMonitor** - Adds monitoring, memory limits, and auto-restart logic
|
||||||
|
3. **Tspm Core** - High-level orchestration with configuration persistence
|
||||||
|
|
||||||
|
The daemon architecture ensures your processes keep running even after you close your terminal. All process communication happens through a robust IPC (Inter-Process Communication) system.
|
||||||
|
|
||||||
|
## 🎮 Programmatic Usage
|
||||||
|
|
||||||
|
TSPM can also be used as a library in your Node.js applications:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Tspm } from '@git.zone/tspm';
|
||||||
|
|
||||||
|
const manager = new Tspm();
|
||||||
|
|
||||||
|
// Start a process
|
||||||
|
const processId = await manager.start({
|
||||||
|
id: 'worker',
|
||||||
|
name: 'Background Worker',
|
||||||
|
command: 'node worker.js',
|
||||||
|
projectDir: process.cwd(),
|
||||||
|
memoryLimitBytes: 512 * 1024 * 1024, // 512MB
|
||||||
|
autorestart: true,
|
||||||
|
watch: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor process
|
||||||
|
const info = await manager.getProcessInfo(processId);
|
||||||
|
console.log(`Process ${info.id} is ${info.status}`);
|
||||||
|
|
||||||
|
// Stop process
|
||||||
|
await manager.stop(processId);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Advanced Features
|
||||||
|
|
||||||
|
### Memory Limit Enforcement
|
||||||
|
TSPM tracks memory usage including all child processes spawned by your application. When a process exceeds its memory limit, it's gracefully restarted.
|
||||||
|
|
||||||
|
### Process Group Tracking
|
||||||
|
Using `ps-tree`, TSPM monitors not just your main process but all child processes it spawns, ensuring complete cleanup on stop/restart.
|
||||||
|
|
||||||
|
### Intelligent Logging
|
||||||
|
Logs are buffered and managed efficiently, preventing memory issues from excessive output while ensuring you don't lose important information.
|
||||||
|
|
||||||
|
### Graceful Shutdown
|
||||||
|
Processes receive SIGTERM first, allowing them to clean up. After a timeout, SIGKILL ensures termination.
|
||||||
|
|
||||||
|
### Configuration Persistence
|
||||||
|
Process configurations are saved, allowing you to restart all processes after a system reboot with a single command.
|
||||||
|
|
||||||
|
## 🛠️ Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://code.foss.global/git.zone/tspm.git
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Start development
|
||||||
|
pnpm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Debugging
|
||||||
|
|
||||||
|
Enable debug mode for verbose logging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export TSPM_DEBUG=true
|
||||||
|
tspm list
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Performance
|
||||||
|
|
||||||
|
TSPM is designed to be lightweight and efficient:
|
||||||
|
- Minimal CPU overhead (typically < 0.5%)
|
||||||
|
- Small memory footprint (~30-50MB for the daemon)
|
||||||
|
- Fast process startup and shutdown
|
||||||
|
- Efficient log buffering and rotation
|
||||||
|
|
||||||
|
## 🤝 Why TSPM?
|
||||||
|
|
||||||
|
Unlike general-purpose process managers, TSPM is built specifically for the TypeScript/Node.js ecosystem:
|
||||||
|
|
||||||
|
- **TypeScript First** - Written in TypeScript, for TypeScript projects
|
||||||
|
- **ESM Native** - Full support for ES modules
|
||||||
|
- **Developer Friendly** - Beautiful CLI output and helpful error messages
|
||||||
|
- **Production Ready** - Battle-tested memory management and error handling
|
||||||
|
- **No Configuration Required** - Sensible defaults that just work
|
||||||
|
- **Modern Architecture** - Async/await throughout, no callback hell
|
||||||
|
|
||||||
|
## License and Legal Information
|
||||||
|
|
||||||
|
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||||
|
|
||||||
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
### Trademarks
|
||||||
|
|
||||||
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||||
|
|
||||||
|
### Company Information
|
||||||
|
|
||||||
|
Task Venture Capital GmbH
|
||||||
|
Registered at District court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
|
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
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.6.1',
|
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
|
||||||
|
231
ts/cli.ts
231
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 });
|
||||||
|
|
||||||
console.log(`Logs for process: ${id} (last ${lines} lines)`);
|
if (!follow) {
|
||||||
console.log('─'.repeat(60));
|
// Static log output
|
||||||
|
console.log(`Logs for process: ${id} (last ${lines} lines)`);
|
||||||
|
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