mirror of
https://github.com/HeyPuter/puter.git
synced 2026-02-22 22:48:58 -06:00
Merge pull request #304 from HeyPuter/eric/stdio-bridge/2
stdio-bridge 2
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) ) {
|
||||
|
||||
@@ -37,7 +37,6 @@ export class PuterShellParser {
|
||||
if ( sp.error ) {
|
||||
throw new Error(sp.error);
|
||||
}
|
||||
console.log('PARSER RESULT', result);
|
||||
return result;
|
||||
}
|
||||
parseScript (input) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
packages/phoenix/src/promise.js
Normal file
57
packages/phoenix/src/promise.js
Normal file
@@ -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])));
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
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 ]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
20
src/IPC.js
20
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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user