mirror of
https://github.com/HeyPuter/puter
synced 2024-11-14 14:03:42 +00:00
Merge branch 'main' of https://github.com/HeyPuter/puter into main
This commit is contained in:
commit
1f7a4782d8
@ -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
|
||||
}
|
||||
//--------------------------------------------------------
|
||||
|
@ -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' }
|
||||
|
@ -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;
|
||||
|
@ -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,60 @@ 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
|
||||
window.report_app_closed(child_process.uuid);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
appInstanceID: child_instance_id,
|
||||
usesSDK: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
@ -27,7 +27,6 @@ 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);
|
||||
|
@ -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() {
|
||||
|
@ -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 }, '*');
|
||||
}
|
||||
}
|
||||
|
@ -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 ) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user