diff --git a/packages/phoenix/packages/pty/exports.js b/packages/phoenix/packages/pty/exports.js index bbab56eff..3f45abf0c 100644 --- a/packages/phoenix/packages/pty/exports.js +++ b/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 . */ +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(); + } + + _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); + } + + + _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 ) { + const { promise, cancel } = this.channel_.get(); + return { + cancel, + promise: promise.then(([which, chunk]) => { + if ( which !== 'data' ) { + return { done: true, value: null }; + } + return { value: chunk }; + }), + + }; + } + + const final_promise = new TeePromise(); + let current_cancel_ = () => {}; + + (async () => { + let n_read = 0; + const chunks = []; + while ( n_read < opt_buffer.length ) { + const { promise, cancel } = this.channel_.get(); + current_cancel_ = cancel; + + 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; + } + + final_promise.resolve({ n_read }); + })(); + + 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) { - if ( ! opt_buffer && this.chunks_.length === 0 ) { - return await this.delegate.read(); - } - - const chunk = await this.getChunk_(); - - if ( ! opt_buffer ) { - return chunk; - } - - this.chunks_.push(chunk); - - while ( this.getTotalBytesReady_() < opt_buffer.length ) { - this.chunks_.push(await this.getChunk_()) - } - - // 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); - } - opt_buffer.set(item, offset); - offset += item.length; - - if ( offset == opt_buffer.length ) break; - } - - // return opt_buffer.length; + 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; } } diff --git a/packages/phoenix/packages/strataparse/parse_impls/StrUntilParserImpl.js b/packages/phoenix/packages/strataparse/parse_impls/StrUntilParserImpl.js index 4ca5b81ab..9f627faf1 100644 --- a/packages/phoenix/packages/strataparse/parse_impls/StrUntilParserImpl.js +++ b/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 }; } } diff --git a/packages/phoenix/packages/strataparse/strata_impls/ContextSwitchingPStratumImpl.js b/packages/phoenix/packages/strataparse/strata_impls/ContextSwitchingPStratumImpl.js index 68205931a..de8190728 100644 --- a/packages/phoenix/packages/strataparse/strata_impls/ContextSwitchingPStratumImpl.js +++ b/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 }; } diff --git a/packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js b/packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js index c8f4832a0..a0fc8ff4d 100644 --- a/packages/phoenix/src/ansi-shell/arg-parsers/simple-parser.js +++ b/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); diff --git a/packages/phoenix/src/ansi-shell/ioutil/SignalReader.js b/packages/phoenix/src/ansi-shell/ioutil/SignalReader.js index 59d69a0a3..b7eafc86e 100644 --- a/packages/phoenix/src/ansi-shell/ioutil/SignalReader.js +++ b/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) ) { diff --git a/packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js b/packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js index b38ffa6de..8dc2626c9 100644 --- a/packages/phoenix/src/ansi-shell/parsing/PuterShellParser.js +++ b/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) { diff --git a/packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js b/packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js index 4bd4d82a2..1fa6e4ae0 100644 --- a/packages/phoenix/src/ansi-shell/parsing/buildParserSecondHalf.js +++ b/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()); diff --git a/packages/phoenix/src/ansi-shell/pipeline/Coupler.js b/packages/phoenix/src/ansi-shell/pipeline/Coupler.js index 43ffe4233..7ad454a74 100644 --- a/packages/phoenix/src/ansi-shell/pipeline/Coupler.js +++ b/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 . */ +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); } } diff --git a/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js b/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js index 2ad938646..186c8cab9 100644 --- a/packages/phoenix/src/ansi-shell/pipeline/Pipeline.js +++ b/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(); } } \ No newline at end of file diff --git a/packages/phoenix/src/ansi-shell/readline/readline.js b/packages/phoenix/src/ansi-shell/readline/readline.js index e7f816d22..0a9c04901 100644 --- a/packages/phoenix/src/ansi-shell/readline/readline.js +++ b/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); diff --git a/packages/phoenix/src/promise.js b/packages/phoenix/src/promise.js new file mode 100644 index 000000000..5c47d2bd1 --- /dev/null +++ b/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.} 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]))); +}; diff --git a/packages/phoenix/src/pty/XDocumentPTT.js b/packages/phoenix/src/pty/XDocumentPTT.js index 13ea657a2..64435d2d2 100644 --- a/packages/phoenix/src/pty/XDocumentPTT.js +++ b/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; } diff --git a/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js b/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js index 44bf6cbbe..983293b30 100644 --- a/packages/phoenix/src/puter-shell/providers/PuterAppCommandProvider.js +++ b/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 . */ +import { Exit } from '../coreutils/coreutil_lib/exit.js'; +import { signals } from '../../ansi-shell/signals.js'; + const BUILT_IN_APPS = [ 'explorer', ]; +const lookup_app = async (id) => { + if (BUILT_IN_APPS.includes(id)) { + return { success: true, path: null }; + } + + 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 }; +}; + export class PuterAppCommandProvider { 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 { success, result } = await request.json(); - + 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 ]); } }; } diff --git a/packages/puter-js/src/modules/UI.js b/packages/puter-js/src/modules/UI.js index 849d4cad8..abfd43c29 100644 --- a/packages/puter-js/src/modules/UI.js +++ b/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{ diff --git a/packages/terminal/src/main.js b/packages/terminal/src/main.js index 785d4c709..0935fad88 100644 --- a/packages/terminal/src/main.js +++ b/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 = { diff --git a/packages/terminal/src/pty/XDocumentANSIShell.js b/packages/terminal/src/pty/XDocumentANSIShell.js index 8df371e1e..886383411 100644 --- a/packages/terminal/src/pty/XDocumentANSIShell.js +++ b/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, }); } diff --git a/src/IPC.js b/src/IPC.js index f8aad6602..6e9b4cc3b 100644 --- a/src/IPC.js +++ b/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); diff --git a/src/UI/UIWindow.js b/src/UI/UIWindow.js index 812959f13..725278e1d 100644 --- a/src/UI/UIWindow.js +++ b/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(); diff --git a/src/helpers.js b/src/helpers.js index f9504ca48..0f43ca7a0 100644 --- a/src/helpers.js +++ b/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); -} \ No newline at end of file +} + +// 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. +};