Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
8f96118e0c | |||
b210efde2a | |||
d8709d8b94 | |||
43799f3431 | |||
f4cbdd51e1 | |||
1340c1c248 | |||
92a6ecac71 | |||
5e26b0ab5f | |||
e09cf38f30 | |||
c694672438 |
43
changelog.md
43
changelog.md
@@ -1,5 +1,48 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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))
|
## 2025-08-30 - 5.3.1 - fix(client(tspmIpcClient))
|
||||||
Use bare topic names for IPC client subscribe/unsubscribe to fix log subscription issues
|
Use bare topic names for IPC client subscribe/unsubscribe to fix log subscription issues
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tspm",
|
"name": "@git.zone/tspm",
|
||||||
"version": "5.3.1",
|
"version": "5.5.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': {}
|
||||||
|
34
readme.md
34
readme.md
@@ -177,11 +177,15 @@ Watch: disabled
|
|||||||
|
|
||||||
#### `tspm logs <id|id:N|name:LABEL> [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
|
||||||
@@ -190,10 +194,20 @@ tspm logs name:my-server
|
|||||||
# View last 100 lines
|
# View last 100 lines
|
||||||
tspm logs name: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 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
|
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`
|
||||||
@@ -285,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.
|
||||||
|
@@ -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.3.1',
|
version: '5.5.0',
|
||||||
description: 'a no fuzz process manager'
|
description: 'a no fuzz process manager'
|
||||||
}
|
}
|
||||||
|
@@ -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';
|
||||||
|
|
||||||
@@ -16,23 +16,92 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
console.error('Error: Please provide a process target');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm logs <id | id:N | name:LABEL> [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 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 resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
const id = resolved.id;
|
const id = resolved.id;
|
||||||
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
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'
|
||||||
@@ -42,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: ${resolved.name || 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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@@ -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';
|
||||||
@@ -51,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');
|
||||||
}
|
}
|
||||||
|
@@ -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 };
|
||||||
|
|
||||||
|
@@ -160,6 +160,55 @@ export class TspmIpcClient {
|
|||||||
await this.ipcClient.subscribe(topic, handler);
|
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 {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe from log updates for a specific process
|
* Unsubscribe from log updates for a specific process
|
||||||
*/
|
*/
|
||||||
|
@@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -18,6 +18,8 @@ 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 restartTimer: NodeJS.Timeout | null = null;
|
||||||
private lastRetryAt: number | null = null;
|
private lastRetryAt: number | null = null;
|
||||||
private readonly MAX_RETRIES = 10;
|
private readonly MAX_RETRIES = 10;
|
||||||
@@ -39,7 +41,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
|
||||||
@@ -87,18 +95,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
|
||||||
@@ -122,6 +139,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 {
|
||||||
@@ -241,12 +266,14 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
`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
|
// Only log memory usage in debug mode to avoid spamming
|
||||||
this.log(
|
if (process.env.TSPM_DEBUG) {
|
||||||
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
|
this.log(
|
||||||
memoryUsage,
|
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
|
||||||
)} (${memoryUsage} bytes)`,
|
memoryUsage,
|
||||||
);
|
)} (${memoryUsage} bytes)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (memoryUsage > memoryLimit) {
|
if (memoryUsage > memoryLimit) {
|
||||||
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
|
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
|
||||||
@@ -284,7 +311,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}`,
|
||||||
@@ -366,6 +393,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,7 +408,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);
|
||||||
@@ -417,4 +455,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -282,6 +298,54 @@ 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
|
// Resolve target (id:n | name:foo | numeric string) to ProcessId
|
||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'resolveTarget',
|
'resolveTarget',
|
||||||
|
@@ -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
|
||||||
@@ -274,6 +286,7 @@ export type IpcMethodMap = {
|
|||||||
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 };
|
||||||
|
Reference in New Issue
Block a user