Prechádzať zdrojové kódy

Merge pull request #304 from HeyPuter/eric/stdio-bridge/2

stdio-bridge 2
Eric Dubé 1 rok pred
rodič
commit
dc2a620b4e

+ 228 - 28
packages/phoenix/packages/pty/exports.js

@@ -16,59 +16,233 @@
  * 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 { TeePromise, raceCase } from '../../src/promise.js';
+
 const encoder = new TextEncoder();
 
 const CHAR_LF = '\n'.charCodeAt(0);
 const CHAR_CR = '\r'.charCodeAt(0);
 
+const DONE = Symbol('done');
+
+class Channel {
+    constructor () {
+        this.chunks_ = [];
+
+        globalThis.chnl = this;
+
+        const events = ['write','consume','change'];
+        for ( const event of events ) {
+            this[`on_${event}_`] = [];
+            this[`emit_${event}_`] = () => {
+                for ( const listener of this[`on_${event}_`] ) {
+                    listener();
+                }
+            };
+        }
+
+        this.on('write', () => { this.emit_change_(); });
+        this.on('consume', () => { this.emit_change_(); });
+    }
+
+    on (event, listener) {
+        this[`on_${event}_`].push(listener);
+    }
+
+    off (event, listener) {
+        const index = this[`on_${event}_`].indexOf(listener);
+        if ( index !== -1 ) {
+            this[`on_${event}_`].splice(index, 1);
+        }
+    }
+
+    get () {
+        const cancel = new TeePromise();
+        const data = new TeePromise();
+        const done = new TeePromise();
+
+        let called = 0;
+
+        const on_data = () => {
+            if ( this.chunks_.length > 0 ) {
+                if ( called > 0 ) {
+                    throw new Error('called more than once');
+                }
+                called++;
+                const chunk = this.chunks_.shift();
+                ( chunk === DONE ? done : data ).resolve(chunk);
+                this.off('write', on_data);
+                this.emit_consume_();
+            }
+        };
+
+        this.on('write', on_data);
+        on_data();
+
+        const to_return = {
+            cancel: () => {
+                this.off('write', on_data);
+                cancel.resolve();
+            },
+            promise: raceCase({
+                cancel,
+                data,
+                done,
+            }),
+        };
+
+        return to_return;
+    }
+
+    write (chunk) {
+        this.chunks_.push(chunk);
+        this.emit_write_();
+    }
+
+    pushback (...chunks) {
+        for ( let i = chunks.length - 1; i >= 0; i-- ) {
+            this.chunks_.unshift(chunks[i]);
+
+        }
+        this.emit_write_();
+    }
+
+    is_empty () {
+        return this.chunks_.length === 0;
+    }
+}
+
 export class BetterReader {
     constructor ({ delegate }) {
         this.delegate = delegate;
         this.chunks_ = [];
+        this.channel_ = new Channel();
+
+        this._init();
     }
 
-    async read (opt_buffer) {
-        if ( ! opt_buffer && this.chunks_.length === 0 ) {
-            return await this.delegate.read();
+    _init () {
+        let working = Promise.resolve();
+        this.channel_.on('consume', async () => {
+            await working;
+            working = new TeePromise();
+            if ( this.channel_.is_empty() ) {
+                await this.intake_();
+            }
+            working.resolve();
+        });
+        this.intake_();
+    }
+
+    async intake_ () {
+        const { value, done } = await this.delegate.read();
+        if ( done ) {
+            this.channel_.write(DONE);
+            return;
         }
+        this.channel_.write(value);
+    }
+
 
-        const chunk = await this.getChunk_();
+    _create_cancel_response () {
+        return {
+            chunk: null,
+            n_read: 0,
+            debug_meta: {
+                source: 'delegate',
+                returning: 'cancelled',
+                this_value_should_not_be_used: true,
+            },
+        };
+    }
 
+    read_and_get_info (opt_buffer, cancel_state) {
         if ( ! opt_buffer ) {
-            return chunk;
+            const { promise, cancel } = this.channel_.get();
+            return {
+                cancel,
+                promise: promise.then(([which, chunk]) => {
+                    if ( which !== 'data' ) {
+                        return { done: true, value: null };
+                    }
+                    return { value: chunk };
+                }),
+
+            };
         }
 
-        this.chunks_.push(chunk);
+        const final_promise = new TeePromise();
+        let current_cancel_ = () => {};
 
-        while ( this.getTotalBytesReady_() < opt_buffer.length ) {
-            this.chunks_.push(await this.getChunk_())
-        }
+        (async () => {
+            let n_read = 0;
+            const chunks = [];
+            while ( n_read < opt_buffer.length ) {
+                const { promise, cancel } = this.channel_.get();
+                current_cancel_ = cancel;
 
-        // TODO: need to handle EOT condition in this loop
-        let offset = 0;
-        for (;;) {
-            let item = this.chunks_.shift();
-            if ( item === undefined ) {
-                throw new Error('calculation is wrong')
-            }
-            if ( offset + item.length > opt_buffer.length ) {
-                const diff = opt_buffer.length - offset;
-                this.chunks_.unshift(item.subarray(diff));
-                item = item.subarray(0, diff);
+                let [which, chunk] = await promise;
+                if ( which === 'done' ) {
+                    break;
+                }
+                if ( which === 'cancel' ) {
+                    this.channel_.pushback(...chunks);
+                    return 
+                }
+                if ( n_read + chunk.length > opt_buffer.length ) {
+                    const diff = opt_buffer.length - n_read;
+                    this.channel_.pushback(chunk.subarray(diff));
+                    chunk = chunk.subarray(0, diff);
+                }
+                chunks.push(chunk);
+                opt_buffer.set(chunk, n_read);
+                n_read += chunk.length;
             }
-            opt_buffer.set(item, offset);
-            offset += item.length;
 
-            if ( offset == opt_buffer.length ) break;
-        }
+            final_promise.resolve({ n_read });
+        })();
 
-        // return opt_buffer.length;
+        return {
+            cancel: () => {
+                current_cancel_();
+            },
+            promise: final_promise,
+        };
+    }
+
+    read_with_cancel (opt_buffer) {
+        const o = this.read_and_get_info(opt_buffer);
+        const { cancel, promise } = o;
+        // const promise = (async () => {
+        //     const { chunk, n_read } = await this.read_and_get_info(opt_buffer, cancel_state);
+        //     return opt_buffer ? n_read : chunk;
+        // })();
+        return {
+            cancel,
+            promise,
+        };
+    }
+
+    async read (opt_buffer) {
+        const { chunk, n_read } = await this.read_and_get_info(opt_buffer).promise;
+        return opt_buffer ? n_read : chunk;
     }
 
     async getChunk_() {
         if ( this.chunks_.length === 0 ) {
-            const { value } = await this.delegate.read();
-            return value;
+            // Wait for either a delegate read to happen, or for a chunk to be added to the buffer from a cancelled read.
+            const delegate_read = this.delegate.read();
+            const [which, result] = await raceCase({
+                delegate: delegate_read,
+                buffer_not_empty: this.waitUntilDataAvailable(),
+            });
+            if (which === 'delegate') {
+                return result;
+            }
+
+            // There's a chunk in the buffer now, so we can use the regular path.
+            // But first, make sure that once the delegate read completes, we save the chunk.
+            this.chunks_.push(result);
         }
 
         const len = this.getTotalBytesReady_();
@@ -85,7 +259,33 @@ export class BetterReader {
     }
 
     getTotalBytesReady_ () {
-        return this.chunks_.reduce((sum, chunk) => sum + chunk.length, 0);
+        return this.chunks_.reduce((sum, chunk) => {
+            return sum + chunk.value.length
+        }, 0);
+    }
+
+    canRead() {
+        return this.getTotalBytesReady_() > 0;
+    }
+
+    async waitUntilDataAvailable() {
+        let resolve_promise;
+        let reject_promise;
+        const promise = new Promise((resolve, reject) => {
+            resolve_promise = resolve;
+            reject_promise = reject;
+        });
+
+        const check = () => {
+            if (this.canRead()) {
+                resolve_promise();
+            } else {
+                setTimeout(check, 0);
+            }
+        };
+        setTimeout(check, 0);
+
+        await promise;
     }
 }
 

+ 0 - 3
packages/phoenix/packages/strataparse/parse_impls/StrUntilParserImpl.js

@@ -23,7 +23,6 @@ export default class StrUntilParserImpl {
     parse (lexer) {
         let text = '';
         for ( ;; ) {
-            console.log('B')
             let { done, value } = lexer.look();
 
             if ( done ) break;
@@ -41,8 +40,6 @@ export default class StrUntilParserImpl {
 
         if ( text.length === 0 ) return;
 
-        console.log('test?', text)
-
         return { $: 'until', text };
     }
 }

+ 0 - 8
packages/phoenix/packages/strataparse/strata_impls/ContextSwitchingPStratumImpl.js

@@ -22,7 +22,6 @@ export default class ContextSwitchingPStratumImpl {
     constructor ({ contexts, entry }) {
         this.contexts = { ...contexts };
         for ( const key in this.contexts ) {
-            console.log('parsers?', this.contexts[key]);
             const new_array = [];
             for ( const parser of this.contexts[key] ) {
                 if ( parser.hasOwnProperty('transition') ) {
@@ -44,7 +43,6 @@ export default class ContextSwitchingPStratumImpl {
         this.lastvalue = null;
     }
     get stack_top () {
-        console.log('stack top?', this.stack[this.stack.length - 1])
         return this.stack[this.stack.length - 1];
     }
     get current_context () {
@@ -55,7 +53,6 @@ export default class ContextSwitchingPStratumImpl {
         const lexer = api.delegate;
 
         const context = this.current_context;
-        console.log('context?', context);
         for ( const spec of context ) {
             {
                 const { done, value } = lexer.look();
@@ -64,7 +61,6 @@ export default class ContextSwitchingPStratumImpl {
                     throw new Error('infinite loop');
                 }
                 this.lastvalue = value;
-                console.log('last value?', value, done);
                 if ( done ) return { done };
             }
 
@@ -76,7 +72,6 @@ export default class ContextSwitchingPStratumImpl {
             }
 
             const subLexer = lexer.fork();
-            // console.log('spec?', spec);
             const result = parser.parse(subLexer);
             if ( result.status === ParseResult.UNRECOGNIZED ) {
                 continue;
@@ -84,11 +79,9 @@ export default class ContextSwitchingPStratumImpl {
             if ( result.status === ParseResult.INVALID ) {
                 return { done: true, value: result };
             }
-            console.log('RESULT', result, spec)
             if ( ! peek ) lexer.join(subLexer);
 
             if ( transition ) {
-                console.log('GOT A TRANSITION')
                 if ( transition.pop ) this.stack.pop();
                 if ( transition.to ) this.stack.push({
                     context_name: transition.to,
@@ -97,7 +90,6 @@ export default class ContextSwitchingPStratumImpl {
 
             if ( result.value.$discard || peek ) return this.next(api);
 
-            console.log('PROVIDING VALUE', result.value);
             return { done: false, value: result.value };
         }
 

+ 0 - 5
packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js

@@ -22,11 +22,6 @@ import { DEFAULT_OPTIONS } from '../../puter-shell/coreutils/coreutil_lib/help.j
 export default {
     name: 'simple-parser',
     async process (ctx, spec) {
-        console.log({
-            ...spec,
-            args: ctx.locals.args
-        });
-
         // Insert standard options
         spec.options = Object.assign(spec.options || {}, DEFAULT_OPTIONS);
 

+ 0 - 1
packages/phoenix/src/ansi-shell/ioutil/SignalReader.js

@@ -48,7 +48,6 @@ export class SignalReader extends ProxyReader {
 
         // show hex for debugging
         // console.log(value.split('').map(c => c.charCodeAt(0).toString(16)).join(' '));
-        console.log('value??', value)
 
         for ( const [key, signal] of mapping ) {
             if ( tmp_value.includes(key) ) {

+ 0 - 1
packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js

@@ -37,7 +37,6 @@ export class PuterShellParser {
         if ( sp.error ) {
             throw new Error(sp.error);
         }
-        console.log('PARSER RESULT', result);
         return result;
     }
     parseScript (input) {

+ 0 - 11
packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js

@@ -54,7 +54,6 @@ class ReducePrimitivesPStratumImpl {
             let text = '';
             for ( const item of contents.results ) {
                 if ( item.$ === 'string.segment' ) {
-                    // console.log('segment?', item.text)
                     text += item.text;
                     continue;
                 }
@@ -86,7 +85,6 @@ class ShellConstructsPStratumImpl {
                 node.commands = [];
             },
             exit ({ node }) {
-                console.log('!!!!!',this.stack_top.node)
                 if ( this.stack_top?.node?.$ === 'script' ) {
                     this.stack_top.node.statements.push(node);
                 }
@@ -96,7 +94,6 @@ class ShellConstructsPStratumImpl {
             },
             next ({ value, lexer }) {
                 if ( value.$ === 'op.line-terminator' ) {
-                    console.log('the stack??', this.stack)
                     this.pop();
                     return;
                 }
@@ -189,7 +186,6 @@ class ShellConstructsPStratumImpl {
             },
             next ({ value, lexer }) {
                 if ( value.$ === 'op.line-terminator' ) {
-                    console.log('well, got here')
                     this.pop();
                     return;
                 }
@@ -223,9 +219,7 @@ class ShellConstructsPStratumImpl {
                 this.stack_top.node.components.push(...node.components);
             },
             next ({ node, value, lexer }) {
-                console.log('WHAT THO', node)
                 if ( value.$ === 'op.line-terminator' && node.quote === null ) {
-                    console.log('well, got here')
                     this.pop();
                     return;
                 }
@@ -292,7 +286,6 @@ class ShellConstructsPStratumImpl {
 
         const lexer = api.delegate;
 
-        console.log('THE NODE', this.stack[0].node);
         // return { done: true, value: { $: 'test' } };
 
         for ( let i=0 ; i < 500 ; i++ ) {
@@ -306,15 +299,12 @@ class ShellConstructsPStratumImpl {
             }
 
             const { state, node } = this.stack_top;
-            console.log('value?', value, done)
-            console.log('state?', state.name);
 
             state.next.call(this, { lexer, value, node, state });
 
             // if ( done ) break;
         }
 
-        console.log('THE NODE', this.stack[0]);
 
         this.done_ = true;
         return { done: false, value: this.stack[0].node };
@@ -433,7 +423,6 @@ export const buildParserSecondHalf = (sp, { multiline } = {}) => {
 
     // sp.add(new ReducePrimitivesPStratumImpl());
     if ( multiline ) {
-        console.log('USING MULTILINE');
         sp.add(new MultilinePStratumImpl());
     } else {
         sp.add(new ShellConstructsPStratumImpl());

+ 23 - 1
packages/phoenix/src/ansi-shell/pipeline/Coupler.js

@@ -16,6 +16,8 @@
  * 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 { TeePromise, raceCase } from "../../promise.js";
+
 export class Coupler {
     static description = `
         Connects a read stream to a write stream.
@@ -26,6 +28,7 @@ export class Coupler {
         this.source = source;
         this.target = target;
         this.on_ = true;
+        this.closed_ = new TeePromise();
         this.isDone = new Promise(rslv => {
             this.resolveIsDone = rslv;
         })
@@ -35,11 +38,29 @@ export class Coupler {
     off () { this.on_ = false; }
     on () { this.on_ = true; }
 
+    close () {
+        this.closed_.resolve({
+            done: true,
+        });
+    }
+
     async listenLoop_ () {
         this.active = true;
         for (;;) {
-            const { value, done } = await this.source.read();
+            let cancel = () => {};
+            let promise;
+            if ( this.source.read_with_cancel !== undefined ) {
+                ({ cancel, promise } = this.source.read_with_cancel());
+            } else {
+                promise = this.source.read();
+            }
+            const [which, result] = await raceCase({
+                source: promise,
+                closed: this.closed_,
+            });
+            const { value, done } = result;
             if ( done ) {
+                cancel();
                 this.source = null;
                 this.target = null;
                 this.active = false;
@@ -47,6 +68,7 @@ export class Coupler {
                 break;
             }
             if ( this.on_ ) {
+                if ( ! value ) debugger;
                 await this.target.write(value);
             }
         }

+ 8 - 26
packages/phoenix/src/ansi-shell/pipeline/Pipeline.js

@@ -38,11 +38,6 @@ class Token {
             throw new Error('expected token node');
         }
 
-        console.log('ast has cst?',
-            ast,
-            ast.components?.[0]?.$cst
-        )
-
         return new Token(ast);
     }
     constructor (ast) {
@@ -53,20 +48,15 @@ class Token {
         // If the only components are of type 'symbol' and 'string.segment'
         // then we can statically resolve the value of the token.
 
-        console.log('checking viability of static resolve', this.ast)
-
         const isStatic = this.ast.components.every(c => {
             return c.$ === 'symbol' || c.$ === 'string.segment';
         });
 
         if ( ! isStatic ) return;
 
-        console.log('doing static thing', this.ast)
-
         // TODO: Variables can also be statically resolved, I think...
         let value = '';
         for ( const component of this.ast.components ) {
-            console.log('component', component);
             value += component.text;
         }
 
@@ -113,7 +103,6 @@ export class PreparedCommand {
         
         // TODO: check that node for command name is of a
         //       supported type - maybe use adapt pattern
-        console.log('ast?', ast);
         const cmd = command_token.maybeStaticallyResolve(ctx);
 
         const { commands } = ctx.registries;
@@ -124,16 +113,13 @@ export class PreparedCommand {
             : command_token;
 
         if ( command === undefined ) {
-            console.log('command token?', command_token);
             throw new ConcreteSyntaxError(
                 `no command: ${JSON.stringify(cmd)}`,
                 command_token.$cst,
             );
-            throw new Error('no command: ' + JSON.stringify(cmd));
         }
 
         // TODO: test this
-        console.log('ast?', ast);
         const inputRedirect = ast.inputRedirects.length > 0 ? (() => {
             const token = Token.createFromAST(ctx, ast.inputRedirects[0]);
             return token.maybeStaticallyResolve(ctx) ?? token;
@@ -172,7 +158,6 @@ export class PreparedCommand {
         // command to run.
         if ( command instanceof Token ) {
             const cmd = await command.resolve(this.ctx);
-            console.log('RUNNING CMD?', cmd)
             const { commandProvider } = this.ctx.externs;
             command = await commandProvider.lookup(cmd, { ctx: this.ctx });
             if ( command === undefined ) {
@@ -314,31 +299,23 @@ export class PreparedCommand {
                 );
                 ctx.locals.exit = -1;
             }
+            if ( ! (e instanceof Exit) ) console.error(e);
         }
 
-        // ctx.externs.in?.close?.();
-        // ctx.externs.out?.close?.();
         await ctx.externs.out.close();
 
         // TODO: need write command from puter-shell before this can be done
         for ( let i=0 ; i < this.outputRedirects.length ; i++ ) {
-            console.log('output redirect??', this.outputRedirects[i]);
             const { filesystem } = this.ctx.platform;
             const outputRedirect = this.outputRedirects[i];
             const dest_path = outputRedirect instanceof Token
                 ? await outputRedirect.resolve(this.ctx)
                 : outputRedirect;
             const path = resolveRelativePath(ctx.vars, dest_path);
-            console.log('it should work?', {
-                path,
-                outputMemWriters,
-            })
             // TODO: error handling here
 
             await filesystem.write(path, outputMemWriters[i].getAsBlob());
         }
-
-        console.log('OUTPUT WRITERS', outputMemWriters);
     }
 }
 
@@ -366,6 +343,11 @@ export class Pipeline {
         let nextIn = ctx.externs.in;
         let lastPipe = null;
 
+        // Create valve to close input pipe when done
+        const pipeline_input_pipe = new Pipe();
+        const valve = new Coupler(nextIn, pipeline_input_pipe.in);
+        nextIn = pipeline_input_pipe.out;
+
         // TOOD: this will eventually defer piping of certain
         //       sub-pipelines to the Puter Shell.
 
@@ -400,8 +382,8 @@ export class Pipeline {
             commandPromises.push(command.execute());
         }
         await Promise.all(commandPromises);
-        console.log('PIPELINE DONE');
-
         await coupler.isDone;
+
+        valve.close();
     }
 }

+ 0 - 4
packages/phoenix/src/ansi-shell/readline/readline.js

@@ -96,7 +96,6 @@ const ReadlineProcessorBuilder = builder => builder
             externs.out.write(externs.prompt);
             externs.out.write(vars.result);
             const invCurPos = vars.result.length - vars.cursor;
-            console.log(invCurPos)
             if ( invCurPos !== 0 ) {
                 externs.out.write(`\x1B[${invCurPos}D`);
             }
@@ -111,8 +110,6 @@ const ReadlineProcessorBuilder = builder => builder
                 }
             }));
             // NEXT: get tab completer for input state
-            console.log('input state', inputState);
-            
             let completer = null;
             if ( inputState.$ === 'redirect' ) {
                 completer = new FileCompleter();
@@ -141,7 +138,6 @@ const ReadlineProcessorBuilder = builder => builder
             const applyCompletion = txt => {
                 const p1 = vars.result.slice(0, vars.cursor);
                 const p2 = vars.result.slice(vars.cursor);
-                console.log({ p1, p2 });
                 vars.result = p1 + txt + p2;
                 vars.cursor += txt.length;
                 externs.out.write(txt);

+ 57 - 0
packages/phoenix/src/promise.js

@@ -0,0 +1,57 @@
+export class TeePromise {
+    static STATUS_PENDING = Symbol('pending');
+    static STATUS_RUNNING = {};
+    static STATUS_DONE = Symbol('done');
+    constructor () {
+        this.status_ = this.constructor.STATUS_PENDING;
+        this.donePromise = new Promise((resolve, reject) => {
+            this.doneResolve = resolve;
+            this.doneReject = reject;
+        });
+    }
+    get status () {
+        return this.status_;
+    }
+    set status (status) {
+        this.status_ = status;
+        if ( status === this.constructor.STATUS_DONE ) {
+            this.doneResolve();
+        }
+    }
+    resolve (value) {
+        this.status_ = this.constructor.STATUS_DONE;
+        this.doneResolve(value);
+    }
+    awaitDone () {
+        return this.donePromise;
+    }
+    then (fn, ...a) {
+        return this.donePromise.then(fn, ...a);
+    }
+
+    reject (err) {
+        this.status_ = this.constructor.STATUS_DONE;
+        this.doneReject(err);
+    }
+
+    /**
+     * @deprecated use then() instead
+     */
+    onComplete(fn) {
+        return this.then(fn);
+    }
+}
+
+/**
+ * raceCase is like Promise.race except it takes an object instead of
+ * an array, and returns the key of the promise that resolves first
+ * as well as the value that it resolved to.
+ * 
+ * @param {Object.<string, Promise>} promise_map 
+ * 
+ * @returns {Promise.<[string, any]>}
+ */
+export const raceCase = async (promise_map) => {
+    return Promise.race(Object.entries(promise_map).map(
+        ([key, promise]) => promise.then(value => [key, value])));
+};

+ 2 - 2
packages/phoenix/src/pty/XDocumentPTT.js

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

+ 72 - 29
packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js

@@ -16,49 +16,92 @@
  * 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',
 ];
 
-export class PuterAppCommandProvider {
+const lookup_app = async (id) => {
+    if (BUILT_IN_APPS.includes(id)) {
+        return { success: true, path: null };
+    }
 
-    async lookup (id) {
-        // Built-in apps will not be returned by the fetch query below, so we handle them separately.
-        if (BUILT_IN_APPS.includes(id)) {
-            return {
-                name: id,
-                path: 'Built-in Puter app',
-                // 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);
-                }
-            };
-        }
+    const request = await fetch(`${puter.APIOrigin}/drivers/call`, {
+        "headers": {
+            "Content-Type": "application/json",
+            "Authorization": `Bearer ${puter.authToken}`,
+        },
+        "body": JSON.stringify({ interface: 'puter-apps', method: 'read', args: { id: { name: id } } }),
+        "method": "POST",
+    });
 
-        const request = await fetch(`${puter.APIOrigin}/drivers/call`, {
-            "headers": {
-                "Content-Type": "application/json",
-                "Authorization": `Bearer ${puter.authToken}`,
-            },
-            "body": JSON.stringify({ interface: 'puter-apps', method: 'read', args: { id: { name: id } } }),
-            "method": "POST",
-        });
+    const { success, result } = await request.json();
+    return { success, path: result?.index_url };
+};
 
-        const { success, result } = await request.json();
+export class PuterAppCommandProvider {
 
+    async lookup (id) {
+        const { success, path } = await lookup_app(id);
         if (!success) return;
 
-        const { name, index_url } = result;
         return {
-            name,
-            path: index_url,
+            name: id,
+            path: path ?? 'Built-in Puter app',
             // 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(id, 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 ]);
             }
         };
     }

+ 17 - 3
packages/puter-js/src/modules/UI.js

@@ -14,7 +14,11 @@ class AppConnection extends EventListener {
     // Whether the target app is open
     #isOpen;
 
-    constructor(messageTarget, appInstanceID, targetAppInstanceID) {
+    // Whether the target app uses the Puter SDK, and so accepts messages
+    // (Closing and close events will still function.)
+    #usesSDK;
+
+    constructor(messageTarget, appInstanceID, targetAppInstanceID, usesSDK) {
         super([
             'message', // The target sent us something with postMessage()
             'close',   // The target app was closed
@@ -23,6 +27,7 @@ class AppConnection extends EventListener {
         this.appInstanceID = appInstanceID;
         this.targetAppInstanceID = targetAppInstanceID;
         this.#isOpen = true;
+        this.#usesSDK = usesSDK;
 
         // TODO: Set this.#puterOrigin to the puter origin
 
@@ -54,12 +59,21 @@ class AppConnection extends EventListener {
         });
     }
 
+    // Does the target app use the Puter SDK? If not, certain features will be unavailable.
+    get usesSDK() { return this.#usesSDK; }
+
+    // Send a message to the target app. Requires the target to use the Puter SDK.
     postMessage(message) {
         if (!this.#isOpen) {
             console.warn('Trying to post message on a closed AppConnection');
             return;
         }
 
+        if (!this.#usesSDK) {
+            console.warn('Trying to post message to a non-SDK app');
+            return;
+        }
+
         this.messageTarget.postMessage({
             msg: 'messageToApp',
             appInstanceID: this.appInstanceID,
@@ -155,7 +169,7 @@ class UI extends EventListener {
         }
 
         if (this.parentInstanceID) {
-            this.#parentAppConnection = new AppConnection(this.messageTarget, this.appInstanceID, this.parentInstanceID);
+            this.#parentAppConnection = new AppConnection(this.messageTarget, this.appInstanceID, this.parentInstanceID, true);
         }
 
         // Tell the host environment that this app is using the Puter SDK and is ready to receive messages,
@@ -374,7 +388,7 @@ class UI extends EventListener {
                 }
                 else if (e.data.msg === 'childAppLaunched') {
                     // execute callback with a new AppConnection to the child
-                    const connection = new AppConnection(this.messageTarget, this.appInstanceID, e.data.child_instance_id);
+                    const connection = new AppConnection(this.messageTarget, this.appInstanceID, e.data.child_instance_id, e.data.uses_sdk);
                     this.#callbackFunctions[e.data.original_msg_id](connection);
                 }
                 else{

+ 0 - 13
packages/terminal/src/main.js

@@ -41,27 +41,14 @@ class XTermIO {
     }
 
     async handleKeyBeforeProcess (evt) {
-        console.log(
-            'right this event might be up or down so it\'s necessary to determine which',
-            evt,
-        );
         if ( evt.key === 'V' && evt.ctrlKey && evt.shiftKey && evt.type === 'keydown' ) {
             const clipboard = navigator.clipboard;
             const text = await clipboard.readText();
-            console.log(
-                'this is the relevant text for this thing that is the thing that is the one that is here',
-                text,
-            );
             this.pty.out.write(text);
         }
     }
 
     handleKey ({ key, domEvent }) {
-        console.log(
-            'key event happened',
-            key,
-            domEvent,
-        );
         const pty = this.pty;
 
         const handlers = {

+ 2 - 2
packages/terminal/src/pty/XDocumentANSIShell.js

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

+ 1 - 19
src/IPC.js

@@ -74,14 +74,6 @@ window.addEventListener('message', async (event) => {
         return;
     }
 
-    const window_for_app_instance = (instance_id) => {
-        return $(`.window[data-element_uuid="${instance_id}"]`).get(0);
-    };
-
-    const iframe_for_app_instance = (instance_id) => {
-        return $(window_for_app_instance(instance_id)).find('.window-app-iframe').get(0);
-    };
-
     const $el_parent_window = $(window_for_app_instance(event.data.appInstanceID));
     const parent_window_id = $el_parent_window.attr('data-id');
     const $el_parent_disable_mask = $el_parent_window.find('.window-disable-mask');
@@ -98,17 +90,7 @@ window.addEventListener('message', async (event) => {
         $(target_iframe).attr('data-appUsesSDK', 'true');
 
         // If we were waiting to launch this as a child app, report to the parent that it succeeded.
-        const child_launch_callback = window.child_launch_callbacks[event.data.appInstanceID];
-        if (child_launch_callback) {
-            const parent_iframe = iframe_for_app_instance(child_launch_callback.parent_instance_id);
-            // send confirmation to requester window
-            parent_iframe.contentWindow.postMessage({
-                msg: 'childAppLaunched',
-                original_msg_id: child_launch_callback.launch_msg_id,
-                child_instance_id: event.data.appInstanceID,
-            }, '*');
-            delete window.child_launch_callbacks[event.data.appInstanceID];
-        }
+        window.report_app_launched(event.data.appInstanceID, { uses_sdk: true });
 
         // Send any saved broadcasts to the new app
         globalThis.services.get('broadcast').sendSavedBroadcastsTo(event.data.appInstanceID);

+ 1 - 22
src/UI/UIWindow.js

@@ -2773,7 +2773,6 @@ window.sidebar_item_droppable = (el_window)=>{
 // closes a window
 $.fn.close = async function(options) {
     options = options || {};
-    console.log(options);
     $(this).each(async function() {
         const el_iframe = $(this).find('.window-app-iframe');
         const app_uses_sdk = el_iframe.length > 0 && el_iframe.attr('data-appUsesSDK') === 'true';
@@ -2853,27 +2852,7 @@ $.fn.close = async function(options) {
             $(`.window[data-parent_uuid="${window_uuid}"]`).close();
 
             // notify other apps that we're closing
-            if (app_uses_sdk) {
-                // notify parent app, if we have one, that we're closing
-                const parent_id = this.dataset['parent_instance_id'];
-                const parent = $(`.window[data-element_uuid="${parent_id}"] .window-app-iframe`).get(0);
-                if (parent) {
-                    parent.contentWindow.postMessage({
-                        msg: 'appClosed',
-                        appInstanceID: window_uuid,
-                    }, '*');
-                }
-
-                // notify child apps, if we have them, that we're closing
-                const children = $(`.window[data-parent_instance_id="${window_uuid}"] .window-app-iframe`);
-                children.each((_, child) => {
-                    child.contentWindow.postMessage({
-                        msg: 'appClosed',
-                        appInstanceID: window_uuid,
-                    }, '*');
-                });
-                // TODO: Once other AppConnections exist, those will need notifying too.
-            }
+            window.report_app_closed(window_uuid);
 
             // remove backdrop
             $(this).closest('.window-backdrop').remove();

+ 62 - 4
src/helpers.js

@@ -1839,8 +1839,6 @@ window.launch_app = async (options)=>{
         // ...and finally append urm_source=puter.com to the URL
         iframe_url.searchParams.append('urm_source', 'puter.com');
 
-        console.log('backgrounded??', app_info.background);
-
         el_win = UIWindow({
             element_uuid: uuid,
             title: title,
@@ -1895,10 +1893,18 @@ window.launch_app = async (options)=>{
 
     (async () => {
         const el = await el_win;
-        console.log('RESOV', el);
         $(el).on('remove', () => {
             const svc_process = globalThis.services.get('process');
             svc_process.unregister(process.uuid);
+
+            // If it's a non-sdk app, report that it launched and closed.
+            // FIXME: This is awkward. Really, we want some way of knowing when it's launched and reporting that immediately instead.
+            const $app_iframe = $(el).find('.window-app-iframe');
+            if ($app_iframe.attr('data-appUsesSdk') !== 'true') {
+                window.report_app_launched(process.uuid, { uses_sdk: false });
+                // We also have to report an extra close event because the real one was sent already
+                window.report_app_closed(process.uuid);
+            }
         });
 
         process.references.el_win = el;
@@ -3512,4 +3518,56 @@ window.change_clock_visible = (clock_visible) => {
     }
 
     $('select.change-clock-visible').val(window.user_preferences.clock_visible);
-}
+}
+
+// Finds the `.window` element for the given app instance ID
+window.window_for_app_instance = (instance_id) => {
+    return $(`.window[data-element_uuid="${instance_id}"]`).get(0);
+};
+
+// Finds the `iframe` element for the given app instance ID
+window.iframe_for_app_instance = (instance_id) => {
+    return $(window_for_app_instance(instance_id)).find('.window-app-iframe').get(0);
+};
+
+// Run any callbacks to say that the app has launched
+window.report_app_launched = (instance_id, { uses_sdk = true }) => {
+    const child_launch_callback = window.child_launch_callbacks[instance_id];
+    if (child_launch_callback) {
+        const parent_iframe = iframe_for_app_instance(child_launch_callback.parent_instance_id);
+        // send confirmation to requester window
+        parent_iframe.contentWindow.postMessage({
+            msg: 'childAppLaunched',
+            original_msg_id: child_launch_callback.launch_msg_id,
+            child_instance_id: instance_id,
+            uses_sdk: uses_sdk,
+        }, '*');
+        delete window.child_launch_callbacks[instance_id];
+    }
+};
+
+// Run any callbacks to say that the app has closed
+window.report_app_closed = (instance_id) => {
+    const el_window = window_for_app_instance(instance_id);
+
+    // notify parent app, if we have one, that we're closing
+    const parent_id = el_window.dataset['parent_instance_id'];
+    const parent = $(`.window[data-element_uuid="${parent_id}"] .window-app-iframe`).get(0);
+    if (parent) {
+        parent.contentWindow.postMessage({
+            msg: 'appClosed',
+            appInstanceID: instance_id,
+        }, '*');
+    }
+
+    // notify child apps, if we have them, that we're closing
+    const children = $(`.window[data-parent_instance_id="${instance_id}"] .window-app-iframe`);
+    children.each((_, child) => {
+        child.contentWindow.postMessage({
+            msg: 'appClosed',
+            appInstanceID: instance_id,
+        }, '*');
+    });
+
+    // TODO: Once other AppConnections exist, those will need notifying too.
+};