From 18a24f614fe09423c953848d27e171361335bed4 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Sun, 1 Sep 2024 22:38:40 -0400 Subject: [PATCH 1/2] dev: update ipc ready and app launched events --- src/gui/src/IPC.js | 54 ++++++++++----- src/gui/src/definitions.js | 29 +++++++- src/gui/src/helpers/launch_app.js | 25 +++---- src/gui/src/services/ExecService.js | 57 +++++++++++++--- src/puter-js/src/lib/xdrpc.js | 6 +- src/puter-js/src/modules/UI.js | 68 ++++++++++++++----- src/puter-js/src/modules/Util.js | 10 ++- src/putility/src/bases/BasicBase.js | 5 +- src/putility/src/bases/FeatureBase.js | 2 +- .../src/features/PropertiesFeature.js | 41 +++++++++-- 10 files changed, 225 insertions(+), 72 deletions(-) diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js index acae4ce7..beeb0f90 100644 --- a/src/gui/src/IPC.js +++ b/src/gui/src/IPC.js @@ -30,6 +30,7 @@ import path from "./lib/path.js"; import UIContextMenu from './UI/UIContextMenu.js'; import update_mouse_position from './helpers/update_mouse_position.js'; import item_icon from './helpers/item_icon.js'; +import { PROCESS_IPC_ATTACHED } from './definitions.js'; window.ipc_handlers = {}; /** @@ -92,18 +93,44 @@ window.addEventListener('message', async (event) => { // New IPC handlers should be registered here. // Do this by calling `register_ipc_handler` of IPCService. if ( window.ipc_handlers.hasOwnProperty(event.data.msg) ) { + const services = globalThis.services; + const svc_process = services.get('process'); + + // Add version info to old puter.js messages + // (and coerce them into the format of new ones) + if ( event.data.$ === undefined ) { + event.data.$ = 'puter-ipc'; + event.data.v = 1; + event.data.parameters = {...event.data}; + delete event.data.parameters.msg; + delete event.data.parameters.appInstanceId; + delete event.data.parameters.env; + delete event.data.parameters.uuid; + } + // The IPC context contains information about the call + const iframe = window.iframe_for_app_instance( + event.data.appInstanceID); + const process = svc_process.get_by_uuid(event.data.appInstanceID); const ipc_context = { - appInstanceId: event.data.appInstanceID, + caller: { + process: process, + app: { + appInstanceID: event.data.appInstanceID, + iframe, + window: $el_parent_window, + }, + }, }; // Registered IPC handlers are an object with a `handle()` // method. We call it "spec" here, meaning specification. const spec = window.ipc_handlers[event.data.msg]; - await spec.handler(event.data, { msg_id, ipc_context }); + let retval = await spec.handler( + event.data.parameters, { msg_id, ipc_context }); + + puter.util.rpc.send(iframe.contentWindow, msg_id, retval); - // Early-return to avoid redundant invokation of any - // legacy IPC handler. return; } @@ -112,25 +139,16 @@ window.addEventListener('message', async (event) => { // READY //------------------------------------------------- if(event.data.msg === 'READY'){ - $(target_iframe).attr('data-appUsesSDK', 'true'); - - // If we were waiting to launch this as a child app, report to the parent that it succeeded. - 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); - - // If `window-active` is set (meanign the window is focused), focus the window one more time - // this is to ensure that the iframe is `definitely` focused and can receive keyboard events (e.g. keydown) - if($el_parent_window.hasClass('window-active')){ - $el_parent_window.focusWindow(); - } + const services = globalThis.services; + const svc_process = services.get('process'); + const process = svc_process.get_by_uuid(event.data.appInstanceID); + process.ipc_status = PROCESS_IPC_ATTACHED; } //------------------------------------------------- // windowFocused //------------------------------------------------- - else if(event.data.msg === 'windowFocused'){ + if(event.data.msg === 'windowFocused'){ // TODO: Respond to this } //-------------------------------------------------------- diff --git a/src/gui/src/definitions.js b/src/gui/src/definitions.js index d738066b..8e5911ed 100644 --- a/src/gui/src/definitions.js +++ b/src/gui/src/definitions.js @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +import { AdvancedBase } from "@heyputer/putility"; + export class Service { construct (o) { this.$puter = {}; @@ -33,20 +35,36 @@ export class Service { export const PROCESS_INITIALIZING = { i18n_key: 'initializing' }; export const PROCESS_RUNNING = { i18n_key: 'running' }; +export const PROCESS_IPC_PENDING = { i18n_key: 'pending' }; +export const PROCESS_IPC_NA = { i18n_key: 'N/A' }; +export const PROCESS_IPC_ATTACHED = { i18n_key: 'attached' }; + // Something is cloning these objects, so '===' checks don't work. // To work around this, the `i` property is used to compare them. export const END_SOFT = { i: 0, end: true, i18n_key: 'end_soft' }; export const END_HARD = { i: 1, end: true, i18n_key: 'end_hard' }; -export class Process { +export class Process extends AdvancedBase{ + static PROPERTIES = { + status: () => PROCESS_INITIALIZING, + ipc_status: () => PROCESS_IPC_PENDING, + } constructor ({ uuid, parent, name, meta }) { + super(); + this.uuid = uuid; this.parent = parent; this.name = name; this.meta = meta; this.references = {}; - - this.status = PROCESS_INITIALIZING; + + Object.defineProperty(this.references, 'iframe', { + get: () => { + // note: Might eventually make sense to make the + // fn on window call here instead. + return window.iframe_for_app_instance(this.uuid); + } + }) this._construct(); } @@ -111,6 +129,11 @@ export class PortalProcess extends Process { }); } } + + send (channel, object, context) { + const target = this.references.iframe.contentWindow; + // NEXT: ... + } }; export class PseudoProcess extends Process { _construct () { this.type_ = 'ui' } diff --git a/src/gui/src/helpers/launch_app.js b/src/gui/src/helpers/launch_app.js index e1569691..00e2d2a7 100644 --- a/src/gui/src/helpers/launch_app.js +++ b/src/gui/src/helpers/launch_app.js @@ -358,25 +358,16 @@ const launch_app = async (options)=>{ } } - (async () => { - const el = await el_win; - $(el).on('remove', () => { - const svc_process = globalThis.services.get('process'); - svc_process.unregister(process.uuid); + const el = await el_win; + process.references.el_win = el; + process.chstatus(PROCESS_RUNNING); - // 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); - } - }); + $(el).on('remove', () => { + const svc_process = globalThis.services.get('process'); + svc_process.unregister(process.uuid); + }); - process.references.el_win = el; - process.chstatus(PROCESS_RUNNING); - })(); + return process; } export default launch_app; diff --git a/src/gui/src/services/ExecService.js b/src/gui/src/services/ExecService.js index 5946a075..c79266bb 100644 --- a/src/gui/src/services/ExecService.js +++ b/src/gui/src/services/ExecService.js @@ -1,4 +1,4 @@ -import { Service } from "../definitions.js"; +import { PROCESS_IPC_ATTACHED, Service } from "../definitions.js"; import launch_app from "../helpers/launch_app.js"; export class ExecService extends Service { @@ -14,20 +14,61 @@ export class ExecService extends Service { } // This method is exposed to apps via IPCService. - launchApp ({ app_name, args }, { ipc_context, msg_id } = {}) { + async launchApp ({ app_name, args }, { ipc_context, msg_id } = {}) { + const app = ipc_context?.caller?.app; + const process = ipc_context?.caller?.process; + // This mechanism will be replated with xdrpc soon const child_instance_id = window.uuidv4(); - window.child_launch_callbacks[child_instance_id] = { - parent_instance_id: event.data.appInstanceID, - launch_msg_id: msg_id, - }; // The "body" of this method is in a separate file - launch_app({ + const child_process = await launch_app({ name: app_name, args: args ?? {}, - parent_instance_id: ipc_context?.appInstanceId, + parent_instance_id: app?.appInstanceID, uuid: child_instance_id, }); + + const send_child_launched_msg = (...a) => { + const parent_iframe = process?.references?.iframe; + parent_iframe.contentWindow.postMessage({ + msg: 'childAppLaunched', + original_msg_id: msg_id, + child_instance_id, + ...a, + }, '*'); + } + + child_process.onchange('ipc_status', value => { + if ( value !== PROCESS_IPC_ATTACHED ) return; + + $(child_process.references.iframe).attr('data-appUsesSDK', 'true'); + + send_child_launched_msg({ uses_sdk: true }); + + // Send any saved broadcasts to the new app + globalThis.services.get('broadcast').sendSavedBroadcastsTo(child_instance_id); + + // If `window-active` is set (meanign the window is focused), focus the window one more time + // this is to ensure that the iframe is `definitely` focused and can receive keyboard events (e.g. keydown) + if(child_process.el_win.hasClass('window-active')){ + child_process.el_win.focusWindow(); + } + }); + + $(child_process.references.el_win).on('remove', () =>{ + const parent_iframe = process?.references?.iframe; + if ($(parent_iframe).attr('data-appUsesSdk') !== 'true') { + send_child_launched_msg({ uses_sdk: false }); + // We also have to report an extra close event because the real one was sent already + console.log('reporting app closed'); + window.report_app_closed(child_process.uuid); + } + }); + + return { + appInstanceID: child_instance_id, + usesSDK: true, + }; } } diff --git a/src/puter-js/src/lib/xdrpc.js b/src/puter-js/src/lib/xdrpc.js index e3d518cd..38b6a343 100644 --- a/src/puter-js/src/lib/xdrpc.js +++ b/src/puter-js/src/lib/xdrpc.js @@ -4,7 +4,7 @@ */ // Since `Symbol` is not clonable, we use a UUID to identify RPCs. -const $SCOPE = '9a9c83a4-7897-43a0-93b9-53217b84fde6'; +export const $SCOPE = '9a9c83a4-7897-43a0-93b9-53217b84fde6'; /** * The CallbackManager is used to manage callbacks for RPCs. @@ -12,7 +12,7 @@ const $SCOPE = '9a9c83a4-7897-43a0-93b9-53217b84fde6'; * the functions that are being called remotely. */ export class CallbackManager { - #messageId = 0; + #messageId = 1; constructor () { this.callbacks = new Map(); @@ -32,6 +32,8 @@ export class CallbackManager { const { id, args } = data; const callback = this.callbacks.get(id); if (callback) { + debugger; + console.log('callback?', callback, id, args); callback(...args); } } diff --git a/src/puter-js/src/modules/UI.js b/src/puter-js/src/modules/UI.js index f5608416..2cf327a5 100644 --- a/src/puter-js/src/modules/UI.js +++ b/src/puter-js/src/modules/UI.js @@ -17,6 +17,16 @@ class AppConnection extends EventListener { // Whether the target app uses the Puter SDK, and so accepts messages // (Closing and close events will still function.) #usesSDK; + + static from (values, { appInstanceID, messageTarget }) { + const connection = new AppConnection( + messageTarget, + appInstanceID, + values.appInstanceID, + values.usesSDK + ); + return connection; + } constructor(messageTarget, appInstanceID, targetAppInstanceID, usesSDK) { super([ @@ -161,6 +171,33 @@ class UI extends EventListener { value: dehydrator.dehydrate(value), }, '*'); } + + #ipc_stub = async function ({ + callback, + method, + parameters, + }) { + let p, resolve; + await new Promise(done_setting_resolve => { + p = new Promise(resolve_ => { + resolve = resolve_; + done_setting_resolve(); + }); + }); + if ( ! resolve ) debugger; + const callback_id = this.util.rpc.registerCallback(resolve); + this.messageTarget?.postMessage({ + $: 'puter-ipc', v: 2, + appInstanceID: this.appInstanceID, + env: this.env, + msg: method, + parameters, + uuid: callback_id, + }, '*'); + const ret = await p; + if ( callback ) callback(ret); + return ret; + } constructor (appInstanceID, parentInstanceID, appID, env, util) { const eventNames = [ @@ -339,7 +376,7 @@ class UI extends EventListener { } // Determine if this is a response to a previous message and if so, is there // a callback function for this message? if answer is yes to both then execute the callback - else if(e.data.original_msg_id && this.#callbackFunctions[e.data.original_msg_id]){ + else if(e.data.original_msg_id !== undefined && this.#callbackFunctions[e.data.original_msg_id]){ if(e.data.msg === 'fileOpenPicked'){ // 1 item returned if(e.data.items.length === 1){ @@ -400,11 +437,6 @@ class UI extends EventListener { // execute callback this.#callbackFunctions[e.data.original_msg_id](new FSItem(e.data.saved_file)); } - 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, e.data.uses_sdk); - this.#callbackFunctions[e.data.original_msg_id](connection); - } else{ // execute callback this.#callbackFunctions[e.data.original_msg_id](e.data); @@ -903,16 +935,20 @@ class UI extends EventListener { } // Returns a Promise - launchApp = function(appName, args, callback) { - return new Promise((resolve) => { - // if appName is an object and args is not set, then appName is actually args - if (typeof appName === 'object' && !args) { - args = appName; - appName = undefined; - } - - this.#postMessageWithCallback('launchApp', resolve, { app_name: appName, args }); - }) + launchApp = async function launchApp(app_name, args, callback) { + const app_info = await this.#ipc_stub({ + method: 'launchApp', + callback, + parameters: { + app_name, + args, + }, + }); + + return AppConnection.from(app_info, { + appInstanceID: this.appInstanceID, + messageTarget: this.messageTarget, + }); } parentApp() { diff --git a/src/puter-js/src/modules/Util.js b/src/puter-js/src/modules/Util.js index 16dbe147..ef7b3e59 100644 --- a/src/puter-js/src/modules/Util.js +++ b/src/puter-js/src/modules/Util.js @@ -1,4 +1,4 @@ -import { CallbackManager, Dehydrator, Hydrator } from "../lib/xdrpc"; +import { $SCOPE, CallbackManager, Dehydrator, Hydrator } from "../lib/xdrpc"; /** * The Util module exposes utilities within puter.js itself. @@ -27,4 +27,12 @@ class UtilRPC { getHydrator ({ target }) { return new Hydrator({ target }); } + + registerCallback (resolve) { + return this.callbackManager.register_callback(resolve); + } + + send (target, id, ...args) { + target.postMessage({ $SCOPE, id, args }, '*'); + } } diff --git a/src/putility/src/bases/BasicBase.js b/src/putility/src/bases/BasicBase.js index 78d121f9..f64ae1af 100644 --- a/src/putility/src/bases/BasicBase.js +++ b/src/putility/src/bases/BasicBase.js @@ -30,8 +30,10 @@ class BasicBase { _get_merged_static_array (key) { const chain = this._get_inheritance_chain(); const values = []; + let last = null; for ( const cls of chain ) { - if ( cls[key] ) { + if ( cls[key] && cls[key] !== last ) { + last = cls[key]; values.push(...cls[key]); } } @@ -39,6 +41,7 @@ class BasicBase { } _get_merged_static_object (key) { + // TODO: check objects by reference - same object in a subclass shouldn't count const chain = this._get_inheritance_chain(); const values = {}; for ( const cls of chain ) { diff --git a/src/putility/src/bases/FeatureBase.js b/src/putility/src/bases/FeatureBase.js index 5ef78b73..9fb6b42b 100644 --- a/src/putility/src/bases/FeatureBase.js +++ b/src/putility/src/bases/FeatureBase.js @@ -25,7 +25,7 @@ class FeatureBase extends BasicBase { this._ = { features: this._get_merged_static_array('FEATURES'), }; - + for ( const feature of this._.features ) { feature.install_in_instance( this, diff --git a/src/putility/src/features/PropertiesFeature.js b/src/putility/src/features/PropertiesFeature.js index 9e2c4b72..90a9572b 100644 --- a/src/putility/src/features/PropertiesFeature.js +++ b/src/putility/src/features/PropertiesFeature.js @@ -17,14 +17,23 @@ * along with this program. If not, see . */ module.exports = { + name: 'Properties', install_in_instance: (instance) => { const properties = instance._get_merged_static_object('PROPERTIES'); + instance.onchange = (name, callback) => { + instance.__properties[name].listeners.push(callback); + }; + + instance.__properties = {}; + for ( const k in properties ) { - if ( typeof properties[k] === 'function' ) { - instance[k] = properties[k](); - continue; - } + const state = { + definition: properties[k], + listeners: [], + value: undefined, + }; + instance.__properties[k] = state; if ( typeof properties[k] === 'object' ) { // This will be supported in the future. @@ -32,7 +41,29 @@ module.exports = { `is not a supported property specification.`); } - instance[k] = properties[k]; + let value = (() => { + if ( typeof properties[k] === 'function' ) { + return properties[k](); + } + + return properties[k]; + })(); + + Object.defineProperty(instance, k, { + get: () => { + return state.value; + }, + set: (value) => { + for ( const listener of instance.__properties[k].listeners ) { + listener(value, { + old_value: instance[k], + }); + } + state.value = value; + }, + }); + + state.value = value; } } } From 5ab3d88393c32c33f620db27816e7d16ac9d2bab Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Mon, 2 Sep 2024 17:36:46 -0400 Subject: [PATCH 2/2] cleanup --- src/gui/src/services/ExecService.js | 1 - src/puter-js/src/lib/xdrpc.js | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/gui/src/services/ExecService.js b/src/gui/src/services/ExecService.js index c79266bb..2146f758 100644 --- a/src/gui/src/services/ExecService.js +++ b/src/gui/src/services/ExecService.js @@ -61,7 +61,6 @@ export class ExecService extends Service { if ($(parent_iframe).attr('data-appUsesSdk') !== 'true') { send_child_launched_msg({ uses_sdk: false }); // We also have to report an extra close event because the real one was sent already - console.log('reporting app closed'); window.report_app_closed(child_process.uuid); } }); diff --git a/src/puter-js/src/lib/xdrpc.js b/src/puter-js/src/lib/xdrpc.js index 38b6a343..8b799b2c 100644 --- a/src/puter-js/src/lib/xdrpc.js +++ b/src/puter-js/src/lib/xdrpc.js @@ -27,13 +27,10 @@ export class CallbackManager { attach_to_source (source) { source.addEventListener('message', event => { const { data } = event; - debugger; if (data && typeof data === 'object' && data.$SCOPE === $SCOPE) { const { id, args } = data; const callback = this.callbacks.get(id); if (callback) { - debugger; - console.log('callback?', callback, id, args); callback(...args); } }