@tequila99/githuman 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/cli/index.js +27 -2
- package/dist/cli/server-runtime.js +1 -0
- package/dist/server/app.js +22 -1
- package/dist/server/errors/terminal.js +4 -0
- package/dist/server/hooks/terminal/local-origin.js +25 -0
- package/dist/server/routes/app-info.js +5 -2
- package/dist/server/routes/terminal.js +63 -0
- package/dist/server/services/diff.service.js +23 -1
- package/dist/server/services/terminal/backend.js +216 -0
- package/dist/server/services/terminal/channel.js +200 -0
- package/dist/server/services/terminal/escape-tail.js +93 -0
- package/dist/server/services/terminal/processes.js +115 -0
- package/dist/server/services/terminal/prompt-tracker.js +15 -0
- package/dist/server/services/terminal/registry.js +167 -0
- package/dist/server/services/terminal/session.js +238 -0
- package/dist/server/services/terminal/socket.js +74 -0
- package/dist/server/services/terminal/tokens.js +32 -0
- package/dist/shared/app/schemas.js +5 -1
- package/dist/shared/diff/preamble.js +77 -0
- package/dist/shared/diff/schemas.js +4 -1
- package/dist/shared/terminal/constants.js +27 -0
- package/dist/shared/terminal/schemas.js +114 -0
- package/dist/shared/terminal/types.js +1 -0
- package/dist/web/assets/AppPage-D_OHhrKS.js +93 -0
- package/dist/web/assets/CreateReviewFabButton-B737aIvn.js +1 -0
- package/dist/web/assets/ErrorNotFound-jc5_L_-4.js +1 -0
- package/dist/web/assets/IndexPage-DzlFTw_b.js +1 -0
- package/dist/web/assets/IndexPage-u_qqIPRL.css +1 -0
- package/dist/web/assets/MainLayout-DaSyo22r.js +8 -0
- package/dist/web/assets/MainLayout-DoniW026.css +1 -0
- package/dist/web/assets/QBadge-BY-jTQ8w.js +1 -0
- package/dist/web/assets/QBtn-Bgm40omM.js +1 -0
- package/dist/web/assets/QSelect-713AWDd_.js +1 -0
- package/dist/web/assets/QSpace-BLu5AyZv.css +1 -0
- package/dist/web/assets/QSpace-BVBU-0oC.js +17 -0
- package/dist/web/assets/ReviewDetailPage-CLM6aU4u.js +1 -0
- package/dist/web/assets/ReviewsPage-bllnaoOS.js +1 -0
- package/dist/web/assets/StatusSwitcher-BZ4FYVnY.js +2 -0
- package/dist/web/assets/{StatusSwitcher-Ca5k9Tjx.css → StatusSwitcher-BxRuEHHQ.css} +1 -1
- package/dist/web/assets/TerminalView-B11UHZv3.css +1 -0
- package/dist/web/assets/TerminalView-Ci9CNTyY.js +19 -0
- package/dist/web/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
- package/dist/web/assets/{abnfDiagram-O67JEVCF-BUX560Qb.js → abnfDiagram-O67JEVCF-4qpB0CGh.js} +1 -1
- package/dist/web/assets/agent-store-DIma0R4U.js +1 -0
- package/dist/web/assets/app-info-store-BzsJOxVc.js +1 -0
- package/dist/web/assets/{arc-dElW-85p.js → arc-DgcGdgmL.js} +1 -1
- package/dist/web/assets/architecture-7GRP2DOG-DG6nf_hE.js +1 -0
- package/dist/web/assets/architectureDiagram-NJMV4G6O-CEL-_i5O.js +36 -0
- package/dist/web/assets/{blockDiagram-BEXU5L5S-D4r5wIie.js → blockDiagram-BEXU5L5S-bMrUf1GU.js} +1 -1
- package/dist/web/assets/{c4Diagram-YGBWAQC7-ww04LJhe.js → c4Diagram-YGBWAQC7-C54mp6dl.js} +1 -1
- package/dist/web/assets/channel-CS4rbjhf.js +1 -0
- package/dist/web/assets/chunk-3FUC2YCW-BtIughAT.js +2 -0
- package/dist/web/assets/{chunk-5DYCD2WN-BmtGl0Xn.js → chunk-5DYCD2WN-BhyMEDuJ.js} +1 -1
- package/dist/web/assets/{chunk-742MDFTN-5p0wX5Yc.js → chunk-742MDFTN-8MczzxE2.js} +1 -1
- package/dist/web/assets/{chunk-7INBJB4K-CWFVY0jp.js → chunk-7INBJB4K-BdS9oIIG.js} +1 -1
- package/dist/web/assets/{chunk-7M6MHVWA-DUgZIYj4.js → chunk-7M6MHVWA-BU5_UwaZ.js} +17 -17
- package/dist/web/assets/{chunk-7PRAP22T-BrhA6Tm7.js → chunk-7PRAP22T-BaAw-gEj.js} +1 -1
- package/dist/web/assets/{chunk-GTNCS2PH-B83nsnuR.js → chunk-GTNCS2PH-DNpH8E_x.js} +1 -1
- package/dist/web/assets/{chunk-GWA4HPMP-cewRmL7f.js → chunk-GWA4HPMP-BvG5tXy2.js} +1 -1
- package/dist/web/assets/{chunk-MBY4JIJT-D18SeqyB.js → chunk-MBY4JIJT-B8XJMHG2.js} +1 -1
- package/dist/web/assets/{chunk-NETBCI7D-5SjiRygT.js → chunk-NETBCI7D-ClCXW1dx.js} +1 -1
- package/dist/web/assets/{chunk-O7XYJQB3-H6mLYErS.js → chunk-O7XYJQB3-BNcB0oiZ.js} +6 -6
- package/dist/web/assets/{chunk-UA2S7LBM-C5VQVDlC.js → chunk-UA2S7LBM-CS5CtIsj.js} +1 -1
- package/dist/web/assets/{chunk-WEXAMYUT-CrOcvEcH.js → chunk-WEXAMYUT-CJaZJWKF.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-BuVPxQhc.js → chunk-XXDRQBXY-BvKO0v-W.js} +1 -1
- package/dist/web/assets/{chunk-Z7XXMR3K-BVP3f1-g.js → chunk-Z7XXMR3K-DP2k1Ci5.js} +1 -1
- package/dist/web/assets/{chunk-ZIGJFQKS-CF-OzGXH.js → chunk-ZIGJFQKS-y4KHfdEv.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-NBCMYWYE-Bd3VDNaV.js → classDiagram-v2-NBCMYWYE-C-kWcloV.js} +7 -7
- package/dist/web/assets/{core-DiiZIGCg.js → core-C37KF-j0.js} +1 -1
- package/dist/web/assets/core-II1Hg8Ir.js +12 -0
- package/dist/web/assets/cose-bilkent-JH36ORCC-CMiVhuwL.js +1 -0
- package/dist/web/assets/create-CSUVwdx4.js +1 -0
- package/dist/web/assets/{cynefin-OW5HDTMX-6l9c0yRf.js → cynefin-OW5HDTMX-C3AwH_vU.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-VND7K2PF-u9Gy0Odz.js → cynefinDiagram-VND7K2PF-DYF_CoB8.js} +1 -1
- package/dist/web/assets/{dagre-6A5THRUB-D1jsEBl4.js → dagre-6A5THRUB-DWCEmsd1.js} +1 -1
- package/dist/web/assets/debounce-rZaFFwsz.js +1 -0
- package/dist/web/assets/{diagram-22UHCM2B-DpyH5n1y.js → diagram-22UHCM2B-oBFhSpao.js} +16 -16
- package/dist/web/assets/{diagram-3UASUU5V-3X1RJV9-.js → diagram-3UASUU5V-Cj9rB07y.js} +1 -1
- package/dist/web/assets/{diagram-ATOU4E4O-B_bvvAua.js → diagram-ATOU4E4O-D4H9BqUQ.js} +1 -1
- package/dist/web/assets/{diagram-CDSNMT55-DheqAuOA.js → diagram-CDSNMT55-qLgJBiIs.js} +1 -1
- package/dist/web/assets/{diagram-MLGK6HIB-CYwe9zYy.js → diagram-MLGK6HIB-Bhhvtx0A.js} +1 -1
- package/dist/web/assets/{diagram-MPIPVDR6-CVd9i23e.js → diagram-MPIPVDR6-O4wokd54.js} +1 -1
- package/dist/web/assets/dist-B_J_HbC0.js +1 -0
- package/dist/web/assets/{ebnfDiagram-ZINNZB2B-B5vtXULf.js → ebnfDiagram-ZINNZB2B-BRU7SszO.js} +1 -1
- package/dist/web/assets/{elk-276RUBZZ-BmMM0tV5.js → elk-276RUBZZ-r0X8f7Sv.js} +1 -1
- package/dist/web/assets/engine-oniguruma-B4iNeJbd.js +1 -0
- package/dist/web/assets/engine-oniguruma-DdQrZxAE.js +1 -0
- package/dist/web/assets/{erDiagram-OPXOYQCR-1ORPVLQR.js → erDiagram-OPXOYQCR-DbgjsGkX.js} +1 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-1Q8-FsiO.js +1 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-BbwsRPsv.js +1 -0
- package/dist/web/assets/ganttDiagram-FUAMR5RP-BwhUglGo.js +292 -0
- package/dist/web/assets/{gitGraph-4MIJSDKK-CXCCnZ1j.js → gitGraph-4MIJSDKK-DA11BsSo.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-X574FWY7-DPDrqSt6.js → gitGraphDiagram-X574FWY7-BdG7d-11.js} +1 -1
- package/dist/web/assets/highlight.worker-BHCdEX06.js +2 -0
- package/dist/web/assets/html-CEES10Qm.js +1 -0
- package/dist/web/assets/html-FFOZEiuQ.js +1 -0
- package/dist/web/assets/i18n-D1K2AGeW.js +1 -0
- package/dist/web/assets/index-BatRsx0U.css +1 -0
- package/dist/web/assets/index-DuCIKkvf.js +2 -0
- package/dist/web/assets/{info-A6RAGUB7-CGg3Xa4X.js → info-A6RAGUB7-BxzfvGqu.js} +1 -1
- package/dist/web/assets/{infoDiagram-VRGFBTTK-vx9EUrdV.js → infoDiagram-VRGFBTTK-veq817ut.js} +1 -1
- package/dist/web/assets/init-app-DKRVN8HV.js +1 -0
- package/dist/web/assets/{is-BB7rJzdU.js → is-EbmiuVzv.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-DL7T0J63.js → ishikawaDiagram-OU5B5YK6-DMuzkArR.js} +1 -1
- package/dist/web/assets/{journeyDiagram-ZHPQQLJL-D45T1Pj5.js → journeyDiagram-ZHPQQLJL-Cqj0kbKe.js} +1 -1
- package/dist/web/assets/{kanban-definition-PNTS6WVX-C9ZnTE1l.js → kanban-definition-PNTS6WVX-DUvral0v.js} +1 -1
- package/dist/web/assets/{line-CLBEcmiH.js → line-DcLaUOyJ.js} +1 -1
- package/dist/web/assets/{linear-B5bBEu0i.js → linear-BUnzfXgk.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-Dj-n65Ou.js → mermaid-parser.core-CY7nVQ2O.js} +3 -3
- package/dist/web/assets/mermaid.core-Cz083qmW.js +44 -0
- package/dist/web/assets/{mindmap-definition-NLK3R4M7-ZX638ce_.js → mindmap-definition-NLK3R4M7--Twjhxcv.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-D-XZHUq_.js → packet-AYTQ26CC-Csf3Pf50.js} +1 -1
- package/dist/web/assets/{pegDiagram-GJSIUBJH-sE-EOKNL.js → pegDiagram-GJSIUBJH-vVjFUYc4.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-DMkVggJA.js → pie-WAS4IAKB-CyUE71ZQ.js} +1 -1
- package/dist/web/assets/{pieDiagram-5QR66LMP-DSdIEQyS.js → pieDiagram-5QR66LMP-CV24sdEs.js} +1 -1
- package/dist/web/assets/pinia-UjhgYLzw.js +1 -0
- package/dist/web/assets/pinia-ri8AYppz.js +1 -0
- package/dist/web/assets/{quadrantDiagram-O4NWA36T-BReXSVss.js → quadrantDiagram-O4NWA36T-GVAUaTqf.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-CJzPwlj6.js → radar-RG4KPBEZ-D7YGMFYX.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-CkhRuR3N.js → railroad-74A4TZTK-BYsDf4cg.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-Bg7f5Sbu.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-ufUodNib.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-YvUtEf_g.js +1 -0
- package/dist/web/assets/{railroadDiagram-XR7U4H2S-DCoF4AEN.js → railroadDiagram-XR7U4H2S-DOyP7Y6B.js} +1 -1
- package/dist/web/assets/{requirementDiagram-PLB6GJNP-B15aXZXI.js → requirementDiagram-PLB6GJNP-C2CJ05_O.js} +1 -1
- package/dist/web/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
- package/dist/web/assets/runtime-core.esm-bundler-CF89Lyp2.js +1 -0
- package/dist/web/assets/{safe-storage-ak36tVRO.js → safe-storage-BpHrcFXl.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-IPEJSGJF-DcWsnYYP.js → sankeyDiagram-IPEJSGJF-rYUTxo-7.js} +1 -1
- package/dist/web/assets/scss-CLKKmMvk.js +1 -0
- package/dist/web/assets/{sequenceDiagram-PO4LG4MO-CXWUNmSl.js → sequenceDiagram-PO4LG4MO-Dy-NGv9K.js} +1 -1
- package/dist/web/assets/src-oBChb5qS.js +1 -0
- package/dist/web/assets/{stateDiagram-v2-GCMORJYK-M9PkKdix.js → stateDiagram-v2-GCMORJYK-_uEXf3B4.js} +7 -7
- package/dist/web/assets/{swimlanes-2SLR337P-B13LpAqh.js → swimlanes-2SLR337P-CcejXyI9.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-I9_cJ2VX.js +8 -0
- package/dist/web/assets/theme-Dv0yhgRD.js +1 -0
- package/dist/web/assets/{timeline-definition-EJHVYXUP-Dli0jMaX.js → timeline-definition-EJHVYXUP-CSry3jW1.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-BwBFpWIC.js → treeView-Q6P3EWNA-CUCgNMVi.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-DYGjPuTv.js → treemap-WGGIJYW6-BtG_xZt4.js} +1 -1
- package/dist/web/assets/use-app-theme-CSRcIN0e.js +1 -0
- package/dist/web/assets/use-panel-width-DNg6NE2M.js +1 -0
- package/dist/web/assets/use-server-events-CYiRauy1.js +1 -0
- package/dist/web/assets/use-virtual-scroll-DTEGubei.js +1 -0
- package/dist/web/assets/{usecaseDiagram-POWQR4AR-Chs4O-o-.js → usecaseDiagram-POWQR4AR-BY2X6VR2.js} +1 -1
- package/dist/web/assets/{vennDiagram-UO4OBE2U-D3GVZcIr.js → vennDiagram-UO4OBE2U-D-khbRsr.js} +1 -1
- package/dist/web/assets/vue-15YBeysQ.js +1 -0
- package/dist/web/assets/{vue-i18n.runtime-D0pkhc7g.js → vue-i18n.runtime-BTqPc4vG.js} +2 -2
- package/dist/web/assets/{wardley-WFR3VGLG-B5YUEVo5.js → wardley-WFR3VGLG-BwwTtxgX.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VNRHLVJA-liqH-4lm.js → wardleyDiagram-VNRHLVJA-N4eN_Mx8.js} +1 -1
- package/dist/web/assets/wasm-S0UXjx1m.js +1 -0
- package/dist/web/assets/{xychartDiagram-PMCCYNJV-C63N6ZvA.js → xychartDiagram-PMCCYNJV-Bp3do5lv.js} +1 -1
- package/dist/web/index.html +7 -7
- package/package.json +11 -3
- package/dist/web/assets/AppPage-BSvF7ik1.js +0 -1
- package/dist/web/assets/CreateReviewFabButton-M0od1QEZ.js +0 -1
- package/dist/web/assets/ErrorNotFound-BAP3n0aC.js +0 -1
- package/dist/web/assets/IndexPage-CztuM7ha.css +0 -1
- package/dist/web/assets/IndexPage-Do-EZOBf.js +0 -1
- package/dist/web/assets/MainLayout-ChxXEt_Q.css +0 -1
- package/dist/web/assets/MainLayout-daEN-VtX.js +0 -7
- package/dist/web/assets/QBtn-BDbJU6_B.js +0 -1
- package/dist/web/assets/QMenu-DXgFwoTJ.js +0 -1
- package/dist/web/assets/QSelect-DzYPZHrW.js +0 -1
- package/dist/web/assets/ReviewDetailPage-DEtNg2UN.js +0 -1
- package/dist/web/assets/ReviewsPage-Dg-iCxsQ.js +0 -1
- package/dist/web/assets/StatusSwitcher-BpebV47C.js +0 -4
- package/dist/web/assets/agent-icon-BJbyp2DP.js +0 -17
- package/dist/web/assets/agent-icon-BUd-vCRS.css +0 -1
- package/dist/web/assets/agent-store-B9E-aiz3.js +0 -1
- package/dist/web/assets/app-info-store-xZiQfdgO.js +0 -1
- package/dist/web/assets/architecture-7GRP2DOG-bgxww3gY.js +0 -1
- package/dist/web/assets/architectureDiagram-NJMV4G6O-CUUKpxro.js +0 -36
- package/dist/web/assets/channel-CdThk9yc.js +0 -1
- package/dist/web/assets/chunk-3FUC2YCW-9i3K5FXP.js +0 -2
- package/dist/web/assets/cose-bilkent-JH36ORCC-ifMLrEkd.js +0 -1
- package/dist/web/assets/create-BkZH2Q0U.js +0 -1
- package/dist/web/assets/debounce-CDIE7CV6.js +0 -1
- package/dist/web/assets/dist-BzuUT3GV.js +0 -1
- package/dist/web/assets/engine-oniguruma-D6DCTEub.js +0 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-BH97sTDs.js +0 -1
- package/dist/web/assets/flowDiagram-KWPJA3E3-CRwhE8ka.js +0 -1
- package/dist/web/assets/ganttDiagram-FUAMR5RP-CAWNdy1S.js +0 -292
- package/dist/web/assets/i18n-D5wmXwHr.js +0 -1
- package/dist/web/assets/index-CgeU6Wuu.css +0 -1
- package/dist/web/assets/index-DWi4DKrl.js +0 -2
- package/dist/web/assets/init-app-CgDkvlRb.js +0 -1
- package/dist/web/assets/mermaid.core-CmPsESsw.js +0 -44
- package/dist/web/assets/pinia-BMN9Drn3.js +0 -1
- package/dist/web/assets/pinia-DmTi2T44.js +0 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-CcS8fVgm.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-BlpYA103.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-C2EznudM.js +0 -1
- package/dist/web/assets/runtime-core.esm-bundler-CUpq88z7.js +0 -1
- package/dist/web/assets/single-flight-r8B7stt1.js +0 -1
- package/dist/web/assets/src-CB-shhJB.js +0 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BmCQPnJC.js +0 -8
- package/dist/web/assets/theme-BMaTNJdz.js +0 -1
- package/dist/web/assets/use-app-theme-C9HTPdVu.js +0 -1
- package/dist/web/assets/use-panel-width-DGbjs_T8.js +0 -1
- package/dist/web/assets/use-virtual-scroll-BVHTKnoC.js +0 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Value } from '@sinclair/typebox/value';
|
|
2
|
+
import { TerminalClientMessageSchema } from "../../../shared/terminal/schemas.js";
|
|
3
|
+
import { TERMINAL_CLIENT_BYTES } from "../../../shared/terminal/constants.js";
|
|
4
|
+
import { errorMessage } from "../../../shared/utils/error-message.js";
|
|
5
|
+
// Close the connection when a client queues more commands than it can need.
|
|
6
|
+
const MAX_PENDING_COMMANDS = 128;
|
|
7
|
+
// Disconnect a client that stays blocked for this long.
|
|
8
|
+
const SLOW_CLIENT_MS = 5000;
|
|
9
|
+
// Block the output of a session when a client holds this much unacknowledged data.
|
|
10
|
+
const CREDIT_PAUSE_BYTES = TERMINAL_CLIENT_BYTES / 16;
|
|
11
|
+
export class TerminalChannel {
|
|
12
|
+
subscribed = new Set();
|
|
13
|
+
subscribing = new Set();
|
|
14
|
+
credit = new Map();
|
|
15
|
+
unsubscribe;
|
|
16
|
+
stopped = false;
|
|
17
|
+
commands = Promise.resolve();
|
|
18
|
+
pendingCommands = 0;
|
|
19
|
+
registry;
|
|
20
|
+
send;
|
|
21
|
+
disconnect;
|
|
22
|
+
slowClientMs;
|
|
23
|
+
constructor(registry, send, disconnect, slowClientMs = SLOW_CLIENT_MS) {
|
|
24
|
+
this.registry = registry;
|
|
25
|
+
this.send = send;
|
|
26
|
+
this.disconnect = disconnect;
|
|
27
|
+
this.slowClientMs = slowClientMs;
|
|
28
|
+
this.unsubscribe = registry.subscribe(message => this.event(message));
|
|
29
|
+
send({ type: 'list', sessions: registry.list });
|
|
30
|
+
}
|
|
31
|
+
receive(value) {
|
|
32
|
+
if (!Value.Check(TerminalClientMessageSchema, value)) {
|
|
33
|
+
this.disconnect();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (++this.pendingCommands > MAX_PENDING_COMMANDS) {
|
|
37
|
+
this.disconnect();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.commands = this.commands
|
|
41
|
+
.then(async () => {
|
|
42
|
+
if (this.stopped)
|
|
43
|
+
return undefined;
|
|
44
|
+
try {
|
|
45
|
+
switch (value.type) {
|
|
46
|
+
case 'create': {
|
|
47
|
+
const session = await this.registry.create(value.cols, value.rows);
|
|
48
|
+
if (!this.stopped)
|
|
49
|
+
this.send({
|
|
50
|
+
type: 'created',
|
|
51
|
+
requestId: value.requestId,
|
|
52
|
+
terminalId: session.id
|
|
53
|
+
});
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case 'subscribe':
|
|
57
|
+
await this.attach(value.terminalId);
|
|
58
|
+
break;
|
|
59
|
+
case 'snapshot':
|
|
60
|
+
await this.snapshot(value.terminalId);
|
|
61
|
+
break;
|
|
62
|
+
case 'input':
|
|
63
|
+
this.registry.get(value.terminalId).write(value.data);
|
|
64
|
+
break;
|
|
65
|
+
case 'line':
|
|
66
|
+
this.registry.get(value.terminalId).line(value.data);
|
|
67
|
+
break;
|
|
68
|
+
case 'signal':
|
|
69
|
+
this.registry.get(value.terminalId).signal(value.signal);
|
|
70
|
+
break;
|
|
71
|
+
case 'resize':
|
|
72
|
+
await this.registry.resize(value.terminalId, value.cols, value.rows);
|
|
73
|
+
break;
|
|
74
|
+
case 'close':
|
|
75
|
+
await this.registry.close(value.terminalId);
|
|
76
|
+
break;
|
|
77
|
+
case 'ack':
|
|
78
|
+
this.ack(value.terminalId, value.sequence);
|
|
79
|
+
break;
|
|
80
|
+
case 'ping':
|
|
81
|
+
this.send({ type: 'pong' });
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (!this.stopped)
|
|
87
|
+
this.send({
|
|
88
|
+
type: 'error',
|
|
89
|
+
...('requestId' in value ? { requestId: value.requestId } : {}),
|
|
90
|
+
code: 'GHT_TERMINAL',
|
|
91
|
+
message: errorMessage(error)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
})
|
|
96
|
+
.finally(() => {
|
|
97
|
+
this.pendingCommands--;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async attach(id) {
|
|
101
|
+
if (this.subscribed.has(id) || this.subscribing.has(id))
|
|
102
|
+
return;
|
|
103
|
+
this.subscribing.add(id);
|
|
104
|
+
try {
|
|
105
|
+
this.registry.attach(id, this);
|
|
106
|
+
await this.snapshot(id);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
this.subscribing.delete(id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async snapshot(id) {
|
|
113
|
+
const session = this.registry.get(id);
|
|
114
|
+
await session.snapshot(message => {
|
|
115
|
+
if (this.stopped)
|
|
116
|
+
return undefined;
|
|
117
|
+
clearTimeout(this.credit.get(id)?.timeout);
|
|
118
|
+
session.block(this, false);
|
|
119
|
+
this.credit.set(id, {
|
|
120
|
+
bytes: 0,
|
|
121
|
+
frames: new Map(),
|
|
122
|
+
lastSequence: message.type === 'snapshot' ? message.sequence : 0
|
|
123
|
+
});
|
|
124
|
+
this.subscribed.add(id);
|
|
125
|
+
this.send(message);
|
|
126
|
+
return undefined;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
ack(id, sequence) {
|
|
130
|
+
const credit = this.credit.get(id);
|
|
131
|
+
if (!credit)
|
|
132
|
+
return;
|
|
133
|
+
if (sequence > credit.lastSequence) {
|
|
134
|
+
this.disconnect();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const [key, bytes] of credit.frames)
|
|
138
|
+
if (key <= sequence) {
|
|
139
|
+
credit.bytes -= bytes;
|
|
140
|
+
credit.frames.delete(key);
|
|
141
|
+
}
|
|
142
|
+
this.flow(id, credit);
|
|
143
|
+
}
|
|
144
|
+
flow(id, credit) {
|
|
145
|
+
if (credit.bytes > CREDIT_PAUSE_BYTES) {
|
|
146
|
+
this.registry.get(id).block(this, true);
|
|
147
|
+
credit.timeout ??= setTimeout(() => this.disconnect(), this.slowClientMs);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
clearTimeout(credit.timeout);
|
|
151
|
+
credit.timeout = undefined;
|
|
152
|
+
this.registry.get(id).block(this, false);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
event(message) {
|
|
156
|
+
if (this.stopped)
|
|
157
|
+
return undefined;
|
|
158
|
+
if (message.type === 'output') {
|
|
159
|
+
if (!this.subscribed.has(message.terminalId))
|
|
160
|
+
return;
|
|
161
|
+
const credit = this.credit.get(message.terminalId);
|
|
162
|
+
if (!credit)
|
|
163
|
+
return;
|
|
164
|
+
const bytes = Buffer.byteLength(message.data);
|
|
165
|
+
credit.bytes += bytes;
|
|
166
|
+
credit.frames.set(message.sequence, bytes);
|
|
167
|
+
credit.lastSequence = message.sequence;
|
|
168
|
+
this.flow(message.terminalId, credit);
|
|
169
|
+
if (credit.bytes > TERMINAL_CLIENT_BYTES) {
|
|
170
|
+
this.disconnect();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (message.type === 'exit') {
|
|
175
|
+
clearTimeout(this.credit.get(message.terminalId)?.timeout);
|
|
176
|
+
this.subscribed.delete(message.terminalId);
|
|
177
|
+
this.credit.delete(message.terminalId);
|
|
178
|
+
}
|
|
179
|
+
this.send(message);
|
|
180
|
+
}
|
|
181
|
+
dispose() {
|
|
182
|
+
if (this.stopped)
|
|
183
|
+
return undefined;
|
|
184
|
+
this.stopped = true;
|
|
185
|
+
for (const [id, credit] of this.credit) {
|
|
186
|
+
clearTimeout(credit.timeout);
|
|
187
|
+
try {
|
|
188
|
+
this.registry.get(id).block(this, false);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* The session already exited. */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
this.unsubscribe();
|
|
195
|
+
this.registry.detach(this);
|
|
196
|
+
this.subscribed.clear();
|
|
197
|
+
this.subscribing.clear();
|
|
198
|
+
this.credit.clear();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Drop an unfinished control string that is too long to keep for a snapshot.
|
|
2
|
+
const MAX_ESCAPE_TAIL = 8192;
|
|
3
|
+
export class EscapeTail {
|
|
4
|
+
// text: no sequence is open. escape: after ESC. csi: control sequence, ends with a final byte.
|
|
5
|
+
// string: OSC, DCS, PM or APC data, ends with BEL or ST. string-escape: after ESC inside a string.
|
|
6
|
+
state = 'text';
|
|
7
|
+
value = '';
|
|
8
|
+
get tail() {
|
|
9
|
+
return this.value;
|
|
10
|
+
}
|
|
11
|
+
push(data) {
|
|
12
|
+
for (const char of data) {
|
|
13
|
+
// 8-bit CSI (U+009B) starts a control sequence in any state.
|
|
14
|
+
if (char === '\u009b') {
|
|
15
|
+
this.state = 'csi';
|
|
16
|
+
this.value = char;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
// 8-bit OSC, DCS, SOS, PM and APC start a control string in any state.
|
|
20
|
+
if (['\u009d', '\u0090', '\u0098', '\u009e', '\u009f'].includes(char)) {
|
|
21
|
+
this.state = 'string';
|
|
22
|
+
this.value = char;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (this.state === 'text') {
|
|
26
|
+
// ESC opens a new sequence. Other text is not part of the tail.
|
|
27
|
+
if (char === '\x1b') {
|
|
28
|
+
this.state = 'escape';
|
|
29
|
+
this.value = char;
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
this.value += char;
|
|
34
|
+
// A long OSC, DCS or APC string must not close the shell.
|
|
35
|
+
if (this.value.length > MAX_ESCAPE_TAIL) {
|
|
36
|
+
this.reset();
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// CAN and SUB cancel the open sequence.
|
|
40
|
+
if (char === '\x18' || char === '\x1a') {
|
|
41
|
+
this.reset();
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (this.state === 'escape') {
|
|
45
|
+
if (char === '[') {
|
|
46
|
+
// ESC [ starts a control sequence.
|
|
47
|
+
this.state = 'csi';
|
|
48
|
+
}
|
|
49
|
+
else if (']PX^_'.includes(char)) {
|
|
50
|
+
// ESC ] P X ^ _ start OSC, DCS, SOS, PM and APC strings.
|
|
51
|
+
this.state = 'string';
|
|
52
|
+
}
|
|
53
|
+
else if (char >= '0' && char <= '~') {
|
|
54
|
+
// A final byte ends a short escape sequence, for example ESC 7.
|
|
55
|
+
this.reset();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else if (this.state === 'csi') {
|
|
59
|
+
if (char >= '@' && char <= '~') {
|
|
60
|
+
// A final byte ends the control sequence.
|
|
61
|
+
this.reset();
|
|
62
|
+
}
|
|
63
|
+
else if (char === '\x1b') {
|
|
64
|
+
// ESC aborts the control sequence and opens a new one.
|
|
65
|
+
this.state = 'escape';
|
|
66
|
+
this.value = char;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (this.state === 'string') {
|
|
70
|
+
if (char === '\x07' || char === '\u009c') {
|
|
71
|
+
// BEL or 8-bit ST ends the string.
|
|
72
|
+
this.reset();
|
|
73
|
+
}
|
|
74
|
+
else if (char === '\x1b') {
|
|
75
|
+
// ESC can start the two-character ST (ESC \).
|
|
76
|
+
this.state = 'string-escape';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (char === '\\') {
|
|
80
|
+
// ESC \ is ST: it ends the string.
|
|
81
|
+
this.reset();
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// ESC with any other character is part of the string data.
|
|
85
|
+
this.state = 'string';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
reset() {
|
|
90
|
+
this.state = 'text';
|
|
91
|
+
this.value = '';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
// Give processes time to react to SIGHUP before SIGKILL.
|
|
4
|
+
const TERMINATE_GRACE_MS = 200;
|
|
5
|
+
// A slow `ps` must not block the caller. The caller treats a timeout as unknown.
|
|
6
|
+
const PS_TIMEOUT_MS = 2000;
|
|
7
|
+
// Bound the memory for the `ps` output on a host with many processes.
|
|
8
|
+
const PS_MAX_BUFFER_BYTES = 4 * 1024 * 1024;
|
|
9
|
+
// PID 1 is init. PID 0 and -1 make kill() reach many processes. Never signal them.
|
|
10
|
+
const INIT_PID = 1;
|
|
11
|
+
// Share the Node callback adapter without starting a shell.
|
|
12
|
+
const execute = promisify(execFile);
|
|
13
|
+
export async function processes() {
|
|
14
|
+
const { stdout } = await execute('ps', ['-axo', 'pid=,ppid=,pgid='], {
|
|
15
|
+
timeout: PS_TIMEOUT_MS,
|
|
16
|
+
maxBuffer: PS_MAX_BUFFER_BYTES
|
|
17
|
+
});
|
|
18
|
+
return stdout
|
|
19
|
+
.trim()
|
|
20
|
+
.split('\n')
|
|
21
|
+
.map(line => {
|
|
22
|
+
const [pid, parent, group] = line.trim().split(/\s+/).map(Number);
|
|
23
|
+
return { pid, parent, group };
|
|
24
|
+
})
|
|
25
|
+
.filter(row => Number.isInteger(row.pid) &&
|
|
26
|
+
Number.isInteger(row.parent) &&
|
|
27
|
+
Number.isInteger(row.group) &&
|
|
28
|
+
row.pid > 0);
|
|
29
|
+
}
|
|
30
|
+
// Returns the process and its descendants, each parent before its children.
|
|
31
|
+
// One pass over a parent-to-children map keeps the cost linear.
|
|
32
|
+
export function descendants(rows, pid) {
|
|
33
|
+
const byPid = new Map();
|
|
34
|
+
const children = new Map();
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
byPid.set(row.pid, row);
|
|
37
|
+
const siblings = children.get(row.parent);
|
|
38
|
+
if (siblings) {
|
|
39
|
+
siblings.push(row.pid);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
children.set(row.parent, [row.pid]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// The queue grows while the loop reads it: this is a breadth-first walk.
|
|
46
|
+
const order = [pid];
|
|
47
|
+
const seen = new Set(order);
|
|
48
|
+
for (const id of order) {
|
|
49
|
+
for (const child of children.get(id) ?? []) {
|
|
50
|
+
if (seen.has(child))
|
|
51
|
+
continue;
|
|
52
|
+
seen.add(child);
|
|
53
|
+
order.push(child);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return order.flatMap(id => byPid.get(id) ?? []);
|
|
57
|
+
}
|
|
58
|
+
export function classifyProcessActivity(rows, pid) {
|
|
59
|
+
if (!rows.some(row => row.pid === pid))
|
|
60
|
+
return 'unknown';
|
|
61
|
+
return descendants(rows, pid).some(row => row.pid !== pid)
|
|
62
|
+
? 'running'
|
|
63
|
+
: 'idle';
|
|
64
|
+
}
|
|
65
|
+
export async function processActivity(pid) {
|
|
66
|
+
try {
|
|
67
|
+
return classifyProcessActivity(await processes(), pid);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return 'unknown';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function signalProcess(pid, signal) {
|
|
74
|
+
// PID 0 and -1 would reach the server's own group or every process of the user.
|
|
75
|
+
if (!Number.isInteger(pid) || Math.abs(pid) <= INIT_PID)
|
|
76
|
+
return;
|
|
77
|
+
try {
|
|
78
|
+
process.kill(pid, signal);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* The process may have exited before the signal. */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function terminateProcesses(pid) {
|
|
85
|
+
if (!Number.isInteger(pid) || pid <= INIT_PID)
|
|
86
|
+
return;
|
|
87
|
+
// One snapshot keeps the targets and the group owners consistent.
|
|
88
|
+
let all = [];
|
|
89
|
+
try {
|
|
90
|
+
all = await processes();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* The backend still terminates its shell. */
|
|
94
|
+
}
|
|
95
|
+
const targets = descendants(all, pid);
|
|
96
|
+
const ids = new Set(targets.map(row => row.pid));
|
|
97
|
+
// Only signal groups that are wholly owned by this shell tree.
|
|
98
|
+
const groups = [
|
|
99
|
+
...new Set(targets.map(row => row.group).filter(group => group > INIT_PID))
|
|
100
|
+
].filter(group => all.filter(row => row.group === group).every(row => ids.has(row.pid)));
|
|
101
|
+
// oxlint-disable-next-line unicorn/no-array-reverse -- the type-aware checker lacks toReversed in this module
|
|
102
|
+
const childrenFirst = targets.slice().reverse();
|
|
103
|
+
const signalAll = (signal) => {
|
|
104
|
+
// A group also reaches a child that started after the snapshot.
|
|
105
|
+
for (const group of groups)
|
|
106
|
+
signalProcess(-group, signal);
|
|
107
|
+
for (const row of childrenFirst)
|
|
108
|
+
signalProcess(row.pid, signal);
|
|
109
|
+
signalProcess(pid, signal);
|
|
110
|
+
};
|
|
111
|
+
signalAll('SIGHUP');
|
|
112
|
+
await new Promise(resolve => setTimeout(resolve, TERMINATE_GRACE_MS));
|
|
113
|
+
// A PID can be reused in the grace time. The risk is accepted: PIDs wrap slowly.
|
|
114
|
+
signalAll('SIGKILL');
|
|
115
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// OSC 133 marks a prompt without adding visible characters.
|
|
2
|
+
export const TERMINAL_PROMPT_MARKER = '\x1b]133;A\x07';
|
|
3
|
+
export class PromptTracker {
|
|
4
|
+
idle = false;
|
|
5
|
+
tail = '';
|
|
6
|
+
input() {
|
|
7
|
+
this.idle = false;
|
|
8
|
+
}
|
|
9
|
+
output(data) {
|
|
10
|
+
const combined = this.tail + data;
|
|
11
|
+
if (combined.includes(TERMINAL_PROMPT_MARKER))
|
|
12
|
+
this.idle = true;
|
|
13
|
+
this.tail = combined.slice(-(TERMINAL_PROMPT_MARKER.length - 1));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { MAX_TERMINAL_SESSIONS, TERMINAL_IDLE_MS, TERMINAL_ORPHAN_MS } from "../../../shared/terminal/constants.js";
|
|
3
|
+
import { TerminalLimitError, TerminalNotFoundError } from "../../errors/terminal.js";
|
|
4
|
+
import { createBackend } from "./backend.js";
|
|
5
|
+
import { processes } from "./processes.js";
|
|
6
|
+
import { TerminalSession } from "./session.js";
|
|
7
|
+
// Check the activity of every session this often.
|
|
8
|
+
const POLL_MS = 5000;
|
|
9
|
+
export class TerminalRegistry {
|
|
10
|
+
entries = new Map();
|
|
11
|
+
listeners = new Set();
|
|
12
|
+
closing = new Set();
|
|
13
|
+
poll;
|
|
14
|
+
creating = 0;
|
|
15
|
+
nextNumber = 1;
|
|
16
|
+
stopped = false;
|
|
17
|
+
checking = false;
|
|
18
|
+
options;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
this.poll = setInterval(() => {
|
|
22
|
+
void this.checkActivity();
|
|
23
|
+
}, options.pollMs ?? POLL_MS);
|
|
24
|
+
this.poll.unref();
|
|
25
|
+
}
|
|
26
|
+
get list() {
|
|
27
|
+
return [...this.entries.values()].map(entry => entry.session.info);
|
|
28
|
+
}
|
|
29
|
+
subscribe(listener) {
|
|
30
|
+
this.listeners.add(listener);
|
|
31
|
+
return () => this.listeners.delete(listener);
|
|
32
|
+
}
|
|
33
|
+
publish(message) {
|
|
34
|
+
for (const listener of this.listeners)
|
|
35
|
+
listener(message);
|
|
36
|
+
}
|
|
37
|
+
publishList() {
|
|
38
|
+
this.publish({ type: 'list', sessions: this.list });
|
|
39
|
+
}
|
|
40
|
+
now() {
|
|
41
|
+
return (this.options.now ?? Date.now)();
|
|
42
|
+
}
|
|
43
|
+
get(id) {
|
|
44
|
+
const entry = this.entries.get(id);
|
|
45
|
+
if (!entry)
|
|
46
|
+
throw new TerminalNotFoundError('Terminal session no longer exists.');
|
|
47
|
+
return entry.session;
|
|
48
|
+
}
|
|
49
|
+
async create(cols, rows) {
|
|
50
|
+
if (this.stopped)
|
|
51
|
+
throw new TerminalNotFoundError('Terminal server is stopping.');
|
|
52
|
+
if (this.entries.size + this.creating >= MAX_TERMINAL_SESSIONS)
|
|
53
|
+
throw new TerminalLimitError('The terminal limit has been reached.');
|
|
54
|
+
this.creating++;
|
|
55
|
+
try {
|
|
56
|
+
const backend = await (this.options.backend ?? createBackend)(this.options.repositoryPath, cols, rows);
|
|
57
|
+
if (this.stopped) {
|
|
58
|
+
await backend.kill();
|
|
59
|
+
throw new TerminalNotFoundError('Terminal server is stopping.');
|
|
60
|
+
}
|
|
61
|
+
const id = randomUUID();
|
|
62
|
+
const session = new TerminalSession(id, `Terminal ${this.nextNumber++}`, backend, cols, rows, message => this.publish(message), () => {
|
|
63
|
+
void this.close(id);
|
|
64
|
+
});
|
|
65
|
+
this.entries.set(id, {
|
|
66
|
+
session,
|
|
67
|
+
subscribers: new Set(),
|
|
68
|
+
orphanedSince: this.now(),
|
|
69
|
+
idleSince: null
|
|
70
|
+
});
|
|
71
|
+
this.options.onLifecycle?.({ type: 'created', id, mode: backend.mode });
|
|
72
|
+
this.publishList();
|
|
73
|
+
return session;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
this.creating--;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
attach(id, subscriber) {
|
|
80
|
+
const entry = this.entries.get(id);
|
|
81
|
+
if (!entry)
|
|
82
|
+
throw new TerminalNotFoundError('Terminal session no longer exists.');
|
|
83
|
+
entry.subscribers.add(subscriber);
|
|
84
|
+
entry.orphanedSince = null;
|
|
85
|
+
entry.idleSince = null;
|
|
86
|
+
}
|
|
87
|
+
detach(subscriber) {
|
|
88
|
+
for (const entry of this.entries.values()) {
|
|
89
|
+
if (!entry.subscribers.delete(subscriber) || entry.subscribers.size)
|
|
90
|
+
continue;
|
|
91
|
+
entry.orphanedSince = this.now();
|
|
92
|
+
entry.idleSince = entry.session.running === 'idle' ? this.now() : null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async checkActivity() {
|
|
96
|
+
if (this.checking || this.stopped)
|
|
97
|
+
return;
|
|
98
|
+
this.checking = true;
|
|
99
|
+
try {
|
|
100
|
+
// One process list serves every session. A failed read leaves them unknown.
|
|
101
|
+
let rows;
|
|
102
|
+
if (this.entries.size > 0) {
|
|
103
|
+
try {
|
|
104
|
+
rows = await (this.options.listProcesses ?? processes)();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
rows = undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const [id, entry] of this.entries) {
|
|
111
|
+
const previous = entry.session.running;
|
|
112
|
+
entry.session.running =
|
|
113
|
+
rows === undefined
|
|
114
|
+
? 'unknown'
|
|
115
|
+
: await entry.session.backend.activity(rows);
|
|
116
|
+
if (previous !== entry.session.running)
|
|
117
|
+
this.publishList();
|
|
118
|
+
if (entry.orphanedSince === null)
|
|
119
|
+
continue;
|
|
120
|
+
const now = this.now();
|
|
121
|
+
if (entry.session.running === 'idle') {
|
|
122
|
+
entry.idleSince ??= now;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
entry.idleSince = null;
|
|
126
|
+
}
|
|
127
|
+
if (now - entry.orphanedSince >= TERMINAL_ORPHAN_MS ||
|
|
128
|
+
(entry.idleSince !== null &&
|
|
129
|
+
now - entry.idleSince >= TERMINAL_IDLE_MS)) {
|
|
130
|
+
this.options.onLifecycle?.({ type: 'timeout', id });
|
|
131
|
+
await this.close(id);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
this.checking = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async resize(id, cols, rows) {
|
|
140
|
+
await this.get(id).resize(cols, rows);
|
|
141
|
+
this.publishList();
|
|
142
|
+
}
|
|
143
|
+
async close(id) {
|
|
144
|
+
const entry = this.entries.get(id);
|
|
145
|
+
if (!entry)
|
|
146
|
+
return;
|
|
147
|
+
this.entries.delete(id);
|
|
148
|
+
this.options.onLifecycle?.({ type: 'closed', id });
|
|
149
|
+
this.publish({ type: 'exit', terminalId: id });
|
|
150
|
+
this.publishList();
|
|
151
|
+
const closing = entry.session.dispose();
|
|
152
|
+
this.closing.add(closing);
|
|
153
|
+
try {
|
|
154
|
+
await closing;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
this.closing.delete(closing);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async dispose() {
|
|
161
|
+
this.stopped = true;
|
|
162
|
+
clearInterval(this.poll);
|
|
163
|
+
await Promise.all([...this.entries.keys()].map(id => this.close(id)));
|
|
164
|
+
await Promise.all(this.closing);
|
|
165
|
+
this.listeners.clear();
|
|
166
|
+
}
|
|
167
|
+
}
|