فهرست منبع

Merge pull request #421 from AtkinsSJ/exit-status

Add exit status codes to `puter.exit()`, and an `exit` builtin to Phoenix
Eric Dubé 1 سال پیش
والد
کامیت
d57980c6cb

+ 8 - 0
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) {

+ 11 - 6
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();
     }
-}
+}

+ 2 - 0
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(),
     }),
 });
 

+ 2 - 0
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 })
         }),
     }));
 };

+ 9 - 0
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);
+        },
+    }
+}

+ 7 - 0
packages/phoenix/src/platform/puter/system.js

@@ -0,0 +1,7 @@
+export const CreateSystemProvider = ({ puterSDK }) => {
+    return {
+        exit: (code) => {
+            puterSDK.exit(code);
+        },
+    }
+}

+ 2 - 0
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,

+ 48 - 0
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 <https://www.gnu.org/licenses/>.
+ */
+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);
+    }
+};

+ 6 - 3
packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js

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

+ 7 - 1
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,
             }, '*');
         }
 

+ 1 - 0
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,
                 });
             }
         });

+ 11 - 2
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,
+        });
     }
-});
+});

+ 1 - 1
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();

+ 3 - 1
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,
         }, '*');
     });