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 639653dac2
commit e355c77a4a
3 changed files with 57 additions and 8 deletions
+2 -2
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;
}
@@ -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 ]);
}
};
}
@@ -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,
});
}