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 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
}
//--------------------------------------------------------

View File

@ -17,6 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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' }

View File

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

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";
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,
};
}
}

View File

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

View File

@ -18,6 +18,16 @@ class AppConnection extends EventListener {
// (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([
'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) {
const eventNames = [
'localeChanged',
@ -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<AppConnection>
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;
}
launchApp = async function launchApp(app_name, args, callback) {
const app_info = await this.#ipc_stub({
method: 'launchApp',
callback,
parameters: {
app_name,
args,
},
});
this.#postMessageWithCallback('launchApp', resolve, { app_name: appName, args });
})
return AppConnection.from(app_info, {
appInstanceID: this.appInstanceID,
messageTarget: this.messageTarget,
});
}
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.
@ -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 }, '*');
}
}

View File

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

View File

@ -17,14 +17,23 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;
}
}
}