Merge pull request #421 from AtkinsSJ/exit-status

Add exit status codes to `puter.exit()`, and an `exit` builtin to Phoenix
This commit is contained in:
Eric Dubé 2024-05-30 12:28:36 -04:00 committed by GitHub
commit d57980c6cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 118 additions and 14 deletions

View File

@ -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) {

View File

@ -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,7 +384,9 @@ 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();

View File

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

View File

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

View File

@ -0,0 +1,9 @@
import process from 'node:process';
export const CreateSystemProvider = () => {
return {
exit: (code) => {
process.exit(code);
},
}
}

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

@ -54,6 +54,7 @@ class AppConnection extends EventListener {
this.#isOpen = false;
this.emit('close', {
appInstanceID: this.targetAppInstanceID,
statusCode: event.data.statusCode,
});
}
});

View File

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

View File

@ -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();

View File

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