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.
This commit is contained in:
KernelDeimos 2024-10-18 23:02:56 -04:00
parent bc5d09fe31
commit c2a475f3c0
7 changed files with 213 additions and 6 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
import putility from '@heyputer/putility';
export class FSHostService extends putility.concepts.Service {
async _init () {
//
}
}

View File

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

View File

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

View File

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