From e355c77a4aa477f45e721feca6ea5e636ba67b13 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Wed, 17 Apr 2024 16:31:31 +0100 Subject: [PATCH] Phoenix: Wait for apps to finish executing, and connect stdio to them After launching an app, if successful, we connect stdio streams to it, and wait for it to exit before we return to the prompt. stdio is implemented as regular AppConnection messages: - stdin: `{ $: 'stdin', data: Uint8Array }` from phoenix -> child - stdout: `{ $: 'stdout', data: Uint8Array }` from child -> phoenix Terminal and Phoenix now communicate with each other using the same style, instead of 'input' and 'output' messages. This will help with eventually running subshells. SIGINT currently is not sent. We also suffer from the same "one more read from stdin happens after app exits" bug that's in PathCommandProvider where I copied the stdin code from. --- packages/phoenix/src/pty/XDocumentPTT.js | 4 +- .../providers/PuterAppCommandProvider.js | 57 +++++++++++++++++-- .../terminal/src/pty/XDocumentANSIShell.js | 4 +- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/packages/phoenix/src/pty/XDocumentPTT.js b/packages/phoenix/src/pty/XDocumentPTT.js index 13ea657a2..64435d2d2 100644 --- a/packages/phoenix/src/pty/XDocumentPTT.js +++ b/packages/phoenix/src/pty/XDocumentPTT.js @@ -38,7 +38,7 @@ export class XDocumentPTT { chunk = encoder.encode(chunk); } terminalConnection.postMessage({ - $: 'output', + $: 'stdout', data: chunk, }); } @@ -52,7 +52,7 @@ export class XDocumentPTT { this.emit('ioctl.set', message); return; } - if (message.$ === 'input') { + if (message.$ === 'stdin') { this.readController.enqueue(message.data); return; } diff --git a/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js b/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js index 44bf6cbbe..8eb678f08 100644 --- a/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js +++ b/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js @@ -16,6 +16,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import { Exit } from '../coreutils/coreutil_lib/exit.js'; +import { signals } from '../../ansi-shell/signals.js'; + const BUILT_IN_APPS = [ 'explorer', ]; @@ -31,8 +34,7 @@ export class PuterAppCommandProvider { // TODO: Parameters and options? async execute(ctx) { const args = {}; // TODO: Passed-in parameters and options would go here - // NOTE: No await here, because launchApp() currently only resolves for Puter SDK apps. - puter.ui.launchApp(id, args); + await puter.ui.launchApp(id, args); } }; } @@ -57,8 +59,55 @@ export class PuterAppCommandProvider { // TODO: Parameters and options? async execute(ctx) { const args = {}; // TODO: Passed-in parameters and options would go here - // NOTE: No await here, yet, because launchApp() currently only resolves for Puter SDK apps. - puter.ui.launchApp(name, args); + const child = await puter.ui.launchApp(name, args); + + // Wait for app to close. + const app_close_promise = new Promise((resolve, reject) => { + child.on('close', () => { + // TODO: Exit codes for apps + resolve({ done: true }); + }); + }); + + // Wait for SIGINT + const sigint_promise = new Promise((resolve, reject) => { + ctx.externs.sig.on((signal) => { + if (signal === signals.SIGINT) { + child.close(); + reject(new Exit(130)); + } + }); + }); + + // We don't connect stdio to non-SDK apps, because they won't make use of it. + if (child.usesSDK) { + const decoder = new TextDecoder(); + child.on('message', message => { + if (message.$ === 'stdout') { + ctx.externs.out.write(decoder.decode(message.data)); + } + }); + + // Repeatedly copy data from stdin to the child, while it's running. + // DRY: Initially copied from PathCommandProvider + let data, done; + const next_data = async () => { + // FIXME: This waits for one more read() after we finish. + ({ value: data, done } = await Promise.race([ + app_close_promise, sigint_promise, ctx.externs.in_.read(), + ])); + if (data) { + child.postMessage({ + $: 'stdin', + data: data, + }); + if (!done) setTimeout(next_data, 0); + } + }; + setTimeout(next_data, 0); + } + + return Promise.race([ app_close_promise, sigint_promise ]); } }; } diff --git a/packages/terminal/src/pty/XDocumentANSIShell.js b/packages/terminal/src/pty/XDocumentANSIShell.js index 8df371e1e..886383411 100644 --- a/packages/terminal/src/pty/XDocumentANSIShell.js +++ b/packages/terminal/src/pty/XDocumentANSIShell.js @@ -53,7 +53,7 @@ export class XDocumentANSIShell { return; } - if (message.$ === 'output') { + if (message.$ === 'stdout') { ptt.out.write(message.data); return; } @@ -69,7 +69,7 @@ export class XDocumentANSIShell { for ( ;; ) { const chunk = (await ptt.in.read()).value; shell.postMessage({ - $: 'input', + $: 'stdin', data: chunk, }); }