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

stdio-bridge 2
This commit is contained in:
Eric Dubé
2024-04-20 19:29:15 -04:00
committed by GitHub
19 changed files with 484 additions and 193 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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])));
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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