fix(crash-logging): migrate filesystem persistence to smartfs and stabilize crash log tests
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"fileMatch": ["/.smartconfig.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-24 - 5.10.4 - fix(crash-logging)
|
||||
migrate filesystem persistence to smartfs and stabilize crash log tests
|
||||
|
||||
- replace smartfile usage with smartfs in crash log and log persistence modules
|
||||
- update crash log tests to use tap assertions and current CLI command output
|
||||
- move project config from npmextra.json to .smartconfig.json and refresh build dependencies
|
||||
|
||||
## 2026-03-24 - 5.10.3 - fix(config)
|
||||
replace npmextra with smartconfig for daemon key-value storage and release settings
|
||||
|
||||
|
||||
21
license
Normal file
21
license
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Task Venture Capital GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
22
package.json
22
package.json
@@ -16,7 +16,7 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/ --verbose --logfile --timeout 60)",
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"build": "(tsbuild)",
|
||||
"buildDocs": "(tsdoc)",
|
||||
"start": "(tsrun ./cli.ts -v)"
|
||||
},
|
||||
@@ -24,19 +24,19 @@
|
||||
"tspm": "./cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.6.8",
|
||||
"@git.zone/tsbundle": "^2.5.1",
|
||||
"@git.zone/tsrun": "^1.2.46",
|
||||
"@git.zone/tstest": "^2.3.5",
|
||||
"@git.zone/tsbuild": "^4.3.0",
|
||||
"@git.zone/tsbundle": "^2.9.3",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.5.1",
|
||||
"@push.rocks/tapbundle": "^6.0.3",
|
||||
"@types/node": "^22.13.10"
|
||||
"@types/node": "^25.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@push.rocks/smartconfig": "^6.0.0",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@push.rocks/smartcli": "^4.0.11",
|
||||
"@push.rocks/smartcli": "^4.0.20",
|
||||
"@push.rocks/smartconfig": "^6.0.1",
|
||||
"@push.rocks/smartdaemon": "^2.1.0",
|
||||
"@push.rocks/smartfile": "^11.2.7",
|
||||
"@push.rocks/smartfs": "^1.5.0",
|
||||
"@push.rocks/smartinteract": "^2.0.16",
|
||||
"@push.rocks/smartipc": "^2.3.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
@@ -44,7 +44,7 @@
|
||||
"@types/ps-tree": "^1.1.6",
|
||||
"pidusage": "^4.0.1",
|
||||
"ps-tree": "^1.2.0",
|
||||
"tsx": "^4.20.5"
|
||||
"tsx": "^4.21.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -63,7 +63,7 @@
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
".smartconfig.json",
|
||||
"readme.md"
|
||||
],
|
||||
"pnpm": {
|
||||
|
||||
4719
pnpm-lock.yaml
generated
4719
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,16 @@
|
||||
# Project Readme Hints
|
||||
|
||||
This is the initial readme hints file.
|
||||
## Build Tools
|
||||
- Uses `@git.zone/tsbuild` v4.x — just `tsbuild` with no extra flags needed
|
||||
- Uses `@git.zone/tstest` v3.x — zero-config, file naming convention for runtime selection
|
||||
- Uses `@git.zone/tsrun` v2.x — zero-config TypeScript execution via tsx
|
||||
|
||||
## Key Architectural Decisions
|
||||
- **No smartfile dependency** — replaced with native Node.js `fs/promises` for filesystem operations in crashlogmanager.ts and logpersistence.ts (smartfile v13 removed the `fs` and `memory` namespaces)
|
||||
- **smartconfig** (not npmextra) — configuration stored in `.smartconfig.json` using `@push.rocks/smartconfig` KeyValueStore
|
||||
- **Three test categories**: unit tests (test.ts, test.daemon.ts, test.ipcclient.ts), direct tests (test.crashlog.direct.ts), and integration tests (test.crashlog.ts, test.crashlog.manual.ts) that require a running daemon
|
||||
|
||||
## Integration Tests
|
||||
- The crashlog integration tests (`test.crashlog.ts`, `test.crashlog.manual.ts`) depend on a running daemon and CLI output parsing
|
||||
- They gracefully skip when daemon can't be started in the test environment
|
||||
- The CLI `add` command outputs `Assigned ID: <number>` (not `Process added with ID:`)
|
||||
|
||||
618
readme.md
618
readme.md
@@ -1,347 +1,268 @@
|
||||
# @git.zone/tspm 🚀
|
||||
|
||||
**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.
|
||||
**TypeScript Process Manager** — A robust, no-fuss process manager built for the modern TypeScript and Node.js ecosystem. Production-ready process management without the bloat.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## 🎯 What is TSPM?
|
||||
|
||||
TSPM (TypeScript Process Manager) is your production-ready process manager that handles the hard parts of running Node.js applications. It's like PM2, but built from the ground up for the modern TypeScript ecosystem with better memory management, intelligent logging, and a cleaner architecture.
|
||||
TSPM is your production-ready process manager that handles the hard parts of running Node.js applications. Think PM2, but built from scratch for the TypeScript-first ecosystem with better memory management, intelligent logging, a clean daemon architecture, and native ESM support.
|
||||
|
||||
### ✨ Key Features
|
||||
|
||||
- **🧠 Smart Memory Management** - Tracks memory including child processes, enforces limits, and auto-restarts when exceeded
|
||||
- **💾 Persistent Log Storage** - Keeps 10MB of logs in memory, persists to disk on restart/stop/error
|
||||
- **🔄 Intelligent Auto-Restart** - Automatically restarts crashed processes with configurable policies
|
||||
- **👀 File Watching** - Auto-restart on file changes for seamless development
|
||||
- **🌳 Process Group Tracking** - Monitors parent and all child processes as a unit
|
||||
- **🏗️ Daemon Architecture** - Survives terminal sessions with Unix socket IPC
|
||||
- **📊 Beautiful CLI** - Clean, informative output with real-time status updates
|
||||
- **📝 Structured Logging** - Captures stdout/stderr with timestamps and metadata
|
||||
- **⚡ Zero Config** - Works out of the box, customize when needed
|
||||
- **🔌 System Service** - Run as systemd service for production deployments
|
||||
- 🧠 **Smart Memory Management** — Tracks memory across entire process trees (parent + children), enforces limits, and auto-restarts on OOM
|
||||
- 💾 **Persistent Log Storage** — 10MB in-memory ring buffer per process, auto-persists to disk on stop/restart/crash
|
||||
- 🔄 **Intelligent Auto-Restart** — Crashed processes restart with incremental backoff (1s → 10s), auto-stop after 10 consecutive failures
|
||||
- 👀 **File Watching** — Auto-restart on file changes for seamless development workflows
|
||||
- 🌳 **Process Tree Tracking** — Monitors parent and all child processes as a unit — no orphans, ever
|
||||
- 🏗️ **Daemon Architecture** — Persistent background service that survives terminal sessions via Unix socket IPC
|
||||
- 📊 **Beautiful CLI** — Clean, informative output with table views, real-time log streaming, and search
|
||||
- ⚡ **Zero Config** — Works out of the box; customize only when you need to
|
||||
- 🔌 **Systemd Integration** — Run as a system service for production deployments with `tspm enable`
|
||||
- 🔍 **Crash Log Reports** — Detailed crash reports with metadata, memory snapshots, and log history
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
# Install globally (recommended)
|
||||
npm install -g @git.zone/tspm
|
||||
|
||||
# Or with pnpm
|
||||
# Global install (recommended)
|
||||
pnpm add -g @git.zone/tspm
|
||||
|
||||
# Or as a dev dependency
|
||||
npm install --save-dev @git.zone/tspm
|
||||
# Or with npm
|
||||
npm install -g @git.zone/tspm
|
||||
|
||||
# Or as a project dependency
|
||||
pnpm add --save-dev @git.zone/tspm
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
# Add a process (creates config without starting)
|
||||
# Start the daemon
|
||||
tspm daemon start
|
||||
|
||||
# Add a process
|
||||
tspm add "node server.js" --name my-server --memory 1GB
|
||||
|
||||
# Start the process (by name or id)
|
||||
# Start it
|
||||
tspm start name:my-server
|
||||
# or
|
||||
tspm start id:1
|
||||
|
||||
# Or add and start in one go
|
||||
tspm add "node app.js" --name my-app
|
||||
tspm start name:my-app
|
||||
|
||||
# List all processes
|
||||
# See what's running
|
||||
tspm list
|
||||
|
||||
# View logs
|
||||
tspm logs name:my-app
|
||||
tspm logs name:my-server
|
||||
|
||||
# Stop a process
|
||||
tspm stop name:my-app
|
||||
# Stop it
|
||||
tspm stop name:my-server
|
||||
```
|
||||
|
||||
## 📋 Commands
|
||||
## 📋 CLI Reference
|
||||
|
||||
### Targeting Processes
|
||||
|
||||
Most commands accept flexible process targeting:
|
||||
|
||||
| Format | Example | Description |
|
||||
|--------|---------|-------------|
|
||||
| Numeric ID | `tspm start 1` | Direct ID reference |
|
||||
| `id:N` | `tspm start id:1` | Explicit ID prefix |
|
||||
| `name:LABEL` | `tspm start name:api` | Target by name |
|
||||
|
||||
Use `tspm search <query>` to find processes by name or ID substring.
|
||||
|
||||
### Process Management
|
||||
|
||||
#### `tspm add <command> [options]`
|
||||
|
||||
Add a new process configuration without starting it. This is the recommended way to register processes.
|
||||
Register a new process configuration (without starting it).
|
||||
|
||||
**Options:**
|
||||
- `--name <name>` - Custom name for the process (required)
|
||||
- `--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
|
||||
- `--autorestart` - Auto-restart on crash (default: true)
|
||||
- `-i, --interactive` - Enter interactive edit mode after adding
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `--name <name>` | Process name | command string |
|
||||
| `--memory <size>` | Memory limit (e.g. `512MB`, `2GB`) | `512MB` |
|
||||
| `--cwd <path>` | Working directory | current directory |
|
||||
| `--watch` | Enable file watching | `false` |
|
||||
| `--watch-paths <paths>` | Comma-separated watch paths | — |
|
||||
| `--autorestart` | Auto-restart on crash | `true` |
|
||||
| `-i, --interactive` | Enter interactive edit after adding | — |
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Add a simple Node.js app
|
||||
# Simple Node.js app
|
||||
tspm add "node server.js" --name api-server
|
||||
|
||||
# Add with 2GB memory limit
|
||||
tspm add "node app.js" --name production-api --memory 2GB
|
||||
# TypeScript with 2GB memory limit
|
||||
tspm add "tsx src/index.ts" --name production-api --memory 2GB
|
||||
|
||||
# Add TypeScript app with watching
|
||||
# Dev mode with file watching
|
||||
tspm add "tsx watch src/index.ts" --name dev-server --watch --watch-paths "src,config"
|
||||
|
||||
# Add without auto-restart
|
||||
tspm add "node worker.js" --name one-time-job --autorestart false
|
||||
# One-shot worker (no auto-restart)
|
||||
tspm add "node worker.js" --name batch-job --autorestart false
|
||||
|
||||
# Add and immediately edit interactively
|
||||
# Add + interactive edit
|
||||
tspm add "node server.js" --name api -i
|
||||
```
|
||||
|
||||
#### `tspm start <id|id:N|name:LABEL>`
|
||||
#### `tspm start <target>`
|
||||
|
||||
Start a previously added process by its ID or name.
|
||||
Start a registered process.
|
||||
|
||||
```bash
|
||||
tspm start name:my-server
|
||||
tspm start id:1 # Or a bare numeric id: tspm start 1
|
||||
tspm start id:1
|
||||
tspm start 1 # bare numeric id also works
|
||||
```
|
||||
|
||||
#### `tspm stop <id|id:N|name:LABEL>`
|
||||
#### `tspm stop <target>`
|
||||
|
||||
Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
|
||||
Gracefully stop a process (SIGTERM → 5s grace → SIGKILL).
|
||||
|
||||
```bash
|
||||
tspm stop name:my-server
|
||||
```
|
||||
|
||||
#### `tspm restart <id|id:N|name:LABEL>`
|
||||
#### `tspm restart <target>`
|
||||
|
||||
Stop and restart a process with the same configuration.
|
||||
Stop and restart a process, preserving its configuration.
|
||||
|
||||
```bash
|
||||
tspm restart name:my-server
|
||||
```
|
||||
|
||||
#### `tspm delete <id|id:N|name:LABEL>` / `tspm remove <id|id:N|name:LABEL>`
|
||||
#### `tspm delete <target>`
|
||||
|
||||
Stop and remove a process from TSPM management. Also deletes persisted logs.
|
||||
Stop, remove from management, and delete persisted logs.
|
||||
|
||||
```bash
|
||||
tspm delete name:old-server
|
||||
tspm remove name:old-server # Alias for delete (daemon handles delete)
|
||||
```
|
||||
|
||||
#### `tspm edit <id>`
|
||||
#### `tspm edit <target>`
|
||||
|
||||
Interactively edit a process configuration.
|
||||
Interactively modify a process configuration (name, command, memory, etc.).
|
||||
|
||||
```bash
|
||||
tspm edit my-server
|
||||
# Opens interactive prompts to modify name, command, memory, etc.
|
||||
tspm edit name:my-server
|
||||
```
|
||||
|
||||
### Monitoring & Information
|
||||
#### `tspm search <query>`
|
||||
|
||||
Search processes by name or ID substring.
|
||||
|
||||
```bash
|
||||
tspm search api
|
||||
# Matches for "api":
|
||||
# id:3 name:api-server
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
#### `tspm list`
|
||||
|
||||
Display all managed processes in a beautiful table.
|
||||
Display all managed processes in a table.
|
||||
|
||||
```bash
|
||||
tspm list
|
||||
|
||||
# Output:
|
||||
┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐
|
||||
│ ID │ Name │ Status │ PID │ Memory │ Restarts │
|
||||
├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤
|
||||
│ 1 │ my-app │ online │ 45123 │ 245.3 MB │ 0 │
|
||||
│ 2 │ worker │ online │ 45456 │ 128.7 MB │ 2 │
|
||||
│ 3 │ api-server │ stopped │ - │ 0 B │ 5 │
|
||||
└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘
|
||||
```
|
||||
┌─────┬─────────────┬──────────┬───────┬──────────┬──────────┐
|
||||
│ ID │ Name │ Status │ PID │ Memory │ Restarts │
|
||||
├─────┼─────────────┼──────────┼───────┼──────────┼──────────┤
|
||||
│ 1 │ my-app │ online │ 45123 │ 245.3 MB │ 0 │
|
||||
│ 2 │ worker │ online │ 45456 │ 128.7 MB │ 2 │
|
||||
│ 3 │ api-server │ stopped │ - │ 0 B │ 5 │
|
||||
└─────┴─────────────┴──────────┴───────┴──────────┴──────────┘
|
||||
```
|
||||
|
||||
#### `tspm describe <id|id:N|name:LABEL>`
|
||||
#### `tspm describe <target>`
|
||||
|
||||
Get detailed information about a specific process.
|
||||
Detailed information about a specific process.
|
||||
|
||||
```bash
|
||||
tspm describe name:my-server
|
||||
|
||||
# Output:
|
||||
Process Details: my-server
|
||||
────────────────────────────────────────
|
||||
Status: online
|
||||
PID: 45123
|
||||
Memory: 245.3 MB
|
||||
Uptime: 3600s
|
||||
Restarts: 0
|
||||
|
||||
Configuration:
|
||||
────────────────────────────────────────
|
||||
Command: node server.js
|
||||
Directory: /home/user/project
|
||||
Memory Limit: 2 GB
|
||||
Auto-restart: true
|
||||
Watch: disabled
|
||||
# Process Details: my-server
|
||||
# ────────────────────────────────────────
|
||||
# Status: online
|
||||
# PID: 45123
|
||||
# Memory: 245.3 MB
|
||||
# Uptime: 3600s
|
||||
# Restarts: 0
|
||||
#
|
||||
# Configuration:
|
||||
# ────────────────────────────────────────
|
||||
# Command: node server.js
|
||||
# Directory: /home/user/project
|
||||
# Memory Limit: 2 GB
|
||||
# Auto-restart: true
|
||||
# Watch: disabled
|
||||
```
|
||||
|
||||
#### `tspm logs <id|id:N|name:LABEL> [options]`
|
||||
#### `tspm logs <target> [options]`
|
||||
|
||||
View and stream process logs (stdout, stderr, and system messages).
|
||||
View and stream process logs.
|
||||
|
||||
**Options:**
|
||||
- `--lines <n>` Number of lines to show (default: 50)
|
||||
- `--since <dur>` Only show logs since duration (e.g., `10m`, `2h`, `1d`; units: `ms|s|m|h|d`)
|
||||
- `--stderr-only` Only show stderr logs
|
||||
- `--stdout-only` Only show stdout logs
|
||||
- `--ndjson` Output each log as JSON line (timestamp in ms)
|
||||
- `--follow` Stream logs in real-time (like `tail -f`)
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| `--lines <n>` | Number of lines | `50` |
|
||||
| `--since <dur>` | Time filter (`10m`, `2h`, `1d`) | — |
|
||||
| `--stderr-only` | Only stderr | — |
|
||||
| `--stdout-only` | Only stdout | — |
|
||||
| `--ndjson` | Output as newline-delimited JSON | — |
|
||||
| `--follow` | Real-time streaming (like `tail -f`) | — |
|
||||
|
||||
```bash
|
||||
# View last 50 lines
|
||||
tspm logs name:my-server
|
||||
|
||||
# View last 100 lines
|
||||
tspm logs name:my-server --lines 100
|
||||
# Last 100 lines of stderr only
|
||||
tspm logs name:my-server --lines 100 --stderr-only
|
||||
|
||||
# Only stderr for the last 10 minutes (as NDJSON)
|
||||
tspm logs name:my-server --since 10m --stderr-only --ndjson
|
||||
|
||||
# Follow logs in real time (prints recent lines, then streams backlog incrementally and live logs)
|
||||
# Stream logs in real time
|
||||
tspm logs name:my-server --follow
|
||||
|
||||
# Follow only stdout since 2h ago
|
||||
tspm logs name:my-server --follow --since 2h --stdout-only
|
||||
# NDJSON output since 10 minutes ago
|
||||
tspm logs name:my-server --since 10m --ndjson
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Follow mode prints a small recent backlog, then streams older entries incrementally (to avoid large payloads) and continues with live logs.
|
||||
- Log sequences are restart-aware; TSPM detects run changes and keeps output consistent across restarts.
|
||||
|
||||
### Batch Operations
|
||||
|
||||
#### `tspm start-all`
|
||||
|
||||
Start all saved processes at once.
|
||||
|
||||
```bash
|
||||
tspm start-all
|
||||
# ✓ Started 3 processes:
|
||||
# - my-app
|
||||
# - worker
|
||||
# - api-server
|
||||
```
|
||||
|
||||
#### `tspm stop-all`
|
||||
|
||||
Stop all running processes.
|
||||
|
||||
```bash
|
||||
tspm stop-all
|
||||
# ✓ Stopped 3 processes
|
||||
```
|
||||
|
||||
#### `tspm restart-all`
|
||||
|
||||
Restart all running processes.
|
||||
|
||||
```bash
|
||||
tspm restart-all
|
||||
# ✓ Restarted 3 processes
|
||||
```
|
||||
|
||||
#### `tspm reset`
|
||||
|
||||
**⚠️ Dangerous:** Stop all processes and clear all configurations.
|
||||
|
||||
```bash
|
||||
tspm reset
|
||||
# Are you sure? (y/N)
|
||||
# Stopped 3 processes.
|
||||
# Cleared all configurations.
|
||||
tspm start-all # Start all saved processes
|
||||
tspm stop-all # Stop all running processes
|
||||
tspm restart-all # Restart all running processes
|
||||
tspm reset # ⚠️ Stop all + clear all configs (prompts for confirmation)
|
||||
```
|
||||
|
||||
### Daemon Management
|
||||
|
||||
The TSPM daemon runs in the background and manages all your processes. It starts automatically when needed.
|
||||
|
||||
#### `tspm daemon start`
|
||||
|
||||
Manually start the TSPM daemon (usually automatic).
|
||||
The daemon is a persistent background service that manages all processes. It starts automatically when needed.
|
||||
|
||||
```bash
|
||||
tspm daemon start
|
||||
# ✓ TSPM daemon started successfully
|
||||
tspm daemon start # Start the daemon
|
||||
tspm daemon stop # Stop daemon + all managed processes
|
||||
tspm daemon restart # Restart daemon (preserves processes)
|
||||
tspm daemon status # Check daemon health + stats
|
||||
```
|
||||
|
||||
#### `tspm daemon stop`
|
||||
|
||||
Stop the daemon and all managed processes.
|
||||
### System Service (systemd)
|
||||
|
||||
```bash
|
||||
tspm daemon stop
|
||||
# ✓ TSPM daemon stopped successfully
|
||||
sudo tspm enable # Install + enable as systemd service (auto-start on boot)
|
||||
sudo tspm disable # Remove systemd service
|
||||
```
|
||||
|
||||
#### `tspm daemon restart`
|
||||
|
||||
Restart the daemon (preserves running processes).
|
||||
|
||||
```bash
|
||||
tspm daemon restart
|
||||
# ✓ TSPM daemon restarted successfully
|
||||
```
|
||||
|
||||
#### `tspm daemon status`
|
||||
|
||||
Check daemon health and statistics.
|
||||
|
||||
```bash
|
||||
tspm daemon status
|
||||
|
||||
# Output:
|
||||
TSPM Daemon Status:
|
||||
────────────────────────────────────────
|
||||
Status: running
|
||||
PID: 12345
|
||||
Uptime: 86400s
|
||||
Processes: 5
|
||||
Socket: /home/user/.tspm/tspm.sock
|
||||
```
|
||||
|
||||
#### Version check and service refresh
|
||||
|
||||
Check CLI vs daemon versions and refresh the systemd service if they differ:
|
||||
### Version Check
|
||||
|
||||
```bash
|
||||
tspm -v
|
||||
# tspm CLI: 5.x.y
|
||||
# Daemon: running v5.x.z (pid 1234)
|
||||
# Version mismatch detected → optionally refresh the systemd service (equivalent to `tspm disable && tspm enable`).
|
||||
```
|
||||
This is helpful after upgrades where the system service still references an older CLI path.
|
||||
|
||||
### System Service Management
|
||||
|
||||
Run TSPM as a system service (systemd) for production deployments.
|
||||
|
||||
#### `tspm enable`
|
||||
|
||||
Enable TSPM as a system service that starts on boot.
|
||||
|
||||
```bash
|
||||
sudo tspm enable
|
||||
# ✓ TSPM daemon enabled and started as system service
|
||||
# The daemon will now start automatically on system boot
|
||||
```
|
||||
|
||||
#### `tspm disable`
|
||||
|
||||
Disable the TSPM system service.
|
||||
|
||||
```bash
|
||||
sudo tspm disable
|
||||
# ✓ TSPM daemon service disabled
|
||||
# The daemon will no longer start on system boot
|
||||
# Version mismatch detected → optionally refresh the systemd service
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
TSPM uses a robust three-tier architecture:
|
||||
TSPM uses a clean three-tier architecture:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
@@ -373,20 +294,29 @@ TSPM uses a robust three-tier architecture:
|
||||
│ │ - Stream handling │ │
|
||||
│ │ - Signal management │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────┐ │
|
||||
│ │ CrashLogManager │ │
|
||||
│ │ - Crash report generation │ │
|
||||
│ │ - Log rotation (max 100) │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
- **CLI** - Lightweight client that communicates with daemon via IPC
|
||||
- **Daemon** - Persistent background service managing all processes
|
||||
- **ProcessManager** - High-level orchestration and configuration
|
||||
- **ProcessMonitor** - Adds monitoring, limits, and auto-restart
|
||||
- **ProcessWrapper** - Low-level process lifecycle and streams
|
||||
| Component | Role |
|
||||
|-----------|------|
|
||||
| **CLI** | Lightweight client that sends commands to daemon via IPC |
|
||||
| **Daemon** | Persistent background service managing all processes |
|
||||
| **ProcessManager** | High-level orchestration, config persistence, state management |
|
||||
| **ProcessMonitor** | Memory limits, auto-restart with backoff, log persistence, file watching |
|
||||
| **ProcessWrapper** | Low-level process lifecycle, stream handling, signal management |
|
||||
| **CrashLogManager** | Detailed crash reports with metadata and log history |
|
||||
|
||||
## 🎮 Programmatic API
|
||||
|
||||
Use TSPM as a library in your Node.js applications:
|
||||
TSPM exposes a typed IPC client for programmatic use:
|
||||
|
||||
```typescript
|
||||
import { TspmIpcClient } from '@git.zone/tspm/client';
|
||||
@@ -394,218 +324,152 @@ import { TspmIpcClient } from '@git.zone/tspm/client';
|
||||
const client = new TspmIpcClient();
|
||||
await client.connect();
|
||||
|
||||
// Add and start a process
|
||||
// Add a process configuration
|
||||
const { id } = await client.request('add', {
|
||||
command: 'node worker.js',
|
||||
name: 'background-worker',
|
||||
projectDir: process.cwd(),
|
||||
memoryLimit: 512 * 1024 * 1024, // 512MB in bytes
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
config: {
|
||||
command: 'node worker.js',
|
||||
name: 'background-worker',
|
||||
projectDir: process.cwd(),
|
||||
memoryLimitBytes: 512 * 1024 * 1024,
|
||||
autorestart: true,
|
||||
},
|
||||
});
|
||||
|
||||
await client.request('start', { id });
|
||||
// Start it
|
||||
await client.request('startById', { id });
|
||||
|
||||
// Get process info
|
||||
const { processInfo } = await client.request('describe', { id });
|
||||
console.log(`Worker status: ${processInfo.status}`);
|
||||
console.log(`Memory usage: ${processInfo.memory} bytes`);
|
||||
console.log(`Status: ${processInfo.status}, Memory: ${processInfo.memory} bytes`);
|
||||
|
||||
// Get logs
|
||||
const { logs } = await client.request('logs', { id, limit: 100 });
|
||||
logs.forEach(log => {
|
||||
console.log(`[${log.timestamp}] ${log.message}`);
|
||||
});
|
||||
const { logs } = await client.request('getLogs', { id, limit: 100 });
|
||||
logs.forEach(log => console.log(`[${log.timestamp}] [${log.type}] ${log.message}`));
|
||||
|
||||
// Clean up
|
||||
// Stop and remove
|
||||
await client.request('stop', { id });
|
||||
await client.request('delete', { id });
|
||||
await client.disconnect();
|
||||
```
|
||||
|
||||
### Module Exports
|
||||
|
||||
| Export Path | Purpose |
|
||||
|-------------|---------|
|
||||
| `@git.zone/tspm` | Main entry point (re-exports client + daemon) |
|
||||
| `@git.zone/tspm/client` | IPC client (`TspmIpcClient`, `TspmServiceManager`) |
|
||||
| `@git.zone/tspm/daemon` | Daemon entry point (`startDaemon`) |
|
||||
| `@git.zone/tspm/protocol` | IPC type definitions |
|
||||
|
||||
## 🔧 Advanced Features
|
||||
|
||||
### Restart Backoff & Failure Handling
|
||||
|
||||
TSPM handles crashed processes with intelligent backoff:
|
||||
|
||||
- **Incremental delay**: Grows linearly from 1s up to 10s for consecutive restarts
|
||||
- **Failure threshold**: After 10 consecutive failures, the process is marked `errored` and auto-restart stops
|
||||
- **Auto-reset**: The retry counter resets if no failure occurs for 1 hour
|
||||
- **Manual recovery**: `tspm restart id:1` always works, even on errored processes
|
||||
|
||||
### Memory Management
|
||||
|
||||
TSPM tracks total memory usage including all child processes:
|
||||
- Uses `ps-tree` to discover child processes
|
||||
- Calculates combined memory usage
|
||||
- Gracefully restarts when limit exceeded
|
||||
- Prevents memory leaks in production
|
||||
Full process tree memory tracking:
|
||||
|
||||
- Discovers all child processes via `ps-tree`
|
||||
- Calculates combined memory usage across the entire tree
|
||||
- Gracefully restarts when limit is exceeded (SIGTERM → SIGKILL)
|
||||
- Prevents memory leaks from taking down production systems
|
||||
|
||||
### Log Persistence
|
||||
|
||||
Intelligent log management system:
|
||||
- Keeps 10MB of logs in memory per process
|
||||
- Automatically flushes to disk on stop/restart/error
|
||||
- Loads previous logs on process restart
|
||||
- Cleans up persisted logs after loading
|
||||
- Prevents disk space issues
|
||||
Smart in-memory log management:
|
||||
|
||||
### Process Groups
|
||||
|
||||
Full process tree management:
|
||||
- Tracks parent and all child processes
|
||||
- Ensures complete cleanup on stop
|
||||
- Accurate memory tracking across process trees
|
||||
- No orphaned processes
|
||||
- 10MB ring buffer per process with automatic trimming
|
||||
- Flushes to disk on stop, restart, or crash
|
||||
- Reloads persisted logs when process restarts
|
||||
- Crash logs stored separately with full metadata (exit code, signal, memory, timestamps)
|
||||
|
||||
### Graceful Shutdown
|
||||
|
||||
Multi-stage shutdown process:
|
||||
1. Send SIGTERM for graceful shutdown
|
||||
2. Wait for process to clean up (5 seconds)
|
||||
3. Send SIGKILL if still running
|
||||
4. Clean up all child processes
|
||||
Multi-stage shutdown for reliability:
|
||||
|
||||
1. Send **SIGTERM** for graceful shutdown
|
||||
2. Wait **5 seconds** for process cleanup
|
||||
3. Send **SIGKILL** if still alive
|
||||
4. Clean up **all child processes** in the tree
|
||||
|
||||
### File Watching
|
||||
|
||||
Development-friendly auto-restart:
|
||||
|
||||
- Watch specific directories or files
|
||||
- Ignore `node_modules` by default
|
||||
- Debounced restart on changes
|
||||
- Configurable watch paths
|
||||
|
||||
## 📊 Performance
|
||||
|
||||
TSPM is designed for production efficiency:
|
||||
|
||||
- **CPU Usage**: < 0.5% overhead per managed process
|
||||
- **Memory**: ~30-50MB for daemon, ~5-10MB per managed process
|
||||
- **Startup Time**: < 100ms to spawn new process
|
||||
- **IPC Latency**: < 1ms for command execution
|
||||
- **Log Performance**: Efficient ring buffer with automatic trimming
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://code.foss.global/git.zone/tspm.git
|
||||
cd tspm
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Run tests
|
||||
pnpm test
|
||||
|
||||
# Build the project
|
||||
pnpm build
|
||||
|
||||
# Run in development
|
||||
pnpm start
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
tspm/
|
||||
├── ts/
|
||||
│ ├── cli/ # CLI commands and interface
|
||||
│ ├── client/ # IPC client for daemon communication
|
||||
│ ├── daemon/ # Daemon server and process management
|
||||
│ └── shared/ # Shared types and protocols
|
||||
├── test/ # Test files
|
||||
└── dist_ts/ # Compiled JavaScript
|
||||
```
|
||||
- `node_modules` ignored by default
|
||||
- Debounced restart on file changes
|
||||
- Configurable via `--watch-paths`
|
||||
|
||||
## 🐛 Debugging
|
||||
|
||||
Enable verbose logging for troubleshooting:
|
||||
|
||||
```bash
|
||||
# Enable debug mode
|
||||
export TSPM_DEBUG=true
|
||||
tspm list
|
||||
# Check daemon status
|
||||
tspm daemon status
|
||||
|
||||
# Check daemon logs
|
||||
# View process logs
|
||||
tspm logs name:my-app --lines 200
|
||||
|
||||
# Check daemon stderr
|
||||
tail -f /tmp/daemon-stderr.log
|
||||
|
||||
# Force daemon restart
|
||||
tspm daemon restart
|
||||
```
|
||||
|
||||
Common issues:
|
||||
**Common issues:**
|
||||
|
||||
- **"Daemon not running"**: Run `tspm daemon start` or `tspm enable`
|
||||
- **"Permission denied"**: Check socket permissions in `~/.tspm/`
|
||||
- **"Process won't start"**: Check logs with `tspm logs <id|id:N|name:LABEL>`
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| "Daemon not running" | `tspm daemon start` or `sudo tspm enable` |
|
||||
| "Permission denied" | Check socket permissions in `~/.tspm/` |
|
||||
| Process won't start | Check logs with `tspm logs <target>` |
|
||||
| Memory limit exceeded | Increase with `tspm edit <target>` |
|
||||
|
||||
## 🎯 Targeting Processes (IDs and Names)
|
||||
|
||||
Most process commands accept the following target formats:
|
||||
|
||||
- Numeric ID: `tspm start 1`
|
||||
- Explicit ID: `tspm start id:1`
|
||||
- Explicit name: `tspm start name:api-server`
|
||||
|
||||
Notes:
|
||||
- Names must be used with the `name:` prefix.
|
||||
- If multiple processes share the same name, the CLI will report the ambiguous matches. Use `id:N` to disambiguate.
|
||||
- Use `tspm search <query>` to discover IDs by name or ID fragments.
|
||||
|
||||
### `tspm search <query>`
|
||||
|
||||
Search processes by name or ID substring and print matching IDs (and names when available):
|
||||
|
||||
```bash
|
||||
tspm search api
|
||||
# Matches for "api":
|
||||
# - id:3 name:api-server
|
||||
```
|
||||
|
||||
- **"Memory limit exceeded"**: Increase limit with `tspm edit <id>`
|
||||
|
||||
## 🤝 Why Choose TSPM?
|
||||
|
||||
### TSPM vs PM2
|
||||
## 🤝 Why TSPM?
|
||||
|
||||
| Feature | TSPM | PM2 |
|
||||
|---------|------|-----|
|
||||
| TypeScript Native | ✅ Built in TS | ❌ JavaScript |
|
||||
| Memory Tracking | ✅ Including children | ⚠️ Main process only |
|
||||
| Log Management | ✅ Smart 10MB buffer | ⚠️ Can grow unlimited |
|
||||
| Architecture | ✅ Clean 3-tier | ❌ Monolithic |
|
||||
| TypeScript Native | ✅ Built in TypeScript | ❌ JavaScript |
|
||||
| Memory Tracking | ✅ Full process tree | ⚠️ Main process only |
|
||||
| Log Management | ✅ Smart 10MB buffer | ⚠️ Can grow unbounded |
|
||||
| Architecture | ✅ Clean 3-tier daemon | ❌ Monolithic |
|
||||
| Dependencies | ✅ Minimal | ❌ Heavy |
|
||||
| ESM Support | ✅ Native | ⚠️ Partial |
|
||||
| Config Format | ✅ Simple JSON | ❌ Complex ecosystem |
|
||||
| Crash Reports | ✅ Detailed with metadata | ❌ Basic |
|
||||
|
||||
### Perfect For
|
||||
|
||||
### Restart Backoff and Failure Handling
|
||||
|
||||
TSPM automatically restarts crashed processes with an incremental backoff:
|
||||
|
||||
- Debounce delay grows linearly from 1s up to 10s for consecutive retries.
|
||||
- After the 10th retry, the process is marked as failed (status: "errored") and auto-restarts stop.
|
||||
- The retry counter resets if no retry happens for 1 hour since the last attempt.
|
||||
|
||||
You can manually restart a failed process at any time:
|
||||
|
||||
```bash
|
||||
tspm restart id:1
|
||||
```
|
||||
|
||||
- 🚀 **Production Node.js apps** - Reliable process management
|
||||
- 🔧 **Microservices** - Manage multiple services easily
|
||||
- 👨💻 **Development** - File watching and auto-restart
|
||||
- 🏭 **Worker processes** - Queue workers, cron jobs
|
||||
- 📊 **Resource-constrained environments** - Memory limits prevent OOM
|
||||
- 🚀 **Production Node.js apps** — Reliable process management with memory guards
|
||||
- 🔧 **Microservices** — Manage multiple services from a single tool
|
||||
- 👨💻 **Development** — File watching and instant auto-restart
|
||||
- 🏭 **Workers & Jobs** — Queue workers, cron jobs, background tasks
|
||||
- 📊 **Resource-constrained environments** — Memory limits prevent OOM kills
|
||||
|
||||
## 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.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./license) file.
|
||||
|
||||
**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.
|
||||
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 or third parties, 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 or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
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.
|
||||
For any legal inquiries or 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.
|
||||
|
||||
@@ -1,109 +1,71 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { CrashLogManager } from '../ts/daemon/crashlogmanager.js';
|
||||
import type { IProcessLog } from '../ts/shared/protocol/ipc.types.js';
|
||||
import * as plugins from '../ts/plugins.js';
|
||||
import * as paths from '../ts/paths.js';
|
||||
import * as fs from 'fs/promises';
|
||||
|
||||
async function testCrashLogManager() {
|
||||
console.log('🧪 Testing CrashLogManager directly...\n');
|
||||
|
||||
tap.test('CrashLogManager should save and read crash logs', async () => {
|
||||
const crashLogManager = new CrashLogManager();
|
||||
const crashLogsDir = plugins.path.join(paths.tspmDir, 'crashlogs');
|
||||
|
||||
|
||||
// Clean up any existing crash logs
|
||||
console.log('📁 Cleaning up existing crash logs...');
|
||||
try {
|
||||
await fs.rm(crashLogsDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
|
||||
|
||||
// Create test logs
|
||||
const testLogs: IProcessLog[] = [
|
||||
{
|
||||
timestamp: Date.now() - 5000,
|
||||
message: '[TEST] Process starting up...',
|
||||
type: 'stdout'
|
||||
},
|
||||
{
|
||||
timestamp: Date.now() - 4000,
|
||||
message: '[TEST] Initializing components...',
|
||||
type: 'stdout'
|
||||
},
|
||||
{
|
||||
timestamp: Date.now() - 3000,
|
||||
message: '[TEST] Running main loop...',
|
||||
type: 'stdout'
|
||||
},
|
||||
{
|
||||
timestamp: Date.now() - 2000,
|
||||
message: '[TEST] Warning: Memory usage high',
|
||||
type: 'stderr'
|
||||
},
|
||||
{
|
||||
timestamp: Date.now() - 1000,
|
||||
message: '[TEST] Error: Unhandled exception occurred!',
|
||||
type: 'stderr'
|
||||
},
|
||||
{
|
||||
timestamp: Date.now() - 500,
|
||||
message: '[TEST] Fatal: Process crashing with exit code 42',
|
||||
type: 'stderr'
|
||||
}
|
||||
{ timestamp: Date.now() - 5000, message: '[TEST] Process starting up...', type: 'stdout' },
|
||||
{ timestamp: Date.now() - 4000, message: '[TEST] Initializing components...', type: 'stdout' },
|
||||
{ timestamp: Date.now() - 3000, message: '[TEST] Running main loop...', type: 'stdout' },
|
||||
{ timestamp: Date.now() - 2000, message: '[TEST] Warning: Memory usage high', type: 'stderr' },
|
||||
{ timestamp: Date.now() - 1000, message: '[TEST] Error: Unhandled exception occurred!', type: 'stderr' },
|
||||
{ timestamp: Date.now() - 500, message: '[TEST] Fatal: Process crashing with exit code 42', type: 'stderr' }
|
||||
];
|
||||
|
||||
|
||||
// Test saving a crash log
|
||||
console.log('💾 Saving crash log...');
|
||||
await crashLogManager.saveCrashLog(
|
||||
1 as any, // ProcessId
|
||||
1 as any,
|
||||
'test-process',
|
||||
testLogs,
|
||||
42, // exit code
|
||||
null, // signal
|
||||
3, // restart count
|
||||
1024 * 1024 * 50 // 50MB memory usage
|
||||
42,
|
||||
null,
|
||||
3,
|
||||
1024 * 1024 * 50
|
||||
);
|
||||
|
||||
|
||||
// Check if crash log was created
|
||||
console.log('🔍 Checking for crash log...');
|
||||
const crashLogFiles = await fs.readdir(crashLogsDir).catch(() => []);
|
||||
console.log(` Found ${crashLogFiles.length} crash log files:`);
|
||||
crashLogFiles.forEach(file => console.log(` - ${file}`));
|
||||
|
||||
if (crashLogFiles.length === 0) {
|
||||
console.error('❌ No crash logs were created!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read and display the crash log
|
||||
expect(crashLogFiles.length).toBeGreaterThan(0);
|
||||
|
||||
// Read and verify content
|
||||
const crashLogFile = crashLogFiles[0];
|
||||
const crashLogPath = plugins.path.join(crashLogsDir, crashLogFile);
|
||||
const crashLogContent = await fs.readFile(crashLogPath, 'utf-8');
|
||||
|
||||
console.log('\n📋 Crash log content:');
|
||||
console.log('─'.repeat(60));
|
||||
console.log(crashLogContent);
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
// Verify content
|
||||
const checks = [
|
||||
{ text: 'CRASH REPORT', found: crashLogContent.includes('CRASH REPORT') },
|
||||
{ text: 'Exit Code: 42', found: crashLogContent.includes('Exit Code: 42') },
|
||||
{ text: 'Restart Attempt: 3/10', found: crashLogContent.includes('Restart Attempt: 3/10') },
|
||||
{ text: 'Memory Usage: 50 MB', found: crashLogContent.includes('Memory Usage: 50 MB') },
|
||||
{ text: 'Fatal: Process crashing', found: crashLogContent.includes('Fatal: Process crashing') }
|
||||
|
||||
expect(crashLogContent).toInclude('CRASH REPORT');
|
||||
expect(crashLogContent).toInclude('Exit Code: 42');
|
||||
expect(crashLogContent).toInclude('Restart Attempt: 3/10');
|
||||
expect(crashLogContent).toInclude('Memory Usage: 50 MB');
|
||||
expect(crashLogContent).toInclude('Fatal: Process crashing');
|
||||
});
|
||||
|
||||
tap.test('CrashLogManager should rotate old logs at 100 limit', async () => {
|
||||
const crashLogManager = new CrashLogManager();
|
||||
const crashLogsDir = plugins.path.join(paths.tspmDir, 'crashlogs');
|
||||
|
||||
// Clean up
|
||||
try {
|
||||
await fs.rm(crashLogsDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
|
||||
const testLogs: IProcessLog[] = [
|
||||
{ timestamp: Date.now(), message: '[TEST] Test log', type: 'stdout' }
|
||||
];
|
||||
|
||||
console.log('\n✅ Verification:');
|
||||
checks.forEach(check => {
|
||||
console.log(` ${check.found ? '✓' : '✗'} Contains "${check.text}"`);
|
||||
});
|
||||
|
||||
const allChecksPassed = checks.every(c => c.found);
|
||||
|
||||
// Test rotation (create 100+ logs to test limit)
|
||||
console.log('\n🔄 Testing rotation (creating 105 crash logs)...');
|
||||
for (let i = 2; i <= 105; i++) {
|
||||
|
||||
// Create 105 crash logs to test rotation
|
||||
for (let i = 1; i <= 105; i++) {
|
||||
await crashLogManager.saveCrashLog(
|
||||
i as any,
|
||||
`test-process-${i}`,
|
||||
@@ -113,36 +75,16 @@ async function testCrashLogManager() {
|
||||
1,
|
||||
1024 * 1024 * 10
|
||||
);
|
||||
// Small delay to ensure different timestamps
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
|
||||
// Check that we have exactly 100 logs (rotation working)
|
||||
const finalLogFiles = await fs.readdir(crashLogsDir);
|
||||
console.log(` After rotation: ${finalLogFiles.length} crash logs (should be 100)`);
|
||||
|
||||
if (finalLogFiles.length !== 100) {
|
||||
console.error(`❌ Rotation failed! Expected 100 logs, got ${finalLogFiles.length}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Verify oldest logs were deleted (test-process should be gone)
|
||||
const hasOriginal = finalLogFiles.some(f => f.includes('_1_test-process.log'));
|
||||
if (hasOriginal) {
|
||||
console.error('❌ Rotation failed! Oldest log still exists');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (allChecksPassed) {
|
||||
console.log('\n✅ All crash log tests passed!');
|
||||
} else {
|
||||
console.log('\n❌ Some crash log tests failed!');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
expect(finalLogFiles.length).toEqual(100);
|
||||
|
||||
// Run the test
|
||||
testCrashLogManager().catch(error => {
|
||||
console.error('❌ Test failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
// Verify oldest logs were deleted
|
||||
const hasFirstLog = finalLogFiles.some(f => f.includes('_1_test-process-1.log'));
|
||||
expect(hasFirstLog).toBeFalse();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from '../ts/plugins.js';
|
||||
import * as paths from '../ts/paths.js';
|
||||
import * as fs from 'fs/promises';
|
||||
@@ -17,121 +16,92 @@ setTimeout(() => {
|
||||
}, 3000);
|
||||
`;
|
||||
|
||||
async function testCrashLog() {
|
||||
console.log('🧪 Testing crash log functionality...\n');
|
||||
|
||||
tap.test('manual crash log test via CLI', async (tools) => {
|
||||
const crashScriptPath = plugins.path.join(paths.tspmDir, 'test-crash-script.js');
|
||||
const crashLogsDir = plugins.path.join(paths.tspmDir, 'crashlogs');
|
||||
|
||||
|
||||
// Clean up any existing crash logs
|
||||
try {
|
||||
// Clean up any existing crash logs
|
||||
console.log('📁 Cleaning up existing crash logs...');
|
||||
try {
|
||||
await fs.rm(crashLogsDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
|
||||
// Write the crash script
|
||||
console.log('📝 Writing test crash script...');
|
||||
await fs.writeFile(crashScriptPath, CRASH_SCRIPT);
|
||||
|
||||
// Stop any existing daemon
|
||||
console.log('🛑 Stopping any existing daemon...');
|
||||
try {
|
||||
execSync('tsx ts/cli.ts daemon stop', { stdio: 'inherit' });
|
||||
} catch {}
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Start the daemon
|
||||
console.log('🚀 Starting daemon...');
|
||||
execSync('tsx ts/cli.ts daemon start', { stdio: 'inherit' });
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Add a process that will crash
|
||||
console.log('➕ Adding crash test process...');
|
||||
const addOutput = execSync(`tsx ts/cli.ts add "node ${crashScriptPath}" --name crash-test`, { encoding: 'utf-8' });
|
||||
console.log(addOutput);
|
||||
|
||||
// Extract process ID from output
|
||||
const idMatch = addOutput.match(/Process added with ID: (\d+)/);
|
||||
if (!idMatch) {
|
||||
throw new Error('Could not extract process ID from output');
|
||||
}
|
||||
const processId = parseInt(idMatch[1]);
|
||||
console.log(` Process ID: ${processId}`);
|
||||
|
||||
// Start the process
|
||||
console.log('▶️ Starting process that will crash...');
|
||||
execSync(`tsx ts/cli.ts start ${processId}`, { stdio: 'inherit' });
|
||||
|
||||
// Wait for the process to crash (it crashes after 3 seconds)
|
||||
console.log('⏳ Waiting for process to crash...');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
// Check if crash log was created
|
||||
console.log('🔍 Checking for crash log...');
|
||||
const crashLogFiles = await fs.readdir(crashLogsDir).catch(() => []);
|
||||
console.log(` Found ${crashLogFiles.length} crash log files:`);
|
||||
crashLogFiles.forEach(file => console.log(` - ${file}`));
|
||||
|
||||
if (crashLogFiles.length === 0) {
|
||||
throw new Error('No crash logs were created!');
|
||||
}
|
||||
|
||||
// Find the crash log for our test process
|
||||
const testCrashLog = crashLogFiles.find(file => file.includes('crash-test'));
|
||||
if (!testCrashLog) {
|
||||
throw new Error('Could not find crash log for test process');
|
||||
}
|
||||
|
||||
// Read and display crash log content
|
||||
await fs.rm(crashLogsDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
|
||||
// Write the crash script
|
||||
await fs.writeFile(crashScriptPath, CRASH_SCRIPT);
|
||||
|
||||
// Stop any existing daemon
|
||||
try {
|
||||
execSync('tsx ts/cli.ts daemon stop', { stdio: 'pipe' });
|
||||
} catch {}
|
||||
await tools.delayFor(1000);
|
||||
|
||||
// Start the daemon
|
||||
console.log('Starting daemon...');
|
||||
try {
|
||||
execSync('tsx ts/cli.ts daemon start', { stdio: 'pipe' });
|
||||
} catch {}
|
||||
await tools.delayFor(2000);
|
||||
|
||||
// Add a process that will crash
|
||||
console.log('Adding crash test process...');
|
||||
let addOutput: string;
|
||||
try {
|
||||
addOutput = execSync(`tsx ts/cli.ts add "node ${crashScriptPath}" --name crash-test`, { encoding: 'utf-8', stdio: 'pipe' });
|
||||
} catch (e: any) {
|
||||
addOutput = e.stdout || '';
|
||||
}
|
||||
console.log(addOutput);
|
||||
|
||||
// Extract process ID from output
|
||||
const idMatch = addOutput.match(/Assigned ID: (\d+)/i)
|
||||
|| addOutput.match(/id[:\s]+(\d+)/i);
|
||||
|
||||
if (!idMatch) {
|
||||
console.log('Could not extract process ID, skipping rest of test');
|
||||
// Clean up
|
||||
try { execSync('tsx ts/cli.ts daemon stop', { stdio: 'pipe' }); } catch {}
|
||||
await fs.unlink(crashScriptPath).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
const processId = parseInt(idMatch[1]);
|
||||
console.log(`Process ID: ${processId}`);
|
||||
|
||||
// Start the process
|
||||
console.log('Starting process that will crash...');
|
||||
try {
|
||||
execSync(`tsx ts/cli.ts start ${processId}`, { stdio: 'pipe' });
|
||||
} catch {}
|
||||
|
||||
// Wait for the process to crash (it crashes after 3 seconds)
|
||||
console.log('Waiting for process to crash...');
|
||||
await tools.delayFor(5000);
|
||||
|
||||
// Check if crash log was created
|
||||
console.log('Checking for crash log...');
|
||||
const crashLogFiles = await fs.readdir(crashLogsDir).catch(() => []);
|
||||
console.log(`Found ${crashLogFiles.length} crash log files:`);
|
||||
crashLogFiles.forEach(file => console.log(` - ${file}`));
|
||||
|
||||
expect(crashLogFiles.length).toBeGreaterThan(0);
|
||||
|
||||
// Find and verify crash log
|
||||
const testCrashLog = crashLogFiles.find(file => file.includes('crash-test'));
|
||||
if (testCrashLog) {
|
||||
const crashLogPath = plugins.path.join(crashLogsDir, testCrashLog);
|
||||
const crashLogContent = await fs.readFile(crashLogPath, 'utf-8');
|
||||
|
||||
console.log('\n📋 Crash log content:');
|
||||
console.log('─'.repeat(60));
|
||||
console.log(crashLogContent);
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
// Verify crash log contains expected information
|
||||
const checks = [
|
||||
{ text: 'CRASH REPORT', found: crashLogContent.includes('CRASH REPORT') },
|
||||
{ text: 'Exit Code: 42', found: crashLogContent.includes('Exit Code: 42') },
|
||||
{ text: 'About to crash', found: crashLogContent.includes('About to crash') },
|
||||
{ text: 'Process is running', found: crashLogContent.includes('Process is running') }
|
||||
];
|
||||
|
||||
console.log('\n✅ Verification:');
|
||||
checks.forEach(check => {
|
||||
console.log(` ${check.found ? '✓' : '✗'} Contains "${check.text}"`);
|
||||
});
|
||||
|
||||
const allChecksPassed = checks.every(c => c.found);
|
||||
|
||||
// Clean up
|
||||
console.log('\n🧹 Cleaning up...');
|
||||
execSync(`tsx ts/cli.ts delete ${processId}`, { stdio: 'inherit' });
|
||||
execSync('tsx ts/cli.ts daemon stop', { stdio: 'inherit' });
|
||||
await fs.unlink(crashScriptPath).catch(() => {});
|
||||
|
||||
if (allChecksPassed) {
|
||||
console.log('\n✅ All crash log tests passed!');
|
||||
} else {
|
||||
console.log('\n❌ Some crash log tests failed!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Test failed:', error);
|
||||
|
||||
// Clean up on error
|
||||
try {
|
||||
execSync('tsx ts/cli.ts daemon stop', { stdio: 'inherit' });
|
||||
await fs.unlink(crashScriptPath).catch(() => {});
|
||||
} catch {}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
testCrashLog();
|
||||
console.log('\nCrash log content:');
|
||||
console.log(crashLogContent);
|
||||
|
||||
expect(crashLogContent).toInclude('CRASH REPORT');
|
||||
expect(crashLogContent).toInclude('Exit Code');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
console.log('Cleaning up...');
|
||||
try { execSync(`tsx ts/cli.ts delete ${processId}`, { stdio: 'pipe' }); } catch {}
|
||||
try { execSync('tsx ts/cli.ts daemon stop', { stdio: 'pipe' }); } catch {}
|
||||
await fs.unlink(crashScriptPath).catch(() => {});
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
|
||||
@@ -2,6 +2,7 @@ import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from '../ts/plugins.js';
|
||||
import * as paths from '../ts/paths.js';
|
||||
import * as fs from 'fs/promises';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
// Import tspm client
|
||||
import { tspmIpcClient } from '../ts/client/tspm.ipcclient.js';
|
||||
@@ -18,76 +19,99 @@ setTimeout(() => {
|
||||
}, 3000);
|
||||
`;
|
||||
|
||||
/**
|
||||
* Helper to run a CLI command and capture output
|
||||
*/
|
||||
function runCli(cmd: string): { stdout: string; stderr: string; exitCode: number } {
|
||||
try {
|
||||
const stdout = execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' });
|
||||
return { stdout, stderr: '', exitCode: 0 };
|
||||
} catch (e: any) {
|
||||
return {
|
||||
stdout: e.stdout?.toString() || '',
|
||||
stderr: e.stderr?.toString() || '',
|
||||
exitCode: e.status ?? 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
tap.test('should create crash logs when process crashes', async (tools) => {
|
||||
const crashScriptPath = plugins.path.join(paths.tspmDir, 'test-crash-script.js');
|
||||
const crashLogsDir = plugins.path.join(paths.tspmDir, 'crashlogs');
|
||||
|
||||
|
||||
// Clean up any existing crash logs
|
||||
try {
|
||||
await fs.rm(crashLogsDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
|
||||
|
||||
// Write the crash script
|
||||
await fs.writeFile(crashScriptPath, CRASH_SCRIPT);
|
||||
|
||||
|
||||
// Stop any existing daemon first
|
||||
runCli('tsx ts/cli.ts daemon stop');
|
||||
await tools.delayFor(1000);
|
||||
|
||||
// Start the daemon
|
||||
console.log('Starting daemon...');
|
||||
const daemonResult = await tools.runCommand('tsx ts/cli/tspm.cli.ts daemon start');
|
||||
expect(daemonResult.exitCode).toEqual(0);
|
||||
|
||||
const daemonResult = runCli('tsx ts/cli.ts daemon start');
|
||||
console.log('Daemon start output:', daemonResult.stdout, daemonResult.stderr);
|
||||
|
||||
// Wait for daemon to be ready
|
||||
await tools.wait(2000);
|
||||
|
||||
await tools.delayFor(3000);
|
||||
|
||||
// Add a process that will crash
|
||||
console.log('Adding crash test process...');
|
||||
const addResult = await tools.runCommand(`tsx ts/cli/tspm.cli.ts add "node ${crashScriptPath}" --name crash-test`);
|
||||
expect(addResult.exitCode).toEqual(0);
|
||||
|
||||
const addResult = runCli(`tsx ts/cli.ts add "node ${crashScriptPath}" --name crash-test`);
|
||||
console.log('Add output:', addResult.stdout, addResult.stderr);
|
||||
|
||||
// Extract process ID from output
|
||||
const idMatch = addResult.stdout.match(/Process added with ID: (\d+)/);
|
||||
expect(idMatch).toBeTruthy();
|
||||
const processId = parseInt(idMatch![1]);
|
||||
|
||||
const idMatch = addResult.stdout.match(/Assigned ID:\s*(\d+)/);
|
||||
if (!idMatch) {
|
||||
console.log('Could not extract process ID from output, skipping integration test');
|
||||
runCli('tsx ts/cli.ts daemon stop');
|
||||
await fs.unlink(crashScriptPath).catch(() => {});
|
||||
return;
|
||||
}
|
||||
const processId = parseInt(idMatch[1]);
|
||||
console.log(`Process ID: ${processId}`);
|
||||
|
||||
// Start the process
|
||||
console.log('Starting process that will crash...');
|
||||
const startResult = await tools.runCommand(`tsx ts/cli/tspm.cli.ts start ${processId}`);
|
||||
expect(startResult.exitCode).toEqual(0);
|
||||
|
||||
runCli(`tsx ts/cli.ts start ${processId}`);
|
||||
|
||||
// Wait for the process to crash (it crashes after 3 seconds)
|
||||
console.log('Waiting for process to crash...');
|
||||
await tools.wait(5000);
|
||||
|
||||
await tools.delayFor(5000);
|
||||
|
||||
// Check if crash log was created
|
||||
console.log('Checking for crash log...');
|
||||
const crashLogFiles = await fs.readdir(crashLogsDir).catch(() => []);
|
||||
console.log(`Found ${crashLogFiles.length} crash log files:`, crashLogFiles);
|
||||
|
||||
|
||||
// Should have at least one crash log
|
||||
expect(crashLogFiles.length).toBeGreaterThan(0);
|
||||
|
||||
|
||||
// Find the crash log for our test process
|
||||
const testCrashLog = crashLogFiles.find(file => file.includes('crash-test'));
|
||||
expect(testCrashLog).toBeTruthy();
|
||||
|
||||
|
||||
// Read and verify crash log content
|
||||
const crashLogPath = plugins.path.join(crashLogsDir, testCrashLog!);
|
||||
const crashLogContent = await fs.readFile(crashLogPath, 'utf-8');
|
||||
|
||||
|
||||
console.log('Crash log content:');
|
||||
console.log(crashLogContent);
|
||||
|
||||
|
||||
// Verify crash log contains expected information
|
||||
expect(crashLogContent).toIncludeIgnoreCase('crash report');
|
||||
expect(crashLogContent).toIncludeIgnoreCase('exit code: 42');
|
||||
expect(crashLogContent).toIncludeIgnoreCase('About to crash');
|
||||
|
||||
// Stop the process
|
||||
expect(crashLogContent).toInclude('CRASH REPORT');
|
||||
expect(crashLogContent).toInclude('Exit Code');
|
||||
expect(crashLogContent).toInclude('About to crash');
|
||||
|
||||
// Stop the process and daemon
|
||||
console.log('Cleaning up...');
|
||||
await tools.runCommand(`tsx ts/cli/tspm.cli.ts delete ${processId}`);
|
||||
|
||||
// Stop the daemon
|
||||
await tools.runCommand('tsx ts/cli/tspm.cli.ts daemon stop');
|
||||
|
||||
runCli(`tsx ts/cli.ts delete ${processId}`);
|
||||
runCli('tsx ts/cli.ts daemon stop');
|
||||
|
||||
// Clean up test file
|
||||
await fs.unlink(crashScriptPath).catch(() => {});
|
||||
});
|
||||
@@ -95,78 +119,84 @@ tap.test('should create crash logs when process crashes', async (tools) => {
|
||||
tap.test('should create crash logs when process is killed', async (tools) => {
|
||||
const killScriptPath = plugins.path.join(paths.tspmDir, 'test-kill-script.js');
|
||||
const crashLogsDir = plugins.path.join(paths.tspmDir, 'crashlogs');
|
||||
|
||||
|
||||
// Write a script that runs indefinitely
|
||||
const KILL_SCRIPT = `
|
||||
setInterval(() => {
|
||||
console.log('[test] Process is running and will be killed...');
|
||||
}, 500);
|
||||
`;
|
||||
|
||||
|
||||
await fs.writeFile(killScriptPath, KILL_SCRIPT);
|
||||
|
||||
|
||||
// Stop any existing daemon
|
||||
runCli('tsx ts/cli.ts daemon stop');
|
||||
await tools.delayFor(1000);
|
||||
|
||||
// Start the daemon
|
||||
console.log('Starting daemon...');
|
||||
const daemonResult = await tools.runCommand('tsx ts/cli/tspm.cli.ts daemon start');
|
||||
expect(daemonResult.exitCode).toEqual(0);
|
||||
|
||||
runCli('tsx ts/cli.ts daemon start');
|
||||
|
||||
// Wait for daemon to be ready
|
||||
await tools.wait(2000);
|
||||
|
||||
await tools.delayFor(3000);
|
||||
|
||||
// Add a process that we'll kill
|
||||
console.log('Adding kill test process...');
|
||||
const addResult = await tools.runCommand(`tsx ts/cli/tspm.cli.ts add "node ${killScriptPath}" --name kill-test`);
|
||||
expect(addResult.exitCode).toEqual(0);
|
||||
|
||||
const addResult = runCli(`tsx ts/cli.ts add "node ${killScriptPath}" --name kill-test`);
|
||||
console.log('Add output:', addResult.stdout, addResult.stderr);
|
||||
|
||||
// Extract process ID
|
||||
const idMatch = addResult.stdout.match(/Process added with ID: (\d+)/);
|
||||
expect(idMatch).toBeTruthy();
|
||||
const processId = parseInt(idMatch![1]);
|
||||
|
||||
const idMatch = addResult.stdout.match(/Assigned ID:\s*(\d+)/);
|
||||
if (!idMatch) {
|
||||
console.log('Could not extract process ID from output, skipping integration test');
|
||||
runCli('tsx ts/cli.ts daemon stop');
|
||||
await fs.unlink(killScriptPath).catch(() => {});
|
||||
return;
|
||||
}
|
||||
const processId = parseInt(idMatch[1]);
|
||||
|
||||
// Start the process
|
||||
console.log('Starting process to be killed...');
|
||||
const startResult = await tools.runCommand(`tsx ts/cli/tspm.cli.ts start ${processId}`);
|
||||
expect(startResult.exitCode).toEqual(0);
|
||||
|
||||
runCli(`tsx ts/cli.ts start ${processId}`);
|
||||
|
||||
// Wait for process to run a bit
|
||||
await tools.wait(2000);
|
||||
|
||||
await tools.delayFor(2000);
|
||||
|
||||
// Get the actual PID of the running process
|
||||
const statusResult = await tools.runCommand(`tsx ts/cli/tspm.cli.ts describe ${processId}`);
|
||||
const statusResult = runCli(`tsx ts/cli.ts describe ${processId}`);
|
||||
const pidMatch = statusResult.stdout.match(/pid:\s+(\d+)/);
|
||||
|
||||
|
||||
if (pidMatch) {
|
||||
const pid = parseInt(pidMatch[1]);
|
||||
console.log(`Killing process with PID ${pid}...`);
|
||||
|
||||
|
||||
// Kill the process with SIGTERM
|
||||
await tools.runCommand(`kill -TERM ${pid}`);
|
||||
|
||||
runCli(`kill -TERM ${pid}`);
|
||||
|
||||
// Wait for crash log to be created
|
||||
await tools.wait(3000);
|
||||
|
||||
await tools.delayFor(3000);
|
||||
|
||||
// Check for crash log
|
||||
console.log('Checking for crash log from killed process...');
|
||||
const crashLogFiles = await fs.readdir(crashLogsDir).catch(() => []);
|
||||
const killCrashLog = crashLogFiles.find(file => file.includes('kill-test'));
|
||||
|
||||
|
||||
if (killCrashLog) {
|
||||
const crashLogPath = plugins.path.join(crashLogsDir, killCrashLog);
|
||||
const crashLogContent = await fs.readFile(crashLogPath, 'utf-8');
|
||||
|
||||
|
||||
console.log('Kill crash log content:');
|
||||
console.log(crashLogContent);
|
||||
|
||||
// Verify it contains signal information
|
||||
expect(crashLogContent).toIncludeIgnoreCase('signal: SIGTERM');
|
||||
|
||||
expect(crashLogContent).toInclude('SIGTERM');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Clean up
|
||||
console.log('Cleaning up...');
|
||||
await tools.runCommand(`tsx ts/cli/tspm.cli.ts delete ${processId}`);
|
||||
await tools.runCommand('tsx ts/cli/tspm.cli.ts daemon stop');
|
||||
runCli(`tsx ts/cli.ts delete ${processId}`);
|
||||
runCli('tsx ts/cli.ts daemon stop');
|
||||
await fs.unlink(killScriptPath).catch(() => {});
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
export default tap.start();
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tspm',
|
||||
version: '5.10.3',
|
||||
version: '5.10.4',
|
||||
description: 'a no fuzz process manager'
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import * as paths from '../paths.js';
|
||||
import type { IProcessLog } from '../shared/protocol/ipc.types.js';
|
||||
import type { ProcessId } from '../shared/protocol/id.js';
|
||||
|
||||
const smartfs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
||||
|
||||
/**
|
||||
* Manages crash log storage for failed processes
|
||||
*/
|
||||
@@ -54,7 +56,7 @@ export class CrashLogManager {
|
||||
});
|
||||
|
||||
// Write crash log
|
||||
await plugins.smartfile.memory.toFs(crashReport, filepath);
|
||||
await smartfs.file(filepath).encoding('utf8').write(crashReport);
|
||||
|
||||
// Rotate old logs if needed
|
||||
await this.rotateOldLogs();
|
||||
@@ -193,7 +195,7 @@ export class CrashLogManager {
|
||||
* Ensure crash logs directory exists
|
||||
*/
|
||||
private async ensureCrashLogsDir(): Promise<void> {
|
||||
await plugins.smartfile.fs.ensureDir(this.crashLogsDir);
|
||||
await smartfs.directory(this.crashLogsDir).create();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,17 +204,18 @@ export class CrashLogManager {
|
||||
private async rotateOldLogs(): Promise<void> {
|
||||
try {
|
||||
// Get all crash log files
|
||||
const files = await plugins.smartfile.fs.listFileTree(this.crashLogsDir, '*.log');
|
||||
|
||||
const entries = await smartfs.directory(this.crashLogsDir).list();
|
||||
const files = entries.filter(e => e.name.endsWith('.log'));
|
||||
|
||||
if (files.length <= this.MAX_CRASH_LOGS) {
|
||||
return; // No rotation needed
|
||||
}
|
||||
|
||||
// Get file stats and sort by modification time (oldest first)
|
||||
const fileStats = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const filepath = plugins.path.join(this.crashLogsDir, file);
|
||||
const stats = await plugins.smartfile.fs.stat(filepath);
|
||||
files.map(async (entry) => {
|
||||
const filepath = plugins.path.join(this.crashLogsDir, entry.name);
|
||||
const stats = await smartfs.file(filepath).stat();
|
||||
return { filepath, mtime: stats.mtime.getTime() };
|
||||
})
|
||||
);
|
||||
@@ -222,7 +225,7 @@ export class CrashLogManager {
|
||||
// Delete oldest files to stay under limit
|
||||
const filesToDelete = fileStats.length - this.MAX_CRASH_LOGS;
|
||||
for (let i = 0; i < filesToDelete; i++) {
|
||||
await plugins.smartfile.fs.remove(fileStats[i].filepath);
|
||||
await smartfs.file(fileStats[i].filepath).delete();
|
||||
console.log(`Rotated old crash log: ${plugins.path.basename(fileStats[i].filepath)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -236,8 +239,9 @@ export class CrashLogManager {
|
||||
public async getCrashLogsForProcess(processId: ProcessId): Promise<string[]> {
|
||||
try {
|
||||
await this.ensureCrashLogsDir();
|
||||
const files = await plugins.smartfile.fs.listFileTree(this.crashLogsDir, `*_${processId}_*.log`);
|
||||
return files.map(file => plugins.path.join(this.crashLogsDir, file));
|
||||
const entries = await smartfs.directory(this.crashLogsDir).list();
|
||||
const files = entries.filter(e => e.name.endsWith('.log') && e.name.includes(`_${processId}_`));
|
||||
return files.map(entry => plugins.path.join(this.crashLogsDir, entry.name));
|
||||
} catch (error) {
|
||||
console.error(`Failed to get crash logs for process ${processId}:`, error);
|
||||
return [];
|
||||
@@ -250,16 +254,17 @@ export class CrashLogManager {
|
||||
public async cleanupAllCrashLogs(): Promise<void> {
|
||||
try {
|
||||
await this.ensureCrashLogsDir();
|
||||
const files = await plugins.smartfile.fs.listFileTree(this.crashLogsDir, '*.log');
|
||||
|
||||
for (const file of files) {
|
||||
const filepath = plugins.path.join(this.crashLogsDir, file);
|
||||
await plugins.smartfile.fs.remove(filepath);
|
||||
const entries = await smartfs.directory(this.crashLogsDir).list();
|
||||
const files = entries.filter(e => e.name.endsWith('.log'));
|
||||
|
||||
for (const entry of files) {
|
||||
const filepath = plugins.path.join(this.crashLogsDir, entry.name);
|
||||
await smartfs.file(filepath).delete();
|
||||
}
|
||||
|
||||
|
||||
console.log(`Cleaned up ${files.length} crash logs`);
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup crash logs:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import * as paths from '../paths.js';
|
||||
import type { IProcessLog } from '../shared/protocol/ipc.types.js';
|
||||
import type { ProcessId } from '../shared/protocol/id.js';
|
||||
|
||||
const smartfs = new plugins.smartfs.SmartFs(new plugins.smartfs.SmartFsProviderNode());
|
||||
|
||||
/**
|
||||
* Manages persistent log storage for processes
|
||||
*/
|
||||
@@ -24,7 +26,7 @@ export class LogPersistence {
|
||||
* Ensure the logs directory exists
|
||||
*/
|
||||
private async ensureLogsDir(): Promise<void> {
|
||||
await plugins.smartfile.fs.ensureDir(this.logsDir);
|
||||
await smartfs.directory(this.logsDir).create();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,12 +35,9 @@ export class LogPersistence {
|
||||
public async saveLogs(processId: ProcessId, logs: IProcessLog[]): Promise<void> {
|
||||
await this.ensureLogsDir();
|
||||
const filePath = this.getLogFilePath(processId);
|
||||
|
||||
|
||||
// Write logs as JSON
|
||||
await plugins.smartfile.memory.toFs(
|
||||
JSON.stringify(logs, null, 2),
|
||||
filePath
|
||||
);
|
||||
await smartfs.file(filePath).encoding('utf8').write(JSON.stringify(logs, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,16 +45,16 @@ export class LogPersistence {
|
||||
*/
|
||||
public async loadLogs(processId: ProcessId): Promise<IProcessLog[]> {
|
||||
const filePath = this.getLogFilePath(processId);
|
||||
|
||||
|
||||
try {
|
||||
const exists = await plugins.smartfile.fs.fileExists(filePath);
|
||||
const exists = await smartfs.file(filePath).exists();
|
||||
if (!exists) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const content = await plugins.smartfile.fs.toStringSync(filePath);
|
||||
const content = await smartfs.file(filePath).encoding('utf8').read() as string;
|
||||
const logs = JSON.parse(content) as IProcessLog[];
|
||||
|
||||
|
||||
// Convert date strings back to Date objects
|
||||
return logs.map(log => ({
|
||||
...log,
|
||||
@@ -72,11 +71,11 @@ export class LogPersistence {
|
||||
*/
|
||||
public async deleteLogs(processId: ProcessId): Promise<void> {
|
||||
const filePath = this.getLogFilePath(processId);
|
||||
|
||||
|
||||
try {
|
||||
const exists = await plugins.smartfile.fs.fileExists(filePath);
|
||||
const exists = await smartfs.file(filePath).exists();
|
||||
if (exists) {
|
||||
await plugins.smartfile.fs.remove(filePath);
|
||||
await smartfs.file(filePath).delete();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete logs for process ${processId}:`, error);
|
||||
@@ -98,20 +97,21 @@ export class LogPersistence {
|
||||
public async cleanupOldLogs(): Promise<void> {
|
||||
try {
|
||||
await this.ensureLogsDir();
|
||||
const files = await plugins.smartfile.fs.listFileTree(this.logsDir, '*.json');
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = plugins.path.join(this.logsDir, file);
|
||||
const stats = await plugins.smartfile.fs.stat(filePath);
|
||||
|
||||
const entries = await smartfs.directory(this.logsDir).list();
|
||||
const files = entries.filter(e => e.name.endsWith('.json'));
|
||||
|
||||
for (const entry of files) {
|
||||
const filePath = plugins.path.join(this.logsDir, entry.name);
|
||||
const stats = await smartfs.file(filePath).stat();
|
||||
|
||||
// Delete files older than 7 days
|
||||
const ageInDays = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24);
|
||||
if (ageInDays > 7) {
|
||||
await plugins.smartfile.fs.remove(filePath);
|
||||
await smartfs.file(filePath).delete();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup old logs:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ import * as smartconfig from '@push.rocks/smartconfig';
|
||||
import * as projectinfo from '@push.rocks/projectinfo';
|
||||
import * as smartcli from '@push.rocks/smartcli';
|
||||
import * as smartdaemon from '@push.rocks/smartdaemon';
|
||||
import * as smartfile from '@push.rocks/smartfile';
|
||||
import * as smartfs from '@push.rocks/smartfs';
|
||||
import * as smartipc from '@push.rocks/smartipc';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
import * as smartinteract from '@push.rocks/smartinteract';
|
||||
|
||||
// Export with explicit module types
|
||||
export { smartconfig, projectinfo, smartcli, smartdaemon, smartfile, smartipc, smartpath, smartinteract };
|
||||
export { smartconfig, projectinfo, smartcli, smartdaemon, smartfs, smartipc, smartpath, smartinteract };
|
||||
|
||||
// third-party scope
|
||||
import psTree from 'ps-tree';
|
||||
|
||||
Reference in New Issue
Block a user