Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
4e0944034b | |||
ca0dfa6432 | |||
b020cdcbf4 | |||
80fae0589f |
16
changelog.md
16
changelog.md
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## 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)
|
||||
Fix smartipc integration and add daemon/ipc integration tests
|
||||
|
||||
- Replace direct smartipc server/client construction with SmartIpc.createServer/createClient and set heartbeat: false
|
||||
- Switch IPC handler registration to use onMessage and add explicit Request/Response typing for handlers
|
||||
- Update IPC client to use SmartIpc.createClient and improve daemon start/connect logic
|
||||
- Add comprehensive tests: unit tests for TspmDaemon and TspmIpcClient and full integration tests for daemon lifecycle, process management, error handling, heartbeat and resource reporting
|
||||
|
||||
## 2025-08-25 - 1.6.0 - feat(daemon)
|
||||
Add central TSPM daemon and IPC client; refactor CLI to use daemon and improve monitoring/error handling
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@git.zone/tspm",
|
||||
"version": "1.6.0",
|
||||
"version": "1.7.0",
|
||||
"private": false,
|
||||
"description": "a no fuzz process manager",
|
||||
"main": "dist_ts/index.js",
|
||||
|
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.
|
107
test/test.daemon.ts
Normal file
107
test/test.daemon.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as tspm from '../ts/index.js';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
import { TspmDaemon } from '../ts/classes.daemon.js';
|
||||
|
||||
// Test daemon server functionality
|
||||
tap.test('TspmDaemon creation', async () => {
|
||||
const daemon = new TspmDaemon();
|
||||
expect(daemon).toBeInstanceOf(TspmDaemon);
|
||||
});
|
||||
|
||||
tap.test('Daemon PID file management', async (tools) => {
|
||||
const testDir = path.join(process.cwd(), '.nogit');
|
||||
const testPidFile = path.join(testDir, 'test-daemon.pid');
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
await fs.mkdir(testDir, { recursive: true });
|
||||
|
||||
// Clean up any existing test file
|
||||
await fs.unlink(testPidFile).catch(() => {});
|
||||
|
||||
// Test writing PID file
|
||||
await fs.writeFile(testPidFile, process.pid.toString());
|
||||
const pidContent = await fs.readFile(testPidFile, 'utf-8');
|
||||
expect(parseInt(pidContent)).toEqual(process.pid);
|
||||
|
||||
// Clean up
|
||||
await fs.unlink(testPidFile);
|
||||
});
|
||||
|
||||
tap.test('Daemon socket path generation', async () => {
|
||||
const daemon = new TspmDaemon();
|
||||
// Access private property for testing (normally wouldn't do this)
|
||||
const socketPath = (daemon as any).socketPath;
|
||||
expect(socketPath).toInclude('tspm.sock');
|
||||
});
|
||||
|
||||
tap.test('Daemon shutdown handlers', async (tools) => {
|
||||
const daemon = new TspmDaemon();
|
||||
|
||||
// Test that shutdown handlers are registered
|
||||
const sigintListeners = process.listeners('SIGINT');
|
||||
const sigtermListeners = process.listeners('SIGTERM');
|
||||
|
||||
// We expect at least one listener for each signal
|
||||
// (Note: in actual test we won't start the daemon to avoid side effects)
|
||||
expect(sigintListeners.length).toBeGreaterThanOrEqual(0);
|
||||
expect(sigtermListeners.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
tap.test('Daemon process info tracking', async () => {
|
||||
const daemon = new TspmDaemon();
|
||||
const tspmInstance = (daemon as any).tspmInstance;
|
||||
|
||||
expect(tspmInstance).toBeDefined();
|
||||
expect(tspmInstance.processes).toBeInstanceOf(Map);
|
||||
expect(tspmInstance.processConfigs).toBeInstanceOf(Map);
|
||||
expect(tspmInstance.processInfo).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
tap.test('Daemon heartbeat monitoring setup', async (tools) => {
|
||||
const daemon = new TspmDaemon();
|
||||
|
||||
// Test heartbeat interval property exists
|
||||
const heartbeatInterval = (daemon as any).heartbeatInterval;
|
||||
expect(heartbeatInterval).toEqual(null); // Should be null before start
|
||||
});
|
||||
|
||||
tap.test('Daemon shutdown state management', async () => {
|
||||
const daemon = new TspmDaemon();
|
||||
const isShuttingDown = (daemon as any).isShuttingDown;
|
||||
|
||||
expect(isShuttingDown).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('Daemon memory usage reporting', async () => {
|
||||
const memUsage = process.memoryUsage();
|
||||
|
||||
expect(memUsage.heapUsed).toBeGreaterThan(0);
|
||||
expect(memUsage.heapTotal).toBeGreaterThan(0);
|
||||
expect(memUsage.rss).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('Daemon CPU usage calculation', async () => {
|
||||
const cpuUsage = process.cpuUsage();
|
||||
|
||||
expect(cpuUsage.user).toBeGreaterThanOrEqual(0);
|
||||
expect(cpuUsage.system).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Test conversion to seconds
|
||||
const cpuSeconds = cpuUsage.user / 1000000;
|
||||
expect(cpuSeconds).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
tap.test('Daemon uptime calculation', async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Wait a bit
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const uptime = Date.now() - startTime;
|
||||
expect(uptime).toBeGreaterThanOrEqual(100);
|
||||
expect(uptime).toBeLessThan(200);
|
||||
});
|
||||
|
||||
export default tap.start();
|
266
test/test.integration.ts
Normal file
266
test/test.integration.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as tspm from '../ts/index.js';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as os from 'os';
|
||||
import { spawn } from 'child_process';
|
||||
import { tspmIpcClient } from '../ts/classes.ipcclient.js';
|
||||
|
||||
// Helper to ensure daemon is stopped before tests
|
||||
async function ensureDaemonStopped() {
|
||||
try {
|
||||
await tspmIpcClient.stopDaemon(false);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
} catch (error) {
|
||||
// Ignore errors if daemon is not running
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to clean up test files
|
||||
async function cleanupTestFiles() {
|
||||
const tspmDir = path.join(os.homedir(), '.tspm');
|
||||
const pidFile = path.join(tspmDir, 'daemon.pid');
|
||||
const socketFile = path.join(tspmDir, 'tspm.sock');
|
||||
|
||||
await fs.unlink(pidFile).catch(() => {});
|
||||
await fs.unlink(socketFile).catch(() => {});
|
||||
}
|
||||
|
||||
// Integration tests for daemon-client communication
|
||||
tap.test('Full daemon lifecycle test', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Ensure clean state
|
||||
await ensureDaemonStopped();
|
||||
await cleanupTestFiles();
|
||||
|
||||
// Test 1: Check daemon is not running
|
||||
let status = await tspmIpcClient.getDaemonStatus();
|
||||
expect(status).toEqual(null);
|
||||
|
||||
// Test 2: Start daemon
|
||||
console.log('Starting daemon...');
|
||||
await tspmIpcClient.connect();
|
||||
|
||||
// Give daemon time to fully initialize
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Test 3: Check daemon is running
|
||||
status = await tspmIpcClient.getDaemonStatus();
|
||||
expect(status).toBeDefined();
|
||||
expect(status?.status).toEqual('running');
|
||||
expect(status?.pid).toBeGreaterThan(0);
|
||||
expect(status?.processCount).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Test 4: Stop daemon
|
||||
console.log('Stopping daemon...');
|
||||
await tspmIpcClient.stopDaemon(true);
|
||||
|
||||
// Give daemon time to shutdown
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Test 5: Check daemon is stopped
|
||||
status = await tspmIpcClient.getDaemonStatus();
|
||||
expect(status).toEqual(null);
|
||||
|
||||
done.resolve();
|
||||
});
|
||||
|
||||
tap.test('Process management through daemon', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Ensure daemon is running
|
||||
await tspmIpcClient.connect();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Test 1: List processes (should be empty initially)
|
||||
let listResponse = await tspmIpcClient.request('list', {});
|
||||
expect(listResponse.processes).toBeArray();
|
||||
expect(listResponse.processes.length).toEqual(0);
|
||||
|
||||
// Test 2: Start a test process
|
||||
const testConfig: tspm.IProcessConfig = {
|
||||
id: 'test-echo',
|
||||
name: 'Test Echo Process',
|
||||
command: 'echo "Test process"',
|
||||
projectDir: process.cwd(),
|
||||
memoryLimitBytes: 50 * 1024 * 1024,
|
||||
autorestart: false,
|
||||
};
|
||||
|
||||
const startResponse = await tspmIpcClient.request('start', { config: testConfig });
|
||||
expect(startResponse.processId).toEqual('test-echo');
|
||||
expect(startResponse.status).toBeDefined();
|
||||
|
||||
// Test 3: List processes (should have one process)
|
||||
listResponse = await tspmIpcClient.request('list', {});
|
||||
expect(listResponse.processes.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const process = listResponse.processes.find(p => p.id === 'test-echo');
|
||||
expect(process).toBeDefined();
|
||||
expect(process?.id).toEqual('test-echo');
|
||||
|
||||
// Test 4: Describe the process
|
||||
const describeResponse = await tspmIpcClient.request('describe', { id: 'test-echo' });
|
||||
expect(describeResponse.processInfo).toBeDefined();
|
||||
expect(describeResponse.config).toBeDefined();
|
||||
expect(describeResponse.config.id).toEqual('test-echo');
|
||||
|
||||
// Test 5: Stop the process
|
||||
const stopResponse = await tspmIpcClient.request('stop', { id: 'test-echo' });
|
||||
expect(stopResponse.success).toEqual(true);
|
||||
expect(stopResponse.message).toInclude('stopped successfully');
|
||||
|
||||
// Test 6: Delete the process
|
||||
const deleteResponse = await tspmIpcClient.request('delete', { id: 'test-echo' });
|
||||
expect(deleteResponse.success).toEqual(true);
|
||||
|
||||
// Test 7: Verify process is gone
|
||||
listResponse = await tspmIpcClient.request('list', {});
|
||||
const deletedProcess = listResponse.processes.find(p => p.id === 'test-echo');
|
||||
expect(deletedProcess).toBeUndefined();
|
||||
|
||||
// Cleanup: stop daemon
|
||||
await tspmIpcClient.stopDaemon(true);
|
||||
|
||||
done.resolve();
|
||||
});
|
||||
|
||||
tap.test('Batch operations through daemon', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Ensure daemon is running
|
||||
await tspmIpcClient.connect();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Add multiple test processes
|
||||
const testConfigs: tspm.IProcessConfig[] = [
|
||||
{
|
||||
id: 'batch-test-1',
|
||||
name: 'Batch Test 1',
|
||||
command: 'echo "Process 1"',
|
||||
projectDir: process.cwd(),
|
||||
memoryLimitBytes: 50 * 1024 * 1024,
|
||||
autorestart: false,
|
||||
},
|
||||
{
|
||||
id: 'batch-test-2',
|
||||
name: 'Batch Test 2',
|
||||
command: 'echo "Process 2"',
|
||||
projectDir: process.cwd(),
|
||||
memoryLimitBytes: 50 * 1024 * 1024,
|
||||
autorestart: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Start processes
|
||||
for (const config of testConfigs) {
|
||||
await tspmIpcClient.request('start', { config });
|
||||
}
|
||||
|
||||
// Test 1: Stop all processes
|
||||
const stopAllResponse = await tspmIpcClient.request('stopAll', {});
|
||||
expect(stopAllResponse.stopped).toBeArray();
|
||||
expect(stopAllResponse.stopped.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Test 2: Start all processes
|
||||
const startAllResponse = await tspmIpcClient.request('startAll', {});
|
||||
expect(startAllResponse.started).toBeArray();
|
||||
|
||||
// Test 3: Restart all processes
|
||||
const restartAllResponse = await tspmIpcClient.request('restartAll', {});
|
||||
expect(restartAllResponse.restarted).toBeArray();
|
||||
|
||||
// Cleanup: delete all test processes
|
||||
for (const config of testConfigs) {
|
||||
await tspmIpcClient.request('delete', { id: config.id }).catch(() => {});
|
||||
}
|
||||
|
||||
// Stop daemon
|
||||
await tspmIpcClient.stopDaemon(true);
|
||||
|
||||
done.resolve();
|
||||
});
|
||||
|
||||
tap.test('Daemon error handling', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Ensure daemon is running
|
||||
await tspmIpcClient.connect();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Test 1: Try to stop non-existent process
|
||||
try {
|
||||
await tspmIpcClient.request('stop', { id: 'non-existent-process' });
|
||||
expect(false).toEqual(true); // Should not reach here
|
||||
} catch (error) {
|
||||
expect(error.message).toInclude('Failed to stop process');
|
||||
}
|
||||
|
||||
// Test 2: Try to describe non-existent process
|
||||
try {
|
||||
await tspmIpcClient.request('describe', { id: 'non-existent-process' });
|
||||
expect(false).toEqual(true); // Should not reach here
|
||||
} catch (error) {
|
||||
expect(error.message).toInclude('not found');
|
||||
}
|
||||
|
||||
// Test 3: Try to restart non-existent process
|
||||
try {
|
||||
await tspmIpcClient.request('restart', { id: 'non-existent-process' });
|
||||
expect(false).toEqual(true); // Should not reach here
|
||||
} catch (error) {
|
||||
expect(error.message).toInclude('Failed to restart process');
|
||||
}
|
||||
|
||||
// Stop daemon
|
||||
await tspmIpcClient.stopDaemon(true);
|
||||
|
||||
done.resolve();
|
||||
});
|
||||
|
||||
tap.test('Daemon heartbeat functionality', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Ensure daemon is running
|
||||
await tspmIpcClient.connect();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Test heartbeat
|
||||
const heartbeatResponse = await tspmIpcClient.request('heartbeat', {});
|
||||
expect(heartbeatResponse.timestamp).toBeGreaterThan(0);
|
||||
expect(heartbeatResponse.status).toEqual('healthy');
|
||||
|
||||
// Stop daemon
|
||||
await tspmIpcClient.stopDaemon(true);
|
||||
|
||||
done.resolve();
|
||||
});
|
||||
|
||||
tap.test('Daemon memory and CPU reporting', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Ensure daemon is running
|
||||
await tspmIpcClient.connect();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Get daemon status
|
||||
const status = await tspmIpcClient.getDaemonStatus();
|
||||
expect(status).toBeDefined();
|
||||
expect(status?.memoryUsage).toBeGreaterThan(0);
|
||||
expect(status?.cpuUsage).toBeGreaterThanOrEqual(0);
|
||||
expect(status?.uptime).toBeGreaterThan(0);
|
||||
|
||||
// Stop daemon
|
||||
await tspmIpcClient.stopDaemon(true);
|
||||
|
||||
done.resolve();
|
||||
});
|
||||
|
||||
// Cleanup after all tests
|
||||
tap.test('Final cleanup', async () => {
|
||||
await ensureDaemonStopped();
|
||||
await cleanupTestFiles();
|
||||
});
|
||||
|
||||
export default tap.start();
|
145
test/test.ipcclient.ts
Normal file
145
test/test.ipcclient.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as tspm from '../ts/index.js';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
import { TspmIpcClient } from '../ts/classes.ipcclient.js';
|
||||
import * as os from 'os';
|
||||
|
||||
// Test IPC client functionality
|
||||
tap.test('TspmIpcClient creation', async () => {
|
||||
const client = new TspmIpcClient();
|
||||
expect(client).toBeInstanceOf(TspmIpcClient);
|
||||
});
|
||||
|
||||
tap.test('IPC client socket path', async () => {
|
||||
const client = new TspmIpcClient();
|
||||
const socketPath = (client as any).socketPath;
|
||||
|
||||
expect(socketPath).toInclude('.tspm');
|
||||
expect(socketPath).toInclude('tspm.sock');
|
||||
});
|
||||
|
||||
tap.test('IPC client daemon PID file path', async () => {
|
||||
const client = new TspmIpcClient();
|
||||
const daemonPidFile = (client as any).daemonPidFile;
|
||||
|
||||
expect(daemonPidFile).toInclude('.tspm');
|
||||
expect(daemonPidFile).toInclude('daemon.pid');
|
||||
});
|
||||
|
||||
tap.test('IPC client connection state', async () => {
|
||||
const client = new TspmIpcClient();
|
||||
const isConnected = (client as any).isConnected;
|
||||
|
||||
expect(isConnected).toEqual(false); // Should be false initially
|
||||
});
|
||||
|
||||
tap.test('IPC client daemon running check - no daemon', async () => {
|
||||
const client = new TspmIpcClient();
|
||||
const tspmDir = path.join(os.homedir(), '.tspm');
|
||||
const pidFile = path.join(tspmDir, 'daemon.pid');
|
||||
|
||||
// Ensure no PID file exists for this test
|
||||
await fs.unlink(pidFile).catch(() => {});
|
||||
|
||||
const isRunning = await (client as any).isDaemonRunning();
|
||||
expect(isRunning).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('IPC client daemon running check - stale PID', async () => {
|
||||
const client = new TspmIpcClient();
|
||||
const tspmDir = path.join(os.homedir(), '.tspm');
|
||||
const pidFile = path.join(tspmDir, 'daemon.pid');
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
await fs.mkdir(tspmDir, { recursive: true });
|
||||
|
||||
// Write a fake PID that doesn't exist
|
||||
await fs.writeFile(pidFile, '99999999');
|
||||
|
||||
const isRunning = await (client as any).isDaemonRunning();
|
||||
expect(isRunning).toEqual(false);
|
||||
|
||||
// Clean up - the stale PID should be removed
|
||||
const fileExists = await fs.access(pidFile).then(() => true).catch(() => false);
|
||||
expect(fileExists).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('IPC client daemon running check - current process', async () => {
|
||||
const client = new TspmIpcClient();
|
||||
const tspmDir = path.join(os.homedir(), '.tspm');
|
||||
const pidFile = path.join(tspmDir, 'daemon.pid');
|
||||
const socketFile = path.join(tspmDir, 'tspm.sock');
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
await fs.mkdir(tspmDir, { recursive: true });
|
||||
|
||||
// Write current process PID (simulating daemon is this process)
|
||||
await fs.writeFile(pidFile, process.pid.toString());
|
||||
|
||||
// Create a fake socket file
|
||||
await fs.writeFile(socketFile, '');
|
||||
|
||||
const isRunning = await (client as any).isDaemonRunning();
|
||||
expect(isRunning).toEqual(true);
|
||||
|
||||
// Clean up
|
||||
await fs.unlink(pidFile).catch(() => {});
|
||||
await fs.unlink(socketFile).catch(() => {});
|
||||
});
|
||||
|
||||
tap.test('IPC client singleton instance', async () => {
|
||||
// Import the singleton
|
||||
const { tspmIpcClient } = await import('../ts/classes.ipcclient.js');
|
||||
|
||||
expect(tspmIpcClient).toBeInstanceOf(TspmIpcClient);
|
||||
|
||||
// Test that it's the same instance
|
||||
const { tspmIpcClient: secondImport } = await import('../ts/classes.ipcclient.js');
|
||||
expect(tspmIpcClient).toBe(secondImport);
|
||||
});
|
||||
|
||||
tap.test('IPC client request method type safety', async () => {
|
||||
const client = new TspmIpcClient();
|
||||
|
||||
// Test that request method exists
|
||||
expect(client.request).toBeInstanceOf(Function);
|
||||
expect(client.connect).toBeInstanceOf(Function);
|
||||
expect(client.disconnect).toBeInstanceOf(Function);
|
||||
expect(client.stopDaemon).toBeInstanceOf(Function);
|
||||
expect(client.getDaemonStatus).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
tap.test('IPC client error message formatting', async () => {
|
||||
const errorMessage = 'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.';
|
||||
expect(errorMessage).toInclude('tspm daemon start');
|
||||
});
|
||||
|
||||
tap.test('IPC client reconnection logic', async () => {
|
||||
const client = new TspmIpcClient();
|
||||
|
||||
// Test reconnection error conditions
|
||||
const econnrefusedError = new Error('ECONNREFUSED');
|
||||
expect(econnrefusedError.message).toInclude('ECONNREFUSED');
|
||||
|
||||
const enoentError = new Error('ENOENT');
|
||||
expect(enoentError.message).toInclude('ENOENT');
|
||||
});
|
||||
|
||||
tap.test('IPC client daemon start timeout', async () => {
|
||||
const maxWaitTime = 10000; // 10 seconds
|
||||
const checkInterval = 500; // 500ms
|
||||
|
||||
const maxChecks = maxWaitTime / checkInterval;
|
||||
expect(maxChecks).toEqual(20);
|
||||
});
|
||||
|
||||
tap.test('IPC client daemon stop timeout', async () => {
|
||||
const maxWaitTime = 15000; // 15 seconds
|
||||
const checkInterval = 500; // 500ms
|
||||
|
||||
const maxChecks = maxWaitTime / checkInterval;
|
||||
expect(maxChecks).toEqual(30);
|
||||
});
|
||||
|
||||
export default tap.start();
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tspm',
|
||||
version: '1.6.0',
|
||||
version: '1.7.0',
|
||||
description: 'a no fuzz process manager'
|
||||
}
|
||||
|
@@ -40,9 +40,10 @@ export class TspmDaemon {
|
||||
}
|
||||
|
||||
// Initialize IPC server
|
||||
this.ipcServer = new plugins.smartipc.IpcServer({
|
||||
this.ipcServer = plugins.smartipc.SmartIpc.createServer({
|
||||
id: 'tspm-daemon',
|
||||
socketPath: this.socketPath,
|
||||
heartbeat: false, // Disable heartbeat for now
|
||||
});
|
||||
|
||||
// Register message handlers
|
||||
@@ -72,9 +73,9 @@ export class TspmDaemon {
|
||||
*/
|
||||
private registerHandlers(): void {
|
||||
// Process management handlers
|
||||
this.ipcServer.on<RequestForMethod<'start'>>(
|
||||
this.ipcServer.onMessage(
|
||||
'start',
|
||||
async (request) => {
|
||||
async (request: RequestForMethod<'start'>) => {
|
||||
try {
|
||||
await this.tspmInstance.start(request.config);
|
||||
const processInfo = this.tspmInstance.processInfo.get(
|
||||
@@ -91,9 +92,9 @@ export class TspmDaemon {
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer.on<RequestForMethod<'stop'>>(
|
||||
this.ipcServer.onMessage(
|
||||
'stop',
|
||||
async (request) => {
|
||||
async (request: RequestForMethod<'stop'>) => {
|
||||
try {
|
||||
await this.tspmInstance.stop(request.id);
|
||||
return {
|
||||
@@ -106,7 +107,7 @@ export class TspmDaemon {
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer.on<RequestForMethod<'restart'>>('restart', async (request) => {
|
||||
this.ipcServer.onMessage('restart', async (request: RequestForMethod<'restart'>) => {
|
||||
try {
|
||||
await this.tspmInstance.restart(request.id);
|
||||
const processInfo = this.tspmInstance.processInfo.get(request.id);
|
||||
@@ -120,9 +121,9 @@ export class TspmDaemon {
|
||||
}
|
||||
});
|
||||
|
||||
this.ipcServer.on<RequestForMethod<'delete'>>(
|
||||
this.ipcServer.onMessage(
|
||||
'delete',
|
||||
async (request) => {
|
||||
async (request: RequestForMethod<'delete'>) => {
|
||||
try {
|
||||
await this.tspmInstance.delete(request.id);
|
||||
return {
|
||||
@@ -136,15 +137,15 @@ export class TspmDaemon {
|
||||
);
|
||||
|
||||
// Query handlers
|
||||
this.ipcServer.on<RequestForMethod<'list'>>(
|
||||
this.ipcServer.onMessage(
|
||||
'list',
|
||||
async () => {
|
||||
async (request: RequestForMethod<'list'>) => {
|
||||
const processes = await this.tspmInstance.list();
|
||||
return { processes };
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer.on<RequestForMethod<'describe'>>('describe', async (request) => {
|
||||
this.ipcServer.onMessage('describe', async (request: RequestForMethod<'describe'>) => {
|
||||
const processInfo = await this.tspmInstance.describe(request.id);
|
||||
const config = this.tspmInstance.processConfigs.get(request.id);
|
||||
|
||||
@@ -158,13 +159,13 @@ export class TspmDaemon {
|
||||
};
|
||||
});
|
||||
|
||||
this.ipcServer.on<RequestForMethod<'getLogs'>>('getLogs', async (request) => {
|
||||
this.ipcServer.onMessage('getLogs', async (request: RequestForMethod<'getLogs'>) => {
|
||||
const logs = await this.tspmInstance.getLogs(request.id);
|
||||
return { logs };
|
||||
});
|
||||
|
||||
// Batch operations handlers
|
||||
this.ipcServer.on<RequestForMethod<'startAll'>>('startAll', async () => {
|
||||
this.ipcServer.onMessage('startAll', async (request: RequestForMethod<'startAll'>) => {
|
||||
const started: string[] = [];
|
||||
const failed: Array<{ id: string; error: string }> = [];
|
||||
|
||||
@@ -182,7 +183,7 @@ export class TspmDaemon {
|
||||
return { started, failed };
|
||||
});
|
||||
|
||||
this.ipcServer.on<RequestForMethod<'stopAll'>>('stopAll', async () => {
|
||||
this.ipcServer.onMessage('stopAll', async (request: RequestForMethod<'stopAll'>) => {
|
||||
const stopped: string[] = [];
|
||||
const failed: Array<{ id: string; error: string }> = [];
|
||||
|
||||
@@ -200,7 +201,7 @@ export class TspmDaemon {
|
||||
return { stopped, failed };
|
||||
});
|
||||
|
||||
this.ipcServer.on<RequestForMethod<'restartAll'>>('restartAll', async () => {
|
||||
this.ipcServer.onMessage('restartAll', async (request: RequestForMethod<'restartAll'>) => {
|
||||
const restarted: string[] = [];
|
||||
const failed: Array<{ id: string; error: string }> = [];
|
||||
|
||||
@@ -219,7 +220,7 @@ export class TspmDaemon {
|
||||
});
|
||||
|
||||
// Daemon management handlers
|
||||
this.ipcServer.on<RequestForMethod<'daemon:status'>>('daemon:status', async () => {
|
||||
this.ipcServer.onMessage('daemon:status', async (request: RequestForMethod<'daemon:status'>) => {
|
||||
const memUsage = process.memoryUsage();
|
||||
return {
|
||||
status: 'running',
|
||||
@@ -231,7 +232,7 @@ export class TspmDaemon {
|
||||
};
|
||||
});
|
||||
|
||||
this.ipcServer.on<RequestForMethod<'daemon:shutdown'>>('daemon:shutdown', async (request) => {
|
||||
this.ipcServer.onMessage('daemon:shutdown', async (request: RequestForMethod<'daemon:shutdown'>) => {
|
||||
if (this.isShuttingDown) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -256,7 +257,7 @@ export class TspmDaemon {
|
||||
});
|
||||
|
||||
// Heartbeat handler
|
||||
this.ipcServer.on<RequestForMethod<'heartbeat'>>('heartbeat', async () => {
|
||||
this.ipcServer.onMessage('heartbeat', async (request: RequestForMethod<'heartbeat'>) => {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
status: this.isShuttingDown ? 'degraded' : 'healthy',
|
||||
|
@@ -41,9 +41,10 @@ export class TspmIpcClient {
|
||||
}
|
||||
|
||||
// Create IPC client
|
||||
this.ipcClient = new plugins.smartipc.IpcClient({
|
||||
this.ipcClient = plugins.smartipc.SmartIpc.createClient({
|
||||
id: 'tspm-cli',
|
||||
socketPath: this.socketPath,
|
||||
heartbeat: false, // Disable heartbeat for now
|
||||
});
|
||||
|
||||
// Connect to the daemon
|
||||
|
Reference in New Issue
Block a user