Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/js/handpty.js

28.0 KiB, 1 run

created by r2519314175:1379, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1/* handpty.js — the machine hand's terminal relay, page side.
2 *
3 * `window.DaimondPty` is to a terminal what `window.DaimondHand` is to a
4 * command: the ONE interface above the wire, so the thing drawing the screen
5 * never learns which transport is attached. It is the sibling of hand.js and it
6 * shares hand.js's link — see "One link, shared" below — because a terminal
7 * session is not a second kind of connection to the machine, it is a second kind
8 * of conversation on the one that already exists.
9 *
10 * ── Why a terminal is not an exec ───────────────────────────────────
11 *
12 * `Req::Exec` decides its input before the command starts and reads its output
13 * afterwards. That covers nearly everything an agent does. It cannot cover
14 * `sudo` asking for a password, `ssh` asking for a passphrase, or anything that
15 * asks the kernel whether it is talking to a terminal and behaves differently
16 * when it is not. Those need a pty, and a pty is a conversation: bytes both
17 * ways, for as long as the program lives, with a size the kernel has to be told
18 * about and told again.
19 *
20 * ── Bytes, not text ─────────────────────────────────────────────────
21 *
22 * `output` arrives as base64 and is decoded HERE, once, at this boundary. What
23 * a subscriber receives is a `Uint8Array` of exactly what the program wrote.
24 * Nothing above this line ever sees base64, and nothing below it ever sees a
25 * string: a pty carries a `cat` of a binary file, a half-written UTF-8 character
26 * at the edge of a read, and control sequences whose meaning is their exact
27 * bytes. One mangled byte draws the rest of the screen wrong, and a lossy
28 * conversion corrupts precisely the case a terminal exists to handle.
29 *
30 * ── A hole is shown, not stitched ───────────────────────────────────
31 *
32 * `output` carries a monotonic `seq`, and a step that is not +1 means a chunk is
33 * missing. hand.js writes a marker INTO the stream at that point, because its
34 * stream is text a model reads and the marker is a sentence. This file must not:
35 * bytes written into a terminal stream are drawn, so a marker would be an escape
36 * sequence's worth of damage on top of the loss. So the gap is surfaced BESIDE
37 * the stream — `onGap` — and the bytes still go through. A terminal stitched
38 * silently over a missing chunk draws a screen that never existed, which is the
39 * one outcome worth going to any length to avoid.
40 *
41 * ── One link, shared ────────────────────────────────────────────────
42 *
43 * hand.js holds ONE port to the extension, opened lazily on the first thing that
44 * needs a hand, and multiplexes runs by the `id` the wire already carries. A
45 * terminal session travels on that same link and is told apart by the same id.
46 * Opening a second port would start a second host process, ask a second approval
47 * question, and give the user two hands where they granted one.
48 *
49 * So this file owns no transport at all. It needs exactly two things from
50 * hand.js, and nothing else:
51 *
52 * DaimondHand.send(msg) -> Promise<void>
53 * Post one wire message on the one link, opening and greeting it
54 * first if need be. Rejects with the sentence the model reads,
55 * verbatim: not installed, declined, stopped part-way.
56 *
57 * DaimondHand.subscribe(id, fn) -> unsubscribe()
58 * Every message the hand sends carrying that `id`, in arrival
59 * order. Plus `{t:'__gone', message, met}` when the link dies,
60 * where `message` is the sentence hand.js already writes and `met`
61 * says whether a hand ever answered in this page.
62 *
63 * ── The fence is not this file's to invent ──────────────────────────
64 *
65 * A terminal session runs a real program on the user's machine, so it goes
66 * through the same fence and the same grant as `Tool::Run`: `fence_spec` in
67 * src/tools.rs computes the compartment, the extension vets it, and the hand
68 * enforces it. This file composes no fence and relaxes none. It refuses an
69 * `open` that arrives without one — not as a security boundary, which it is not
70 * placed to be, but because a caller that forgot the fence has a bug, and the
71 * sentence saying so is more use than a refusal from two layers down.
72 */
73(function () {
74 'use strict';
75
76 /// What a caller reads when this page has no hand relay at all. Distinct
77 /// from "no hand is paired": the relay is part of the app, so its absence
78 /// means the page is broken rather than the machine unequipped.
79 var NO_RELAY = 'This page has no machine hand relay loaded, so no terminal can be opened. '
80 + 'That is a fault in the app rather than anything about the user\'s machine: '
81 + 'js/hand.js has not been loaded. Nothing else is affected.';
82
83 /// What a caller reads when hand.js is present but predates terminals.
84 ///
85 /// Kept apart from `NO_RELAY` because the instruction differs: one is a page
86 /// missing a file, the other a page whose relay cannot carry these messages,
87 /// and telling a user their hand is broken when the page is old would send
88 /// them to reinstall software that is working.
89 var NO_CARRY = 'The machine hand relay in this page cannot carry terminal messages. '
90 + 'The hand itself may be perfectly healthy — it is this page that is older than '
91 + 'the terminal. Reload the app, and if it persists the app needs updating. '
92 + 'Commands can still be run; only the interactive terminal is unavailable.';
93
94 /// The longest the page waits for `opened` after asking for a terminal.
95 ///
96 /// Long, because a human sits inside it: the first thing on this link puts
97 /// the approval window on the user's screen, and hand.js holds the request
98 /// in order until it is answered. Bounded all the same, because a question
99 /// nobody answers must still end as a refusal rather than as a terminal that
100 /// never draws anything and never says why.
101 var OPEN_WAIT = 60000;
102
103 /// How much output is held for a session nobody has subscribed to yet.
104 ///
105 /// A program can write its first screen before the caller has attached a
106 /// renderer, and a terminal that misses its own first screen is broken. What
107 /// will not fit is dropped from the OLDEST end and counted, and the count is
108 /// reported as a gap on attachment — the same rule as everywhere else in
109 /// this file: what is lost is said, never smoothed over.
110 var BUFFER_MAX = 256 * 1024;
111
112 /// The largest terminal this page will ask for. A size is two `u16`s on the
113 /// wire and a number outside that is not a big terminal, it is a bug on its
114 /// way to becoming a frame the hand cannot read.
115 var CELLS_MAX = 65535;
116
117 /// Sessions believed to be open, by id.
118 var live = {};
119
120 /// Serial for minted ids, so two terminals in one page never collide.
121 var serial = 0;
122
123 // ── The link ────────────────────────────────────────────────────
124
125 /// The hand relay, or null when this page has none.
126 function hand() {
127 return (window.DaimondHand && typeof window.DaimondHand === 'object')
128 ? window.DaimondHand : null;
129 }
130
131 /// Whether the relay in this page can carry terminal messages at all.
132 ///
133 /// Feature-detected rather than assumed, so a page whose hand.js predates
134 /// terminals refuses with a sentence instead of throwing on a missing
135 /// method — the difference between a user who knows to reload and one
136 /// watching a blank panel.
137 function carries() {
138 var h = hand();
139 return !!(h && typeof h.send === 'function' && typeof h.subscribe === 'function');
140 }
141
142 /// Why this page cannot carry a terminal, or '' when it can.
143 function whyNot() {
144 if (!hand()) return NO_RELAY;
145 if (!carries()) return NO_CARRY;
146 return '';
147 }
148
149 // ── Bytes ───────────────────────────────────────────────────────
150
151 /// Base64 to the bytes it stands for.
152 ///
153 /// `atob` yields a binary string — one UTF-16 unit per byte, each below
154 /// 256 — which is masked back down to bytes. Going through a string is not
155 /// elegant and is the only decoder a page has without a dependency; the mask
156 /// is what makes it exact rather than nearly right.
157 function bytesOf(b64) {
158 var s = atob(b64);
159 var out = new Uint8Array(s.length);
160 for (var i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 0xff;
161 return out;
162 }
163
164 /// Bytes to the base64 the wire carries them as.
165 ///
166 /// Chunked, because `String.fromCharCode.apply` on a large array overflows
167 /// the argument stack — a paste of a long file is exactly the case that
168 /// finds it.
169 function b64Of(u8) {
170 var s = '';
171 for (var i = 0; i < u8.length; i += 0x8000) {
172 s += String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000));
173 }
174 return btoa(s);
175 }
176
177 /// Whatever a caller typed, as bytes.
178 ///
179 /// A string is taken as text and encoded UTF-8, which is what a keyboard
180 /// produces; anything array-like is taken as the bytes it already is.
181 function asBytes(data) {
182 if (data == null) return new Uint8Array(0);
183 if (typeof data === 'string') return new TextEncoder().encode(data);
184 if (data instanceof Uint8Array) return data;
185 if (data instanceof ArrayBuffer) return new Uint8Array(data);
186 if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
187 if (Array.isArray(data)) return new Uint8Array(data);
188 return new TextEncoder().encode(String(data));
189 }
190
191 // ── Sessions ────────────────────────────────────────────────────
192
193 /// A fresh session record.
194 function session(id) {
195 return {
196 id: id,
197 pid: 0,
198 seq: null, // set by the FIRST output; where the hand starts counting is its business
199 subs: null, // the handlers, once someone attaches
200 off: null, // unsubscribe from the link
201 buf: [], // output held for a subscriber that has not attached
202 bufLen: 0,
203 dropped: 0, // bytes the buffer could not hold
204 gaps: 0, // holes seen, for a caller that wants to say so on screen
205 done: false,
206 timer: null,
207 resolve: null,
208 reject: null,
209 };
210 }
211
212 /// Call one handler without letting it take the relay down with it.
213 ///
214 /// A renderer that throws on one frame must not stop the next one arriving:
215 /// the bytes are the machine's, and losing them because the drawing code has
216 /// a bug turns a visual fault into a lost session.
217 function fire(s, name, arg) {
218 if (!s.subs || typeof s.subs[name] !== 'function') return;
219 try { s.subs[name](arg); } catch (e) { /* the renderer's problem, not the link's */ }
220 }
221
222 /// Give a subscriber the bytes, or hold them until there is one.
223 function deliver(s, u8) {
224 if (!u8.length) return;
225 if (s.subs) { fire(s, 'onOutput', u8); return; }
226 s.buf.push(u8);
227 s.bufLen += u8.length;
228 while (s.bufLen > BUFFER_MAX && s.buf.length > 1) {
229 var old = s.buf.shift();
230 s.bufLen -= old.length;
231 s.dropped += old.length;
232 }
233 }
234
235 /// Hand a newly attached subscriber everything that arrived before it.
236 function flush(s) {
237 var held = s.buf;
238 var lost = s.dropped;
239 s.buf = [];
240 s.bufLen = 0;
241 s.dropped = 0;
242 if (lost) {
243 s.gaps++;
244 fire(s, 'onGap', {
245 dropped: lost,
246 reason: 'Output arrived before anything was drawing it, and more of it than the page '
247 + 'would hold, so the oldest ' + lost + ' bytes were dropped. The screen below '
248 + 'starts part-way through.',
249 });
250 }
251 for (var i = 0; i < held.length; i++) fire(s, 'onOutput', held[i]);
252 }
253
254 /// Finish a session once, whichever way it finished.
255 ///
256 /// `how` is the ending as a caller reads it: an exit status where the
257 /// program had one, and a sentence where the link died instead.
258 function settle(s, how) {
259 if (s.done) return;
260 s.done = true;
261 clearTimeout(s.timer);
262 delete live[s.id];
263 // A caller still waiting on the opening is owed the sentence rather than
264 // a promise that never settles: `reject` is cleared the moment `opened`
265 // arrives, so its presence here means the terminal never opened at all.
266 if (s.reject) {
267 var rej = s.reject;
268 s.resolve = null;
269 s.reject = null;
270 rej(new Error(how.refusal || how.reason || 'The terminal closed before it opened.'));
271 }
272 fire(s, 'onClosed', how);
273 if (s.off) { try { s.off(); } catch (e) { /* already gone */ } s.off = null; }
274 }
275
276 /// Accumulate one `output`, in order, and pass it on.
277 ///
278 /// The FIRST output sets the baseline — where the hand starts counting is
279 /// the hand's business, and hand.js's `absorb` and the extension's own check
280 /// both say the same. Assuming zero would open every session with a hole it
281 /// did not have, which is the same failure as hiding one: the marker stops
282 /// meaning anything.
283 function absorb(s, msg) {
284 var want = s.seq;
285 if (want !== null && msg.seq !== want) {
286 s.gaps++;
287 fire(s, 'onGap', {
288 expected: want,
289 got: msg.seq,
290 missing: msg.seq - want,
291 backwards: msg.seq < want,
292 reason: msg.seq < want
293 ? 'The terminal\'s output went backwards, from ' + want + ' to ' + msg.seq
294 + '. What is drawn after this point is not what the program wrote.'
295 : 'The terminal is missing ' + (msg.seq - want) + ' chunk(s) of output, '
296 + 'between sequence ' + want + ' and ' + msg.seq + '. The screen below '
297 + 'has a hole in it and cannot be trusted as a transcript.',
298 });
299 }
300 s.seq = msg.seq + 1;
301 var u8;
302 try { u8 = bytesOf(String(msg.data == null ? '' : msg.data)); }
303 catch (e) {
304 fire(s, 'onError', 'The machine hand sent a chunk of terminal output that is not '
305 + 'base64, so those bytes are lost. The rest of the session carries on.');
306 return;
307 }
308 deliver(s, u8);
309 }
310
311 /// One message about a session, routed.
312 ///
313 /// Only a RECOGNISED type does anything, exactly as in hand.js: a host
314 /// sending something the page does not understand is not evidence that a
315 /// terminal is alive, and treating it as such is how a promise is held open
316 /// for ever.
317 function fromHand(s, msg) {
318 if (!msg || typeof msg.t !== 'string') return;
319
320 if (msg.t === 'opened') {
321 s.pid = Number(msg.pid) || 0;
322 clearTimeout(s.timer);
323 if (s.resolve) {
324 var res = s.resolve;
325 s.resolve = null;
326 s.reject = null;
327 res({ id: s.id, pid: s.pid });
328 }
329 return;
330 }
331 if (msg.t === 'output') { absorb(s, msg); return; }
332 if (msg.t === 'closed') {
333 settle(s, {
334 id: s.id,
335 exit: typeof msg.exit === 'number' ? msg.exit : -1,
336 killed: !!msg.killed,
337 gaps: s.gaps,
338 });
339 return;
340 }
341 if (msg.t === 'refused') {
342 // Whole sentences, written for a person or a model to act on. Passed
343 // through untouched: wrapping one loses the only instruction it gives.
344 settle(s, { id: s.id, exit: -1, killed: false, gaps: s.gaps, refusal: msg.reason || '' });
345 return;
346 }
347 if (msg.t === 'error') {
348 // An error ABOUT a session is a note, not an ending — the same rule
349 // hand.js follows. The extension reports a sequence gap this way and
350 // then carries on sending output; treating it as a settlement throws
351 // away a session that was going to keep working.
352 fire(s, 'onError', msg.message || 'The machine hand reported a problem with this terminal.');
353 return;
354 }
355 if (msg.t === '__gone') {
356 // The link died. `met` is hand.js's own record of whether a hand ever
357 // answered in this page, and it is the whole difference between "you
358 // have not installed it" and "it stopped" — two different instructions
359 // to a user, and telling someone to install what they already have
360 // wastes their afternoon.
361 settle(s, {
362 id: s.id,
363 exit: -1,
364 killed: true,
365 gaps: s.gaps,
366 stopped: !!msg.met,
367 absent: !msg.met,
368 reason: msg.message || NO_RELAY,
369 });
370 }
371 }
372
373 // ── Opening one ─────────────────────────────────────────────────
374
375 /// A caller-supplied id, checked, or a fresh one.
376 ///
377 /// The id is echoed on every message about the session, so an unbounded or
378 /// unprintable one is a frame the hand cannot send. The extension enforces
379 /// the same limits on the way past; this refuses earlier, where the caller
380 /// can still be told which of its own values was wrong.
381 function idFor(spec) {
382 var id = spec && typeof spec.id === 'string' ? spec.id : '';
383 if (!id) {
384 serial++;
385 return 'pty-' + serial + '-' + Math.random().toString(36).slice(2, 8);
386 }
387 return id;
388 }
389
390 /// What is wrong with an `open`, in a whole sentence, or '' when nothing is.
391 ///
392 /// Deliberately short. The compartment is checked by the extension and
393 /// ENFORCED by the hand, which knows what it granted; nothing here pretends
394 /// otherwise. What it does is catch a caller that forgot the fence, because
395 /// a refusal naming `fence_spec` is more use than one from two layers down
396 /// that can only say the fence was missing.
397 function wrongOpen(spec) {
398 if (!spec || typeof spec !== 'object') {
399 return 'A terminal needs a request saying what to run: {argv, cwd, env, size, fence}.';
400 }
401 if (!Array.isArray(spec.argv) || !spec.argv.length
402 || !spec.argv.every(function (a) { return typeof a === 'string'; })) {
403 return 'A terminal needs argv: the program and its arguments, as an array of strings. '
404 + 'A shell is a perfectly ordinary thing to put in argv[0] — it is a shell STRING '
405 + 'that has no meaning here, because there is nothing to interpret one.';
406 }
407 if (typeof spec.cwd !== 'string' || spec.cwd.charAt(0) !== '/') {
408 return 'A terminal needs cwd: an absolute working directory inside the fence. '
409 + 'The hand does not guess what a relative path is relative to.';
410 }
411 if (!spec.fence || typeof spec.fence !== 'object' || Array.isArray(spec.fence)) {
412 return 'A terminal needs a fence saying what the session may touch: {rw, ro, deny, net}. '
413 + 'It is composed by fence_spec in src/tools.rs from the Diamond\'s bounds and the '
414 + 'folder the user granted, exactly as it is for a command — this relay does not '
415 + 'compose one, and a session with no compartment is not opened.';
416 }
417 var rw = Array.isArray(spec.fence.rw) ? spec.fence.rw : [];
418 var ro = Array.isArray(spec.fence.ro) ? spec.fence.ro : [];
419 if (!rw.length && !ro.length) {
420 return 'That fence names no root at all, so the session could not read the directory it '
421 + 'would start in. Say what it may work under.';
422 }
423 return '';
424 }
425
426 /// A size the wire can carry, from whatever the caller offered.
427 function sizeOf(size) {
428 var cols = Math.floor(Number((size && size.cols) || 80));
429 var rows = Math.floor(Number((size && size.rows) || 24));
430 if (!(cols > 0)) cols = 80;
431 if (!(rows > 0)) rows = 24;
432 return { cols: Math.min(cols, CELLS_MAX), rows: Math.min(rows, CELLS_MAX) };
433 }
434
435 /// Open a terminal and attach a program to it.
436 ///
437 /// `spec` is the wire's own `open` request, composed by the caller that owns
438 /// the fence — the Rust side, via `fence_spec`. It is passed through
439 /// unchanged but for an id and a size, so there is ONE place a request is
440 /// composed and it is the one that holds the compartment.
441 ///
442 /// `subs` is optional and may also be attached later with `subscribe`;
443 /// passing it here is the safe order, because a program can write its first
444 /// screen before this promise resolves.
445 ///
446 /// # Returns
447 /// A promise resolving to `{id, pid}` when the hand says `opened`, and
448 /// rejecting with the refusal verbatim when it will not.
449 function open(spec, subs) {
450 if (typeof spec === 'string') {
451 try { spec = JSON.parse(spec); }
452 catch (e) { return Promise.reject(new Error('The terminal request could not be read: ' + e.message)); }
453 }
454 var no = whyNot();
455 if (no) return Promise.reject(new Error(no));
456 var bad = wrongOpen(spec);
457 if (bad) return Promise.reject(new Error(bad));
458
459 var id = idFor(spec);
460 var s = session(id);
461 if (subs) s.subs = subs;
462 live[id] = s;
463
464 // EVERY FIELD THE COMPOSER SENT, and not a list of the ones this file
465 // happened to know about.
466 //
467 // It WAS such a list until 2026-08-24, and the list was one field out of
468 // date. `pty_request` grew `toolkits` when a Diamond's granted toolchain
469 // began travelling beside the fence — the hand cannot check a fence naming
470 // `~/.cargo/registry` against the granted root unless it is TOLD which
471 // toolchain was granted — and this end went on sending the same six
472 // fields. So a session in a Diamond granted git arrived at the extension
473 // with `~/.gitconfig` in its fence and no toolchain named, was refused by
474 // the extension's own correct rule, and the owner could not open a
475 // terminal at all. The two ends had not disagreed about the fence; one of
476 // them had simply stopped copying part of the request.
477 //
478 // The compartment is composed in ONE place, in Rust, and `wrongOpen` above
479 // says so in as many words. A relay that re-lists the fields is a second
480 // composer holding an older idea of what a request is, so this one
481 // re-lists nothing: what arrived is forwarded whole, and only what this
482 // end OWNS is set over the top of it. The id is this end's because only it
483 // knows which sessions this page already has open; the size is normalised
484 // because the wire carries two cell counts and a caller may hand over
485 // anything; `env` and `argv` are pinned to the shapes the wire requires.
486 // The extension checks what arrives and the hand enforces it, so a field
487 // this end does not understand is not this end's to drop.
488 var msg = {};
489 for (var k in spec) {
490 if (Object.prototype.hasOwnProperty.call(spec, k)) msg[k] = spec[k];
491 }
492 msg.t = 'open';
493 msg.id = id;
494 msg.argv = spec.argv.slice();
495 msg.env = Array.isArray(spec.env) ? spec.env : [];
496 msg.size = sizeOf(spec.size);
497
498 return new Promise(function (resolve, reject) {
499 s.resolve = resolve;
500 s.reject = reject;
501 s.off = hand().subscribe(id, function (m) { fromHand(s, m); });
502 s.timer = setTimeout(function () {
503 settle(s, {
504 id: id, exit: -1, killed: false, gaps: s.gaps,
505 refusal: 'Daimond asked for a terminal and the machine hand did not open one. The '
506 + 'approval window may still be waiting — the Daimond Hands toolbar icon carries '
507 + 'the question until it is answered. Answer it and try again.',
508 });
509 }, OPEN_WAIT);
510 hand().send(msg).catch(function (e) {
511 // hand.js's rejection is already a whole sentence about a hand
512 // that is missing, declined or stopped. Verbatim, or the user is
513 // told to fix the wrong thing.
514 settle(s, { id: id, exit: -1, killed: false, gaps: s.gaps, refusal: (e && e.message) || NO_RELAY });
515 });
516 });
517 }
518
519 /// Attach a renderer to a session, and receive everything it has already
520 /// said.
521 ///
522 /// `subs` is `{onOutput, onGap, onClosed, onError}`, all optional:
523 ///
524 /// onOutput(Uint8Array) exactly the bytes the program wrote
525 /// onGap({expected, got, missing, backwards, dropped, reason})
526 /// output is missing; what follows is not continuous
527 /// onClosed({exit, killed, gaps, stopped, absent, reason, refusal})
528 /// the session is over, one way or another
529 /// onError(sentence) a note about the session that did not end it
530 ///
531 /// # Returns
532 /// A function that detaches. Detaching does not close the session; use
533 /// `close` for that.
534 function subscribe(id, subs) {
535 var s = live[id];
536 if (!s) return function () {};
537 s.subs = subs || null;
538 if (s.subs) flush(s);
539 return function () { s.subs = null; };
540 }
541
542 /// Send keystrokes to a terminal.
543 ///
544 /// Raw, and not a line: a terminal is a byte stream, and `Ctrl-C`, an arrow
545 /// key and a bracketed paste are all just bytes the program is entitled to
546 /// see as they were typed. A string is encoded UTF-8; anything array-like is
547 /// sent as the bytes it already is.
548 function input(id, data) {
549 var no = whyNot();
550 if (no) return Promise.reject(new Error(no));
551 if (!live[id]) {
552 return Promise.reject(new Error('There is no terminal "' + id + '" in this page to type into. '
553 + 'It has closed, or it was never opened here.'));
554 }
555 var u8 = asBytes(data);
556 if (!u8.length) return Promise.resolve();
557 return hand().send({ t: 'input', id: id, data: b64Of(u8) });
558 }
559
560 /// Tell the kernel the window changed size, which tells the program.
561 ///
562 /// A program asks the kernel how big its terminal is, not the page, so it
563 /// has to be told at the pty and told again on every change — a `less` that
564 /// thinks it has 24 rows on an 80-row screen is the visible symptom of
565 /// forgetting the second half.
566 function resize(id, cols, rows) {
567 var no = whyNot();
568 if (no) return Promise.reject(new Error(no));
569 if (!live[id]) {
570 return Promise.reject(new Error('There is no terminal "' + id + '" in this page to resize.'));
571 }
572 return hand().send({ t: 'resize', id: id, size: sizeOf({ cols: cols, rows: rows }) });
573 }
574
575 /// Ask a terminal's program to stop.
576 ///
577 /// **There is no `close` on the wire, and that is deliberate.** A session
578 /// ends when the program does, and the way to end a program is to signal it
579 /// — the same `Req::Signal` any other run is ended with, carrying this
580 /// session's id. So this asks, and the authoritative ending remains the
581 /// `closed` message the hand sends when the program has actually gone.
582 ///
583 /// # Arguments
584 /// * `id` - The session.
585 /// * `sig` - `'term'` to ask, `'kill'` to insist, `'int'` to interrupt as
586 /// `Ctrl-C` would. Asking is the default: a shell given `SIGTERM` writes
587 /// out its history, and one given `SIGKILL` does not.
588 function close(id, sig) {
589 var no = whyNot();
590 if (no) return Promise.reject(new Error(no));
591 if (!live[id]) return Promise.resolve();
592 var which = (sig === 'kill' || sig === 'int') ? sig : 'term';
593 return hand().send({ t: 'signal', id: id, sig: which });
594 }
595
596 /// Forget a session locally, without asking the machine anything.
597 ///
598 /// For a caller tearing down its own view: the program is still running and
599 /// the hand still owns it. Ending the LINK is what ends the program, and
600 /// hand.js does that when the page goes away.
601 function forget(id) {
602 var s = live[id];
603 if (!s) return;
604 settle(s, {
605 id: id, exit: -1, killed: false, gaps: s.gaps,
606 reason: 'The page let go of this terminal. The program is the hand\'s until the link closes.',
607 });
608 }
609
610 /// What can be said about terminals here, without opening anything.
611 ///
612 /// It never rejects: a caller asking what is attached is owed an answer, and
613 /// the sentence explaining a "no" is otherwise lost. `carries` is about THIS
614 /// PAGE — whether its relay can carry these messages at all — and is a
615 /// different question from whether the machine's hand can allocate a pty,
616 /// which the hand answers in its own `caps` and which is read from there.
617 function status() {
618 var no = whyNot();
619 var mine = { carries: carries(), sessions: Object.keys(live).length };
620 if (no) {
621 return Promise.resolve(JSON.stringify(Object.assign({
622 paired: false, transport: 'none', caps: [], reason: no,
623 }, mine)));
624 }
625 return hand().status().then(function (raw) {
626 var j;
627 try { j = JSON.parse(raw); } catch (e) { j = { paired: false, caps: [], reason: String(raw) }; }
628 return JSON.stringify(Object.assign(j, mine));
629 }, function (e) {
630 return JSON.stringify(Object.assign({
631 paired: false, transport: 'none', caps: [], reason: (e && e.message) || no || NO_RELAY,
632 }, mine));
633 });
634 }
635
636 /// The sessions this page believes are open.
637 function sessions() {
638 return Object.keys(live);
639 }
640
641 window.DaimondPty = {
642 open: open,
643 subscribe: subscribe,
644 input: input,
645 resize: resize,
646 close: close,
647 forget: forget,
648 status: status,
649 sessions: sessions,
650 /// Test only. The waits are tens of seconds by design, and a test cannot
651 /// spend a minute proving a terminal never opened. Same-origin callers
652 /// only, and the worst one can do with it is make its own sessions give
653 /// up sooner.
654 _setWaitsForTest: function (o) {
655 o = (typeof o === 'number') ? { open: o } : (o || {});
656 if (o.open > 0) OPEN_WAIT = o.open;
657 if (o.buffer > 0) BUFFER_MAX = o.buffer;
658 return { open: OPEN_WAIT, buffer: BUFFER_MAX };
659 },
660 };
661})();