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.
This commit is contained in:
Sam Atkins 2024-04-17 16:31:31 +01:00
parent 2890f19bfd
commit 3526d5d9eb
3 changed files with 57 additions and 8 deletions

View File

@ -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;
}

View File

@ -16,6 +16,9 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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 ]);
}
};
}

View File

@ -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,
});
}