dev: update ipc ready and app launched events

This commit is contained in:
KernelDeimos 2024-09-01 22:38:40 -04:00 committed by Eric Dubé
parent 65c23ecd93
commit 18a24f614f
10 changed files with 225 additions and 72 deletions

View File

@ -30,6 +30,7 @@ import path from "./lib/path.js";
import UIContextMenu from './UI/UIContextMenu.js'; import UIContextMenu from './UI/UIContextMenu.js';
import update_mouse_position from './helpers/update_mouse_position.js'; import update_mouse_position from './helpers/update_mouse_position.js';
import item_icon from './helpers/item_icon.js'; import item_icon from './helpers/item_icon.js';
import { PROCESS_IPC_ATTACHED } from './definitions.js';
window.ipc_handlers = {}; window.ipc_handlers = {};
/** /**
@ -92,18 +93,44 @@ window.addEventListener('message', async (event) => {
// New IPC handlers should be registered here. // New IPC handlers should be registered here.
// Do this by calling `register_ipc_handler` of IPCService. // Do this by calling `register_ipc_handler` of IPCService.
if ( window.ipc_handlers.hasOwnProperty(event.data.msg) ) { 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 // 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 = { 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()` // Registered IPC handlers are an object with a `handle()`
// method. We call it "spec" here, meaning specification. // method. We call it "spec" here, meaning specification.
const spec = window.ipc_handlers[event.data.msg]; 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; return;
} }
@ -112,25 +139,16 @@ window.addEventListener('message', async (event) => {
// READY // READY
//------------------------------------------------- //-------------------------------------------------
if(event.data.msg === 'READY'){ if(event.data.msg === 'READY'){
$(target_iframe).attr('data-appUsesSDK', 'true'); const services = globalThis.services;
const svc_process = services.get('process');
// If we were waiting to launch this as a child app, report to the parent that it succeeded. const process = svc_process.get_by_uuid(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);
// 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();
}
process.ipc_status = PROCESS_IPC_ATTACHED;
} }
//------------------------------------------------- //-------------------------------------------------
// windowFocused // windowFocused
//------------------------------------------------- //-------------------------------------------------
else if(event.data.msg === 'windowFocused'){ if(event.data.msg === 'windowFocused'){
// TODO: Respond to this // TODO: Respond to this
} }
//-------------------------------------------------------- //--------------------------------------------------------

View File

@ -17,6 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { AdvancedBase } from "@heyputer/putility";
export class Service { export class Service {
construct (o) { construct (o) {
this.$puter = {}; this.$puter = {};
@ -33,20 +35,36 @@ export class Service {
export const PROCESS_INITIALIZING = { i18n_key: 'initializing' }; export const PROCESS_INITIALIZING = { i18n_key: 'initializing' };
export const PROCESS_RUNNING = { i18n_key: 'running' }; 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. // Something is cloning these objects, so '===' checks don't work.
// To work around this, the `i` property is used to compare them. // 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_SOFT = { i: 0, end: true, i18n_key: 'end_soft' };
export const END_HARD = { i: 1, end: true, i18n_key: 'end_hard' }; 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 }) { constructor ({ uuid, parent, name, meta }) {
super();
this.uuid = uuid; this.uuid = uuid;
this.parent = parent; this.parent = parent;
this.name = name; this.name = name;
this.meta = meta; this.meta = meta;
this.references = {}; 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(); 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 { export class PseudoProcess extends Process {
_construct () { this.type_ = 'ui' } _construct () { this.type_ = 'ui' }

View File

@ -358,25 +358,16 @@ const launch_app = async (options)=>{
} }
} }
(async () => { const el = await el_win;
const el = await el_win; process.references.el_win = el;
$(el).on('remove', () => { process.chstatus(PROCESS_RUNNING);
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. $(el).on('remove', () => {
// FIXME: This is awkward. Really, we want some way of knowing when it's launched and reporting that immediately instead. const svc_process = globalThis.services.get('process');
const $app_iframe = $(el).find('.window-app-iframe'); svc_process.unregister(process.uuid);
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; return process;
process.chstatus(PROCESS_RUNNING);
})();
} }
export default launch_app; export default launch_app;

View File

@ -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"; import launch_app from "../helpers/launch_app.js";
export class ExecService extends Service { export class ExecService extends Service {
@ -14,20 +14,61 @@ export class ExecService extends Service {
} }
// This method is exposed to apps via IPCService. // 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 // This mechanism will be replated with xdrpc soon
const child_instance_id = window.uuidv4(); 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 // The "body" of this method is in a separate file
launch_app({ const child_process = await launch_app({
name: app_name, name: app_name,
args: args ?? {}, args: args ?? {},
parent_instance_id: ipc_context?.appInstanceId, parent_instance_id: app?.appInstanceID,
uuid: child_instance_id, 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,
};
} }
} }

View File

@ -4,7 +4,7 @@
*/ */
// Since `Symbol` is not clonable, we use a UUID to identify RPCs. // 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. * 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. * the functions that are being called remotely.
*/ */
export class CallbackManager { export class CallbackManager {
#messageId = 0; #messageId = 1;
constructor () { constructor () {
this.callbacks = new Map(); this.callbacks = new Map();
@ -32,6 +32,8 @@ export class CallbackManager {
const { id, args } = data; const { id, args } = data;
const callback = this.callbacks.get(id); const callback = this.callbacks.get(id);
if (callback) { if (callback) {
debugger;
console.log('callback?', callback, id, args);
callback(...args); callback(...args);
} }
} }

View File

@ -18,6 +18,16 @@ class AppConnection extends EventListener {
// (Closing and close events will still function.) // (Closing and close events will still function.)
#usesSDK; #usesSDK;
static from (values, { appInstanceID, messageTarget }) {
const connection = new AppConnection(
messageTarget,
appInstanceID,
values.appInstanceID,
values.usesSDK
);
return connection;
}
constructor(messageTarget, appInstanceID, targetAppInstanceID, usesSDK) { constructor(messageTarget, appInstanceID, targetAppInstanceID, usesSDK) {
super([ super([
'message', // The target sent us something with postMessage() 'message', // The target sent us something with postMessage()
@ -162,6 +172,33 @@ class UI extends EventListener {
}, '*'); }, '*');
} }
#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) { constructor (appInstanceID, parentInstanceID, appID, env, util) {
const eventNames = [ const eventNames = [
'localeChanged', 'localeChanged',
@ -339,7 +376,7 @@ class UI extends EventListener {
} }
// Determine if this is a response to a previous message and if so, is there // 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 // 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'){ if(e.data.msg === 'fileOpenPicked'){
// 1 item returned // 1 item returned
if(e.data.items.length === 1){ if(e.data.items.length === 1){
@ -400,11 +437,6 @@ class UI extends EventListener {
// execute callback // execute callback
this.#callbackFunctions[e.data.original_msg_id](new FSItem(e.data.saved_file)); 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{ else{
// execute callback // execute callback
this.#callbackFunctions[e.data.original_msg_id](e.data); this.#callbackFunctions[e.data.original_msg_id](e.data);
@ -903,16 +935,20 @@ class UI extends EventListener {
} }
// Returns a Promise<AppConnection> // Returns a Promise<AppConnection>
launchApp = function(appName, args, callback) { launchApp = async function launchApp(app_name, args, callback) {
return new Promise((resolve) => { const app_info = await this.#ipc_stub({
// if appName is an object and args is not set, then appName is actually args method: 'launchApp',
if (typeof appName === 'object' && !args) { callback,
args = appName; parameters: {
appName = undefined; app_name,
} args,
},
});
this.#postMessageWithCallback('launchApp', resolve, { app_name: appName, args }); return AppConnection.from(app_info, {
}) appInstanceID: this.appInstanceID,
messageTarget: this.messageTarget,
});
} }
parentApp() { parentApp() {

View File

@ -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. * The Util module exposes utilities within puter.js itself.
@ -27,4 +27,12 @@ class UtilRPC {
getHydrator ({ target }) { getHydrator ({ target }) {
return new Hydrator({ target }); return new Hydrator({ target });
} }
registerCallback (resolve) {
return this.callbackManager.register_callback(resolve);
}
send (target, id, ...args) {
target.postMessage({ $SCOPE, id, args }, '*');
}
} }

View File

@ -30,8 +30,10 @@ class BasicBase {
_get_merged_static_array (key) { _get_merged_static_array (key) {
const chain = this._get_inheritance_chain(); const chain = this._get_inheritance_chain();
const values = []; const values = [];
let last = null;
for ( const cls of chain ) { for ( const cls of chain ) {
if ( cls[key] ) { if ( cls[key] && cls[key] !== last ) {
last = cls[key];
values.push(...cls[key]); values.push(...cls[key]);
} }
} }
@ -39,6 +41,7 @@ class BasicBase {
} }
_get_merged_static_object (key) { _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 chain = this._get_inheritance_chain();
const values = {}; const values = {};
for ( const cls of chain ) { for ( const cls of chain ) {

View File

@ -17,14 +17,23 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
module.exports = { module.exports = {
name: 'Properties',
install_in_instance: (instance) => { install_in_instance: (instance) => {
const properties = instance._get_merged_static_object('PROPERTIES'); 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 ) { for ( const k in properties ) {
if ( typeof properties[k] === 'function' ) { const state = {
instance[k] = properties[k](); definition: properties[k],
continue; listeners: [],
} value: undefined,
};
instance.__properties[k] = state;
if ( typeof properties[k] === 'object' ) { if ( typeof properties[k] === 'object' ) {
// This will be supported in the future. // This will be supported in the future.
@ -32,7 +41,29 @@ module.exports = {
`is not a supported property specification.`); `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;
} }
} }
} }