Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
4160b3f031 | |||
fa50ce40c8 | |||
8f96118e0c | |||
b210efde2a | |||
d8709d8b94 | |||
43799f3431 | |||
f4cbdd51e1 | |||
1340c1c248 | |||
92a6ecac71 | |||
5e26b0ab5f | |||
e09cf38f30 | |||
c694672438 | |||
3b21a338fb | |||
28680309ad | |||
833573eb10 | |||
ebc20a9232 |
70
changelog.md
70
changelog.md
@@ -1,5 +1,75 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.6.0 - feat(processmonitor)
|
||||||
|
Add CPU monitoring and display CPU in process list
|
||||||
|
|
||||||
|
- CLI: show a CPU column in the `tspm list` output (adds formatting and placeholder name display)
|
||||||
|
- Daemon: ProcessMonitor now collects CPU usage for the process group in addition to memory
|
||||||
|
- Daemon: ProcessMonitor exposes getLastCpuUsage() and ProcessManager syncs CPU values into IProcessInfo
|
||||||
|
- Non-breaking: UI and internal stats enriched to surface CPU metrics for processes
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.5.0 - feat(logs)
|
||||||
|
Improve logs streaming and backlog delivery; add CLI filters and ndjson output
|
||||||
|
|
||||||
|
- CLI: add new logs options: --since, --stderr-only, --stdout-only and --ndjson; enhance streaming output and gap detection
|
||||||
|
- CLI: fetch backlog conditionally (honoring --since) and print filtered results before live streaming
|
||||||
|
- Client: add TspmIpcClient.requestLogsBacklogStream, onStream and onBacklogTopic helpers to receive backlog chunks and streams
|
||||||
|
- Daemon: add logs:subscribe IPC handler to stream backlog entries to requesting client in small batches
|
||||||
|
- Protocol: extend IPC types with LogsSubscribeRequest/Response and register 'logs:subscribe' method
|
||||||
|
- Dependency: bump @push.rocks/smartipc to ^2.3.0 to support the streaming/IPC changes
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.4.2 - fix(cli/process/logs)
|
||||||
|
Reset log sequence on process restart to avoid false log gap warnings
|
||||||
|
|
||||||
|
- Track process runId when streaming logs and initialize lastRunId from fetched logs
|
||||||
|
- When a new runId is detected, reset lastSeq so that subsequent streamed logs are accepted (prevents spurious gap warnings)
|
||||||
|
- Emit an informational message when a restart/runId change is detected to aid debugging of log streams
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.4.1 - fix(processmonitor)
|
||||||
|
Bump tsbuild devDependency and relax ps-tree callback typing in ProcessMonitor
|
||||||
|
|
||||||
|
- Update devDependency @git.zone/tsbuild from ^2.6.7 to ^2.6.8
|
||||||
|
- Change psTree callback types in ts/daemon/processmonitor.ts to accept any error and ReadonlyArray for children to improve type compatibility
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.4.0 - feat(daemon)
|
||||||
|
Add CLI systemd service refresh on version mismatch and fix daemon memory leak; update dependencies
|
||||||
|
|
||||||
|
- CLI: when client and daemon versions differ, prompt to refresh the systemd service and optionally disable/enable the service automatically
|
||||||
|
- Daemon: clear pidusage state for PIDs on process exit/stop to prevent memory leaks in long-running monitors
|
||||||
|
- Client: expose smartdaemon in client plugin exports and fix import path for tspm.servicemanager
|
||||||
|
- Package: tighten dependency ranges (set specific versions) and add @types for pidusage and ps-tree
|
||||||
|
- Misc: ensure IPC disconnects and PID/socket handling improvements were integrated alongside the above changes
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.3.2 - fix(daemon)
|
||||||
|
Improve daemon log delivery and process monitor memory accounting; gate debug output and update tests to numeric ProcessId
|
||||||
|
|
||||||
|
- Deliver process logs only to subscribed clients instead of broadcasting to all connections (reduce unnecessary IPC traffic and noise)
|
||||||
|
- Implement incremental log memory accounting in ProcessMonitor using an estimateLogSize helper and WeakMap to avoid repeated JSON.stringify and reduce CPU/memory overhead
|
||||||
|
- Seed the incremental size map when loading persisted logs so memory accounting is accurate after restart
|
||||||
|
- Trim logs incrementally by subtracting estimated sizes of removed entries (avoids O(n) recalculation)
|
||||||
|
- Gate verbose console/debug output behind TSPM_DEBUG to prevent spamming in normal runs (applies to ProcessWrapper and ProcessMonitor)
|
||||||
|
- Improve process wrapper stdout/stderr debug logging to be conditional on debug mode
|
||||||
|
- Update tests to use numeric ProcessId via toProcessId(...) for consistency with typed IDs
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.3.1 - fix(client(tspmIpcClient))
|
||||||
|
Use bare topic names for IPC client subscribe/unsubscribe to fix log subscription issues
|
||||||
|
|
||||||
|
- Updated ts/client/tspm.ipcclient.ts to call ipcClient.subscribe/unsubscribe with the bare topic (e.g. 'logs.<id>') instead of prefixed 'topic:<...>'.
|
||||||
|
- Added comments clarifying that the IpcClient registers the 'topic:' prefix internally.
|
||||||
|
- Fixes incorrect topic registration that could prevent log streaming handlers from receiving messages.
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.3.0 - feat(cli/daemon/processmonitor)
|
||||||
|
Add flexible target resolution and search command; improve restart/backoff and error handling
|
||||||
|
|
||||||
|
- Add new cli command `search` to find processes by id or name fragment.
|
||||||
|
- Allow flexible process targets in CLI commands (accepts numeric id, id:<n>, or name:<label>) for start/stop/restart/delete/describe/logs/edit commands.
|
||||||
|
- Introduce a new daemon IPC method `resolveTarget` to normalize user-provided targets to ProcessId (supports id:<n>, name:<label>, or bare numeric id).
|
||||||
|
- Keep `remove` as a CLI alias but daemon exposes `delete` only; CLI resolves targets and always calls daemon `delete`.
|
||||||
|
- Implement scheduled restart/backoff in ProcessMonitor with incremental debounce, max retries, and a 1-hour reset window.
|
||||||
|
- Emit a `failed` event from ProcessMonitor when max restart attempts are exceeded; ProcessManager listens and marks processes as `errored` and clears pid.
|
||||||
|
- Ensure desired state is set to `stopped` before deleting a process to avoid race conditions.
|
||||||
|
- Improve cli output messages to include resolved names alongside numeric ids where available.
|
||||||
|
|
||||||
## 2025-08-30 - 5.2.0 - feat(cli)
|
## 2025-08-30 - 5.2.0 - feat(cli)
|
||||||
Preserve CLI environment when adding processes, simplify edit flow, and refresh README docs
|
Preserve CLI environment when adding processes, simplify edit flow, and refresh README docs
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tspm",
|
"name": "@git.zone/tspm",
|
||||||
"version": "5.2.0",
|
"version": "5.6.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "a no fuzz process manager",
|
"description": "a no fuzz process manager",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"tspm": "./cli.js"
|
"tspm": "./cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.6.7",
|
"@git.zone/tsbuild": "^2.6.8",
|
||||||
"@git.zone/tsbundle": "^2.5.1",
|
"@git.zone/tsbundle": "^2.5.1",
|
||||||
"@git.zone/tsrun": "^1.2.46",
|
"@git.zone/tsrun": "^1.2.46",
|
||||||
"@git.zone/tstest": "^2.3.5",
|
"@git.zone/tstest": "^2.3.5",
|
||||||
@@ -38,8 +38,10 @@
|
|||||||
"@push.rocks/smartdaemon": "^2.0.9",
|
"@push.rocks/smartdaemon": "^2.0.9",
|
||||||
"@push.rocks/smartfile": "^11.2.7",
|
"@push.rocks/smartfile": "^11.2.7",
|
||||||
"@push.rocks/smartinteract": "^2.0.16",
|
"@push.rocks/smartinteract": "^2.0.16",
|
||||||
"@push.rocks/smartipc": "^2.2.2",
|
"@push.rocks/smartipc": "^2.3.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
|
"@types/pidusage": "^2.0.5",
|
||||||
|
"@types/ps-tree": "^1.1.6",
|
||||||
"pidusage": "^4.0.1",
|
"pidusage": "^4.0.1",
|
||||||
"ps-tree": "^1.2.0",
|
"ps-tree": "^1.2.0",
|
||||||
"tsx": "^4.20.5"
|
"tsx": "^4.20.5"
|
||||||
|
84
pnpm-lock.yaml
generated
84
pnpm-lock.yaml
generated
@@ -27,11 +27,17 @@ importers:
|
|||||||
specifier: ^2.0.16
|
specifier: ^2.0.16
|
||||||
version: 2.0.16
|
version: 2.0.16
|
||||||
'@push.rocks/smartipc':
|
'@push.rocks/smartipc':
|
||||||
specifier: ^2.2.2
|
specifier: ^2.3.0
|
||||||
version: 2.2.2
|
version: 2.3.0
|
||||||
'@push.rocks/smartpath':
|
'@push.rocks/smartpath':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
|
'@types/pidusage':
|
||||||
|
specifier: ^2.0.5
|
||||||
|
version: 2.0.5
|
||||||
|
'@types/ps-tree':
|
||||||
|
specifier: ^1.1.6
|
||||||
|
version: 1.1.6
|
||||||
pidusage:
|
pidusage:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
@@ -43,8 +49,8 @@ importers:
|
|||||||
version: 4.20.5
|
version: 4.20.5
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@git.zone/tsbuild':
|
'@git.zone/tsbuild':
|
||||||
specifier: ^2.6.7
|
specifier: ^2.6.8
|
||||||
version: 2.6.7
|
version: 2.6.8
|
||||||
'@git.zone/tsbundle':
|
'@git.zone/tsbundle':
|
||||||
specifier: ^2.5.1
|
specifier: ^2.5.1
|
||||||
version: 2.5.1
|
version: 2.5.1
|
||||||
@@ -530,8 +536,8 @@ packages:
|
|||||||
'@esm-bundle/chai@4.3.4-fix.0':
|
'@esm-bundle/chai@4.3.4-fix.0':
|
||||||
resolution: {integrity: sha512-26SKdM4uvDWlY8/OOOxSB1AqQWeBosCX3wRYUZO7enTAj03CtVxIiCimYVG2WpULcyV51qapK4qTovwkUr5Mlw==}
|
resolution: {integrity: sha512-26SKdM4uvDWlY8/OOOxSB1AqQWeBosCX3wRYUZO7enTAj03CtVxIiCimYVG2WpULcyV51qapK4qTovwkUr5Mlw==}
|
||||||
|
|
||||||
'@git.zone/tsbuild@2.6.7':
|
'@git.zone/tsbuild@2.6.8':
|
||||||
resolution: {integrity: sha512-nLRYk1V4gxdEAp5mbLYNdr/in9mFA26L4MPKBKqzASID4lXSYya5sDbLRdDTv+mD0ZRBgdn6e+WMylA0SU4hSw==}
|
resolution: {integrity: sha512-g1z7+MxiYD0xMfuqn8NSWitbfK1OaF0Qolmw7WOmUsHmNF60T1AR02Lo4DtNmnjSpchA+xzDFAQzL1xTcQA39w==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tsbundle@2.5.1':
|
'@git.zone/tsbundle@2.5.1':
|
||||||
@@ -769,8 +775,8 @@ packages:
|
|||||||
'@push.rocks/isounique@1.0.5':
|
'@push.rocks/isounique@1.0.5':
|
||||||
resolution: {integrity: sha512-Z0BVqZZOCif1THTbIKWMgg0wxCzt9CyBtBBqQJiZ+jJ0KlQFrQHNHrPt81/LXe/L4x0cxWsn0bpL6W5DNSvNLw==}
|
resolution: {integrity: sha512-Z0BVqZZOCif1THTbIKWMgg0wxCzt9CyBtBBqQJiZ+jJ0KlQFrQHNHrPt81/LXe/L4x0cxWsn0bpL6W5DNSvNLw==}
|
||||||
|
|
||||||
'@push.rocks/levelcache@3.1.1':
|
'@push.rocks/levelcache@3.2.0':
|
||||||
resolution: {integrity: sha512-+JpDNEt+EuvmbtADGH9SkODxBy+slHDDzs43mAbuMbwpVvi6uNuMK0Mkhrfz9UFpxUSp+cJE/jl/OxdpD0xL1A==}
|
resolution: {integrity: sha512-Ch0Oguta2I0SVi704kHghhBcgfyfS92ua1elRu9d8X1/9LMRYuqvvBAnyXyFxQzI3S8q8QC6EkRdd8CAAYSzRg==}
|
||||||
|
|
||||||
'@push.rocks/lik@6.1.0':
|
'@push.rocks/lik@6.1.0':
|
||||||
resolution: {integrity: sha512-BoSAIRFNryQ8Sd5EP+35ZBj6vAQ1C60/XjZIO2O65XDyLG8xz7xJ+u5Wm8/fjIJ0WX3h8GkkaCz2tJM34nFT3A==}
|
resolution: {integrity: sha512-BoSAIRFNryQ8Sd5EP+35ZBj6vAQ1C60/XjZIO2O65XDyLG8xz7xJ+u5Wm8/fjIJ0WX3h8GkkaCz2tJM34nFT3A==}
|
||||||
@@ -805,6 +811,9 @@ packages:
|
|||||||
'@push.rocks/smartcache@1.0.16':
|
'@push.rocks/smartcache@1.0.16':
|
||||||
resolution: {integrity: sha512-UAXf74eDuH4/RebJhydIbHlYVR3ACYJjniEY/9ZePblu7bIPgwFZqLBE9g1lcKVogbH9yY62dk3rSpgBzenyfQ==}
|
resolution: {integrity: sha512-UAXf74eDuH4/RebJhydIbHlYVR3ACYJjniEY/9ZePblu7bIPgwFZqLBE9g1lcKVogbH9yY62dk3rSpgBzenyfQ==}
|
||||||
|
|
||||||
|
'@push.rocks/smartcache@1.0.18':
|
||||||
|
resolution: {integrity: sha512-3+cmLu9chbnmi4yD4kjlFP/Tn4NReaZIoicEcGTtwbcokTrSDMs3YPdJzIpDZkAs83PW7OcVSHa3Ak5KU5OWzA==}
|
||||||
|
|
||||||
'@push.rocks/smartchok@1.1.1':
|
'@push.rocks/smartchok@1.1.1':
|
||||||
resolution: {integrity: sha512-WmNigGmn1muBJMANVuJb4F8x3TzgYrnn6YZm6ixTsG+0WFbYevivEwp+J4S7npobLHsR7ynf+Ky8LxRYmsL50A==}
|
resolution: {integrity: sha512-WmNigGmn1muBJMANVuJb4F8x3TzgYrnn6YZm6ixTsG+0WFbYevivEwp+J4S7npobLHsR7ynf+Ky8LxRYmsL50A==}
|
||||||
|
|
||||||
@@ -832,6 +841,9 @@ packages:
|
|||||||
'@push.rocks/smartenv@5.0.13':
|
'@push.rocks/smartenv@5.0.13':
|
||||||
resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==}
|
resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==}
|
||||||
|
|
||||||
|
'@push.rocks/smarterror@2.0.1':
|
||||||
|
resolution: {integrity: sha512-iCcH1D8tlDJgMFsaJ6lhdOTKhbU0KoprNv9MRP9o7691QOx4JEDXiHtr/lNtxVo8BUtdb9CF6kazaknO9KuORA==}
|
||||||
|
|
||||||
'@push.rocks/smartexit@1.0.23':
|
'@push.rocks/smartexit@1.0.23':
|
||||||
resolution: {integrity: sha512-WmwKYcwbHBByoABhHHB+PAjr5475AtD/xBh1mDcqPrFsOOUOZq3BBUdpq25wI3ccu/SZB5IwaimiVzadls6HkA==}
|
resolution: {integrity: sha512-WmwKYcwbHBByoABhHHB+PAjr5475AtD/xBh1mDcqPrFsOOUOZq3BBUdpq25wI3ccu/SZB5IwaimiVzadls6HkA==}
|
||||||
|
|
||||||
@@ -865,8 +877,8 @@ packages:
|
|||||||
'@push.rocks/smartinteract@2.0.16':
|
'@push.rocks/smartinteract@2.0.16':
|
||||||
resolution: {integrity: sha512-eltvVRRUKBKd77DSFA4DPY2g4V4teZLNe8A93CDy/WglglYcUjxMoLY/b0DFTWCWKYT+yjk6Fe6p0FRrvX9Yvg==}
|
resolution: {integrity: sha512-eltvVRRUKBKd77DSFA4DPY2g4V4teZLNe8A93CDy/WglglYcUjxMoLY/b0DFTWCWKYT+yjk6Fe6p0FRrvX9Yvg==}
|
||||||
|
|
||||||
'@push.rocks/smartipc@2.2.2':
|
'@push.rocks/smartipc@2.3.0':
|
||||||
resolution: {integrity: sha512-pkWqp2nQH7p5zD9Efh5KNX2O0+gFWL6bxbdd6SdDh4gP8Gb0b3Sn87Tpedghpc/d+LCVql+1pUf6OlvMQpD5Yw==}
|
resolution: {integrity: sha512-/btC/DHf+2PWF6Qiq0oHHP7XHzacgYfHAShIts2ZXS+nhpvSyjucNzB2ErNUPHLMITNXGUSu5Wpt7sfvIQzxJQ==}
|
||||||
|
|
||||||
'@push.rocks/smartjson@5.0.20':
|
'@push.rocks/smartjson@5.0.20':
|
||||||
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
|
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
|
||||||
@@ -1012,6 +1024,9 @@ packages:
|
|||||||
'@push.rocks/tapbundle@6.0.3':
|
'@push.rocks/tapbundle@6.0.3':
|
||||||
resolution: {integrity: sha512-SuP14V6TPdtd1y1CYTvwTKJdpHa7EzY55NfaaEMxW4oRKvHgJiOiPEiR/IrtL9tSiDMSfrx12waTMgZheYaBug==}
|
resolution: {integrity: sha512-SuP14V6TPdtd1y1CYTvwTKJdpHa7EzY55NfaaEMxW4oRKvHgJiOiPEiR/IrtL9tSiDMSfrx12waTMgZheYaBug==}
|
||||||
|
|
||||||
|
'@push.rocks/taskbuffer@3.1.10':
|
||||||
|
resolution: {integrity: sha512-jT+FxRSk0+IP17q9LD1/Ks8GJBn5TZWgLtfnKRHW/LAZ1bHX/2ARZvAV8fm1T4WMU5s7PyId+y4fkoohG/5Nkg==}
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@3.1.7':
|
'@push.rocks/taskbuffer@3.1.7':
|
||||||
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
|
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
|
||||||
|
|
||||||
@@ -1647,9 +1662,15 @@ packages:
|
|||||||
'@types/parse5@6.0.3':
|
'@types/parse5@6.0.3':
|
||||||
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
|
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
|
||||||
|
|
||||||
|
'@types/pidusage@2.0.5':
|
||||||
|
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
|
||||||
|
|
||||||
'@types/ping@0.4.4':
|
'@types/ping@0.4.4':
|
||||||
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
|
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
|
||||||
|
|
||||||
|
'@types/ps-tree@1.1.6':
|
||||||
|
resolution: {integrity: sha512-PtrlVaOaI44/3pl3cvnlK+GxOM3re2526TJvPvh7W+keHIXdV4TE0ylpPBAcvFQCbGitaTXwL9u+RF7qtVeazQ==}
|
||||||
|
|
||||||
'@types/qs@6.14.0':
|
'@types/qs@6.14.0':
|
||||||
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
||||||
|
|
||||||
@@ -5599,7 +5620,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 4.3.20
|
'@types/chai': 4.3.20
|
||||||
|
|
||||||
'@git.zone/tsbuild@2.6.7':
|
'@git.zone/tsbuild@2.6.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@git.zone/tspublish': 1.10.3
|
'@git.zone/tspublish': 1.10.3
|
||||||
'@push.rocks/early': 4.0.4
|
'@push.rocks/early': 4.0.4
|
||||||
@@ -6015,21 +6036,21 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/isounique@1.0.5': {}
|
'@push.rocks/isounique@1.0.5': {}
|
||||||
|
|
||||||
'@push.rocks/levelcache@3.1.1':
|
'@push.rocks/levelcache@3.2.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartbucket': 3.3.10
|
'@push.rocks/smartbucket': 3.3.10
|
||||||
'@push.rocks/smartcache': 1.0.16
|
'@push.rocks/smartcache': 1.0.18
|
||||||
'@push.rocks/smartenv': 5.0.13
|
'@push.rocks/smartenv': 5.0.13
|
||||||
'@push.rocks/smartexit': 1.0.23
|
'@push.rocks/smartexit': 1.0.23
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.0.20
|
'@push.rocks/smartjson': 5.0.20
|
||||||
'@push.rocks/smartpath': 5.1.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartstring': 4.0.15
|
'@push.rocks/smartstring': 4.0.15
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@push.rocks/taskbuffer': 3.1.7
|
'@push.rocks/taskbuffer': 3.1.10
|
||||||
'@tsclass/tsclass': 4.4.4
|
'@tsclass/tsclass': 9.2.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- aws-crt
|
- aws-crt
|
||||||
|
|
||||||
@@ -6158,6 +6179,14 @@ snapshots:
|
|||||||
'@pushrocks/smartpromise': 3.1.10
|
'@pushrocks/smartpromise': 3.1.10
|
||||||
'@pushrocks/smarttime': 4.0.1
|
'@pushrocks/smarttime': 4.0.1
|
||||||
|
|
||||||
|
'@push.rocks/smartcache@1.0.18':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smarterror': 2.0.1
|
||||||
|
'@push.rocks/smarthash': 3.2.3
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/smarttime': 4.1.1
|
||||||
|
|
||||||
'@push.rocks/smartchok@1.1.1':
|
'@push.rocks/smartchok@1.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
@@ -6237,6 +6266,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
|
||||||
|
'@push.rocks/smarterror@2.0.1':
|
||||||
|
dependencies:
|
||||||
|
clean-stack: 1.3.0
|
||||||
|
make-error-cause: 2.3.0
|
||||||
|
|
||||||
'@push.rocks/smartexit@1.0.23':
|
'@push.rocks/smartexit@1.0.23':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.1.0
|
'@push.rocks/lik': 6.1.0
|
||||||
@@ -6326,7 +6360,7 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
inquirer: 11.1.0
|
inquirer: 11.1.0
|
||||||
|
|
||||||
'@push.rocks/smartipc@2.2.2':
|
'@push.rocks/smartipc@2.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
@@ -6443,7 +6477,7 @@ snapshots:
|
|||||||
'@push.rocks/smartnpm@2.0.6':
|
'@push.rocks/smartnpm@2.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/consolecolor': 2.0.3
|
'@push.rocks/consolecolor': 2.0.3
|
||||||
'@push.rocks/levelcache': 3.1.1
|
'@push.rocks/levelcache': 3.2.0
|
||||||
'@push.rocks/smartarchive': 4.2.2
|
'@push.rocks/smartarchive': 4.2.2
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
@@ -6745,6 +6779,16 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@push.rocks/taskbuffer@3.1.10':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/lik': 6.2.2
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smartlog': 3.1.8
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/smartrx': 3.0.10
|
||||||
|
'@push.rocks/smarttime': 4.1.1
|
||||||
|
'@push.rocks/smartunique': 3.0.9
|
||||||
|
|
||||||
'@push.rocks/taskbuffer@3.1.7':
|
'@push.rocks/taskbuffer@3.1.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
@@ -7592,8 +7636,12 @@ snapshots:
|
|||||||
|
|
||||||
'@types/parse5@6.0.3': {}
|
'@types/parse5@6.0.3': {}
|
||||||
|
|
||||||
|
'@types/pidusage@2.0.5': {}
|
||||||
|
|
||||||
'@types/ping@0.4.4': {}
|
'@types/ping@0.4.4': {}
|
||||||
|
|
||||||
|
'@types/ps-tree@1.1.6': {}
|
||||||
|
|
||||||
'@types/qs@6.14.0': {}
|
'@types/qs@6.14.0': {}
|
||||||
|
|
||||||
'@types/randomatic@3.1.5': {}
|
'@types/randomatic@3.1.5': {}
|
||||||
|
120
readme.md
120
readme.md
@@ -38,21 +38,23 @@ npm install --save-dev @git.zone/tspm
|
|||||||
# Add a process (creates config without starting)
|
# Add a process (creates config without starting)
|
||||||
tspm add "node server.js" --name my-server --memory 1GB
|
tspm add "node server.js" --name my-server --memory 1GB
|
||||||
|
|
||||||
# Start the process
|
# Start the process (by name or id)
|
||||||
tspm start my-server
|
tspm start name:my-server
|
||||||
|
# or
|
||||||
|
tspm start id:1
|
||||||
|
|
||||||
# Or add and start in one go
|
# Or add and start in one go
|
||||||
tspm add "node app.js" --name my-app
|
tspm add "node app.js" --name my-app
|
||||||
tspm start my-app
|
tspm start name:my-app
|
||||||
|
|
||||||
# List all processes
|
# List all processes
|
||||||
tspm list
|
tspm list
|
||||||
|
|
||||||
# View logs
|
# View logs
|
||||||
tspm logs my-app
|
tspm logs name:my-app
|
||||||
|
|
||||||
# Stop a process
|
# Stop a process
|
||||||
tspm stop my-app
|
tspm stop name:my-app
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📋 Commands
|
## 📋 Commands
|
||||||
@@ -86,38 +88,38 @@ tspm add "tsx watch src/index.ts" --name dev-server --watch --watch-paths "src,c
|
|||||||
tspm add "node worker.js" --name one-time-job --autorestart false
|
tspm add "node worker.js" --name one-time-job --autorestart false
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm start <id>`
|
#### `tspm start <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Start a previously added process by its ID or name.
|
Start a previously added process by its ID or name.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm start my-server
|
tspm start name:my-server
|
||||||
tspm start 1 # Can also use numeric ID
|
tspm start id:1 # Or a bare numeric id: tspm start 1
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm stop <id>`
|
#### `tspm stop <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
|
Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm stop my-server
|
tspm stop name:my-server
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm restart <id>`
|
#### `tspm restart <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Stop and restart a process with the same configuration.
|
Stop and restart a process with the same configuration.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm restart my-server
|
tspm restart name:my-server
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm delete <id>` / `tspm remove <id>`
|
#### `tspm delete <id|id:N|name:LABEL>` / `tspm remove <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Stop and remove a process from TSPM management. Also deletes persisted logs.
|
Stop and remove a process from TSPM management. Also deletes persisted logs.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm delete old-server
|
tspm delete name:old-server
|
||||||
tspm remove old-server # Alias for delete
|
tspm remove name:old-server # Alias for delete (daemon handles delete)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm edit <id>`
|
#### `tspm edit <id>`
|
||||||
@@ -148,12 +150,12 @@ tspm list
|
|||||||
└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘
|
└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm describe <id>`
|
#### `tspm describe <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Get detailed information about a specific process.
|
Get detailed information about a specific process.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm describe my-server
|
tspm describe name:my-server
|
||||||
|
|
||||||
# Output:
|
# Output:
|
||||||
Process Details: my-server
|
Process Details: my-server
|
||||||
@@ -173,25 +175,39 @@ Auto-restart: true
|
|||||||
Watch: disabled
|
Watch: disabled
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm logs <id> [options]`
|
#### `tspm logs <id|id:N|name:LABEL> [options]`
|
||||||
|
|
||||||
View process logs (stdout and stderr combined).
|
View and stream process logs (stdout, stderr, and system messages).
|
||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
- `--lines <n>` - Number of lines to display (default: 50)
|
- `--lines <n>` Number of lines to show (default: 50)
|
||||||
- `--follow` - Stream logs in real-time (like `tail -f`)
|
- `--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`)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# View last 50 lines
|
# View last 50 lines
|
||||||
tspm logs my-server
|
tspm logs name:my-server
|
||||||
|
|
||||||
# View last 100 lines
|
# View last 100 lines
|
||||||
tspm logs my-server --lines 100
|
tspm logs name:my-server --lines 100
|
||||||
|
|
||||||
# Follow logs in real-time
|
# Only stderr for the last 10 minutes (as NDJSON)
|
||||||
tspm logs my-server --follow
|
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)
|
||||||
|
tspm logs name:my-server --follow
|
||||||
|
|
||||||
|
# Follow only stdout since 2h ago
|
||||||
|
tspm logs name:my-server --follow --since 2h --stdout-only
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
### Batch Operations
|
||||||
|
|
||||||
#### `tspm start-all`
|
#### `tspm start-all`
|
||||||
@@ -283,6 +299,18 @@ Processes: 5
|
|||||||
Socket: /home/user/.tspm/tspm.sock
|
Socket: /home/user/.tspm/tspm.sock
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Version check and service refresh
|
||||||
|
|
||||||
|
Check CLI vs daemon versions and refresh the systemd service if they differ:
|
||||||
|
|
||||||
|
```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
|
### System Service Management
|
||||||
|
|
||||||
Run TSPM as a system service (systemd) for production deployments.
|
Run TSPM as a system service (systemd) for production deployments.
|
||||||
@@ -496,7 +524,31 @@ Common issues:
|
|||||||
|
|
||||||
- **"Daemon not running"**: Run `tspm daemon start` or `tspm enable`
|
- **"Daemon not running"**: Run `tspm daemon start` or `tspm enable`
|
||||||
- **"Permission denied"**: Check socket permissions in `~/.tspm/`
|
- **"Permission denied"**: Check socket permissions in `~/.tspm/`
|
||||||
- **"Process won't start"**: Check logs with `tspm logs <id>`
|
- **"Process won't start"**: Check logs with `tspm logs <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
|
## 🎯 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>`
|
- **"Memory limit exceeded"**: Increase limit with `tspm edit <id>`
|
||||||
|
|
||||||
## 🤝 Why Choose TSPM?
|
## 🤝 Why Choose TSPM?
|
||||||
@@ -515,6 +567,20 @@ Common issues:
|
|||||||
|
|
||||||
### Perfect For
|
### 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
|
- 🚀 **Production Node.js apps** - Reliable process management
|
||||||
- 🔧 **Microservices** - Manage multiple services easily
|
- 🔧 **Microservices** - Manage multiple services easily
|
||||||
- 👨💻 **Development** - File watching and auto-restart
|
- 👨💻 **Development** - File watching and auto-restart
|
||||||
@@ -538,4 +604,4 @@ 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 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.
|
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.
|
||||||
|
@@ -5,6 +5,7 @@ import * as fs from 'fs/promises';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { tspmIpcClient, TspmIpcClient } from '../ts/client/tspm.ipcclient.js';
|
import { tspmIpcClient, TspmIpcClient } from '../ts/client/tspm.ipcclient.js';
|
||||||
|
import { toProcessId } from '../ts/shared/protocol/id.js';
|
||||||
|
|
||||||
// Helper to ensure daemon is stopped before tests
|
// Helper to ensure daemon is stopped before tests
|
||||||
async function ensureDaemonStopped() {
|
async function ensureDaemonStopped() {
|
||||||
@@ -160,7 +161,7 @@ tap.test('Process management through daemon', async (tools) => {
|
|||||||
|
|
||||||
// Test 2: Start a test process
|
// Test 2: Start a test process
|
||||||
const testConfig: tspm.IProcessConfig = {
|
const testConfig: tspm.IProcessConfig = {
|
||||||
id: 'test-echo',
|
id: toProcessId(1001),
|
||||||
name: 'Test Echo Process',
|
name: 'Test Echo Process',
|
||||||
command: 'echo "Test process"',
|
command: 'echo "Test process"',
|
||||||
projectDir: process.cwd(),
|
projectDir: process.cwd(),
|
||||||
@@ -172,7 +173,7 @@ tap.test('Process management through daemon', async (tools) => {
|
|||||||
config: testConfig,
|
config: testConfig,
|
||||||
});
|
});
|
||||||
console.log('Start response:', startResponse);
|
console.log('Start response:', startResponse);
|
||||||
expect(startResponse.processId).toEqual('test-echo');
|
expect(startResponse.processId).toEqual(1001);
|
||||||
expect(startResponse.status).toBeDefined();
|
expect(startResponse.status).toBeDefined();
|
||||||
|
|
||||||
// Test 3: List processes (should have one process)
|
// Test 3: List processes (should have one process)
|
||||||
@@ -180,27 +181,27 @@ tap.test('Process management through daemon', async (tools) => {
|
|||||||
console.log('List after start:', listResponse);
|
console.log('List after start:', listResponse);
|
||||||
expect(listResponse.processes.length).toBeGreaterThanOrEqual(1);
|
expect(listResponse.processes.length).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
const procInfo = listResponse.processes.find((p) => p.id === 'test-echo');
|
const procInfo = listResponse.processes.find((p) => p.id === toProcessId(1001));
|
||||||
expect(procInfo).toBeDefined();
|
expect(procInfo).toBeDefined();
|
||||||
expect(procInfo?.id).toEqual('test-echo');
|
expect(procInfo?.id).toEqual(1001);
|
||||||
|
|
||||||
// Test 4: Describe the process
|
// Test 4: Describe the process
|
||||||
const describeResponse = await tspmIpcClient.request('describe', {
|
const describeResponse = await tspmIpcClient.request('describe', {
|
||||||
id: 'test-echo',
|
id: toProcessId(1001),
|
||||||
});
|
});
|
||||||
console.log('Describe:', describeResponse);
|
console.log('Describe:', describeResponse);
|
||||||
expect(describeResponse.processInfo).toBeDefined();
|
expect(describeResponse.processInfo).toBeDefined();
|
||||||
expect(describeResponse.config).toBeDefined();
|
expect(describeResponse.config).toBeDefined();
|
||||||
expect(describeResponse.config.id).toEqual('test-echo');
|
expect(describeResponse.config.id).toEqual(1001);
|
||||||
|
|
||||||
// Test 5: Stop the process
|
// Test 5: Stop the process
|
||||||
const stopResponse = await tspmIpcClient.request('stop', { id: 'test-echo' });
|
const stopResponse = await tspmIpcClient.request('stop', { id: toProcessId(1001) });
|
||||||
console.log('Stop response:', stopResponse);
|
console.log('Stop response:', stopResponse);
|
||||||
expect(stopResponse.success).toEqual(true);
|
expect(stopResponse.success).toEqual(true);
|
||||||
|
|
||||||
// Test 6: Delete the process
|
// Test 6: Delete the process
|
||||||
const deleteResponse = await tspmIpcClient.request('delete', {
|
const deleteResponse = await tspmIpcClient.request('delete', {
|
||||||
id: 'test-echo',
|
id: toProcessId(1001),
|
||||||
});
|
});
|
||||||
console.log('Delete response:', deleteResponse);
|
console.log('Delete response:', deleteResponse);
|
||||||
expect(deleteResponse.success).toEqual(true);
|
expect(deleteResponse.success).toEqual(true);
|
||||||
@@ -208,9 +209,7 @@ tap.test('Process management through daemon', async (tools) => {
|
|||||||
// Test 7: Verify process is gone
|
// Test 7: Verify process is gone
|
||||||
listResponse = await tspmIpcClient.request('list', {});
|
listResponse = await tspmIpcClient.request('list', {});
|
||||||
console.log('List after delete:', listResponse);
|
console.log('List after delete:', listResponse);
|
||||||
const deletedProcess = listResponse.processes.find(
|
const deletedProcess = listResponse.processes.find((p) => p.id === toProcessId(1001));
|
||||||
(p) => p.id === 'test-echo',
|
|
||||||
);
|
|
||||||
expect(deletedProcess).toBeUndefined();
|
expect(deletedProcess).toBeUndefined();
|
||||||
|
|
||||||
// Cleanup: stop daemon
|
// Cleanup: stop daemon
|
||||||
@@ -241,7 +240,7 @@ tap.test('Batch operations through daemon', async (tools) => {
|
|||||||
// Add multiple test processes
|
// Add multiple test processes
|
||||||
const testConfigs: tspm.IProcessConfig[] = [
|
const testConfigs: tspm.IProcessConfig[] = [
|
||||||
{
|
{
|
||||||
id: 'batch-test-1',
|
id: toProcessId(1101),
|
||||||
name: 'Batch Test 1',
|
name: 'Batch Test 1',
|
||||||
command: 'echo "Process 1"',
|
command: 'echo "Process 1"',
|
||||||
projectDir: process.cwd(),
|
projectDir: process.cwd(),
|
||||||
@@ -249,7 +248,7 @@ tap.test('Batch operations through daemon', async (tools) => {
|
|||||||
autorestart: false,
|
autorestart: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'batch-test-2',
|
id: toProcessId(1102),
|
||||||
name: 'Batch Test 2',
|
name: 'Batch Test 2',
|
||||||
command: 'echo "Process 2"',
|
command: 'echo "Process 2"',
|
||||||
projectDir: process.cwd(),
|
projectDir: process.cwd(),
|
||||||
@@ -308,7 +307,7 @@ tap.test('Daemon error handling', async (tools) => {
|
|||||||
|
|
||||||
// Test 1: Try to stop non-existent process
|
// Test 1: Try to stop non-existent process
|
||||||
try {
|
try {
|
||||||
await tspmIpcClient.request('stop', { id: 'non-existent-process' });
|
await tspmIpcClient.request('stop', { id: toProcessId(99999) });
|
||||||
expect(false).toEqual(true); // Should not reach here
|
expect(false).toEqual(true); // Should not reach here
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toInclude('Failed to stop process');
|
expect(error.message).toInclude('Failed to stop process');
|
||||||
@@ -316,7 +315,7 @@ tap.test('Daemon error handling', async (tools) => {
|
|||||||
|
|
||||||
// Test 2: Try to describe non-existent process
|
// Test 2: Try to describe non-existent process
|
||||||
try {
|
try {
|
||||||
await tspmIpcClient.request('describe', { id: 'non-existent-process' });
|
await tspmIpcClient.request('describe', { id: toProcessId(99999) });
|
||||||
expect(false).toEqual(true); // Should not reach here
|
expect(false).toEqual(true); // Should not reach here
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toInclude('not found');
|
expect(error.message).toInclude('not found');
|
||||||
@@ -324,7 +323,7 @@ tap.test('Daemon error handling', async (tools) => {
|
|||||||
|
|
||||||
// Test 3: Try to restart non-existent process
|
// Test 3: Try to restart non-existent process
|
||||||
try {
|
try {
|
||||||
await tspmIpcClient.request('restart', { id: 'non-existent-process' });
|
await tspmIpcClient.request('restart', { id: toProcessId(99999) });
|
||||||
expect(false).toEqual(true); // Should not reach here
|
expect(false).toEqual(true); // Should not reach here
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toInclude('Failed to restart process');
|
expect(error.message).toInclude('Failed to restart process');
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as tspm from '../ts/index.js';
|
import * as tspm from '../ts/index.js';
|
||||||
|
import { toProcessId } from '../ts/shared/protocol/id.js';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
// Basic module import test
|
// Basic module import test
|
||||||
@@ -51,7 +52,7 @@ async function exampleUsingIpcClient() {
|
|||||||
// Start a process using the request method
|
// Start a process using the request method
|
||||||
await client.request('start', {
|
await client.request('start', {
|
||||||
config: {
|
config: {
|
||||||
id: 'web-server',
|
id: toProcessId(2001),
|
||||||
name: 'Web Server',
|
name: 'Web Server',
|
||||||
projectDir: '/path/to/web/project',
|
projectDir: '/path/to/web/project',
|
||||||
command: 'npm run serve',
|
command: 'npm run serve',
|
||||||
@@ -65,7 +66,7 @@ async function exampleUsingIpcClient() {
|
|||||||
// Start another process
|
// Start another process
|
||||||
await client.request('start', {
|
await client.request('start', {
|
||||||
config: {
|
config: {
|
||||||
id: 'api-server',
|
id: toProcessId(2002),
|
||||||
name: 'API Server',
|
name: 'API Server',
|
||||||
projectDir: '/path/to/api/project',
|
projectDir: '/path/to/api/project',
|
||||||
command: 'npm run api',
|
command: 'npm run api',
|
||||||
@@ -80,13 +81,13 @@ async function exampleUsingIpcClient() {
|
|||||||
|
|
||||||
// Get logs from a process
|
// Get logs from a process
|
||||||
const logs = await client.request('getLogs', {
|
const logs = await client.request('getLogs', {
|
||||||
id: 'web-server',
|
id: toProcessId(2001),
|
||||||
lines: 20,
|
lines: 20,
|
||||||
});
|
});
|
||||||
console.log('Web server logs:', logs.logs);
|
console.log('Web server logs:', logs.logs);
|
||||||
|
|
||||||
// Stop a process
|
// Stop a process
|
||||||
await client.request('stop', { id: 'api-server' });
|
await client.request('stop', { id: toProcessId(2002) });
|
||||||
|
|
||||||
// Handle graceful shutdown
|
// Handle graceful shutdown
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tspm',
|
name: '@git.zone/tspm',
|
||||||
version: '5.2.0',
|
version: '5.6.0',
|
||||||
description: 'a no fuzz process manager'
|
description: 'a no fuzz process manager'
|
||||||
}
|
}
|
||||||
|
@@ -22,13 +22,14 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
);
|
);
|
||||||
console.log(' disable Disable TSPM system service');
|
console.log(' disable Disable TSPM system service');
|
||||||
console.log('\nProcess Commands:');
|
console.log('\nProcess Commands:');
|
||||||
console.log(' start <script> Start a process');
|
console.log(' start <id|id:N|name:LBL> Start a process');
|
||||||
console.log(' list List all processes');
|
console.log(' list List all processes');
|
||||||
console.log(' stop <id> Stop a process');
|
console.log(' stop <id|id:N|name:LBL> Stop a process');
|
||||||
console.log(' restart <id> Restart a process');
|
console.log(' restart <id|id:N|name:LBL> Restart a process');
|
||||||
console.log(' delete <id> Delete a process');
|
console.log(' delete <id|id:N|name:LBL> Delete a process');
|
||||||
console.log(' describe <id> Show details for a process');
|
console.log(' describe <id|id:N|name:LBL> Show details for a process');
|
||||||
console.log(' logs <id> Show logs for a process');
|
console.log(' logs <id|id:N|name:LBL> Show logs for a process');
|
||||||
|
console.log(' search <query> Find processes by id/name');
|
||||||
console.log(' start-all Start all saved processes');
|
console.log(' start-all Start all saved processes');
|
||||||
console.log(' stop-all Stop all processes');
|
console.log(' stop-all Stop all processes');
|
||||||
console.log(' restart-all Restart all processes');
|
console.log(' restart-all Restart all processes');
|
||||||
|
@@ -8,23 +8,25 @@ export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
['delete', 'remove'],
|
['delete', 'remove'],
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm delete <id> | tspm remove <id>');
|
console.log('Usage: tspm delete <id|id:N|name:LABEL> | tspm remove <id|id:N|name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if command was 'remove' to use the new IPC route, otherwise 'delete'
|
// Determine if command was 'remove' to use the new IPC route, otherwise 'delete'
|
||||||
const cmd = String(argvArg._[0]);
|
const cmd = String(argvArg._[0]);
|
||||||
const useRemove = cmd === 'remove';
|
const isRemoveAlias = cmd === 'remove';
|
||||||
console.log(`${useRemove ? 'Removing' : 'Deleting'} process: ${id}`);
|
console.log(`${isRemoveAlias ? 'Removing' : 'Deleting'} process: ${target}`);
|
||||||
const response = await tspmIpcClient.request(useRemove ? 'remove' : 'delete', { id } as any);
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
// Always call daemon 'delete'; 'remove' is CLI alias only
|
||||||
|
const response = await tspmIpcClient.request('delete', { id: resolved.id } as any);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
console.log(`✓ ${response.message || (useRemove ? 'Removed successfully' : 'Deleted successfully')}`);
|
console.log(`✓ ${response.message || (isRemoveAlias ? 'Removed successfully' : 'Deleted successfully')}`);
|
||||||
} else {
|
} else {
|
||||||
console.error(`✗ Failed to ${useRemove ? 'remove' : 'delete'} process: ${response.message}`);
|
console.error(`✗ Failed to ${isRemoveAlias ? 'remove' : 'delete'} process: ${response.message}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ actionLabel: 'delete/remove process' },
|
{ actionLabel: 'delete/remove process' },
|
||||||
|
@@ -9,16 +9,17 @@ export function registerDescribeCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'describe',
|
'describe',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm describe <id>');
|
console.log('Usage: tspm describe <id | id:N | name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await tspmIpcClient.request('describe', { id });
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
const response = await tspmIpcClient.request('describe', { id: resolved.id });
|
||||||
|
|
||||||
console.log(`Process Details: ${id}`);
|
console.log(`Process Details: ${response.config.name || resolved.id}`);
|
||||||
console.log('─'.repeat(40));
|
console.log('─'.repeat(40));
|
||||||
console.log(`Status: ${response.processInfo.status}`);
|
console.log(`Status: ${response.processInfo.status}`);
|
||||||
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
|
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
|
||||||
|
@@ -9,17 +9,16 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'edit',
|
'edit',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const idRaw = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!idRaw) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID to edit');
|
console.error('Error: Please provide a process target to edit');
|
||||||
console.log('Usage: tspm edit <id>');
|
console.log('Usage: tspm edit <id | id:N | name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = idRaw;
|
// Resolve and load current config
|
||||||
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
// Load current config
|
const { config } = await tspmIpcClient.request('describe', { id: resolved.id });
|
||||||
const { config } = await tspmIpcClient.request('describe', { id });
|
|
||||||
|
|
||||||
// Interactive editing is temporarily disabled - needs smartinteract API update
|
// Interactive editing is temporarily disabled - needs smartinteract API update
|
||||||
console.log('Interactive editing is temporarily disabled.');
|
console.log('Interactive editing is temporarily disabled.');
|
||||||
@@ -63,7 +62,7 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateResponse = await tspmIpcClient.request('update', {
|
const updateResponse = await tspmIpcClient.request('update', {
|
||||||
id,
|
id: resolved.id,
|
||||||
updates,
|
updates,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,4 +72,3 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
{ actionLabel: 'edit process config' },
|
{ actionLabel: 'edit process config' },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -20,13 +20,13 @@ export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
|
|
||||||
console.log('Process List:');
|
console.log('Process List:');
|
||||||
console.log(
|
console.log(
|
||||||
'┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐',
|
'┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┬─────────┐',
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
'│ ID │ Name │ Status │ PID │ Memory │ Restarts │',
|
'│ ID │ Name │ Status │ PID │ Memory │ CPU │ Restarts │',
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
'├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤',
|
'├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┼──────────┤',
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const proc of processes) {
|
for (const proc of processes) {
|
||||||
@@ -38,13 +38,18 @@ export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
: '\x1b[33m';
|
: '\x1b[33m';
|
||||||
const resetColor = '\x1b[0m';
|
const resetColor = '\x1b[0m';
|
||||||
|
|
||||||
|
const cpuStr = typeof proc.cpu === 'number' && isFinite(proc.cpu)
|
||||||
|
? `${proc.cpu.toFixed(1)}%`
|
||||||
|
: '-';
|
||||||
|
// Name is not part of IProcessInfo; show ID as placeholder for now
|
||||||
|
const nameDisplay = String(proc.id);
|
||||||
console.log(
|
console.log(
|
||||||
`│ ${pad(String(proc.id), 7)} │ ${pad(String(proc.id), 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad((proc.pid || '-').toString(), 9)} │ ${pad(formatMemory(proc.memory), 8)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
`│ ${pad(String(proc.id), 7)} │ ${pad(nameDisplay, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad((proc.pid || '-').toString(), 9)} │ ${pad(formatMemory(proc.memory), 8)} │ ${pad(cpuStr, 8)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘',
|
'└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┴──────────┘',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ actionLabel: 'list processes' },
|
{ actionLabel: 'list processes' },
|
||||||
|
@@ -2,7 +2,7 @@ import * as plugins from '../../plugins.js';
|
|||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
import { getBool, getNumber } from '../../helpers/argv.js';
|
import { getBool, getNumber, getString } from '../../helpers/argv.js';
|
||||||
import { formatLog } from '../../helpers/formatting.js';
|
import { formatLog } from '../../helpers/formatting.js';
|
||||||
import { withStreamingLifecycle } from '../../helpers/lifecycle.js';
|
import { withStreamingLifecycle } from '../../helpers/lifecycle.js';
|
||||||
|
|
||||||
@@ -11,26 +11,97 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'logs',
|
'logs',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm logs <id> [options]');
|
console.log('Usage: tspm logs <id | id:N | name:LABEL> [options]');
|
||||||
console.log('\nOptions:');
|
console.log('\nOptions:');
|
||||||
console.log(' --lines <n> Number of lines to show (default: 50)');
|
console.log(' --lines <n> Number of lines to show (default: 50)');
|
||||||
console.log(' --follow Stream logs in real-time (like tail -f)');
|
console.log(' --since <dur> Only show logs since duration (e.g., 10m, 2h, 1d)');
|
||||||
|
console.log(' --stderr-only Only show stderr logs');
|
||||||
|
console.log(' --stdout-only Only show stdout logs');
|
||||||
|
console.log(' --ndjson Output each log as JSON line');
|
||||||
|
console.log(' --follow Stream logs in real-time (like tail -f)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = getNumber(argvArg, 'lines', 50);
|
const lines = getNumber(argvArg, 'lines', 50);
|
||||||
const follow = getBool(argvArg, 'follow', 'f');
|
const follow = getBool(argvArg, 'follow', 'f');
|
||||||
|
const sinceSpec = getString(argvArg, 'since');
|
||||||
|
const stderrOnly = getBool(argvArg, 'stderr-only');
|
||||||
|
const stdoutOnly = getBool(argvArg, 'stdout-only');
|
||||||
|
const ndjson = getBool(argvArg, 'ndjson');
|
||||||
|
|
||||||
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
const parseDuration = (spec?: string): number | undefined => {
|
||||||
|
if (!spec) return undefined;
|
||||||
|
const m = spec.trim().match(/^(\d+)(ms|s|m|h|d)?$/i);
|
||||||
|
if (!m) return undefined;
|
||||||
|
const val = Number(m[1]);
|
||||||
|
const unit = (m[2] || 'm').toLowerCase();
|
||||||
|
const mult = unit === 'ms' ? 1 : unit === 's' ? 1000 : unit === 'm' ? 60000 : unit === 'h' ? 3600000 : 86400000;
|
||||||
|
return Date.now() - val * mult;
|
||||||
|
};
|
||||||
|
const sinceTime = parseDuration(sinceSpec);
|
||||||
|
const typesFilter: Array<'stdout' | 'stderr' | 'system'> | undefined =
|
||||||
|
stderrOnly && !stdoutOnly
|
||||||
|
? ['stderr']
|
||||||
|
: stdoutOnly && !stderrOnly
|
||||||
|
? ['stdout']
|
||||||
|
: undefined; // all
|
||||||
|
|
||||||
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
const id = resolved.id;
|
||||||
|
const response = await tspmIpcClient.request('getLogs', { id, lines: sinceTime ? 0 : lines });
|
||||||
|
|
||||||
if (!follow) {
|
if (!follow) {
|
||||||
// One-shot mode - auto-disconnect handled by registerIpcCommand
|
// One-shot mode - auto-disconnect handled by registerIpcCommand
|
||||||
console.log(`Logs for process: ${id} (last ${lines} lines)`);
|
const filtered = response.logs.filter((l) => {
|
||||||
|
if (typesFilter && !typesFilter.includes(l.type)) return false;
|
||||||
|
if (sinceTime && new Date(l.timestamp).getTime() < sinceTime) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
console.log(`Logs for process: ${id} (${sinceTime ? 'since ' + new Date(sinceTime).toLocaleString() : 'last ' + lines + ' lines'})`);
|
||||||
console.log('─'.repeat(60));
|
console.log('─'.repeat(60));
|
||||||
for (const log of response.logs) {
|
for (const log of filtered) {
|
||||||
|
if (ndjson) {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
...log,
|
||||||
|
timestamp: new Date(log.timestamp).getTime(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||||
|
const prefix =
|
||||||
|
log.type === 'stdout'
|
||||||
|
? '[OUT]'
|
||||||
|
: log.type === 'stderr'
|
||||||
|
? '[ERR]'
|
||||||
|
: '[SYS]';
|
||||||
|
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Streaming mode
|
||||||
|
console.log(`Logs for process: ${resolved.name || id} (streaming...)`);
|
||||||
|
console.log('─'.repeat(60));
|
||||||
|
|
||||||
|
// Prepare backlog printing state and stream handler
|
||||||
|
let lastSeq = 0;
|
||||||
|
let lastRunId: string | undefined = undefined;
|
||||||
|
const printLog = (log: any) => {
|
||||||
|
if (typesFilter && !typesFilter.includes(log.type)) return;
|
||||||
|
if (sinceTime && new Date(log.timestamp).getTime() < sinceTime) return;
|
||||||
|
if (ndjson) {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
...log,
|
||||||
|
timestamp: new Date(log.timestamp).getTime(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||||
const prefix =
|
const prefix =
|
||||||
log.type === 'stdout'
|
log.type === 'stdout'
|
||||||
@@ -40,43 +111,53 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
: '[SYS]';
|
: '[SYS]';
|
||||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||||
}
|
}
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Streaming mode
|
// Print initial backlog (already fetched via getLogs)
|
||||||
console.log(`Logs for process: ${id} (streaming...)`);
|
|
||||||
console.log('─'.repeat(60));
|
|
||||||
|
|
||||||
let lastSeq = 0;
|
|
||||||
for (const log of response.logs) {
|
for (const log of response.logs) {
|
||||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
printLog(log);
|
||||||
const prefix =
|
|
||||||
log.type === 'stdout'
|
|
||||||
? '[OUT]'
|
|
||||||
: log.type === 'stderr'
|
|
||||||
? '[ERR]'
|
|
||||||
: '[SYS]';
|
|
||||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
|
||||||
if (log.seq !== undefined) lastSeq = Math.max(lastSeq, log.seq);
|
if (log.seq !== undefined) lastSeq = Math.max(lastSeq, log.seq);
|
||||||
|
if ((log as any).runId) lastRunId = (log as any).runId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request additional backlog delivered as incremental messages to avoid large payloads
|
||||||
|
try {
|
||||||
|
const disposeBacklog = tspmIpcClient.onBacklogTopic(id, (log: any) => {
|
||||||
|
if (log.runId && log.runId !== lastRunId) {
|
||||||
|
console.log(`[INFO] Detected process restart (runId changed).`);
|
||||||
|
lastSeq = -1;
|
||||||
|
lastRunId = log.runId;
|
||||||
|
}
|
||||||
|
if (log.seq !== undefined && log.seq <= lastSeq) return;
|
||||||
|
if (log.seq !== undefined && log.seq > lastSeq + 1) {
|
||||||
|
console.log(
|
||||||
|
`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
printLog({ ...log, timestamp: new Date(log.timestamp) });
|
||||||
|
if (log.seq !== undefined) lastSeq = log.seq;
|
||||||
|
});
|
||||||
|
await tspmIpcClient.requestLogsBacklogStream(id, { lines: sinceTime ? undefined : lines, sinceTime, types: typesFilter });
|
||||||
|
// Dispose backlog handler after a short grace (backlog is finite)
|
||||||
|
setTimeout(() => disposeBacklog(), 10000);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
await withStreamingLifecycle(
|
await withStreamingLifecycle(
|
||||||
async () => {
|
async () => {
|
||||||
await tspmIpcClient.subscribe(id, (log: any) => {
|
await tspmIpcClient.subscribe(id, (log: any) => {
|
||||||
|
// Reset sequence if runId changed (e.g., process restarted)
|
||||||
|
if (log.runId && log.runId !== lastRunId) {
|
||||||
|
console.log(`[INFO] Detected process restart (runId changed).`);
|
||||||
|
lastSeq = -1;
|
||||||
|
lastRunId = log.runId;
|
||||||
|
}
|
||||||
if (log.seq !== undefined && log.seq <= lastSeq) return;
|
if (log.seq !== undefined && log.seq <= lastSeq) return;
|
||||||
if (log.seq !== undefined && log.seq > lastSeq + 1) {
|
if (log.seq !== undefined && log.seq > lastSeq + 1) {
|
||||||
console.log(
|
console.log(
|
||||||
`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`,
|
`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
printLog(log);
|
||||||
const prefix =
|
|
||||||
log.type === 'stdout'
|
|
||||||
? '[OUT]'
|
|
||||||
: log.type === 'stderr'
|
|
||||||
? '[ERR]'
|
|
||||||
: '[SYS]';
|
|
||||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
|
||||||
if (log.seq !== undefined) lastSeq = log.seq;
|
if (log.seq !== undefined) lastSeq = log.seq;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import { toProcessId } from '../../../shared/protocol/id.js';
|
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
|
||||||
@@ -11,9 +10,9 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const arg = argvArg._[1];
|
const arg = argvArg._[1];
|
||||||
if (!arg) {
|
if (!arg) {
|
||||||
console.error('Error: Please provide a process ID or "all"');
|
console.error('Error: Please provide a process target or "all"');
|
||||||
console.log('Usage:');
|
console.log('Usage:');
|
||||||
console.log(' tspm restart <id>');
|
console.log(' tspm restart <id | id:N | name:LABEL>');
|
||||||
console.log(' tspm restart all');
|
console.log(' tspm restart all');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -33,12 +32,13 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = String(arg);
|
const target = String(arg);
|
||||||
console.log(`Restarting process: ${id}`);
|
console.log(`Restarting process: ${target}`);
|
||||||
const response = await tspmIpcClient.request('restart', { id: toProcessId(id) });
|
const resolved = await tspmIpcClient.request('resolveTarget', { target });
|
||||||
|
const response = await tspmIpcClient.request('restart', { id: resolved.id });
|
||||||
|
|
||||||
console.log(`✓ Process restarted successfully`);
|
console.log(`✓ Process restarted successfully`);
|
||||||
console.log(` ID: ${response.processId}`);
|
console.log(` ID: ${response.processId}${resolved.name ? ` (name: ${resolved.name})` : ''}`);
|
||||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||||
console.log(` Status: ${response.status}`);
|
console.log(` Status: ${response.status}`);
|
||||||
},
|
},
|
||||||
|
62
ts/cli/commands/process/search.ts
Normal file
62
ts/cli/commands/process/search.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
|
import type { CliArguments } from '../../types.js';
|
||||||
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
|
||||||
|
export function registerSearchCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||||
|
registerIpcCommand(
|
||||||
|
smartcli,
|
||||||
|
'search',
|
||||||
|
async (argvArg: CliArguments) => {
|
||||||
|
const query = String(argvArg._[1] || '').trim();
|
||||||
|
if (!query) {
|
||||||
|
console.error('Error: Please provide a search query');
|
||||||
|
console.log('Usage: tspm search <name-fragment | id-fragment>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch list of processes, then enrich with names via describe
|
||||||
|
const listRes = await tspmIpcClient.request('list', {});
|
||||||
|
const processes = listRes.processes;
|
||||||
|
|
||||||
|
// If there are no processes, short-circuit
|
||||||
|
if (processes.length === 0) {
|
||||||
|
console.log('No processes found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerQ = query.toLowerCase();
|
||||||
|
const matches: Array<{ id: number; name?: string }> = [];
|
||||||
|
|
||||||
|
// Collect describe calls to obtain names
|
||||||
|
for (const proc of processes) {
|
||||||
|
try {
|
||||||
|
const desc = await tspmIpcClient.request('describe', { id: proc.id });
|
||||||
|
const name = desc.config.name || '';
|
||||||
|
const idStr = String(proc.id);
|
||||||
|
if (name.toLowerCase().includes(lowerQ) || idStr.includes(query)) {
|
||||||
|
matches.push({ id: proc.id, name });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore describe errors for individual processes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
console.log(`No matches for "${query}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Matches for "${query}":`);
|
||||||
|
for (const m of matches) {
|
||||||
|
if (m.name) {
|
||||||
|
console.log(`- id:${m.id}\tname:${m.name}`);
|
||||||
|
} else {
|
||||||
|
console.log(`- id:${m.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ actionLabel: 'search processes' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@@ -10,17 +10,18 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'start',
|
'start',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID to start');
|
console.error('Error: Please provide a process target to start');
|
||||||
console.log('Usage: tspm start <id>');
|
console.log('Usage: tspm start <id | id:N | name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Starting process id ${id}...`);
|
console.log(`Starting process: ${target}...`);
|
||||||
const response = await tspmIpcClient.request('startById', { id });
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
const response = await tspmIpcClient.request('startById', { id: resolved.id });
|
||||||
console.log('✓ Process started');
|
console.log('✓ Process started');
|
||||||
console.log(` ID: ${response.processId}`);
|
console.log(` ID: ${response.processId}${resolved.name ? ` (name: ${resolved.name})` : ''}`);
|
||||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||||
console.log(` Status: ${response.status}`);
|
console.log(` Status: ${response.status}`);
|
||||||
},
|
},
|
||||||
|
@@ -8,15 +8,16 @@ export function registerStopCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'stop',
|
'stop',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm stop <id>');
|
console.log('Usage: tspm stop <id | id:N | name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Stopping process: ${id}`);
|
console.log(`Stopping process: ${target}`);
|
||||||
const response = await tspmIpcClient.request('stop', { id });
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
const response = await tspmIpcClient.request('stop', { id: resolved.id });
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
console.log(`✓ ${response.message}`);
|
console.log(`✓ ${response.message}`);
|
||||||
|
@@ -2,6 +2,7 @@ import * as plugins from './plugins.js';
|
|||||||
import { tspmIpcClient } from '../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../client/tspm.ipcclient.js';
|
||||||
import * as paths from '../paths.js';
|
import * as paths from '../paths.js';
|
||||||
import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js';
|
import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js';
|
||||||
|
import { TspmServiceManager } from '../client/tspm.servicemanager.js';
|
||||||
|
|
||||||
// Import command registration functions
|
// Import command registration functions
|
||||||
import { registerDefaultCommand } from './commands/default.js';
|
import { registerDefaultCommand } from './commands/default.js';
|
||||||
@@ -10,6 +11,7 @@ import { registerAddCommand } from './commands/process/add.js';
|
|||||||
import { registerStopCommand } from './commands/process/stop.js';
|
import { registerStopCommand } from './commands/process/stop.js';
|
||||||
import { registerRestartCommand } from './commands/process/restart.js';
|
import { registerRestartCommand } from './commands/process/restart.js';
|
||||||
import { registerDeleteCommand } from './commands/process/delete.js';
|
import { registerDeleteCommand } from './commands/process/delete.js';
|
||||||
|
import { registerSearchCommand } from './commands/process/search.js';
|
||||||
import { registerListCommand } from './commands/process/list.js';
|
import { registerListCommand } from './commands/process/list.js';
|
||||||
import { registerDescribeCommand } from './commands/process/describe.js';
|
import { registerDescribeCommand } from './commands/process/describe.js';
|
||||||
import { registerLogsCommand } from './commands/process/logs.js';
|
import { registerLogsCommand } from './commands/process/logs.js';
|
||||||
@@ -50,6 +52,38 @@ export const run = async (): Promise<void> => {
|
|||||||
console.log(
|
console.log(
|
||||||
`Daemon: running v${status.version || 'unknown'} (pid ${status.pid})`,
|
`Daemon: running v${status.version || 'unknown'} (pid ${status.pid})`,
|
||||||
);
|
);
|
||||||
|
// If versions mismatch, offer to refresh the systemd service
|
||||||
|
if (status.version && status.version !== cliVersion) {
|
||||||
|
console.log('\nVersion mismatch detected:');
|
||||||
|
console.log(` CLI: v${cliVersion}`);
|
||||||
|
console.log(` Daemon: v${status.version}`);
|
||||||
|
console.log(
|
||||||
|
'\nThis can happen after upgrading tspm. The systemd service may still point to an older version.\n' +
|
||||||
|
'You can refresh the service (equivalent to "tspm disable" then "tspm enable").',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ask the user for confirmation
|
||||||
|
const confirm = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
||||||
|
'Refresh the systemd service now?',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
if (confirm) {
|
||||||
|
try {
|
||||||
|
const sm = new TspmServiceManager();
|
||||||
|
console.log('Refreshing TSPM system service...');
|
||||||
|
await sm.disableService();
|
||||||
|
await sm.enableService();
|
||||||
|
console.log('✓ Service refreshed. Daemon restarted via systemd.');
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(
|
||||||
|
'Failed to refresh service automatically. You can try manually:\n tspm disable && tspm enable',
|
||||||
|
);
|
||||||
|
console.error(err?.message || String(err));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Skipped service refresh.');
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('Daemon: not running');
|
console.log('Daemon: not running');
|
||||||
}
|
}
|
||||||
@@ -74,6 +108,7 @@ export const run = async (): Promise<void> => {
|
|||||||
registerDescribeCommand(smartcliInstance);
|
registerDescribeCommand(smartcliInstance);
|
||||||
registerLogsCommand(smartcliInstance);
|
registerLogsCommand(smartcliInstance);
|
||||||
registerEditCommand(smartcliInstance);
|
registerEditCommand(smartcliInstance);
|
||||||
|
registerSearchCommand(smartcliInstance);
|
||||||
|
|
||||||
// Batch commands
|
// Batch commands
|
||||||
registerStartAllCommand(smartcliInstance);
|
registerStartAllCommand(smartcliInstance);
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
// Minimal plugin set for lightweight client startup
|
// Minimal plugin set for lightweight client startup
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
import * as smartdaemon from '@push.rocks/smartdaemon';
|
||||||
import * as smartipc from '@push.rocks/smartipc';
|
import * as smartipc from '@push.rocks/smartipc';
|
||||||
|
|
||||||
export { path, smartipc };
|
export { path, smartdaemon, smartipc };
|
||||||
|
|
||||||
|
@@ -155,7 +155,58 @@ export class TspmIpcClient {
|
|||||||
|
|
||||||
const id = toProcessId(processId);
|
const id = toProcessId(processId);
|
||||||
const topic = `logs.${id}`;
|
const topic = `logs.${id}`;
|
||||||
await this.ipcClient.subscribe(`topic:${topic}`, handler);
|
// Note: IpcClient.subscribe expects the bare topic (without the 'topic:' prefix)
|
||||||
|
// and will register a handler for 'topic:<topic>' internally.
|
||||||
|
await this.ipcClient.subscribe(topic, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request backlog logs as a stream from the daemon.
|
||||||
|
* The actual stream will be delivered via the 'stream' event.
|
||||||
|
*/
|
||||||
|
public async requestLogsBacklogStream(
|
||||||
|
processId: ProcessId | number | string,
|
||||||
|
opts: { lines?: number; sinceTime?: number; types?: Array<'stdout' | 'stderr' | 'system'> } = {},
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.ipcClient || !this.isConnected) {
|
||||||
|
throw new Error('Not connected to daemon');
|
||||||
|
}
|
||||||
|
const id = toProcessId(processId);
|
||||||
|
await this.request('logs:subscribe' as any, {
|
||||||
|
id,
|
||||||
|
lines: opts.lines,
|
||||||
|
sinceTime: opts.sinceTime,
|
||||||
|
types: opts.types,
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a handler for incoming streams (e.g., backlog logs)
|
||||||
|
*/
|
||||||
|
public onStream(
|
||||||
|
handler: (info: any, readable: NodeJS.ReadableStream) => void,
|
||||||
|
): void {
|
||||||
|
if (!this.ipcClient) throw new Error('Not connected to daemon');
|
||||||
|
// smartipc emits 'stream' with (info, readable)
|
||||||
|
(this.ipcClient as any).on('stream', handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a temporary handler for backlog topic messages for a specific process
|
||||||
|
*/
|
||||||
|
public onBacklogTopic(
|
||||||
|
processId: ProcessId | number | string,
|
||||||
|
handler: (log: any) => void,
|
||||||
|
): () => void {
|
||||||
|
if (!this.ipcClient) throw new Error('Not connected to daemon');
|
||||||
|
const id = toProcessId(processId);
|
||||||
|
const topicType = `topic:logs.backlog.${id}`;
|
||||||
|
(this.ipcClient as any).onMessage(topicType, handler);
|
||||||
|
return () => {
|
||||||
|
try {
|
||||||
|
(this.ipcClient as any).messageHandlers?.delete?.(topicType);
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,7 +219,8 @@ export class TspmIpcClient {
|
|||||||
|
|
||||||
const id = toProcessId(processId);
|
const id = toProcessId(processId);
|
||||||
const topic = `logs.${id}`;
|
const topic = `logs.${id}`;
|
||||||
await this.ipcClient.unsubscribe(`topic:${topic}`);
|
// Pass bare topic; client handles 'topic:' prefix internally
|
||||||
|
await this.ipcClient.unsubscribe(topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as paths from '../paths.js';
|
import * as paths from '../paths.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -156,6 +156,11 @@ export class ProcessManager extends EventEmitter {
|
|||||||
this.updateProcessInfo(config.id, { pid: undefined });
|
this.updateProcessInfo(config.id, { pid: undefined });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up failure handler to mark process as errored
|
||||||
|
monitor.on('failed', () => {
|
||||||
|
this.updateProcessInfo(config.id, { status: 'errored', pid: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
await monitor.start();
|
await monitor.start();
|
||||||
|
|
||||||
// Wait a moment for the process to spawn and get its PID
|
// Wait a moment for the process to spawn and get its PID
|
||||||
@@ -327,6 +332,11 @@ export class ProcessManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark errored on failure events
|
||||||
|
newMonitor.on('failed', () => {
|
||||||
|
this.updateProcessInfo(id, { status: 'errored', pid: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
this.logger.info(`Successfully restarted process with id '${id}'`);
|
this.logger.info(`Successfully restarted process with id '${id}'`);
|
||||||
} catch (error: Error | unknown) {
|
} catch (error: Error | unknown) {
|
||||||
const processError = new ProcessError(
|
const processError = new ProcessError(
|
||||||
@@ -428,6 +438,13 @@ export class ProcessManager extends EventEmitter {
|
|||||||
info.uptime = uptime;
|
info.uptime = uptime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update memory and cpu from latest monitor readings
|
||||||
|
info.memory = monitor.getLastMemoryUsage();
|
||||||
|
const cpu = monitor.getLastCpuUsage();
|
||||||
|
if (Number.isFinite(cpu)) {
|
||||||
|
info.cpu = cpu;
|
||||||
|
}
|
||||||
|
|
||||||
// Update restart count
|
// Update restart count
|
||||||
info.restarts = monitor.getRestartCount();
|
info.restarts = monitor.getRestartCount();
|
||||||
|
|
||||||
|
@@ -18,6 +18,14 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
private processId?: ProcessId;
|
private processId?: ProcessId;
|
||||||
private currentLogMemorySize: number = 0;
|
private currentLogMemorySize: number = 0;
|
||||||
private readonly MAX_LOG_MEMORY_SIZE = 10 * 1024 * 1024; // 10MB
|
private readonly MAX_LOG_MEMORY_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
// Track approximate size per log to avoid O(n) JSON stringify on every update
|
||||||
|
private logSizeMap: WeakMap<IProcessLog, number> = new WeakMap();
|
||||||
|
private restartTimer: NodeJS.Timeout | null = null;
|
||||||
|
private lastRetryAt: number | null = null;
|
||||||
|
private readonly MAX_RETRIES = 10;
|
||||||
|
private readonly RESET_WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
||||||
|
private lastMemoryUsage: number = 0;
|
||||||
|
private lastCpuUsage: number = 0;
|
||||||
|
|
||||||
constructor(config: IMonitorConfig & { id?: ProcessId }) {
|
constructor(config: IMonitorConfig & { id?: ProcessId }) {
|
||||||
super();
|
super();
|
||||||
@@ -35,7 +43,13 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
const persistedLogs = await this.logPersistence.loadLogs(this.processId);
|
const persistedLogs = await this.logPersistence.loadLogs(this.processId);
|
||||||
if (persistedLogs.length > 0) {
|
if (persistedLogs.length > 0) {
|
||||||
this.logs = persistedLogs;
|
this.logs = persistedLogs;
|
||||||
this.currentLogMemorySize = LogPersistence.calculateLogMemorySize(this.logs);
|
// Recalculate size once from scratch and seed the size map
|
||||||
|
this.currentLogMemorySize = 0;
|
||||||
|
for (const log of this.logs) {
|
||||||
|
const size = this.estimateLogSize(log);
|
||||||
|
this.logSizeMap.set(log, size);
|
||||||
|
this.currentLogMemorySize += size;
|
||||||
|
}
|
||||||
this.logger.info(`Loaded ${persistedLogs.length} persisted logs from disk`);
|
this.logger.info(`Loaded ${persistedLogs.length} persisted logs from disk`);
|
||||||
|
|
||||||
// Delete the persisted file after loading
|
// Delete the persisted file after loading
|
||||||
@@ -83,18 +97,27 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
this.processWrapper.on('log', (log: IProcessLog): void => {
|
this.processWrapper.on('log', (log: IProcessLog): void => {
|
||||||
// Store the log in our buffer
|
// Store the log in our buffer
|
||||||
this.logs.push(log);
|
this.logs.push(log);
|
||||||
console.error(`[ProcessMonitor:${this.config.name}] Received log (type=${log.type}): ${log.message}`);
|
if (process.env.TSPM_DEBUG) {
|
||||||
console.error(`[ProcessMonitor:${this.config.name}] Logs array now has ${this.logs.length} items`);
|
console.error(
|
||||||
|
`[ProcessMonitor:${this.config.name}] Received log (type=${log.type}): ${log.message}`,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
`[ProcessMonitor:${this.config.name}] Logs array now has ${this.logs.length} items`,
|
||||||
|
);
|
||||||
|
}
|
||||||
this.logger.debug(`ProcessMonitor received log: ${log.message}`);
|
this.logger.debug(`ProcessMonitor received log: ${log.message}`);
|
||||||
|
|
||||||
// Update memory size tracking
|
// Update memory size tracking incrementally
|
||||||
this.currentLogMemorySize = LogPersistence.calculateLogMemorySize(this.logs);
|
const approxSize = this.estimateLogSize(log);
|
||||||
|
this.logSizeMap.set(log, approxSize);
|
||||||
|
this.currentLogMemorySize += approxSize;
|
||||||
|
|
||||||
// Trim logs if they exceed memory limit (10MB)
|
// Trim logs if they exceed memory limit (10MB)
|
||||||
while (this.currentLogMemorySize > this.MAX_LOG_MEMORY_SIZE && this.logs.length > 1) {
|
while (this.currentLogMemorySize > this.MAX_LOG_MEMORY_SIZE && this.logs.length > 1) {
|
||||||
// Remove oldest logs until we're under the memory limit
|
// Remove oldest logs until we're under the memory limit
|
||||||
this.logs.shift();
|
const removed = this.logs.shift()!;
|
||||||
this.currentLogMemorySize = LogPersistence.calculateLogMemorySize(this.logs);
|
const removedSize = this.logSizeMap.get(removed) ?? this.estimateLogSize(removed);
|
||||||
|
this.currentLogMemorySize -= removedSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-emit the log event for upstream handlers
|
// Re-emit the log event for upstream handlers
|
||||||
@@ -118,6 +141,14 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
this.logger.info(exitMsg);
|
this.logger.info(exitMsg);
|
||||||
this.log(exitMsg);
|
this.log(exitMsg);
|
||||||
|
|
||||||
|
// Clear pidusage internal state for this PID to prevent memory leaks
|
||||||
|
try {
|
||||||
|
const pidToClear = this.processWrapper?.getPid();
|
||||||
|
if (pidToClear) {
|
||||||
|
(plugins.pidusage as any)?.clear?.(pidToClear);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
// Flush logs to disk on exit
|
// Flush logs to disk on exit
|
||||||
if (this.processId && this.logs.length > 0) {
|
if (this.processId && this.logs.length > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -132,10 +163,7 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
this.emit('exit', code, signal);
|
this.emit('exit', code, signal);
|
||||||
|
|
||||||
if (!this.stopped) {
|
if (!this.stopped) {
|
||||||
this.logger.info('Restarting process...');
|
this.scheduleRestart('exit');
|
||||||
this.log('Restarting process...');
|
|
||||||
this.restartCount++;
|
|
||||||
this.spawnProcess();
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Not restarting process because monitor is stopped',
|
'Not restarting process because monitor is stopped',
|
||||||
@@ -164,10 +192,7 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this.stopped) {
|
if (!this.stopped) {
|
||||||
this.logger.info('Restarting process due to error...');
|
this.scheduleRestart('error');
|
||||||
this.log('Restarting process due to error...');
|
|
||||||
this.restartCount++;
|
|
||||||
this.spawnProcess();
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug('Not restarting process because monitor is stopped');
|
this.logger.debug('Not restarting process because monitor is stopped');
|
||||||
}
|
}
|
||||||
@@ -185,6 +210,49 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule a restart with incremental debounce and failure cutoff.
|
||||||
|
*/
|
||||||
|
private scheduleRestart(reason: 'exit' | 'error'): void {
|
||||||
|
const now = Date.now();
|
||||||
|
// Reset window: if last retry was more than 1 hour ago, reset counter
|
||||||
|
if (this.lastRetryAt && now - this.lastRetryAt >= this.RESET_WINDOW_MS) {
|
||||||
|
this.logger.info('Resetting retry counter after 1 hour window');
|
||||||
|
this.restartCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already at or above max retries?
|
||||||
|
if (this.restartCount >= this.MAX_RETRIES) {
|
||||||
|
const msg = 'Maximum restart attempts reached. Marking process as failed.';
|
||||||
|
this.logger.warn(msg);
|
||||||
|
this.log(msg);
|
||||||
|
this.stopped = true;
|
||||||
|
// Emit a specific event so manager can set status to errored
|
||||||
|
this.emit('failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment and compute delay (1..10 seconds)
|
||||||
|
this.restartCount++;
|
||||||
|
const delaySec = Math.min(this.restartCount, 10);
|
||||||
|
const msg = `Restarting process in ${delaySec}s (attempt ${this.restartCount}/${this.MAX_RETRIES}) due to ${reason}...`;
|
||||||
|
this.logger.info(msg);
|
||||||
|
this.log(msg);
|
||||||
|
|
||||||
|
// Clear existing timer if any, then schedule
|
||||||
|
if (this.restartTimer) {
|
||||||
|
clearTimeout(this.restartTimer);
|
||||||
|
}
|
||||||
|
this.lastRetryAt = now;
|
||||||
|
this.restartTimer = setTimeout(() => {
|
||||||
|
// If stopped in the meantime, do not spawn
|
||||||
|
if (this.stopped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.spawnProcess();
|
||||||
|
}, delaySec * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monitor the process group's memory usage. If the total memory exceeds the limit,
|
* Monitor the process group's memory usage. If the total memory exceeds the limit,
|
||||||
* kill the process group so that the 'exit' handler can restart it.
|
* kill the process group so that the 'exit' handler can restart it.
|
||||||
@@ -194,18 +262,24 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
memoryLimit: number,
|
memoryLimit: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const memoryUsage = await this.getProcessGroupMemory(pid);
|
const { memory: memoryUsage, cpu: cpuUsage } = await this.getProcessGroupStats(pid);
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Memory usage for PID ${pid}: ${this.humanReadableBytes(memoryUsage)} (${memoryUsage} bytes)`,
|
`Memory usage for PID ${pid}: ${this.humanReadableBytes(memoryUsage)} (${memoryUsage} bytes)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only log to the process log at longer intervals to avoid spamming
|
// Store latest readings
|
||||||
this.log(
|
this.lastMemoryUsage = memoryUsage;
|
||||||
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
|
this.lastCpuUsage = cpuUsage;
|
||||||
memoryUsage,
|
|
||||||
)} (${memoryUsage} bytes)`,
|
// Only log memory usage in debug mode to avoid spamming
|
||||||
);
|
if (process.env.TSPM_DEBUG) {
|
||||||
|
this.log(
|
||||||
|
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
|
||||||
|
memoryUsage,
|
||||||
|
)} (${memoryUsage} bytes)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (memoryUsage > memoryLimit) {
|
if (memoryUsage > memoryLimit) {
|
||||||
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
|
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
|
||||||
@@ -235,7 +309,7 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Get the total memory usage (in bytes) for the process group (the main process and its children).
|
* Get the total memory usage (in bytes) for the process group (the main process and its children).
|
||||||
*/
|
*/
|
||||||
private getProcessGroupMemory(pid: number): Promise<number> {
|
private getProcessGroupStats(pid: number): Promise<{ memory: number; cpu: number }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Getting memory usage for process group with PID ${pid}`,
|
`Getting memory usage for process group with PID ${pid}`,
|
||||||
@@ -243,7 +317,7 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
|
|
||||||
plugins.psTree(
|
plugins.psTree(
|
||||||
pid,
|
pid,
|
||||||
(err: Error | null, children: Array<{ PID: string }>) => {
|
(err: any, children: ReadonlyArray<{ PID: string }>) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
const processError = new ProcessError(
|
const processError = new ProcessError(
|
||||||
`Failed to get process tree: ${err.message}`,
|
`Failed to get process tree: ${err.message}`,
|
||||||
@@ -265,7 +339,7 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
|
|
||||||
plugins.pidusage(
|
plugins.pidusage(
|
||||||
pids,
|
pids,
|
||||||
(err: Error | null, stats: Record<string, { memory: number }>) => {
|
(err: Error | null, stats: Record<string, { memory: number; cpu: number }>) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
const processError = new ProcessError(
|
const processError = new ProcessError(
|
||||||
`Failed to get process usage stats: ${err.message}`,
|
`Failed to get process usage stats: ${err.message}`,
|
||||||
@@ -277,14 +351,16 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let totalMemory = 0;
|
let totalMemory = 0;
|
||||||
|
let totalCpu = 0;
|
||||||
for (const key in stats) {
|
for (const key in stats) {
|
||||||
totalMemory += stats[key].memory;
|
totalMemory += stats[key].memory;
|
||||||
|
totalCpu += Number.isFinite(stats[key].cpu) ? stats[key].cpu : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Total memory for process group: ${this.humanReadableBytes(totalMemory)}`,
|
`Total memory for process group: ${this.humanReadableBytes(totalMemory)}`,
|
||||||
);
|
);
|
||||||
resolve(totalMemory);
|
resolve({ memory: totalMemory, cpu: totalCpu });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -325,6 +401,13 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
clearInterval(this.intervalId);
|
clearInterval(this.intervalId);
|
||||||
}
|
}
|
||||||
if (this.processWrapper) {
|
if (this.processWrapper) {
|
||||||
|
// Clear pidusage state for current PID before stopping to avoid leaks
|
||||||
|
try {
|
||||||
|
const pidToClear = this.processWrapper.getPid();
|
||||||
|
if (pidToClear) {
|
||||||
|
(plugins.pidusage as any)?.clear?.(pidToClear);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
this.processWrapper.stop();
|
this.processWrapper.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,7 +416,11 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
* Get the current logs from the process
|
* Get the current logs from the process
|
||||||
*/
|
*/
|
||||||
public getLogs(limit?: number): IProcessLog[] {
|
public getLogs(limit?: number): IProcessLog[] {
|
||||||
console.error(`[ProcessMonitor:${this.config.name}] getLogs called, logs.length=${this.logs.length}, limit=${limit}`);
|
if (process.env.TSPM_DEBUG) {
|
||||||
|
console.error(
|
||||||
|
`[ProcessMonitor:${this.config.name}] getLogs called, logs.length=${this.logs.length}, limit=${limit}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
this.logger.debug(`Getting logs, total stored: ${this.logs.length}`);
|
this.logger.debug(`Getting logs, total stored: ${this.logs.length}`);
|
||||||
if (limit && limit > 0) {
|
if (limit && limit > 0) {
|
||||||
return this.logs.slice(-limit);
|
return this.logs.slice(-limit);
|
||||||
@@ -369,6 +456,20 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
return this.processWrapper?.isRunning() || false;
|
return this.processWrapper?.isRunning() || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get last measured memory usage for the process group (bytes)
|
||||||
|
*/
|
||||||
|
public getLastMemoryUsage(): number {
|
||||||
|
return this.lastMemoryUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get last measured CPU usage for the process group (sum of group, percent)
|
||||||
|
*/
|
||||||
|
public getLastCpuUsage(): number {
|
||||||
|
return this.lastCpuUsage;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method for logging messages with the instance name.
|
* Helper method for logging messages with the instance name.
|
||||||
*/
|
*/
|
||||||
@@ -376,4 +477,17 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
const prefix = this.config.name ? `[${this.config.name}] ` : '';
|
const prefix = this.config.name ? `[${this.config.name}] ` : '';
|
||||||
console.log(prefix + message);
|
console.log(prefix + message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate approximate memory size in bytes for a log entry.
|
||||||
|
* Keeps CPU low by avoiding JSON.stringify on the full array.
|
||||||
|
*/
|
||||||
|
private estimateLogSize(log: IProcessLog): number {
|
||||||
|
const messageBytes = Buffer.byteLength(log.message || '', 'utf8');
|
||||||
|
const typeBytes = Buffer.byteLength(log.type || '', 'utf8');
|
||||||
|
const runIdBytes = Buffer.byteLength((log as any).runId || '', 'utf8');
|
||||||
|
// Rough overhead for object structure, keys, timestamp/seq values
|
||||||
|
const overhead = 64;
|
||||||
|
return messageBytes + typeBytes + runIdBytes + overhead;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -90,9 +90,19 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
|
|
||||||
// Capture stdout
|
// Capture stdout
|
||||||
if (this.process.stdout) {
|
if (this.process.stdout) {
|
||||||
console.error(`[ProcessWrapper] Setting up stdout listener for process ${this.process.pid}`);
|
if (process.env.TSPM_DEBUG) {
|
||||||
|
console.error(
|
||||||
|
`[ProcessWrapper] Setting up stdout listener for process ${this.process.pid}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
this.process.stdout.on('data', (data) => {
|
this.process.stdout.on('data', (data) => {
|
||||||
console.error(`[ProcessWrapper] Received stdout data from PID ${this.process?.pid}: ${data.toString().substring(0, 100)}`);
|
if (process.env.TSPM_DEBUG) {
|
||||||
|
console.error(
|
||||||
|
`[ProcessWrapper] Received stdout data from PID ${this.process?.pid}: ${data
|
||||||
|
.toString()
|
||||||
|
.substring(0, 100)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
// Add data to remainder buffer and split by newlines
|
// Add data to remainder buffer and split by newlines
|
||||||
const text = this.stdoutRemainder + data.toString();
|
const text = this.stdoutRemainder + data.toString();
|
||||||
const lines = text.split('\n');
|
const lines = text.split('\n');
|
||||||
@@ -102,7 +112,9 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
|
|
||||||
// Process complete lines
|
// Process complete lines
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
console.error(`[ProcessWrapper] Processing stdout line: ${line}`);
|
if (process.env.TSPM_DEBUG) {
|
||||||
|
console.error(`[ProcessWrapper] Processing stdout line: ${line}`);
|
||||||
|
}
|
||||||
this.logger.debug(`Captured stdout: ${line}`);
|
this.logger.debug(`Captured stdout: ${line}`);
|
||||||
this.addLog('stdout', line);
|
this.addLog('stdout', line);
|
||||||
}
|
}
|
||||||
|
@@ -97,9 +97,25 @@ export class TspmDaemon {
|
|||||||
this.tspmInstance.on('process:log', ({ processId, log }) => {
|
this.tspmInstance.on('process:log', ({ processId, log }) => {
|
||||||
// Publish to topic for this process
|
// Publish to topic for this process
|
||||||
const topic = `logs.${processId}`;
|
const topic = `logs.${processId}`;
|
||||||
// Broadcast to all connected clients subscribed to this topic
|
// Deliver only to subscribed clients
|
||||||
if (this.ipcServer) {
|
if (this.ipcServer) {
|
||||||
this.ipcServer.broadcast(`topic:${topic}`, log);
|
try {
|
||||||
|
const topicIndex = (this.ipcServer as any).topicIndex as Map<string, Set<string>> | undefined;
|
||||||
|
const subscribers = topicIndex?.get(topic);
|
||||||
|
if (subscribers && subscribers.size > 0) {
|
||||||
|
// Send directly to subscribers for this topic
|
||||||
|
for (const clientId of subscribers) {
|
||||||
|
this.ipcServer
|
||||||
|
.sendToClient(clientId, `topic:${topic}`, log)
|
||||||
|
.catch((err: any) => {
|
||||||
|
// Surface but don't fail the loop
|
||||||
|
console.error('[IPC] sendToClient error:', err?.message || err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[IPC] Topic delivery error:', err?.message || err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -208,6 +224,8 @@ export class TspmDaemon {
|
|||||||
async (request: RequestForMethod<'delete'>) => {
|
async (request: RequestForMethod<'delete'>) => {
|
||||||
try {
|
try {
|
||||||
const id = toProcessId(request.id);
|
const id = toProcessId(request.id);
|
||||||
|
// Ensure desired state reflects stopped before deletion
|
||||||
|
await this.tspmInstance.setDesiredState(id, 'stopped');
|
||||||
await this.tspmInstance.delete(id);
|
await this.tspmInstance.delete(id);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -246,18 +264,7 @@ export class TspmDaemon {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.ipcServer.onMessage(
|
// Note: 'remove' is only a CLI alias. Daemon exposes 'delete' only.
|
||||||
'remove',
|
|
||||||
async (request: RequestForMethod<'remove'>) => {
|
|
||||||
try {
|
|
||||||
const id = toProcessId(request.id);
|
|
||||||
await this.tspmInstance.delete(id);
|
|
||||||
return { success: true, message: `Process ${id} deleted successfully` };
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to remove process: ${error.message}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'list',
|
'list',
|
||||||
@@ -291,6 +298,106 @@ export class TspmDaemon {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Stream backlog logs and let client subscribe to live topic separately
|
||||||
|
this.ipcServer.onMessage(
|
||||||
|
'logs:subscribe',
|
||||||
|
async (
|
||||||
|
request: RequestForMethod<'logs:subscribe'>,
|
||||||
|
clientId: string,
|
||||||
|
) => {
|
||||||
|
const id = toProcessId(request.id);
|
||||||
|
// Determine backlog set
|
||||||
|
const allLogs = await this.tspmInstance.getLogs(id);
|
||||||
|
let filtered = allLogs;
|
||||||
|
if (request.types && request.types.length) {
|
||||||
|
filtered = filtered.filter((l) => request.types!.includes(l.type));
|
||||||
|
}
|
||||||
|
if (request.sinceTime && request.sinceTime > 0) {
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(l) => new Date(l.timestamp).getTime() >= request.sinceTime!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const lines = request.lines && request.lines > 0 ? request.lines : 0;
|
||||||
|
if (lines > 0 && filtered.length > lines) {
|
||||||
|
filtered = filtered.slice(-lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send backlog entries directly to the requesting client as topic messages
|
||||||
|
// in small batches to avoid overwhelming the transport or client.
|
||||||
|
const chunkSize = 200;
|
||||||
|
for (let i = 0; i < filtered.length; i += chunkSize) {
|
||||||
|
const chunk = filtered.slice(i, i + chunkSize);
|
||||||
|
await Promise.allSettled(
|
||||||
|
chunk.map((entry) =>
|
||||||
|
this.ipcServer.sendToClient(
|
||||||
|
clientId,
|
||||||
|
`topic:logs.backlog.${id}`,
|
||||||
|
{
|
||||||
|
...entry,
|
||||||
|
timestamp: new Date(entry.timestamp).getTime(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// Yield a bit between chunks
|
||||||
|
await new Promise((r) => setTimeout(r, 5));
|
||||||
|
}
|
||||||
|
return { ok: true } as any;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resolve target (id:n | name:foo | numeric string) to ProcessId
|
||||||
|
this.ipcServer.onMessage(
|
||||||
|
'resolveTarget',
|
||||||
|
async (request: RequestForMethod<'resolveTarget'>) => {
|
||||||
|
const raw = String(request.target || '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
throw new Error('Empty target');
|
||||||
|
}
|
||||||
|
|
||||||
|
// id:<n>
|
||||||
|
if (/^id:\s*\d+$/i.test(raw)) {
|
||||||
|
const idNum = raw.split(':')[1].trim();
|
||||||
|
const id = toProcessId(idNum);
|
||||||
|
const config = this.tspmInstance.processConfigs.get(id);
|
||||||
|
if (!config) throw new Error(`Process ${id} not found`);
|
||||||
|
return { id, name: config.name } as ResponseForMethod<'resolveTarget'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// name:<label>
|
||||||
|
if (/^name:/i.test(raw)) {
|
||||||
|
const name = raw.slice(raw.indexOf(':') + 1).trim();
|
||||||
|
if (!name) throw new Error('Missing name after name:');
|
||||||
|
const matches = Array.from(this.tspmInstance.processConfigs.values()).filter(
|
||||||
|
(c) => (c.name || '').trim() === name,
|
||||||
|
);
|
||||||
|
if (matches.length === 0) {
|
||||||
|
throw new Error(`No process found with name "${name}"`);
|
||||||
|
}
|
||||||
|
if (matches.length > 1) {
|
||||||
|
const ids = matches.map((c) => String(c.id)).join(', ');
|
||||||
|
throw new Error(
|
||||||
|
`Multiple processes found with name "${name}": ids [${ids}]. Please use id:<n>.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { id: matches[0].id, name } as ResponseForMethod<'resolveTarget'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bare numeric id
|
||||||
|
if (/^\d+$/.test(raw)) {
|
||||||
|
const id = toProcessId(raw);
|
||||||
|
const config = this.tspmInstance.processConfigs.get(id);
|
||||||
|
if (!config) throw new Error(`Process ${id} not found`);
|
||||||
|
return { id, name: config.name } as ResponseForMethod<'resolveTarget'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown format
|
||||||
|
throw new Error(
|
||||||
|
'Unsupported target format. Use numeric id (e.g. 1), id:<n> (e.g. id:1), or name:<label> (e.g. name:api).',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Batch operations handlers
|
// Batch operations handlers
|
||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'startAll',
|
'startAll',
|
||||||
|
@@ -139,6 +139,18 @@ export interface GetLogsResponse {
|
|||||||
logs: IProcessLog[];
|
logs: IProcessLog[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subscribe and stream backlog logs
|
||||||
|
export interface LogsSubscribeRequest {
|
||||||
|
id: ProcessId;
|
||||||
|
lines?: number; // number of backlog lines
|
||||||
|
sinceTime?: number; // ms epoch
|
||||||
|
types?: Array<IProcessLog['type']>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogsSubscribeResponse {
|
||||||
|
ok: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// Start all command
|
// Start all command
|
||||||
export interface StartAllRequest {
|
export interface StartAllRequest {
|
||||||
// No parameters needed
|
// No parameters needed
|
||||||
@@ -240,14 +252,6 @@ export interface AddResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove (delete config and stop if running)
|
// Remove (delete config and stop if running)
|
||||||
export interface RemoveRequest {
|
|
||||||
id: ProcessId;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RemoveResponse {
|
|
||||||
success: boolean;
|
|
||||||
message?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update (modify existing config)
|
// Update (modify existing config)
|
||||||
export interface UpdateRequest {
|
export interface UpdateRequest {
|
||||||
@@ -260,6 +264,16 @@ export interface UpdateResponse {
|
|||||||
config: IProcessConfig;
|
config: IProcessConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve a user-provided target (id:n or name:foo or numeric string) to a ProcessId
|
||||||
|
export interface ResolveTargetRequest {
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolveTargetResponse {
|
||||||
|
id: ProcessId;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Type mappings for methods
|
// Type mappings for methods
|
||||||
export type IpcMethodMap = {
|
export type IpcMethodMap = {
|
||||||
start: { request: StartRequest; response: StartResponse };
|
start: { request: StartRequest; response: StartResponse };
|
||||||
@@ -269,10 +283,10 @@ export type IpcMethodMap = {
|
|||||||
delete: { request: DeleteRequest; response: DeleteResponse };
|
delete: { request: DeleteRequest; response: DeleteResponse };
|
||||||
add: { request: AddRequest; response: AddResponse };
|
add: { request: AddRequest; response: AddResponse };
|
||||||
update: { request: UpdateRequest; response: UpdateResponse };
|
update: { request: UpdateRequest; response: UpdateResponse };
|
||||||
remove: { request: RemoveRequest; response: RemoveResponse };
|
|
||||||
list: { request: ListRequest; response: ListResponse };
|
list: { request: ListRequest; response: ListResponse };
|
||||||
describe: { request: DescribeRequest; response: DescribeResponse };
|
describe: { request: DescribeRequest; response: DescribeResponse };
|
||||||
getLogs: { request: GetLogsRequest; response: GetLogsResponse };
|
getLogs: { request: GetLogsRequest; response: GetLogsResponse };
|
||||||
|
'logs:subscribe': { request: LogsSubscribeRequest; response: LogsSubscribeResponse };
|
||||||
startAll: { request: StartAllRequest; response: StartAllResponse };
|
startAll: { request: StartAllRequest; response: StartAllResponse };
|
||||||
stopAll: { request: StopAllRequest; response: StopAllResponse };
|
stopAll: { request: StopAllRequest; response: StopAllResponse };
|
||||||
restartAll: { request: RestartAllRequest; response: RestartAllResponse };
|
restartAll: { request: RestartAllRequest; response: RestartAllResponse };
|
||||||
@@ -286,6 +300,7 @@ export type IpcMethodMap = {
|
|||||||
response: DaemonShutdownResponse;
|
response: DaemonShutdownResponse;
|
||||||
};
|
};
|
||||||
heartbeat: { request: HeartbeatRequest; response: HeartbeatResponse };
|
heartbeat: { request: HeartbeatRequest; response: HeartbeatResponse };
|
||||||
|
resolveTarget: { request: ResolveTargetRequest; response: ResolveTargetResponse };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper type to extract request type for a method
|
// Helper type to extract request type for a method
|
||||||
|
Reference in New Issue
Block a user