From c2a475f3c08eec0146dbaae8be0d8faa22b39bff Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Fri, 18 Oct 2024 23:02:56 -0400 Subject: [PATCH] dev: lay the groundwork for client FS relay Adds XDIncomingService to manage messages from another window. IPC now registers with XDIncoming and reports whether it was handled, that way messages intended to reach GUI can be reliably ignored. On GUI-side, XDIncomingService will be used by FSRelayService (not yet implemented) to handle requests for filesystem operations from another window. On App-side, XDIncomingService might be used by the PostMessageFS client-side filesystem implementation terminal to listen for filesystem events that were relayed by GUI (after a permission check) from their original websocket source. Either that, or we'll open a filesystem socket for each app using that app's token. NoPuterYetService was added because IPC.js is loaded before globalThis.puter exists, and was the easiest way to still allow IPC's listener to be registered with XDIncomingService. APIAccessService was added. This service currently holds auth_token and api_token and does nothing else. FilesystemService listens to this to maintain a websocket connection. APIAccessService will help to manage the complexity of all further code dependent on being informed about changes to the auth token or origin. Currently in puter.js these are passed around to several modules which manage the same piece of state information independantly. --- src/gui/src/IPC.js | 21 +++++-- src/puter-js/src/index.js | 40 +++++++++++++ src/puter-js/src/services/APIAccess.js | 24 ++++++++ src/puter-js/src/services/FSHost.js | 7 +++ src/puter-js/src/services/Filesystem.js | 80 +++++++++++++++++++++++++ src/puter-js/src/services/NoPuterYet.js | 20 +++++++ src/puter-js/src/services/XDIncoming.js | 27 +++++++++ 7 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 src/puter-js/src/services/APIAccess.js create mode 100644 src/puter-js/src/services/FSHost.js create mode 100644 src/puter-js/src/services/Filesystem.js create mode 100644 src/puter-js/src/services/NoPuterYet.js create mode 100644 src/puter-js/src/services/XDIncoming.js diff --git a/src/gui/src/IPC.js b/src/gui/src/IPC.js index 6e7234e45..7d4823e80 100644 --- a/src/gui/src/IPC.js +++ b/src/gui/src/IPC.js @@ -44,12 +44,12 @@ window.ipc_handlers = {}; * * Precautions are taken to ensure proper usage of appInstanceIDs and other sensitive information. */ -window.addEventListener('message', async (event) => { +const ipc_listener = async (event, handled) => { const app_env = event.data?.env ?? 'app'; // Only process messages from apps if(app_env !== 'app') - return; + return handled.resolve(false); // -------------------------------------------------------- // A response to a GUI message received from the app. @@ -61,7 +61,7 @@ window.addEventListener('message', async (event) => { delete window.appCallbackFunctions[event.data.original_msg_id]; // Done - return; + return handled.resolve(false); } // -------------------------------------------------------- @@ -70,18 +70,20 @@ window.addEventListener('message', async (event) => { // `data` and `msg` are required if(!event.data || !event.data.msg){ - return; + return handled.resolve(false); } // `appInstanceID` is required if(!event.data.appInstanceID){ console.error(`appInstanceID is needed`); - return; + return handled.resolve(false); }else if(!window.app_instance_ids.has(event.data.appInstanceID)){ console.error(`appInstanceID is invalid`); - return; + return handled.resolve(false); } + handled.resolve(true); + const $el_parent_window = $(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'); @@ -1522,4 +1524,11 @@ window.addEventListener('message', async (event) => { status_code, }); } +}; + +if ( ! window.when_puter_happens ) window.when_puter_happens = []; +window.when_puter_happens.push(async () => { + await puter.services.wait_for_init(['xd-incoming']); + const svc_xdIncoming = puter.services.get('xd-incoming'); + svc_xdIncoming.register_filter_listener(ipc_listener); }); diff --git a/src/puter-js/src/index.js b/src/puter-js/src/index.js index 5dc977951..a7d2f1267 100644 --- a/src/puter-js/src/index.js +++ b/src/puter-js/src/index.js @@ -12,6 +12,12 @@ import * as utils from './lib/utils.js'; import path from './lib/path.js'; import Util from './modules/Util.js'; import Drivers from './modules/Drivers.js'; +import putility from '@heyputer/putility'; +import { FSHostService } from './services/FSHost.js'; +import { FilesystemService } from './services/Filesystem.js'; +import { APIAccessService } from './services/APIAccess.js'; +import { XDIncomingService } from './services/XDIncoming.js'; +import { NoPuterYetService } from './services/NoPuterYet.js'; window.puter = (function() { 'use strict'; @@ -60,7 +66,12 @@ window.puter = (function() { constructor(options) { options = options ?? {}; + // "modules" in puter.js are external interfaces for the developer this.modules_ = []; + // "services" in puter.js are used by modules and may interact with each other + const context = new putility.libs.context.Context(); + this.services = new putility.system.ServiceManager({ context }); + context.services = this.services; // Holds the query parameters found in the current URL let URLParams = new URLSearchParams(window.location.search); @@ -137,6 +148,35 @@ window.puter = (function() { this.APIOrigin = 'https://api.' + URLParams.get('puter.domain'); } + if ( this.env !== 'app' ) { + this.services.register('no-puter-yet', NoPuterYetService); + this.services.register('filesystem', FilesystemService); + this.services.register('api-access', APIAccessService); + this.services.register('xd-incoming', XDIncomingService); + // this.services.register('fs-host', FSHostService); + } + + // When api-access is initialized, bind `.authToken` and + // `.APIOrigin` as a 1-1 mapping with the `puter` global + (async () => { + await this.services.wait_for_init(['api-access']); + const svc_apiAccess = this.services.get('api-access'); + + svc_apiAccess.authToken = this.authToken; + svc_apiAccess.APIOrigin = this.APIOrigin; + ['authToken', 'APIOrigin'].forEach(key => { + Object.defineProperty(this, key, { + get () { + return svc_apiAccess[key]; + }, + set (v) { + svc_apiAccess[key] = v; + return true; + } + }); + }); + })(); + // The SDK is running in the Puter GUI (i.e. 'gui') if(this.env === 'gui'){ this.authToken = window.auth_token; diff --git a/src/puter-js/src/services/APIAccess.js b/src/puter-js/src/services/APIAccess.js new file mode 100644 index 000000000..bceaf818a --- /dev/null +++ b/src/puter-js/src/services/APIAccess.js @@ -0,0 +1,24 @@ +import putility from "@heyputer/putility"; + +const { TTopics } = putility.traits; + +/** + * Manages the auth token and origin used to communicate with + * Puter's API + */ +export class APIAccessService extends putility.concepts.Service { + static TOPICS = ['update']; + + static PROPERTIES = { + auth_token: { + post_set () { + this.as(TTopics).pub('update'); + } + }, + api_origin: { + post_set () { + this.as(TTopics).pub('update'); + } + }, + }; +} diff --git a/src/puter-js/src/services/FSHost.js b/src/puter-js/src/services/FSHost.js new file mode 100644 index 000000000..a9aeccdec --- /dev/null +++ b/src/puter-js/src/services/FSHost.js @@ -0,0 +1,7 @@ +import putility from '@heyputer/putility'; + +export class FSHostService extends putility.concepts.Service { + async _init () { + // + } +} diff --git a/src/puter-js/src/services/Filesystem.js b/src/puter-js/src/services/Filesystem.js new file mode 100644 index 000000000..0c30e8058 --- /dev/null +++ b/src/puter-js/src/services/Filesystem.js @@ -0,0 +1,80 @@ +import putility from "@heyputer/putility"; + +export class FilesystemService extends putility.concepts.Service { + static PROPERTIES = { + // filesystem: + }; + + static DEPENDS = ['api-access']; + static HOOKS = [ + { + service: 'api-access', + event: 'update', + description: ` + re-initialize the socket connection whenever the + authentication token or API origin is changed. + `, + async do () { + console.log('do() was called'); + this.initializeSocket(); + } + } + ] + + _init () { + console.log('does this init get called'); + this.initializeSocket(); + } + + initializeSocket () { + console.log('THIS IS RUNNING'); + if (this.socket) { + this.socket.disconnect(); + } + + this.socket = io(this.APIOrigin, { + auth: { + auth_token: this.authToken, + } + }); + + this.bindSocketEvents(); + } + + bindSocketEvents() { + this.socket.on('connect', () => { + if(puter.debugMode) + console.log('FileSystem Socket: Connected', this.socket.id); + }); + + this.socket.on('disconnect', () => { + if(puter.debugMode) + console.log('FileSystem Socket: Disconnected'); + }); + + this.socket.on('reconnect', (attempt) => { + if(puter.debugMode) + console.log('FileSystem Socket: Reconnected', this.socket.id); + }); + + this.socket.on('reconnect_attempt', (attempt) => { + if(puter.debugMode) + console.log('FileSystem Socket: Reconnection Attemps', attempt); + }); + + this.socket.on('reconnect_error', (error) => { + if(puter.debugMode) + console.log('FileSystem Socket: Reconnection Error', error); + }); + + this.socket.on('reconnect_failed', () => { + if(puter.debugMode) + console.log('FileSystem Socket: Reconnection Failed'); + }); + + this.socket.on('error', (error) => { + if(puter.debugMode) + console.error('FileSystem Socket Error:', error); + }); + } +} diff --git a/src/puter-js/src/services/NoPuterYet.js b/src/puter-js/src/services/NoPuterYet.js new file mode 100644 index 000000000..004a47203 --- /dev/null +++ b/src/puter-js/src/services/NoPuterYet.js @@ -0,0 +1,20 @@ +import putility from "@heyputer/putility"; + +/** + * Runs commands on the special `window.when_puter_happens` global, for + * situations where the `puter` global doesn't exist soon enough. + */ +export class NoPuterYetService extends putility.concepts.Service { + _init () { + if ( ! window.when_puter_happens ) return; + if ( puter && puter.env !== 'gui' ) return; + + if ( ! Array.isArray(window.when_puter_happens) ) { + window.when_puter_happens = [window.when_puter_happens]; + } + + for ( const fn of window.when_puter_happens ) { + fn({ context: this._.context }); + } + } +} diff --git a/src/puter-js/src/services/XDIncoming.js b/src/puter-js/src/services/XDIncoming.js new file mode 100644 index 000000000..ce5833ea3 --- /dev/null +++ b/src/puter-js/src/services/XDIncoming.js @@ -0,0 +1,27 @@ +import putility from "@heyputer/putility"; + +const TeePromise = putility.libs.promise.TeePromise; + +/** + * Manages message events from the window object. + */ +export class XDIncomingService extends putility.concepts.Service { + _construct () { + this.filter_listeners_ = []; + this.tagged_listeners_ = {}; + } + + _init () { + window.addEventListener('message', async event => { + for ( const fn of this.filter_listeners_ ) { + const tp = new TeePromise(); + fn(event, tp); + if ( await tp ) return; + } + }); + } + + register_filter_listener (fn) { + this.filter_listeners_.push(fn); + } +}