Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
35b6a6a8d0 | |||
50c5fdb0ea | |||
4e0944034b | |||
ca0dfa6432 |
20
changelog.md
20
changelog.md
@@ -1,5 +1,25 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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": "1.8.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,56 @@
|
|||||||
# TSPM Refactoring Plan: Central Daemon Architecture
|
# TSPM Real-Time Log Streaming Implementation Plan
|
||||||
|
|
||||||
## Problem Analysis
|
## Overview
|
||||||
|
Implementing real-time log streaming (tailing) functionality for TSPM using SmartIPC's pub/sub capabilities.
|
||||||
|
|
||||||
Currently, each `startAsDaemon` creates an isolated tspm instance with no coordination:
|
## Approach: Hybrid Request + Subscribe
|
||||||
|
1. Initial getLogs request to fetch historical logs up to current point
|
||||||
|
2. Subscribe to pub/sub channel for real-time updates
|
||||||
|
3. Use sequence numbers to detect and handle gaps/duplicates
|
||||||
|
4. Per-process topics for granular subscriptions
|
||||||
|
|
||||||
- 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
|
### Core Changes
|
||||||
|
- [x] Update IProcessLog interface with seq and runId fields
|
||||||
|
- [x] Add nextSeq and runId fields to ProcessWrapper class
|
||||||
|
- [x] Update addLog() methods to include sequencing
|
||||||
|
- [x] Implement pub/sub publishing in daemon
|
||||||
|
|
||||||
### 1. Central Daemon Manager (`ts/classes.daemon.ts`)
|
### IPC Client Updates
|
||||||
|
- [x] Add subscribe/unsubscribe methods to TspmIpcClient
|
||||||
|
- [ ] Implement log streaming handler
|
||||||
|
- [ ] Add connection state management for subscriptions
|
||||||
|
|
||||||
- Single daemon instance managing ALL processes
|
### CLI Enhancement
|
||||||
- Runs continuously in background
|
- [x] Add --follow flag to logs command
|
||||||
- Uses Unix socket for IPC at `~/.tspm/tspm.sock`
|
- [x] Implement streaming output with proper formatting
|
||||||
- Maintains single source of truth for process state
|
- [x] Handle Ctrl+C gracefully to unsubscribe
|
||||||
|
|
||||||
### 2. IPC Communication Layer (`ts/classes.ipc.ts`)
|
### Reliability Features
|
||||||
|
- [x] Add backpressure handling (drop oldest when buffer full)
|
||||||
|
- [x] Implement gap detection and recovery
|
||||||
|
- [x] Add process restart detection via runId
|
||||||
|
|
||||||
- **Framework**: Use `@push.rocks/smartipc` v2.0.1
|
### Testing
|
||||||
- **Server**: SmartIpc server in daemon using Unix Domain Socket
|
- [x] Test basic log streaming
|
||||||
- **Client**: SmartIpc client in CLI for all operations
|
- [x] Test gap recovery
|
||||||
- **Socket Path**: `~/.tspm/tspm.sock` (Unix) or named pipe (Windows)
|
- [x] Test high-volume logging scenarios
|
||||||
- **Protocol**: Type-safe request/response with SmartIpc's built-in patterns
|
- [x] Test process restart handling
|
||||||
- **Features**:
|
|
||||||
- Automatic reconnection with exponential backoff
|
|
||||||
- Heartbeat monitoring for daemon health
|
|
||||||
- Type-safe message contracts
|
|
||||||
- **Auto-start**: CLI starts daemon if connection fails
|
|
||||||
|
|
||||||
### 3. New CLI Commands
|
|
||||||
|
|
||||||
- `tspm enable` - Start central daemon using systemd/launchd
|
|
||||||
- `tspm disable` - Stop and disable central daemon
|
|
||||||
- `tspm status` - Show daemon status
|
|
||||||
- Remove `startAsDaemon` (replaced by daemon + `tspm start`)
|
|
||||||
|
|
||||||
### 4. Refactored CLI (`ts/cli.ts`)
|
|
||||||
|
|
||||||
All commands become daemon clients:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Before: Direct process management
|
|
||||||
await tspm.start(config);
|
|
||||||
|
|
||||||
// After: Send to daemon
|
|
||||||
await ipcClient.request('start', config);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. File Structure Changes
|
|
||||||
|
|
||||||
```
|
|
||||||
ts/
|
|
||||||
├── classes.daemon.ts # New: Central daemon server
|
|
||||||
├── classes.ipc.ts # New: IPC client/server
|
|
||||||
├── classes.tspm.ts # Modified: Used by daemon only
|
|
||||||
├── cli.ts # Modified: Becomes thin client
|
|
||||||
└── classes.daemonmanager.ts # New: Systemd/launchd integration
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Steps
|
|
||||||
|
|
||||||
### Phase 1: Core Infrastructure
|
|
||||||
|
|
||||||
- [ ] Add `@push.rocks/smartipc` dependency (v2.0.1)
|
|
||||||
- [ ] Create IPC message type definitions for all operations
|
|
||||||
- [ ] Implement daemon server with SmartIpc server
|
|
||||||
- [ ] Create IPC client wrapper for CLI
|
|
||||||
- [ ] Add daemon lifecycle management (enable/disable)
|
|
||||||
|
|
||||||
### Phase 2: CLI Refactoring
|
|
||||||
|
|
||||||
- [ ] Convert all CLI commands to SmartIpc client requests
|
|
||||||
- [ ] Add daemon auto-start logic with connection monitoring
|
|
||||||
- [ ] Leverage SmartIpc's built-in reconnection and error handling
|
|
||||||
- [ ] Implement type-safe message contracts for all commands
|
|
||||||
|
|
||||||
### Phase 3: Migration & Cleanup
|
|
||||||
|
|
||||||
- [ ] Migrate existing config to daemon-compatible format
|
|
||||||
- [ ] Remove `startAsDaemon` command
|
|
||||||
- [ ] Add migration guide for users
|
|
||||||
|
|
||||||
## Technical Details
|
## Technical Details
|
||||||
|
|
||||||
### IPC Implementation with SmartIpc
|
### Sequence Numbering
|
||||||
|
- Each log entry gets incrementing seq number per process
|
||||||
|
- runId changes on process restart
|
||||||
|
- Client tracks lastSeq to detect gaps
|
||||||
|
|
||||||
```typescript
|
### Topic Structure
|
||||||
// Daemon server setup
|
- Format: `logs.<processId>`
|
||||||
import { SmartIpc } from '@push.rocks/smartipc';
|
- Daemon publishes to topic on new log entries
|
||||||
|
- Clients subscribe to specific process topics
|
||||||
|
|
||||||
const ipcServer = SmartIpc.createServer({
|
### Backpressure Strategy
|
||||||
id: 'tspm-daemon',
|
- Circular buffer of 10,000 entries per process
|
||||||
socketPath: '~/.tspm/tspm.sock', // Unix socket
|
- Drop oldest entries when buffer full
|
||||||
});
|
- Client can detect gaps via sequence numbers
|
||||||
|
|
||||||
// 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: '1.8.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();
|
||||||
|
|
||||||
|
@@ -44,13 +44,33 @@ export class TspmIpcClient {
|
|||||||
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);
|
||||||
@@ -110,6 +130,30 @@ export class TspmIpcClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
*/
|
*/
|
||||||
@@ -176,18 +220,15 @@ export class TspmIpcClient {
|
|||||||
|
|
||||||
console.log(`Started daemon process with PID: ${daemonProcess.pid}`);
|
console.log(`Started daemon process with PID: ${daemonProcess.pid}`);
|
||||||
|
|
||||||
// Wait for daemon to be ready (check for socket file)
|
// Wait for daemon to be ready using SmartIPC's helper
|
||||||
const maxWaitTime = 10000; // 10 seconds
|
try {
|
||||||
const startTime = Date.now();
|
await plugins.smartipc.SmartIpc.waitForServer({
|
||||||
|
socketPath: this.socketPath,
|
||||||
while (Date.now() - startTime < maxWaitTime) {
|
timeoutMs: 15000,
|
||||||
if (await this.isDaemonRunning()) {
|
});
|
||||||
return;
|
} catch (error) {
|
||||||
}
|
throw new Error(`Daemon failed to start: ${error.message}`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Daemon failed to start within timeout period');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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);
|
||||||
|
@@ -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
|
||||||
|
78
ts/cli.ts
78
ts/cli.ts
@@ -426,20 +426,84 @@ 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);
|
console.error('Error getting logs:', error.message);
|
||||||
|
@@ -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