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.
+};