From 7674da4cd225bcad34079251c5600fc32e32248b Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 23 May 2024 12:54:06 +0100 Subject: [PATCH 1/3] feat: Add exit status code to apps `puter.exit()` now takes a status code, similar to the exit status on desktop OSes. This is passed to the appClosed event, so that eg a parent app can know whether its child app ran successfully. --- packages/puter-js/src/index.js | 8 +++++++- packages/puter-js/src/modules/UI.js | 1 + src/IPC.js | 13 +++++++++++-- src/UI/UIWindow.js | 2 +- src/helpers.js | 4 +++- 5 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/puter-js/src/index.js b/packages/puter-js/src/index.js index ac2710867..df4c55ee6 100644 --- a/packages/puter-js/src/index.js +++ b/packages/puter-js/src/index.js @@ -262,10 +262,16 @@ window.puter = (function() { this.updateSubmodules(); } - exit = function() { + exit = function(statusCode = 0) { + if (statusCode && (typeof statusCode !== 'number')) { + console.warn('puter.exit() requires status code to be a number. Treating it as 1'); + statusCode = 1; + } + window.parent.postMessage({ msg: "exit", appInstanceID: this.appInstanceID, + statusCode, }, '*'); } diff --git a/packages/puter-js/src/modules/UI.js b/packages/puter-js/src/modules/UI.js index a8bf3c620..7bf7e53c0 100644 --- a/packages/puter-js/src/modules/UI.js +++ b/packages/puter-js/src/modules/UI.js @@ -54,6 +54,7 @@ class AppConnection extends EventListener { this.#isOpen = false; this.emit('close', { appInstanceID: this.targetAppInstanceID, + statusCode: event.data.statusCode, }); } }); diff --git a/src/IPC.js b/src/IPC.js index a04cba01a..28249782d 100644 --- a/src/IPC.js +++ b/src/IPC.js @@ -1201,6 +1201,15 @@ window.addEventListener('message', async (event) => { // exit //-------------------------------------------------------- else if(event.data.msg === 'exit'){ - $(window.window_for_app_instance(event.data.appInstanceID)).close({bypass_iframe_messaging: true}); + // Ensure status code is a number. Convert any truthy non-numbers to 1. + let status_code = event.data.statusCode ?? 0; + if (status_code && (typeof status_code !== 'number')) { + status_code = 1; + } + + $(window.window_for_app_instance(event.data.appInstanceID)).close({ + bypass_iframe_messaging: true, + status_code, + }); } -}); \ No newline at end of file +}); diff --git a/src/UI/UIWindow.js b/src/UI/UIWindow.js index 06def551a..f8356b208 100644 --- a/src/UI/UIWindow.js +++ b/src/UI/UIWindow.js @@ -2887,7 +2887,7 @@ $.fn.close = async function(options) { $(`.window[data-parent_uuid="${window_uuid}"]`).close(); // notify other apps that we're closing - window.report_app_closed(window_uuid); + window.report_app_closed(window_uuid, options.status_code ?? 0); // remove backdrop $(this).closest('.window-backdrop').remove(); diff --git a/src/helpers.js b/src/helpers.js index 5e15a53a2..65f1c1559 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -3511,7 +3511,7 @@ window.report_app_launched = (instance_id, { uses_sdk = true }) => { }; // Run any callbacks to say that the app has closed -window.report_app_closed = (instance_id) => { +window.report_app_closed = (instance_id, status_code) => { const el_window = window.window_for_app_instance(instance_id); // notify parent app, if we have one, that we're closing @@ -3521,6 +3521,7 @@ window.report_app_closed = (instance_id) => { parent.contentWindow.postMessage({ msg: 'appClosed', appInstanceID: instance_id, + statusCode: status_code ?? 0, }, '*'); } @@ -3530,6 +3531,7 @@ window.report_app_closed = (instance_id) => { child.contentWindow.postMessage({ msg: 'appClosed', appInstanceID: instance_id, + statusCode: status_code ?? 0, }, '*'); }); From 5de305202656a172b187dac87543d6c1c69a2958 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 23 May 2024 12:55:53 +0100 Subject: [PATCH 2/3] feat(phoenix): Respond to exit status codes - Detect exit status of Puter apps, now that that's available. - Store the exit status of each pipeline. - Display a message when the exit status was non-zero. That message is temporary, until we have a better way of displaying or querying it, such as the `$?` shell variable. --- packages/phoenix/src/ansi-shell/ANSIShell.js | 8 ++++++++ .../phoenix/src/ansi-shell/pipeline/Pipeline.js | 17 +++++++++++------ .../providers/PuterAppCommandProvider.js | 9 ++++++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/phoenix/src/ansi-shell/ANSIShell.js b/packages/phoenix/src/ansi-shell/ANSIShell.js index 68f260a48..b1b4a1c74 100644 --- a/packages/phoenix/src/ansi-shell/ANSIShell.js +++ b/packages/phoenix/src/ansi-shell/ANSIShell.js @@ -160,6 +160,7 @@ export class ANSIShell extends EventTarget { } this.ctx.externs.out.write('error: ' + e.message + '\n'); console.log(e); + this.ctx.locals.exit = -1; return; } } @@ -225,6 +226,13 @@ export class ANSIShell extends EventTarget { const pipeline = await Pipeline.createFromAST(executionCtx, ast); await pipeline.execute(executionCtx); + + // Store exit code for the next pipeline + // TODO: This feels like a hacky way of doing this. + this.ctx.locals.exit = executionCtx.locals.exit; + if ( this.ctx.locals.exit ) { + this.ctx.externs.out.write(`Exited with code ${this.ctx.locals.exit}\n`); + } } expandPromptString (str) { diff --git a/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js b/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js index 186c8cab9..590d96e4b 100644 --- a/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js +++ b/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js @@ -247,14 +247,14 @@ export class PreparedCommand { if ( ! ctx.cmdExecState.valid ) { ctx.locals.exit = -1; await ctx.externs.out.close(); - return; + return 1; } if ( ctx.cmdExecState.printHelpAndExit ) { ctx.locals.exit = 0; await printUsage(command, ctx.externs.out, ctx.vars); await ctx.externs.out.close(); - return; + return 0; } let execute = command.execute.bind(command); @@ -291,13 +291,14 @@ export class PreparedCommand { command.name + ': ' + e.message + '\x1B[0m\n' ); + exit_code = -1; } else { await ctx.externs.err.write( '\x1B[31;1m' + command.name + ': ' + e.toString() + '\x1B[0m\n' ); - ctx.locals.exit = -1; + exit_code = -1; } if ( ! (e instanceof Exit) ) console.error(e); } @@ -316,6 +317,8 @@ export class PreparedCommand { await filesystem.write(path, outputMemWriters[i].getAsBlob()); } + + return exit_code; } } @@ -348,7 +351,7 @@ export class Pipeline { const valve = new Coupler(nextIn, pipeline_input_pipe.in); nextIn = pipeline_input_pipe.out; - // TOOD: this will eventually defer piping of certain + // TODO: this will eventually defer piping of certain // sub-pipelines to the Puter Shell. for ( let i=0 ; i < preparedCommands.length ; i++ ) { @@ -381,9 +384,11 @@ export class Pipeline { const command = preparedCommands[i]; commandPromises.push(command.execute()); } - await Promise.all(commandPromises); + const results = await Promise.all(commandPromises); + // TODO: Consider what to do about intermediate exit codes + ctx.locals.exit = results[results.length-1]; await coupler.isDone; valve.close(); } -} \ No newline at end of file +} diff --git a/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js b/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js index 2467d7807..53875ea26 100644 --- a/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js +++ b/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js @@ -57,9 +57,12 @@ export class PuterAppCommandProvider { // Wait for app to close. const app_close_promise = new Promise((resolve, reject) => { - child.on('close', () => { - // TODO: Exit codes for apps - resolve({ done: true }); + child.on('close', (data) => { + if ((data.statusCode ?? 0) != 0) { + reject(new Exit(data.statusCode)); + } else { + resolve({ done: true }); + } }); }); From 3184d3482c7b95c0fd1fc0745555ff82fc9a8c99 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 23 May 2024 15:27:15 +0100 Subject: [PATCH 3/3] feat(phoenix): Implement an `exit` builtin --- packages/phoenix/src/main_cli.js | 2 + packages/phoenix/src/main_puter.js | 2 + packages/phoenix/src/platform/node/system.js | 9 ++++ packages/phoenix/src/platform/puter/system.js | 7 +++ .../src/puter-shell/coreutils/__exports__.js | 2 + .../phoenix/src/puter-shell/coreutils/exit.js | 48 +++++++++++++++++++ 6 files changed, 70 insertions(+) create mode 100644 packages/phoenix/src/platform/node/system.js create mode 100644 packages/phoenix/src/platform/puter/system.js create mode 100644 packages/phoenix/src/puter-shell/coreutils/exit.js diff --git a/packages/phoenix/src/main_cli.js b/packages/phoenix/src/main_cli.js index 9f3106c32..1f1808eee 100644 --- a/packages/phoenix/src/main_cli.js +++ b/packages/phoenix/src/main_cli.js @@ -21,6 +21,7 @@ import { launchPuterShell } from './puter-shell/main.js'; import { NodeStdioPTT } from './pty/NodeStdioPTT.js'; import { CreateFilesystemProvider } from './platform/node/filesystem.js'; import { CreateEnvProvider } from './platform/node/env.js'; +import { CreateSystemProvider } from './platform/node/system.js'; import { parseArgs } from '@pkgjs/parseargs'; import capcon from 'capture-console'; import fs from 'fs'; @@ -64,6 +65,7 @@ const ctx = new Context({ name: 'node', filesystem: CreateFilesystemProvider(), env: CreateEnvProvider(), + system: CreateSystemProvider(), }), }); diff --git a/packages/phoenix/src/main_puter.js b/packages/phoenix/src/main_puter.js index 2102fc078..73d03085a 100644 --- a/packages/phoenix/src/main_puter.js +++ b/packages/phoenix/src/main_puter.js @@ -22,6 +22,7 @@ import { CreateFilesystemProvider } from './platform/puter/filesystem.js'; import { CreateDriversProvider } from './platform/puter/drivers.js'; import { XDocumentPTT } from './pty/XDocumentPTT.js'; import { CreateEnvProvider } from './platform/puter/env.js'; +import { CreateSystemProvider } from './platform/puter/system.js'; window.main_shell = async () => { const config = {}; @@ -73,6 +74,7 @@ window.main_shell = async () => { filesystem: CreateFilesystemProvider({ puterSDK }), drivers: CreateDriversProvider({ puterSDK }), env: CreateEnvProvider({ config }), + system: CreateSystemProvider({ puterSDK }) }), })); }; diff --git a/packages/phoenix/src/platform/node/system.js b/packages/phoenix/src/platform/node/system.js new file mode 100644 index 000000000..ec83ebfe6 --- /dev/null +++ b/packages/phoenix/src/platform/node/system.js @@ -0,0 +1,9 @@ +import process from 'node:process'; + +export const CreateSystemProvider = () => { + return { + exit: (code) => { + process.exit(code); + }, + } +} diff --git a/packages/phoenix/src/platform/puter/system.js b/packages/phoenix/src/platform/puter/system.js new file mode 100644 index 000000000..320ec73a7 --- /dev/null +++ b/packages/phoenix/src/platform/puter/system.js @@ -0,0 +1,7 @@ +export const CreateSystemProvider = ({ puterSDK }) => { + return { + exit: (code) => { + puterSDK.exit(code); + }, + } +} diff --git a/packages/phoenix/src/puter-shell/coreutils/__exports__.js b/packages/phoenix/src/puter-shell/coreutils/__exports__.js index 2ff3c9d21..7d35eb963 100644 --- a/packages/phoenix/src/puter-shell/coreutils/__exports__.js +++ b/packages/phoenix/src/puter-shell/coreutils/__exports__.js @@ -31,6 +31,7 @@ import module_dirname from './dirname.js' import module_echo from './echo.js' import module_env from './env.js' import module_errno from './errno.js' +import module_exit from './exit.js' import module_false from './false.js' import module_grep from './grep.js' import module_head from './head.js' @@ -75,6 +76,7 @@ export default { "echo": module_echo, "env": module_env, "errno": module_errno, + "exit": module_exit, "false": module_false, "grep": module_grep, "head": module_head, diff --git a/packages/phoenix/src/puter-shell/coreutils/exit.js b/packages/phoenix/src/puter-shell/coreutils/exit.js new file mode 100644 index 000000000..a31649846 --- /dev/null +++ b/packages/phoenix/src/puter-shell/coreutils/exit.js @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 Puter Technologies Inc. + * + * This file is part of Phoenix Shell. + * + * Phoenix Shell is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { Exit } from './coreutil_lib/exit.js'; + +export default { + name: 'exit', + usage: 'exit [CODE]', + description: 'Exit the shell and return the given CODE. If no argument is given, uses the most recent return code.', + args: { + $: 'simple-parser', + allowPositionals: true + }, + execute: async ctx => { + const { positionals, exit } = ctx.locals; + + let status_code = 0; + + if (positionals.length === 0) { + status_code = exit; + } else if (positionals.length === 1) { + const maybe_number = Number(positionals[0]); + if (Number.isInteger(maybe_number)) { + status_code = maybe_number; + } + } else { + await ctx.externs.err.write('exit: Too many arguments'); + throw new Exit(1); + } + + ctx.platform.system.exit(status_code); + } +};