From dbf29d1b1a1cb2b97b87fc3efddb5c1fe48832c2 Mon Sep 17 00:00:00 2001 From: DH Date: Sun, 7 Sep 2025 01:26:10 +0300 Subject: [PATCH] extension-host: implement mobile support use interfaces to create external components and launchers fixed typo in readme --- README.md | 2 +- package-lock.json | 10 + package.json | 5 +- rpcsx-ui-kit/src/generators.ts | 97 +++--- rpcsx-ui/src/core/component.json | 161 +++++++--- rpcsx-ui/src/core/lib/Component.d.ts | 2 +- rpcsx-ui/src/core/lib/NativeTarget.ts | 11 + rpcsx-ui/src/core/lib/NativeTarget.web.ts | 3 + rpcsx-ui/src/core/{server => lib}/Target.ts | 5 - .../core/server/ComponentActivation.web.ts | 2 +- rpcsx-ui/src/core/server/ComponentInstance.ts | 21 +- rpcsx-ui/src/core/server/Launcher.ts | 52 ---- rpcsx-ui/src/core/server/Objects.ts | 72 ++++- rpcsx-ui/src/core/server/Target.web.ts | 26 -- rpcsx-ui/src/core/server/extension-api.ts | 37 --- rpcsx-ui/src/core/server/main.ts | 84 +++--- .../core/server/registerBuiltinLaunchers.ts | 3 - .../server/registerBuiltinLaunchers.web.ts | 132 -------- rpcsx-ui/src/explorer/server/Component.ts | 4 +- rpcsx-ui/src/explorer/server/main.ts | 6 +- rpcsx-ui/src/extension-host/component.json | 43 ++- .../server/extension-api.ts} | 66 ++-- .../extension-host/server/extension-host.ts | 5 +- rpcsx-ui/src/extension-host/server/main.ts | 20 +- .../server/registerBuiltinLaunchers.ts | 86 ++++++ .../server/registerBuiltinLaunchers.web.ts} | 283 +++++++++++++----- rpcsx-ui/src/fs/server/fs.ts | 32 +- rpcsx-ui/src/fs/server/fs.web.ts | 32 +- rpcsx-ui/src/fs/server/main.ts | 16 +- rpcsx-ui/src/github/server/main.ts | 4 +- rpcsx-ui/src/progress/server/main.ts | 12 +- 31 files changed, 777 insertions(+), 557 deletions(-) create mode 100644 rpcsx-ui/src/core/lib/NativeTarget.ts create mode 100644 rpcsx-ui/src/core/lib/NativeTarget.web.ts rename rpcsx-ui/src/core/{server => lib}/Target.ts (80%) delete mode 100644 rpcsx-ui/src/core/server/Launcher.ts delete mode 100644 rpcsx-ui/src/core/server/Target.web.ts delete mode 100644 rpcsx-ui/src/core/server/extension-api.ts delete mode 100644 rpcsx-ui/src/core/server/registerBuiltinLaunchers.ts delete mode 100644 rpcsx-ui/src/core/server/registerBuiltinLaunchers.web.ts rename rpcsx-ui/src/{core/server/extension-api.web.ts => extension-host/server/extension-api.ts} (61%) create mode 100644 rpcsx-ui/src/extension-host/server/registerBuiltinLaunchers.ts rename rpcsx-ui/src/{core/server/Extension.ts => extension-host/server/registerBuiltinLaunchers.web.ts} (55%) diff --git a/README.md b/README.md index 5234366..72c9831 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - `npm install` - `npm run build:web` - `npm run dev:ui` -- `npm run dev:wev:server` +- `npm run dev:web:server` ## Build Guide for Android diff --git a/package-lock.json b/package-lock.json index 6da41a2..c8b6838 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "react": "19.0.0", "react-dom": "19.0.0", "react-native": "0.79.6", + "react-native-device-info": "^14.0.4", "react-native-gesture-handler": "~2.24.0", "react-native-reanimated": "~3.17.4", "react-native-safe-area-context": "^5.4.0", @@ -14155,6 +14156,15 @@ } } }, + "node_modules/react-native-device-info": { + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-14.0.4.tgz", + "integrity": "sha512-NX0wMAknSDBeFnEnSFQ8kkAcQrFHrG4Cl0mVjoD+0++iaKrOupiGpBXqs8xR0SeJyPC5zpdPl4h/SaBGly6UxA==", + "license": "MIT", + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/react-native-edge-to-edge": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz", diff --git a/package.json b/package.json index 4fcd58f..0c4d73f 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "expo-constants": "~17.1.7", "expo-dev-client": "^5.2.4", "expo-document-picker": "^13.1.6", + "expo-file-system": "~18.1.11", "expo-font": "~13.3.2", "expo-haptics": "~14.1.4", "expo-image": "~2.4.0", @@ -60,13 +61,13 @@ "react": "19.0.0", "react-dom": "19.0.0", "react-native": "0.79.6", + "react-native-device-info": "^14.0.4", "react-native-gesture-handler": "~2.24.0", "react-native-reanimated": "~3.17.4", "react-native-safe-area-context": "^5.4.0", "react-native-screens": "~4.11.1", "react-native-web": "^0.20.0", - "react-native-webview": "13.13.5", - "expo-file-system": "~18.1.11" + "react-native-webview": "13.13.5" }, "devDependencies": { "@expo/metro-config": "~0.20.0", diff --git a/rpcsx-ui-kit/src/generators.ts b/rpcsx-ui-kit/src/generators.ts index dfafa14..7ec1f69 100644 --- a/rpcsx-ui-kit/src/generators.ts +++ b/rpcsx-ui-kit/src/generators.ts @@ -711,17 +711,17 @@ type ${uLabel}Interface = { ${"methods" in iface ? iface.methods && Object.keys(iface.methods).map(method => { const methodTypeLabel = generateComponentLabelName(component, `${name}-${method}`, true); if ("params" in (iface.methods as any)[method]) { - return ` ${generateLabelName(method, false)}(caller: Component, request: ${methodTypeLabel}Request): ${methodTypeLabel}Response | Promise<${methodTypeLabel}Response>`; + return ` ${generateLabelName(method, false)}(caller: ComponentRef, request: ${methodTypeLabel}Request): ${methodTypeLabel}Response | Promise<${methodTypeLabel}Response>`; } else { - return ` ${generateLabelName(method, false)}(caller: Component): ${methodTypeLabel}Response | Promise<${methodTypeLabel}Response>`; + return ` ${generateLabelName(method, false)}(caller: ComponentRef): ${methodTypeLabel}Response | Promise<${methodTypeLabel}Response>`; } }).join("\n") : ""} ${"notifications" in iface ? iface.notifications && Object.keys(iface.notifications).map(notification => { const methodTypeLabel = generateComponentLabelName(component, `${name}-${notification}`, true); if ("params" in (iface.notifications as any)[notification]) { - return ` ${generateLabelName(notification, false)}(caller: Component, request: ${methodTypeLabel}Request): void | Promise;` + return ` ${generateLabelName(notification, false)}(caller: ComponentRef, request: ${methodTypeLabel}Request): void | Promise;` } else { - return ` ${generateLabelName(notification, false)}(caller: Component): void | Promise;`; + return ` ${generateLabelName(notification, false)}(caller: ComponentRef): void | Promise;`; } }).join("\n") : ""} };` @@ -849,34 +849,34 @@ export class ${uLabel}ComponentObject implements ComponentObject { constructor(public impl: ${uLabel}Interface, public name: string) {} - call(caller: Component, method: string, params: Json | undefined): Promise | Json | void { + call(caller: ComponentRef, method: string, params: Json | undefined): Promise | Json | void { void caller, params; switch (method) { ${"methods" in iface ? iface.methods && Object.keys(iface.methods).map(method => { - if ("params" in (iface.methods as any)[method]) { - return ` case "${method}": return this.impl.${generateLabelName(method, false)}(caller, params as any);\n` - } else { - return ` case "${method}": return this.impl.${generateLabelName(method, false)}(caller);\n` - } -}).join("\n") : ""} + if ("params" in (iface.methods as any)[method]) { + return ` case "${method}": return this.impl.${generateLabelName(method, false)}(caller, params as any);\n` + } else { + return ` case "${method}": return this.impl.${generateLabelName(method, false)}(caller);\n` + } + }).join("\n") : ""} default: throw createError(ErrorCode.MethodNotFound); } } - notify(caller: Component, method: string, params: Json | undefined): void | Promise { + notify(caller: ComponentRef, method: string, params: Json | undefined): void | Promise { void caller, params; switch (method) { ${"notifications" in iface ? iface.notifications && Object.keys(iface.notifications).map(notification => { - if ("params" in (iface.notifications as any)[notification]) { - return ` case "${notification}": return this.impl.${generateLabelName(notification, false)}(caller, params as any);\n` - } else { - return ` case "${notification}": return this.impl.${generateLabelName(notification, false)}(caller);\n` - } -}).join("\n") : ""} + if ("params" in (iface.notifications as any)[notification]) { + return ` case "${notification}": return this.impl.${generateLabelName(notification, false)}(caller, params as any);\n` + } else { + return ` case "${notification}": return this.impl.${generateLabelName(notification, false)}(caller);\n` + } + }).join("\n") : ""} default: throw createError(ErrorCode.MethodNotFound); @@ -897,12 +897,12 @@ ${"methods" in iface ? iface.methods && Object.keys(iface.methods).map(method => const methodTypeLabel = generateComponentLabelName(component, `${name}-${method}`, true); if ("params" in (iface.methods as any)[method]) { return ` async ${generateLabelName(method, false)}(request: ${methodTypeLabel}Request): Promise<${methodTypeLabel}Response> { - return (await ${component == "core" ? "" : "core."}objectCall({ object: this.id, method: "${method}", params: request})).result as any; + return await ${component == "core" ? "" : "core."}objectCall({ object: this.id, method: "${method}", params: request}) as any; } ` } else { return ` async ${generateLabelName(method, false)}(): Promise<${methodTypeLabel}Response> { - return (await ${component == "core" ? "" : "core."}objectCall({ object: this.id, method: "${method}", params: {}})).result as any; + return await ${component == "core" ? "" : "core."}objectCall({ object: this.id, method: "${method}", params: {}}) as any; } ` } @@ -1019,7 +1019,7 @@ export function onAny${uLabel}Created(handler: () => Promise | void) { `; } - generateView(component: string, _path: string, name: string) { + generateView(_component: string, _path: string, name: string) { this.viewBody += ` export function push${name}View(target: Window, params: ${name}Props) { return target.pushView("${name}", params); @@ -1045,8 +1045,8 @@ type ComponentObject = { typeId: string; name: string; impl: object; - call(caller: Component, method: string, params: Json | undefined): Promise | Json | void; - notify(caller: Component, method: string, params: Json | undefined): void | Promise; + call(caller: ComponentRef, method: string, params: Json | undefined): Promise | Json | void; + notify(caller: ComponentRef, method: string, params: Json | undefined): void | Promise; dispose(): void | Promise; }; @@ -1074,24 +1074,28 @@ class ServerPrivateApiGenerator implements ContributionGenerator { generateEvent(component: string, event: object, name: string) { const label = generateComponentLabelName(component, name, true); if (Object.keys(event).length == 0) { - this.body += ` -export function send${label}Event(receiver: Component) { + this.body += `export function on${label}(handler: () => Promise | void) { + return thisComponent().onEvent(thisComponent(), "${name}", handler as any); +} +export function send${label}Event(receiver: ComponentRef) { return receiver.sendEvent("${name}"); } export function emit${label}Event() { return thisComponent().emitEvent("${name}"); } -\n`; - return; - } - - this.body += ` -export function send${label}Event(receiver: Component, params: ${label}Event) { +`; + } else { + this.body += `export function on${label}(handler: (event: ${label}Event) => Promise | void) { + return thisComponent().onEvent(thisComponent(), "${name}", handler as any); +} +export function send${label}Event(receiver: ComponentRef, params: ${label}Event) { return receiver.sendEvent("${name}", params); } export function emit${label}Event(params: ${label}Event) { return thisComponent().emitEvent("${name}", params); -}\n`; +} +`; + } } generateMethod(component: string, method: object, name: string) { @@ -1111,7 +1115,7 @@ export function emit${label}Event(params: ${label}Event) { const label = generateComponentLabelName(component, name, false); const uLabel = generateComponentLabelName(component, name, true); this.body += ` -export async function call${uLabel}(caller: Component, params: ${uLabel}Request): Promise<${uLabel}Response> { +export async function call${uLabel}(caller: ComponentRef, params: ${uLabel}Request): Promise<${uLabel}Response> { return impl.${method.handler}(caller, params); } @@ -1140,7 +1144,7 @@ export async function ${label}(params: ${uLabel}Request): Promise<${uLabel}Respo const label = generateComponentLabelName(component, name, false); const uLabel = generateComponentLabelName(component, name, true); this.body += ` -export async function notify${uLabel}(caller: Component, params: ${uLabel}Request) { +export async function notify${uLabel}(caller: ComponentRef, params: ${uLabel}Request) { impl.${notification.handler}(caller, params); } export async function ${label}(params: ${uLabel}Request) { @@ -1184,25 +1188,21 @@ export async function create${uLabel}Object Promise | void) { - return core.onObjectCreated((params) => { + return ${component == "core" ? "" : "core."}onObjectCreated((params) => { if (params.interface == "${component}/${name}") { handler(new ${uLabel}(params.object)); } }); } export function onAny${uLabel}Created(handler: () => Promise | void) { - return core.onObjectCreated((params) => { + return ${component == "core" ? "" : "core."}onObjectCreated((params) => { if (params.interface == "${component}/${name}") { handler(); } }); } `; - } } @@ -1212,6 +1212,7 @@ ${this.callBody.length > 0 || this.notifyBody.length > 0 ? 'import * as impl fro import { createError } from "$core/Error"; import { thisComponent } from "$/component-info"; import * as core from "$core"; +import { isJsonObject } from "$core/Json"; ${this.viewBody && "import { Window } from '$core/Window';"} export { thisComponent } from "$/component-info"; @@ -1221,8 +1222,8 @@ type ComponentObject = { typeId: string; name: string; impl: object; - call(caller: Component, method: string, params: Json | undefined): Promise | Json | void; - notify(caller: Component, method: string, params: Json | undefined): void | Promise; + call(caller: ComponentRef, method: string, params: Json | undefined): Promise | Json | void; + notify(caller: ComponentRef, method: string, params: Json | undefined): void | Promise; dispose(): void | Promise; }; @@ -1244,13 +1245,13 @@ export function ownObjects() { return Object.values(objects); } -export async function call(caller: Component, method: string, params: Json | undefined): Promise { +export async function call(caller: ComponentRef, method: string, params: Json | undefined): Promise { void caller, params; switch (method) { ${this.callBody} case "$/object/call": - if (params && typeof params.method == "string" && typeof params.object == 'number') { + if (params && isJsonObject(params) && typeof params.method == "string" && typeof params.object == 'number') { if (params.object in objects) { return objects[params.object].call(caller, params.method, "params" in params ? params.params as any : undefined @@ -1267,13 +1268,13 @@ ${this.callBody} } } -export async function notify(caller: Component, method: string, params: Json | undefined) { +export async function notify(caller: ComponentRef, method: string, params: Json | undefined) { void caller, params; switch (method) { ${this.notifyBody} case "$/object/notify": - if (params && typeof params.notification == "string" && typeof params.object == 'number') { + if (params && isJsonObject(params) && typeof params.notification == "string" && typeof params.object == 'number') { if (params.object in objects) { return objects[params.object].notify(caller, params.notification, "params" in params ? params.params as any : undefined @@ -1801,11 +1802,11 @@ class ${componentLabel}ComponentImpl implements IComponentImpl { } } - async call(caller: Component, method: string, params?: Json) { + async call(caller: ComponentRef, method: string, params?: Json) { return await api.call(caller, method, params); } - async notify(caller: Component, notification: string, params?: Json) { + async notify(caller: ComponentRef, notification: string, params?: Json) { await api.notify(caller, notification, params); } }; diff --git a/rpcsx-ui/src/core/component.json b/rpcsx-ui/src/core/component.json index 2ca449e..a52c1e3 100644 --- a/rpcsx-ui/src/core/component.json +++ b/rpcsx-ui/src/core/component.json @@ -308,43 +308,6 @@ } } }, - "extension/load": { - "handler": "loadExtension", - "params": { - "id": { - "type": "string" - } - } - }, - "extension/unload": { - "handler": "unloadExtension", - "params": { - "id": { - "type": "string" - } - } - }, - "extension/install": { - "handler": "installExtension", - "params": { - "path": { - "type": "string" - } - }, - "returns": { - "id": { - "type": "string" - } - } - }, - "extension/remove": { - "handler": "removeExtension", - "params": { - "id": { - "type": "string" - } - } - }, "settings/set": { "handler": "handleSettingsSet", "params": { @@ -452,11 +415,22 @@ "type": "json" } }, - "returns": { - "result": { + "returns": "json" + }, + "component-call": { + "handler": "handleCall", + "params": { + "caller": { + "type": "string" + }, + "method": { + "type": "string" + }, + "params": { "type": "json" } - } + }, + "returns": "json" } }, "notifications": { @@ -481,6 +455,20 @@ "type": "json" } } + }, + "component-notify": { + "handler": "handleNotify", + "params": { + "caller": { + "type": "string" + }, + "notification": { + "type": "string" + }, + "params": { + "type": "json" + } + } } }, "events": { @@ -504,14 +492,101 @@ "interfaces": { "external-component": { "methods": { - "call": { - "params": "json", + "object-call": { + "params": { + "object": { + "type": "number" + }, + "method": { + "type": "string" + }, + "params": { + "type": "json" + } + }, "returns": "json" + }, + "call": { + "params": { + "method": { + "type": "string" + }, + "params": { + "type": "json" + } + }, + "returns": "json" + }, + "initialize": { + }, + "activate": { + "params": { + "settings": { + "type": "json" + } + } + }, + "deactivate": {}, + "dispose": {}, + "get-pid": { + "returns": "number" } }, "notifications": { "notify": { - "params": "json" + "params": { + "method": { + "type": "string" + }, + "params": { + "type": "json" + } + } + }, + "object-notify": { + "params": { + "object": { + "type": "number" + }, + "method": { + "type": "string" + }, + "params": { + "type": "json" + } + } + }, + "object-destroy": { + "params": { + "object": { + "type": "number" + }, + "interface-name": { + "type": "string" + } + } + } + } + }, + "launcher": { + "methods": { + "launch": { + "params": { + "path": { + "type": "string" + }, + "args": { + "type": "array", + "item-type": "string" + }, + "manifest": { + "type": "extension-info" + }, + "launcher-params": { + "type": "json-object" + } + }, + "returns": "number" } } } diff --git a/rpcsx-ui/src/core/lib/Component.d.ts b/rpcsx-ui/src/core/lib/Component.d.ts index 3d3a26a..de4f58b 100644 --- a/rpcsx-ui/src/core/lib/Component.d.ts +++ b/rpcsx-ui/src/core/lib/Component.d.ts @@ -17,7 +17,7 @@ declare global { export type ComponentId = string; - export type Component = { + export type ComponentRef = { getId(): ComponentId; onClose(listener: () => void | Promise): IDisposable; sendEvent(event: string, params?: any): void; diff --git a/rpcsx-ui/src/core/lib/NativeTarget.ts b/rpcsx-ui/src/core/lib/NativeTarget.ts new file mode 100644 index 0000000..9d4423d --- /dev/null +++ b/rpcsx-ui/src/core/lib/NativeTarget.ts @@ -0,0 +1,11 @@ +import { Target } from "./Target"; +import { Platform } from 'react-native'; +import DeviceInfo from 'react-native-device-info'; + +export function getDeviceArchitecture() { + const abis = DeviceInfo.supportedAbisSync(); + const abi = (abis && abis.length > 0) ? abis[0] : 'unknown'; + return abi.includes("arm64") || abi.includes("aarch64") ? "aarch64" : "x64"; +} + +export const NativeTarget = new Target("elf", getDeviceArchitecture(), Platform.OS); diff --git a/rpcsx-ui/src/core/lib/NativeTarget.web.ts b/rpcsx-ui/src/core/lib/NativeTarget.web.ts new file mode 100644 index 0000000..d7da684 --- /dev/null +++ b/rpcsx-ui/src/core/lib/NativeTarget.web.ts @@ -0,0 +1,3 @@ +import { Target } from "./Target"; + +export const NativeTarget = new Target(process.platform === "win32" ? "pe" : "elf", process.arch, process.platform); diff --git a/rpcsx-ui/src/core/server/Target.ts b/rpcsx-ui/src/core/lib/Target.ts similarity index 80% rename from rpcsx-ui/src/core/server/Target.ts rename to rpcsx-ui/src/core/lib/Target.ts index 68d368b..d4663e4 100644 --- a/rpcsx-ui/src/core/server/Target.ts +++ b/rpcsx-ui/src/core/lib/Target.ts @@ -14,13 +14,8 @@ export class Target { return new Target(parts[0], parts[1], parts[2]); } - static native() { - return nativeTarget; - } - format(): string { return this.fileFormat + "-" + this.arch + "-" + this.platform; } } -const nativeTarget = new Target("none", "none", "none"); diff --git a/rpcsx-ui/src/core/server/ComponentActivation.web.ts b/rpcsx-ui/src/core/server/ComponentActivation.web.ts index 9096baf..408c2d5 100644 --- a/rpcsx-ui/src/core/server/ComponentActivation.web.ts +++ b/rpcsx-ui/src/core/server/ComponentActivation.web.ts @@ -8,7 +8,7 @@ export function onComponentActivation(component: ComponentInstance) { const impl = component.getImpl(); const createRendererComponent = (webContents: Electron.WebContents) => { - const rendererComponent: Component = { + const rendererComponent: ComponentRef = { getId: () => ":renderer", onClose: (listener) => { const wrapped = async () => { diff --git a/rpcsx-ui/src/core/server/ComponentInstance.ts b/rpcsx-ui/src/core/server/ComponentInstance.ts index 05782f2..ee9b8b5 100644 --- a/rpcsx-ui/src/core/server/ComponentInstance.ts +++ b/rpcsx-ui/src/core/server/ComponentInstance.ts @@ -20,8 +20,11 @@ export type IComponentImpl = IDisposable & { initialize(eventEmitter: (event: string, params: Json) => void): void | Promise; activate(context: ComponentContext, settings: Json, signal?: AbortSignal): void | Promise; deactivate(context: ComponentContext): void | Promise; - call?(caller: Component, method: string, params: Json | undefined): Promise; - notify?(caller: Component, notification: string, params: Json | undefined): Promise; + call?(caller: ComponentRef, method: string, params: Json | undefined): Promise; + notify?(caller: ComponentRef, notification: string, params: Json | undefined): Promise; + objectCall?(caller: ComponentRef, object: number, method: string, params: Json | undefined): Promise; + objectNotify?(caller: ComponentRef, object: number, notification: string, params: Json | undefined): Promise; + objectDestroy?(caller: ComponentRef, object: number, interfaceName: string): Promise; getPid?(): number; } @@ -50,7 +53,7 @@ export class ComponentInstance implements ComponentContext { this.externalEventEmitter[`${sender.getId()}/${event}`]?.emit(params); } - private createCallerView(caller: ComponentInstance): Component { + private createCallerView(caller: ComponentInstance): ComponentRef { return { getId: () => caller.getId(), onClose: (listener) => caller.onEvent(this, deactivateEvent, listener), @@ -219,6 +222,10 @@ export class ComponentInstance implements ComponentContext { throw createError(ErrorCode.InvalidParams, `${caller.getId()}: component ${this.getName()} has no interface method support`); } + if (this.impl.objectCall) { + return this.impl.objectCall(this.createCallerView(caller), objectId, method, params); + } + return await this.impl.call(this.createCallerView(caller), `$/object/call`, { object: objectId, method, @@ -235,6 +242,10 @@ export class ComponentInstance implements ComponentContext { throw createError(ErrorCode.InvalidParams, `${caller.getId()}: component ${this.getName()} has no interface support`); } + if (this.impl.objectNotify) { + return this.impl.objectNotify(this.createCallerView(caller), objectId, notification, params); + } + return await this.impl.notify(this.createCallerView(caller), `$/object/notify`, { object: objectId, notification, @@ -251,6 +262,10 @@ export class ComponentInstance implements ComponentContext { throw createError(ErrorCode.InvalidParams, `${caller.getId()}: component ${this.getName()} has no interface support`); } + if (this.impl.objectDestroy) { + return this.impl.objectDestroy(this.createCallerView(caller), objectId, interfaceName); + } + return await this.impl.notify(this.createCallerView(caller), `$/object/destroy`, { objectId, interface: interfaceName diff --git a/rpcsx-ui/src/core/server/Launcher.ts b/rpcsx-ui/src/core/server/Launcher.ts deleted file mode 100644 index ac8f82d..0000000 --- a/rpcsx-ui/src/core/server/Launcher.ts +++ /dev/null @@ -1,52 +0,0 @@ - -// FIXME: remove this import -import type { Readable, Writable } from 'stream'; -import { Target } from "./Target"; - -export type Process = { - stdin: Writable; - stdout: Readable; - stderr: Readable; - kill: (signal: number | NodeJS.Signals) => void; - on: (event: 'close' | 'exit', listener: (...args: any[]) => void) => void; - once: (event: 'close' | 'exit', listener: (...args: any[]) => void) => void; - off: (event: 'close' | 'exit', listener: (...args: any[]) => void) => void; - getPid(): number; -} - -export type LaunchParams = { - launcherRequirements: object; - signal?: AbortSignal; -} - -export type Launcher = { - launch: (path: string, args: string[], params: LaunchParams) => Promise | Process; -} - -const launcherStorage: { - [key: string]: Launcher -} = {}; - -export function addLauncher(target: Target, launcher: Launcher) { - launcherStorage[target.format()] = launcher; -} - -export function deleteLauncher(target: Target) { - delete launcherStorage[target.format()]; -} - -export function getLauncher(target: Target | string) { - if (target instanceof Target) { - target = target.format(); - } - - if (target in launcherStorage) { - return launcherStorage[target]; - } - - return undefined; -} - -export function getLauncherList() { - return Object.keys(launcherStorage); -} diff --git a/rpcsx-ui/src/core/server/Objects.ts b/rpcsx-ui/src/core/server/Objects.ts index a5714fd..92b0a0b 100644 --- a/rpcsx-ui/src/core/server/Objects.ts +++ b/rpcsx-ui/src/core/server/Objects.ts @@ -1,5 +1,5 @@ import { createError } from "lib/Error"; -import { ComponentInstance, findComponentById } from "./ComponentInstance"; +import { ComponentInstance, findComponentById, registerComponent, unregisterComponent } from "./ComponentInstance"; import * as self from '$'; let nextObjectId = 0; @@ -22,19 +22,21 @@ export function unregisterInterface(component: ComponentInstance, interfaceName: iface.forEach(async objectId => { const instance = objects[objectId]; - delete objects[objectId]; + if (instance) { + delete objects[objectId]; - const component = findComponentById(instance.owner); + const component = findComponentById(instance.owner); - try { - await component?.objectDestroy(component, instance.interfaceName, objectId); - } catch {} + try { + await component?.objectDestroy(component, instance.interfaceName, objectId); + } catch { } + } }); delete interfaceObjects[id]; } -export function createObject(caller: Component, objectName: string, interfaceName: string) { +export async function createObject(caller: ComponentRef, objectName: string, interfaceName: string) { if (!(interfaceName in interfaceObjects)) { throw createError(ErrorCode.InvalidParams, `Unknown interface ${interfaceName}`); } @@ -49,10 +51,46 @@ export function createObject(caller: Component, objectName: string, interfaceNam interfaceObjects[interfaceName].add(objectId); caller.onClose(() => destroyObject(caller, objectId)); self.emitObjectCreatedEvent({ interface: interfaceName, object: objectId }); + + if (interfaceName == "core/external-component") { + const externalComponent = self.toExternalComponent(objectId); + const pid = 0; // FIXME: await externalComponent.getPid(); + registerComponent({ name: objectName, version: "0.0.1" }, { + initialize: (): void | Promise => { + return externalComponent.initialize(); + }, + activate: (_context: ComponentContext, settings: Json, _signal?: AbortSignal): void | Promise => { + return externalComponent.activate({ settings }); + }, + deactivate: (_context: ComponentContext) => { + return externalComponent.deactivate(); + }, + call: (_caller: ComponentRef, method: string, params: Json | undefined) => { + return externalComponent.call({ method, params: params ?? null }); + }, + notify: async (_caller: ComponentRef, notification: string, params: Json | undefined) => { + await externalComponent.notify({ method: notification, params: params ?? null }); + }, + objectCall: (_caller: ComponentRef, object: number, method: string, params: Json | undefined) => { + return externalComponent.objectCall({ object, method, params: params ?? null }); + }, + objectNotify: async (_caller: ComponentRef, object: number, notification: string, params: Json | undefined) => { + await externalComponent.objectNotify({ object, method: notification, params: params ?? null }); + }, + objectDestroy: async (_caller: ComponentRef, object: number, interfaceName: string) => { + await externalComponent.objectDestroy({ object, interfaceName }); + }, + getPid: () => pid, + dispose: async () => { + await externalComponent.dispose(); + }, + }); + } + return objectId; } -export function destroyObject(caller: Component, objectId: number) { +export async function destroyObject(caller: ComponentRef, objectId: number) { const instance = objects[objectId]; if (!instance) { @@ -63,6 +101,12 @@ export function destroyObject(caller: Component, objectId: number) { throw createError(ErrorCode.InvalidRequest, `${caller.getId()}: Cannot destroy object '${instance.objectName}' created by component '${instance.owner}'`); } + if (instance.interfaceName == "core/external-component") { + try { + await unregisterComponent(instance.objectName); + } catch {} + } + delete objects[objectId]; interfaceObjects[instance.interfaceName]?.delete(objectId); } @@ -95,9 +139,13 @@ export function getName(objectId: number) { return objectInstance.objectName; } -export function call(caller: Component, objectId: number, method: string, params: Json) { +export function call(caller: ComponentRef, objectId: number, method: string, params: Json) { const instance = objects[objectId]; + if (!instance) { + throw createError(ErrorCode.InvalidRequest, `Failed to find instance ${objectId}`); + } + const callerComponent = findComponentById(caller.getId()); if (!callerComponent) { throw createError(ErrorCode.InvalidRequest, "Cannot find caller component"); @@ -111,9 +159,13 @@ export function call(caller: Component, objectId: number, method: string, params return component.objectCall(callerComponent, objectId, method, params); } -export function notify(caller: Component, objectId: number, notification: string, params: Json) { +export function notify(caller: ComponentRef, objectId: number, notification: string, params: Json) { const instance = objects[objectId]; + if (!instance) { + throw createError(ErrorCode.InvalidRequest, `Failed to find instance ${objectId}`); + } + const callerComponent = findComponentById(caller.getId()); if (!callerComponent) { throw createError(ErrorCode.InvalidRequest, "Cannot find caller component"); diff --git a/rpcsx-ui/src/core/server/Target.web.ts b/rpcsx-ui/src/core/server/Target.web.ts deleted file mode 100644 index c590d30..0000000 --- a/rpcsx-ui/src/core/server/Target.web.ts +++ /dev/null @@ -1,26 +0,0 @@ -export class Target { - constructor( - public fileFormat: string, - public arch: string, - public platform: string, - ) { } - - static parse(triple: string) { - const parts = triple.split('-'); - if (parts.length != 3) { - return undefined; - } - - return new Target(parts[0], parts[1], parts[2]); - } - - static native() { - return nativeTarget; - } - - format(): string { - return this.fileFormat + "-" + this.arch + "-" + this.platform; - } -} - -const nativeTarget = new Target(process.platform === "win32" ? "pe" : "elf", process.arch, process.platform); diff --git a/rpcsx-ui/src/core/server/extension-api.ts b/rpcsx-ui/src/core/server/extension-api.ts deleted file mode 100644 index 0e287b9..0000000 --- a/rpcsx-ui/src/core/server/extension-api.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createError } from '$/Error'; - -import { NativeModule, requireNativeModule } from 'expo'; - -declare class ExtensionLoaderModule extends NativeModule { - loadExtension(path: string): Promise; - unloadExtension(id: number): Promise; - call(extension: number, method: string, params: string): Promise; - notify(extension: number, notification: string, params: string): Promise; - sendResponse(methodId: number, body: string): Promise; -} - -const nativeLoader = requireNativeModule('ExtensionLoader'); - -const loadedExtensions: Record = {}; - -export async function loadExtension(request: ExtensionLoadRequest): Promise { - const nativeId = await nativeLoader.loadExtension(request.id); - loadedExtensions[request.id] = nativeId; -} - -export async function unloadExtension(request: ExtensionUnloadRequest): Promise { - const nativeId = loadedExtensions[request.id]; - if (nativeId === undefined) { - throw createError(ErrorCode.InvalidRequest, `extension ${request.id} is not loaded`); - } - - await nativeLoader.unloadExtension(nativeId); -} - -export async function installExtension(_request: ExtensionInstallRequest): Promise { - throw createError(ErrorCode.InvalidRequest); -} - -export async function removeExtension(_request: ExtensionRemoveRequest): Promise { - throw createError(ErrorCode.InvalidRequest); -} diff --git a/rpcsx-ui/src/core/server/main.ts b/rpcsx-ui/src/core/server/main.ts index 326987e..3b5a8df 100644 --- a/rpcsx-ui/src/core/server/main.ts +++ b/rpcsx-ui/src/core/server/main.ts @@ -4,14 +4,11 @@ import { ComponentInstance, findComponentById, getActivatedComponentList, getCom import * as instance from './ComponentInstance'; import * as settings from './Settings'; import { Schema, SchemaError, SchemaObject, validateObject } from 'lib/Schema'; -import * as extensionApi from './extension-api'; -import { registerBuiltinLaunchers } from './registerBuiltinLaunchers'; import { initialize } from './initialize'; import * as objects from './Objects'; import { initializeRenderer } from './initialize-renderer'; initialize(); -registerBuiltinLaunchers(); export async function activate() { try { @@ -73,7 +70,7 @@ export async function deactivate() { } } -export async function activateComponent(_caller: Component, request: ComponentActivateRequest): Promise { +export async function activateComponent(_caller: ComponentRef, request: ComponentActivateRequest): Promise { const component = findComponentById(request.id); if (!component) { throw createError(ErrorCode.InvalidParams, `component ${request.id} not found`); @@ -82,7 +79,7 @@ export async function activateComponent(_caller: Component, request: ComponentAc await component.activate(); } -export async function deactivateComponent(_caller: Component, request: ComponentDeactivateRequest): Promise { +export async function deactivateComponent(_caller: ComponentRef, request: ComponentDeactivateRequest): Promise { const component = findComponentById(request.id); if (!component) { throw createError(ErrorCode.InvalidParams, `component ${request.id} not found`); @@ -91,22 +88,6 @@ export async function deactivateComponent(_caller: Component, request: Component await component.deactivate(); } -export async function loadExtension(_caller: Component, request: ExtensionLoadRequest): Promise { - return extensionApi.loadExtension(request); -} - -export async function unloadExtension(_caller: Component, request: ExtensionUnloadRequest): Promise { - return extensionApi.unloadExtension(request); -} - -export async function installExtension(_caller: Component, request: ExtensionInstallRequest): Promise { - return extensionApi.installExtension(request); -} - -export async function removeExtension(_caller: Component, request: ExtensionRemoveRequest): Promise { - return extensionApi.removeExtension(request); -} - function getComponentInstanceSettings(instance: ComponentInstance) { const schema = instance.getContribution("settings"); if (!schema) { @@ -123,7 +104,7 @@ function getComponentInstanceSettings(instance: ComponentInstance) { }; } -function getComponentSettings(component: Component) { +function getComponentSettings(component: ComponentRef) { if (component.getId() == ":renderer") { // for renderer collect settings for all activated components @@ -178,7 +159,7 @@ function getObjectMember(object: any, path: string[]) { } -export async function handleSettingsSet(caller: Component, request: SettingsSetRequest): Promise { +export async function handleSettingsSet(caller: ComponentRef, request: SettingsSetRequest): Promise { const path = request.path.split("/"); const name = path.pop(); @@ -212,53 +193,82 @@ export async function handleSettingsSet(caller: Component, request: SettingsSetR settings.save().catch(e => console.error("failed to save settings", e)); } -export async function handleSettingsGet(caller: Component, request: SettingsGetRequest): Promise { +export async function handleSettingsGet(caller: ComponentRef, request: SettingsGetRequest): Promise { const { settings, schema } = getComponentSettings(caller); const path = request.path.split("/"); return { value: getObjectMember(settings, path), schema: getObjectMember(schema, path) }; } -export async function shutdown(caller: Component, _request: ShutdownRequest): Promise { +export async function shutdown(caller: ComponentRef, _request: ShutdownRequest): Promise { console.warn(`shutdown invoked by ${caller.getId()}`); await instance.uninitializeComponent(self.thisComponent().getManifest()); } -export async function handleObjectCreate(caller: Component, request: ObjectCreateRequest): Promise { +export async function handleObjectCreate(caller: ComponentRef, request: ObjectCreateRequest): Promise { return { - object: objects.createObject(caller, request.name, request.interface) + object: await objects.createObject(caller, request.name, request.interface) }; } -export async function handleObjectDestroy(caller: Component, request: ObjectDestroyRequest): Promise { +export async function handleObjectDestroy(caller: ComponentRef, request: ObjectDestroyRequest): Promise { return objects.destroyObject(caller, request.object) } -export async function handleFindObject(_caller: Component, request: ObjectFindRequest): Promise { +export async function handleFindObject(_caller: ComponentRef, request: ObjectFindRequest): Promise { return { object: objects.findObject(request.interfaceName, request.objectName) }; } -export async function handleObjectGetName(_caller: Component, request: ObjectGetNameRequest): Promise { +export async function handleObjectGetName(_caller: ComponentRef, request: ObjectGetNameRequest): Promise { return { name: objects.getName(request.object) }; } - -export async function handleObjectGetList(_caller: Component, request: ObjectGetListRequest): Promise { +export async function handleObjectGetList(_caller: ComponentRef, request: ObjectGetListRequest): Promise { return { objects: objects.getObjectList(request.interface) }; } -export async function handleObjectCall(caller: Component, request: ObjectCallRequest): Promise { - return { - result: await objects.call(caller, request.object, request.method, request.params) ?? {} - }; +export async function handleObjectCall(caller: ComponentRef, request: ObjectCallRequest): Promise { + return await objects.call(caller, request.object, request.method, request.params) ?? {} } -export async function handleObjectNotify(caller: Component, request: ObjectNotifyRequest) { +export async function handleObjectNotify(caller: ComponentRef, request: ObjectNotifyRequest) { return objects.notify(caller, request.object, request.notification, request.params); } + +export async function handleCall(_caller: ComponentRef, request: ComponentCallRequest): Promise { + const callerComponent = findComponentById(request.caller); + if (!callerComponent) { + throw createError(ErrorCode.InvalidRequest, `Cannot find caller component ${request.caller}`); + } + + const [componentName, ...method] = request.method.split("/"); + const component = findComponentById(componentName); + + if (!component) { + throw createError(ErrorCode.InvalidRequest, `Cannot find component ${componentName}`); + } + + return await component.call(callerComponent, method.join("/"), request.params) ?? {}; +} + +export async function handleNotify(_caller: ComponentRef, request: ComponentNotifyRequest) { + const callerComponent = findComponentById(request.caller); + if (!callerComponent) { + throw createError(ErrorCode.InvalidRequest, `Cannot find caller component ${request.caller}`); + } + + const [componentName, ...notification] = request.notification.split("/"); + const component = findComponentById(componentName); + + if (!component) { + throw createError(ErrorCode.InvalidRequest, `Cannot find component ${componentName}`); + } + + return component.notify(callerComponent, notification.join("/"), request.params); +} diff --git a/rpcsx-ui/src/core/server/registerBuiltinLaunchers.ts b/rpcsx-ui/src/core/server/registerBuiltinLaunchers.ts deleted file mode 100644 index 66221d6..0000000 --- a/rpcsx-ui/src/core/server/registerBuiltinLaunchers.ts +++ /dev/null @@ -1,3 +0,0 @@ - - -export function registerBuiltinLaunchers() {} diff --git a/rpcsx-ui/src/core/server/registerBuiltinLaunchers.web.ts b/rpcsx-ui/src/core/server/registerBuiltinLaunchers.web.ts deleted file mode 100644 index 2d26fd2..0000000 --- a/rpcsx-ui/src/core/server/registerBuiltinLaunchers.web.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { dirname } from "path"; -import { addLauncher, Launcher, LaunchParams, Process } from "./Launcher"; -import { Target } from "./Target"; -import { fork, spawn } from "child_process"; -import { Duplex } from "stream"; -import { EventEmitter } from "events"; -import { fileURLToPath } from "url"; - -const nativeLauncher: Launcher = { - launch: async (path: string, args: string[], params: LaunchParams) => { - path = fileURLToPath(path); - const newProcess = spawn(path, args, { - argv0: path, - cwd: dirname(process.execPath), - signal: params.signal, - stdio: 'pipe' - }); - - newProcess.stdout.setEncoding('utf8'); - newProcess.stderr.setEncoding('utf8'); - const pid = newProcess.pid ?? 0; - - const result: Process = { - stdin: newProcess.stdin, - stdout: newProcess.stdout, - stderr: newProcess.stderr, - - kill: (signal: number | NodeJS.Signals) => { - newProcess.kill(signal); - }, - on: (event: string, handler: (...args: any[]) => void) => { - newProcess.on(event, handler); - }, - once: (event: string, handler: (...args: any[]) => void) => { - newProcess.once(event, handler); - }, - off: (event: string, handler: (...args: any[]) => void) => { - newProcess.off(event, handler); - }, - getPid: () => pid - }; - - return result; - } -}; - -const nodeLauncher: Launcher = { - launch: async (path: string, args: string[], params: LaunchParams) => { - path = fileURLToPath(path); - const newProcess = fork(path, args, { - signal: params.signal, - stdio: 'pipe', - cwd: dirname(path), - }); - - newProcess.stdout!.setEncoding('utf8'); - newProcess.stderr!.setEncoding('utf8'); - const pid = newProcess.pid ?? 0; - - const result: Process = { - stdin: newProcess.stdin!, - stdout: newProcess.stdout!, - stderr: newProcess.stderr!, - - kill: (signal: number | NodeJS.Signals) => { - newProcess.kill(signal); - }, - on: (event: string, handler: (...args: any[]) => void) => { - newProcess.on(event, handler); - }, - once: (event: string, handler: (...args: any[]) => void) => { - newProcess.once(event, handler); - }, - off: (event: string, handler: (...args: any[]) => void) => { - newProcess.off(event, handler); - }, - getPid: () => pid - }; - - return result; - } -}; - -const inlineLauncher: Launcher = { - launch: async (path: string, args: string[], params: LaunchParams) => { - path = fileURLToPath(path); - const imported = await import(path); - - if (!("activate" in imported) || typeof imported.activate != 'function') { - throw new Error(`${path}: invalid inline module`); - } - - const eventEmitter = new EventEmitter(); - const stdin = new Duplex(); - const stdout = new Duplex(); - const stderr = new Duplex(); - imported.activate(eventEmitter, args, params.launcherRequirements, stdin, stdout, stderr); - - const result: Process = { - stdin: stdin, - stdout: stdout, - stderr: stderr, - - kill: (_signal: number | NodeJS.Signals) => { - if (("deactivate" in imported) && typeof imported.deactivate == 'function') { - imported.deactivate(); - } - }, - on: (event: string, handler: (...args: any[]) => void) => { - eventEmitter.on(event, handler); - }, - once: (event: string, handler: (...args: any[]) => void) => { - eventEmitter.once(event, handler); - }, - off: (event: string, handler: (...args: any[]) => void) => { - eventEmitter.off(event, handler); - }, - getPid() { - return 0; - }, - }; - - return result; - } -}; - -export function registerBuiltinLaunchers() { - addLauncher(Target.native(), nativeLauncher); - addLauncher(new Target("js", "any", "node"), nodeLauncher); - addLauncher(new Target("js", "any", "inline"), inlineLauncher); -} - diff --git a/rpcsx-ui/src/explorer/server/Component.ts b/rpcsx-ui/src/explorer/server/Component.ts index 2b619d2..3872253 100644 --- a/rpcsx-ui/src/explorer/server/Component.ts +++ b/rpcsx-ui/src/explorer/server/Component.ts @@ -8,7 +8,7 @@ import * as core from "$core"; export class ExplorerComponent implements IDisposable { items: ExplorerItem[] = []; progressToItem: Record = {}; - subscriptions: Record = {}; + subscriptions: Record = {}; refreshAbortController = new AbortController(); refreshImmediate: NodeJS.Immediate | undefined = undefined; describedLocations = new Set(); @@ -218,7 +218,7 @@ export class ExplorerComponent implements IDisposable { }); } - async get(caller: Component, params: ExplorerGetRequest): Promise { + async get(caller: ComponentRef, params: ExplorerGetRequest): Promise { // this.refresh(); const progressChannel = params.channel ?? (await progress.progressCreate({ diff --git a/rpcsx-ui/src/explorer/server/main.ts b/rpcsx-ui/src/explorer/server/main.ts index 61209e9..b39d5a0 100644 --- a/rpcsx-ui/src/explorer/server/main.ts +++ b/rpcsx-ui/src/explorer/server/main.ts @@ -14,7 +14,7 @@ export function deactivate() { component = undefined; } -export async function handleAdd(_caller: Component, params: ExplorerAddRequest) { +export async function handleAdd(_caller: ComponentRef, params: ExplorerAddRequest) { if (!component) { throw createError(ErrorCode.InvalidRequest); } @@ -22,7 +22,7 @@ export async function handleAdd(_caller: Component, params: ExplorerAddRequest) return component.add(params); } -export function handleRemove(_caller: Component, params: ExplorerRemoveRequest) { +export function handleRemove(_caller: ComponentRef, params: ExplorerRemoveRequest) { if (!component) { throw createError(ErrorCode.InvalidRequest); } @@ -30,7 +30,7 @@ export function handleRemove(_caller: Component, params: ExplorerRemoveRequest) return component.remove(params); } -export async function handleGet(caller: Component, params: ExplorerGetRequest): Promise { +export async function handleGet(caller: ComponentRef, params: ExplorerGetRequest): Promise { if (!component) { throw createError(ErrorCode.InvalidRequest); } diff --git a/rpcsx-ui/src/extension-host/component.json b/rpcsx-ui/src/extension-host/component.json index f3e5997..4affcce 100644 --- a/rpcsx-ui/src/extension-host/component.json +++ b/rpcsx-ui/src/extension-host/component.json @@ -11,5 +11,46 @@ { "name": "fs" } - ] + ], + "contributions": { + "methods": { + "load": { + "handler": "loadExtension", + "params": { + "id": { + "type": "string" + } + } + }, + "unload": { + "handler": "unloadExtension", + "params": { + "id": { + "type": "string" + } + } + }, + "install": { + "handler": "installExtension", + "params": { + "path": { + "type": "string" + } + }, + "returns": { + "id": { + "type": "string" + } + } + }, + "remove": { + "handler": "removeExtension", + "params": { + "id": { + "type": "string" + } + } + } + } + } } \ No newline at end of file diff --git a/rpcsx-ui/src/core/server/extension-api.web.ts b/rpcsx-ui/src/extension-host/server/extension-api.ts similarity index 61% rename from rpcsx-ui/src/core/server/extension-api.web.ts rename to rpcsx-ui/src/extension-host/server/extension-api.ts index 17784f1..b6bd735 100644 --- a/rpcsx-ui/src/core/server/extension-api.web.ts +++ b/rpcsx-ui/src/extension-host/server/extension-api.ts @@ -1,14 +1,16 @@ -import * as path from '$/path'; +import * as path from '$core/path'; import * as fs from '$fs'; -import { findComponent, findComponentById, unregisterComponent } from './ComponentInstance'; -import { createError } from 'lib/Error'; -import { getLauncher } from './Launcher'; -import { Extension } from './Extension'; +import * as core from '$core'; +import { createError } from '$core/Error'; -export async function loadExtension(request: ExtensionLoadRequest): Promise { - if (findComponentById(request.id)) { - return; - } +export async function loadExtension(request: ExtensionHostLoadRequest): Promise { + try { + const componentObject = await core.findExternalComponentObject(request.id); + + if (componentObject) { + return; + } + } catch { } const localExtensionsPath = path.join(await fs.fsGetBuiltinResourcesLocation(undefined), "extensions"); const extensionManifestLocation = path.join(localExtensionsPath, request.id, "extension.json"); @@ -29,29 +31,29 @@ export async function loadExtension(request: ExtensionLoadRequest): Promise { - try { - return launcher.launch(path.join(localExtensionsPath, request.id, manifest.executable), manifest.args ?? [], { - launcherRequirements: manifest.launcher.requirements ?? {}, - }); - } catch { - throw createError(ErrorCode.InternalError, `${request.id}: failed to spawn extension process`); - } - })(); - - new Extension(manifest, process); + try { + await launcher.launch({ + path: path.join(localExtensionsPath, request.id, manifest.executable), + args: manifest.args ?? [], + manifest, + launcherParams: manifest.launcher.requirements ?? {} + }); + } catch (e) { + throw createError(ErrorCode.InternalError, `${request.id}: failed to spawn extension process: ${e}`); + } } -export async function unloadExtension(request: ExtensionUnloadRequest): Promise { - await unregisterComponent(request.id); +export async function unloadExtension(request: ExtensionHostUnloadRequest): Promise { + const componentObject = await core.findExternalComponentObject(request.id); + await componentObject.destroy(); } -export async function installExtension(request: ExtensionInstallRequest): Promise { +export async function installExtension(request: ExtensionHostInstallRequest): Promise { // FIXME: unpack package const extensionManifestLocation = path.join(request.path, "extension.json"); @@ -71,15 +73,21 @@ export async function installExtension(request: ExtensionInstallRequest): Promis } })(); - if (findComponent(manifest.name[0].text, manifest.version)) { - throw createError(ErrorCode.InvalidRequest, `extension ${request.path} already installed`); - } + void manifest; throw createError(ErrorCode.InternalError, "not implemented"); } -export async function removeExtension(request: ExtensionRemoveRequest): Promise { - if (findComponentById(request.id)) { +export async function removeExtension(request: ExtensionHostRemoveRequest): Promise { + const component = await (async () => { + try { + return await core.findExternalComponentObject(request.id); + } catch { + return undefined; + } + })(); + + if (component) { throw createError(ErrorCode.InvalidRequest, `extension ${request.id} in use`); } diff --git a/rpcsx-ui/src/extension-host/server/extension-host.ts b/rpcsx-ui/src/extension-host/server/extension-host.ts index b836b4b..1373c92 100644 --- a/rpcsx-ui/src/extension-host/server/extension-host.ts +++ b/rpcsx-ui/src/extension-host/server/extension-host.ts @@ -1,6 +1,7 @@ import * as core from "$core"; import * as fs from '$fs'; import * as path from '$core/path'; +import * as extensionApi from './extension-api'; export async function activateLocalExtensions(list: Set) { try { @@ -17,7 +18,7 @@ export async function activateLocalExtensions(list: Set) { } try { - await core.extensionLoad({ id: entry.name }); + await extensionApi.loadExtension({ id: entry.name }); } catch (e) { console.error(`failed to load local extension ${entry.name}`, e); continue; @@ -28,7 +29,7 @@ export async function activateLocalExtensions(list: Set) { } catch (e) { console.error(`failed to activate extension ${entry.name}`, e); try { - await core.extensionUnload({ id: entry.name }); + await extensionApi.unloadExtension({ id: entry.name }); } catch (e) { console.error(`failed to unload extension ${entry.name}`, e); } diff --git a/rpcsx-ui/src/extension-host/server/main.ts b/rpcsx-ui/src/extension-host/server/main.ts index 512a9b7..53e44bc 100644 --- a/rpcsx-ui/src/extension-host/server/main.ts +++ b/rpcsx-ui/src/extension-host/server/main.ts @@ -1,9 +1,12 @@ import { activateLocalExtensions } from './extension-host'; import * as core from "$core"; +import * as extensionApi from './extension-api'; +import { registerBuiltinLaunchers } from './registerBuiltinLaunchers'; const activatedExtensions = new Set(); -export function activate() { +export async function activate() { + await registerBuiltinLaunchers(); return activateLocalExtensions(activatedExtensions); } @@ -19,3 +22,18 @@ export async function deactivate() { } } +export async function loadExtension(_caller: ComponentRef, request: ExtensionHostLoadRequest): Promise { + return extensionApi.loadExtension(request); +} + +export async function unloadExtension(_caller: ComponentRef, request: ExtensionHostUnloadRequest): Promise { + return extensionApi.unloadExtension(request); +} + +export async function installExtension(_caller: ComponentRef, request: ExtensionHostInstallRequest): Promise { + return extensionApi.installExtension(request); +} + +export async function removeExtension(_caller: ComponentRef, request: ExtensionHostRemoveRequest): Promise { + return extensionApi.removeExtension(request); +} diff --git a/rpcsx-ui/src/extension-host/server/registerBuiltinLaunchers.ts b/rpcsx-ui/src/extension-host/server/registerBuiltinLaunchers.ts new file mode 100644 index 0000000..222e5fc --- /dev/null +++ b/rpcsx-ui/src/extension-host/server/registerBuiltinLaunchers.ts @@ -0,0 +1,86 @@ +import { NativeModule, requireNativeModule } from 'expo'; +import { NativeTarget } from '$core/NativeTarget'; +import * as core from "$core"; +import * as self from "$"; + +declare class ExtensionLoaderModule extends NativeModule { + loadExtension(path: string): Promise; + unloadExtension(id: number): Promise; + call(extension: number, method: string, params: string): Promise; + notify(extension: number, notification: string, params: string): Promise; + sendResponse(methodId: number, body: string): Promise; +} + +const nativeLoader = requireNativeModule('ExtensionLoader'); + +class NativeProtocol implements ExternalComponentInterface { + constructor( + private objectId: number, + private extensionId: number, + public manifest: ExtensionInfo) { + } + + async activate(_caller: ComponentRef, request: ExternalComponentActivateRequest) { + return JSON.parse(await nativeLoader.call(this.extensionId, "$/activate", JSON.stringify(request))); + } + + deactivate(_caller: ComponentRef): ExternalComponentDeactivateResponse | Promise { + return nativeLoader.notify(this.extensionId, "$/deactivate", "{}"); + } + + async call(_caller: ComponentRef, request: ExternalComponentCallRequest) { + return JSON.parse(await nativeLoader.call(this.extensionId, request.method, JSON.stringify(request.params))); + } + + dispose() { + nativeLoader.unloadExtension(this.extensionId); + } + + getPid() { + return 0; + } + + async initialize() { + return JSON.parse(await nativeLoader.call(this.extensionId, "$/initialize", "{}")); + } + + notify(_caller: ComponentRef, request: ExternalComponentNotifyRequest): void | Promise { + return nativeLoader.notify(this.extensionId, request.method, JSON.stringify(request.params)); + } + + async objectCall(_caller: ComponentRef, request: ExternalComponentObjectCallRequest) { + return JSON.parse(await nativeLoader.call(this.extensionId, "$/object/activate", JSON.stringify(request))); + } + + objectDestroy(caller: ComponentRef, request: ExternalComponentObjectDestroyRequest): void | Promise { + return nativeLoader.notify(this.extensionId, "$/object/destroy", JSON.stringify(request)); + } + + objectNotify(caller: ComponentRef, request: ExternalComponentObjectNotifyRequest): void | Promise { + return nativeLoader.notify(this.extensionId, "$/object/notify", JSON.stringify(request)); + } + + getObjectId() { + return this.objectId; + } +} + +class InlineLauncher implements LauncherInterface { + async launch(_caller: ComponentRef, request: LauncherLaunchRequest): Promise { + const extensionId = await nativeLoader.loadExtension(request.path); + const protocol = await core.createExternalComponentObject(request.manifest.name[0].text, NativeProtocol, extensionId, request.manifest); + + try { + await protocol.initialize(); + return protocol.getObjectId(); + } catch (e) { + self.destroyObject(protocol.getObjectId()); + nativeLoader.unloadExtension(extensionId); + throw e; + } + } +} + +export async function registerBuiltinLaunchers() { + await core.createLauncherObject(NativeTarget.format(), InlineLauncher); +} diff --git a/rpcsx-ui/src/core/server/Extension.ts b/rpcsx-ui/src/extension-host/server/registerBuiltinLaunchers.web.ts similarity index 55% rename from rpcsx-ui/src/core/server/Extension.ts rename to rpcsx-ui/src/extension-host/server/registerBuiltinLaunchers.web.ts index 804aafa..084259b 100644 --- a/rpcsx-ui/src/core/server/Extension.ts +++ b/rpcsx-ui/src/extension-host/server/registerBuiltinLaunchers.web.ts @@ -1,8 +1,24 @@ -import { Process } from './Launcher'; - +import { dirname } from "path"; +import { Target } from "$core/Target"; +import { NativeTarget } from "$core/NativeTarget"; +import { fork, spawn } from "child_process"; +import { Duplex, Readable, Writable } from "stream"; +import { EventEmitter } from "events"; +import { fileURLToPath } from "url"; +import * as self from "$"; +import * as core from "$core"; import packageJson from '../../../../package.json' with { type: "json" }; -import { findComponent, getComponentId, registerComponent, uninitializeComponent, IComponentImpl } from './ComponentInstance'; -import { createError } from 'lib/Error'; + +type Process = { + stdin: Writable; + stdout: Readable; + stderr: Readable; + kill: (signal: number | NodeJS.Signals) => void; + on: (event: 'close' | 'exit', listener: (...args: any[]) => void) => void; + once: (event: 'close' | 'exit', listener: (...args: any[]) => void) => void; + off: (event: 'close' | 'exit', listener: (...args: any[]) => void) => void; + getPid(): number; +} type ResponseValue = void | object | null | number | string | boolean | []; type ResponseError = { @@ -13,14 +29,13 @@ type ResponseError = { type Response = ResponseValue | ResponseError | void; type ErrorHandler = (error: ResponseError) => void; - const clientInfo: ClientInfo = Object.freeze({ name: packageJson.name, version: packageJson.version, capabilities: {} }); -export class Extension implements IComponentImpl { +class JsonRpcProtocol implements ExternalComponentInterface { private alive = true; private expectedResponses: { [key: number]: { @@ -39,23 +54,25 @@ export class Extension implements IComponentImpl { private componentManifest: ComponentManifest; constructor( - public readonly manifest: ExtensionInfo, - public readonly extensionProcess: Process) { + private objectId: number, + public readonly extensionProcess: Process, + public manifest: ExtensionInfo) { + + this.componentManifest = { + name: manifest.name[0].text, + version: manifest.version, + }; extensionProcess.stdout.on('data', (message: string) => { this.receive(message); }); + extensionProcess.stderr.on('data', (message: string) => { this.debugLog(message); }); this.debugLog("Starting"); - this.componentManifest = { - ...this.manifest, - name: this.manifest.name[0].text - }; - extensionProcess.on('exit', () => { this.debugLog("Exit"); this.alive = false; @@ -65,10 +82,8 @@ export class Extension implements IComponentImpl { clearTimeout(this.responseWatchdog); } - uninitializeComponent(this.componentManifest); + // uninitializeComponent(this.componentManifest); }); - - registerComponent(this.componentManifest, this); } getPid() { @@ -81,7 +96,7 @@ export class Extension implements IComponentImpl { if (line.length == 0) { continue; } - process.stderr.write(`${date} [${this.manifest.name[0].text}-v${this.manifest.version}] ${line}\n`); + process.stderr.write(`${date} [${this.componentManifest.name}-v${this.componentManifest.version}] ${line}\n`); } } @@ -92,31 +107,14 @@ export class Extension implements IComponentImpl { const response = await this.callMethod("$/initialize", request); - let isInvalid = false; - - if (response.extension.name[0].text != this.manifest.name[0].text) { - this.debugLog(`executable sends unexpected name ${response.extension.name}`); - isInvalid = true; - } - - if (response.extension.version != this.manifest.version) { - this.debugLog(`executable sends unexpected version ${response.extension.version}`); - isInvalid = true; - } - - // FIXME: register contributions - - if (isInvalid) { - throw createError(ErrorCode.InvalidRequest, "Extension initialize request returns invalid name/version"); - } + this.componentManifest = { + ...response.extension, + name: response.extension.name[0].text, + }; } - async activate(_context: ComponentContext, settings: JsonObject, signal?: AbortSignal | undefined) { - const request: ActivateRequest = { - settings - }; - - return await this.callMethod("$/activate", request, signal); + async activate(_caller: ComponentRef, request: ExternalComponentActivateRequest) { + return await this.callMethod("$/activate", request); } async deactivate() { @@ -184,6 +182,18 @@ export class Extension implements IComponentImpl { } } + objectCall(_caller: ComponentRef, request: ExternalComponentObjectCallRequest): ExternalComponentObjectCallResponse | Promise { + return this.callMethod("$/object/call", request); + } + + objectDestroy(_caller: ComponentRef, request: ExternalComponentObjectDestroyRequest): void | Promise { + return this.sendNotify("$/object/destroy", request); + } + + objectNotify(_caller: ComponentRef, request: ExternalComponentObjectNotifyRequest): void | Promise { + return this.sendNotify("$/object/notify", request); + } + async callMethod(method: string, params: object | [] | string | number | boolean | null = null, signal?: AbortSignal) { const id = this.nextMessageId++; const abortHandler = () => this.cancel({ id }); @@ -221,16 +231,16 @@ export class Extension implements IComponentImpl { this.send({ jsonrpc: "2.0", notification, params }); } - async call(_caller: Component, method: string, params?: Json): Promise { - return this.callMethod(method, params); + call(_caller: ComponentRef, params: ExternalComponentCallRequest): Promise { + return this.callMethod(params.method, params.params); } - async notify(_caller: Component, notification: string, params: Json | undefined) { - this.sendNotify(notification, params); + notify(_caller: ComponentRef, params: ExternalComponentNotifyRequest) { + return this.sendNotify(params.method, params.params); } - async cancel(request: CancelRequest) { - await this.sendNotify("$/cancel", request); + cancel(request: CancelRequest) { + return this.sendNotify("$/cancel", request); } onError(handler: ErrorHandler) { @@ -344,36 +354,20 @@ export class Extension implements IComponentImpl { } if ("method" in message) { - const componentMethod = message["method"] as string; - const params = "params" in message ? message["params"] as JsonObject : undefined; - const [componentName, ...method] = componentMethod.split("/"); - const component = findComponent(componentName); - const self = findComponent(this.componentManifest.name); - - if (!component || !self) { - this.send({ - jsonrpc: "2.0", id, error: { - code: ErrorCode.MethodNotFound, - message: componentMethod - } - }); - - return; - } + const method = message["method"] as string; + const params = "params" in message ? message["params"] as JsonObject : null; if (id !== null) { try { - const result = await component.call(self, method.join("/"), params); + const result = await core.componentCall({ caller: this.manifest.name[0].text, method, params }); this.send({ jsonrpc: "2.0", id, result }); } catch (error) { this.send({ jsonrpc: "2.0", id, error }); } } else { - try { - component.notify(self, method.join("/"), params); - } catch (error) { + core.componentNotify({ caller: this.manifest.name[0].text, notification: method, params }).catch(error => { this.send({ jsonrpc: "2.0", error }); - } + }); } return; @@ -400,7 +394,7 @@ export class Extension implements IComponentImpl { if (requestDeadline <= now) { delete this.expectedResponses[request]; this.debugLog(`wait for response timed out (request ${request})`); - this.cancel({ id: parseInt(request) }); + this.cancel({ id: parseInt(request) }).catch(e => console.warn(`cancellation of request ${request} failed`, e)); response.reject({ code: ErrorCode.TimedOut }); } else if (nextDeadline < 0 || nextDeadline > requestDeadline) { nextDeadline = requestDeadline; @@ -419,8 +413,157 @@ export class Extension implements IComponentImpl { } } - getId() { - return getComponentId(this.componentManifest); + getObjectId() { + return this.objectId; } } + +class NativeLauncher implements LauncherInterface { + async launch(_caller: ComponentRef, request: LauncherLaunchRequest): Promise { + const path = fileURLToPath(request.path); + const newProcess = spawn(path, request.args, { + argv0: path, + cwd: dirname(process.execPath), + stdio: 'pipe' + }); + + newProcess.stdout.setEncoding('utf8'); + newProcess.stderr.setEncoding('utf8'); + const pid = newProcess.pid ?? 0; + + const wrappedNewProcess: Process = { + stdin: newProcess.stdin, + stdout: newProcess.stdout, + stderr: newProcess.stderr, + + kill: (signal: number | NodeJS.Signals) => { + newProcess.kill(signal); + }, + on: (event: string, handler: (...args: any[]) => void) => { + newProcess.on(event, handler); + }, + once: (event: string, handler: (...args: any[]) => void) => { + newProcess.once(event, handler); + }, + off: (event: string, handler: (...args: any[]) => void) => { + newProcess.off(event, handler); + }, + getPid: () => pid + }; + + const protocol = await core.createExternalComponentObject(request.manifest.name[0].text, JsonRpcProtocol, wrappedNewProcess, request.manifest); + + try { + await protocol.initialize(); + return protocol.getObjectId(); + } catch (e) { + self.destroyObject(protocol.getObjectId()); + newProcess.kill("SIGKILL"); + throw e; + } + } +}; + +class NodeLauncher implements LauncherInterface { + async launch(_caller: ComponentRef, request: LauncherLaunchRequest): Promise { + const path = fileURLToPath(request.path); + const newProcess = fork(path, request.args, { + stdio: 'pipe', + cwd: dirname(path), + }); + + newProcess.stdout!.setEncoding('utf8'); + newProcess.stderr!.setEncoding('utf8'); + const pid = newProcess.pid ?? 0; + + const wrappedNewProcess: Process = { + stdin: newProcess.stdin!, + stdout: newProcess.stdout!, + stderr: newProcess.stderr!, + + kill: (signal: number | NodeJS.Signals) => { + newProcess.kill(signal); + }, + on: (event: string, handler: (...args: any[]) => void) => { + newProcess.on(event, handler); + }, + once: (event: string, handler: (...args: any[]) => void) => { + newProcess.once(event, handler); + }, + off: (event: string, handler: (...args: any[]) => void) => { + newProcess.off(event, handler); + }, + getPid: () => pid + }; + + const protocol = await core.createExternalComponentObject(request.manifest.name[0].text, JsonRpcProtocol, wrappedNewProcess, request.manifest); + + try { + await protocol.initialize(); + return protocol.getObjectId(); + } catch (e) { + self.destroyObject(protocol.getObjectId()); + newProcess.kill("SIGKILL"); + throw e; + } + } +}; + +class InlineLauncher implements LauncherInterface { + async launch(_caller: ComponentRef, request: LauncherLaunchRequest): Promise { + const path = fileURLToPath(request.path); + const imported = await import(path); + + if (!("activate" in imported) || typeof imported.activate != 'function') { + throw new Error(`${path}: invalid inline module`); + } + + const eventEmitter = new EventEmitter(); + const stdin = new Duplex(); + const stdout = new Duplex(); + const stderr = new Duplex(); + imported.activate(eventEmitter, request.args, request.launcherParams, stdin, stdout, stderr); + + const wrappedProcess: Process = { + stdin: stdin, + stdout: stdout, + stderr: stderr, + + kill: (_signal: number | NodeJS.Signals) => { + if (("deactivate" in imported) && typeof imported.deactivate == 'function') { + imported.deactivate(); + } + }, + on: (event: string, handler: (...args: any[]) => void) => { + eventEmitter.on(event, handler); + }, + once: (event: string, handler: (...args: any[]) => void) => { + eventEmitter.once(event, handler); + }, + off: (event: string, handler: (...args: any[]) => void) => { + eventEmitter.off(event, handler); + }, + getPid() { + return 0; + }, + }; + + const protocol = await core.createExternalComponentObject(request.manifest.name[0].text, JsonRpcProtocol, wrappedProcess, request.manifest); + + try { + await protocol.initialize(); + return protocol.getObjectId(); + } catch (e) { + self.destroyObject(protocol.getObjectId()); + throw e; + } + } +}; + +export async function registerBuiltinLaunchers() { + await core.createLauncherObject(NativeTarget.format(), NativeLauncher); + await core.createLauncherObject(new Target("js", "any", "node").format(), NodeLauncher); + await core.createLauncherObject(new Target("js", "any", "inline").format(), InlineLauncher); +} + diff --git a/rpcsx-ui/src/fs/server/fs.ts b/rpcsx-ui/src/fs/server/fs.ts index bf1b1c2..17f7a41 100644 --- a/rpcsx-ui/src/fs/server/fs.ts +++ b/rpcsx-ui/src/fs/server/fs.ts @@ -8,14 +8,14 @@ import * as core from '$core'; class NativeFile implements FileInterface { constructor(private id: number, private handle: FileHandle) { } - close(_caller: Component) { + close(_caller: ComponentRef) { core.objectDestroy({ object: this.id }); this.handle.close(); } - read(_caller: Component, request: FsFileReadRequest): FsFileReadResponse { + read(_caller: ComponentRef, request: FsFileReadRequest): FsFileReadResponse { this.handle.offset = request.offset; const result = this.handle.readBytes(request.size); @@ -24,7 +24,7 @@ class NativeFile implements FileInterface { }; } - write(_caller: Component, request: FsFileWriteRequest): FsFileWriteResponse { + write(_caller: ComponentRef, request: FsFileWriteRequest): FsFileWriteResponse { this.handle.offset = request.offset; this.handle.writeBytes(Uint8Array.from(request.data)); return request.data.length; @@ -36,7 +36,7 @@ class NativeFile implements FileInterface { } class NativeFileSystem implements FileSystemInterface { - async open(_caller: Component, request: FsFileSystemOpenRequest): Promise { + async open(_caller: ComponentRef, request: FsFileSystemOpenRequest): Promise { try { const descriptor = new File(request.uri).open(); const object = await self.createFileObject(request.uri, NativeFile, descriptor); @@ -46,7 +46,7 @@ class NativeFileSystem implements FileSystemInterface { } } - async readToString(_caller: Component, request: FsFileSystemReadToStringRequest): Promise { + async readToString(_caller: ComponentRef, request: FsFileSystemReadToStringRequest): Promise { try { return new File(request.uri).text(); } catch (e) { @@ -54,7 +54,7 @@ class NativeFileSystem implements FileSystemInterface { } } - async writeString(_caller: Component, request: FsFileSystemWriteStringRequest) { + async writeString(_caller: ComponentRef, request: FsFileSystemWriteStringRequest) { try { return new File(request.uri).write(request.string); } catch (e) { @@ -62,7 +62,7 @@ class NativeFileSystem implements FileSystemInterface { } } - async readDir(_caller: Component, request: FsFileSystemReadDirRequest): Promise { + async readDir(_caller: ComponentRef, request: FsFileSystemReadDirRequest): Promise { try { const result = new Directory(request).list(); @@ -81,7 +81,7 @@ class NativeFileSystem implements FileSystemInterface { } } - async stat(_caller: Component, request: FsFileSystemStatRequest): Promise { + async stat(_caller: ComponentRef, request: FsFileSystemStatRequest): Promise { try { const result = new File(request); @@ -103,50 +103,50 @@ export async function uninitialize() { await Promise.all(self.ownObjects().map(object => object.dispose())); } -export async function open(_caller: Component, request: FsOpenRequest): Promise { +export async function open(_caller: ComponentRef, request: FsOpenRequest): Promise { const protocol = new URL(request.uri).protocol || "file:"; const object = await self.findFileSystemObject(protocol); return await object.open(request); } -export async function readToString(_caller: Component, request: FsReadToStringRequest): Promise { +export async function readToString(_caller: ComponentRef, request: FsReadToStringRequest): Promise { const protocol = new URL(request.uri).protocol || "file:"; const object = await self.findFileSystemObject(protocol); return await object.readToString(request); } -export async function writeString(_caller: Component, request: FsWriteStringRequest): Promise { +export async function writeString(_caller: ComponentRef, request: FsWriteStringRequest): Promise { const protocol = new URL(request.uri).protocol || "file:"; const object = await self.findFileSystemObject(protocol); return await object.writeString(request); } -export async function readDir(_caller: Component, request: FsReadDirRequest): Promise { +export async function readDir(_caller: ComponentRef, request: FsReadDirRequest): Promise { const protocol = new URL(request).protocol || "file:"; const object = await self.findFileSystemObject(protocol); return await object.readDir(request); } -export async function stat(_caller: Component, request: FsStatRequest): Promise { +export async function stat(_caller: ComponentRef, request: FsStatRequest): Promise { const protocol = new URL(request).protocol || "file:"; const object = await self.findFileSystemObject(protocol); return await object.stat(request); } -export function getBuiltinResourcesLocation(_caller: Component, _request: FsGetBuiltinResourcesLocationRequest): FsGetBuiltinResourcesLocationResponse { +export function getBuiltinResourcesLocation(_caller: ComponentRef, _request: FsGetBuiltinResourcesLocationRequest): FsGetBuiltinResourcesLocationResponse { return Paths.document.uri; } -export function getConfigLocation(_caller: Component, _request: FsGetConfigLocationRequest): FsGetConfigLocationResponse { +export function getConfigLocation(_caller: ComponentRef, _request: FsGetConfigLocationRequest): FsGetConfigLocationResponse { return Paths.document.uri; } -export async function openDirectorySelector(caller: Component, request: FsOpenDirectorySelectorRequest): Promise { +export async function openDirectorySelector(caller: ComponentRef, request: FsOpenDirectorySelectorRequest): Promise { try { return (await pickDirectory({ requestLongTermAccess: true diff --git a/rpcsx-ui/src/fs/server/fs.web.ts b/rpcsx-ui/src/fs/server/fs.web.ts index 8a37138..24364c1 100644 --- a/rpcsx-ui/src/fs/server/fs.web.ts +++ b/rpcsx-ui/src/fs/server/fs.web.ts @@ -15,14 +15,14 @@ function parseUri(uri: string) { class NativeFile implements FileInterface { constructor(private id: number, private handle: nodeFs.FileHandle) { } - async close(_caller: Component) { + async close(_caller: ComponentRef) { core.objectDestroy({ object: this.id }); await this.handle.close(); } - async read(_caller: Component, request: FsFileReadRequest): Promise { + async read(_caller: ComponentRef, request: FsFileReadRequest): Promise { const buffer = new Uint8Array(request.size); const result = await this.handle.read(buffer, 0, request.size, request.offset); @@ -31,7 +31,7 @@ class NativeFile implements FileInterface { }; } - async write(_caller: Component, request: FsFileWriteRequest): Promise { + async write(_caller: ComponentRef, request: FsFileWriteRequest): Promise { const result = await this.handle.write(Uint8Array.from(request.data), { position: request.offset }); @@ -82,7 +82,7 @@ function toFileType(fileType: WithFileType) { } class NativeFileSystem implements FileSystemInterface { - async open(_caller: Component, request: FsFileSystemOpenRequest): Promise { + async open(_caller: ComponentRef, request: FsFileSystemOpenRequest): Promise { const filePath = parseUri(request.uri).pathname; try { @@ -94,7 +94,7 @@ class NativeFileSystem implements FileSystemInterface { } } - async readToString(_caller: Component, request: FsFileSystemReadToStringRequest): Promise { + async readToString(_caller: ComponentRef, request: FsFileSystemReadToStringRequest): Promise { const filePath = parseUri(request.uri).pathname; try { @@ -104,7 +104,7 @@ class NativeFileSystem implements FileSystemInterface { } } - async writeString(_caller: Component, request: FsFileSystemWriteStringRequest) { + async writeString(_caller: ComponentRef, request: FsFileSystemWriteStringRequest) { const filePath = parseUri(request.uri).pathname; try { @@ -114,7 +114,7 @@ class NativeFileSystem implements FileSystemInterface { } } - async readDir(_caller: Component, request: FsFileSystemReadDirRequest): Promise { + async readDir(_caller: ComponentRef, request: FsFileSystemReadDirRequest): Promise { const path = parseUri(request).pathname; try { const result = await nodeFs.readdir(path, { withFileTypes: true }); @@ -134,7 +134,7 @@ class NativeFileSystem implements FileSystemInterface { } } - async stat(_caller: Component, request: FsFileSystemStatRequest): Promise { + async stat(_caller: ComponentRef, request: FsFileSystemStatRequest): Promise { const path = parseUri(request).pathname; try { @@ -158,42 +158,42 @@ export async function uninitialize() { await Promise.all(self.ownObjects().map(object => object.dispose())); } -export async function open(_caller: Component, request: FsOpenRequest): Promise { +export async function open(_caller: ComponentRef, request: FsOpenRequest): Promise { const protocol = parseUri(request.uri).protocol || "file:"; const object = await self.findFileSystemObject(protocol); return await object.open(request); } -export async function readToString(_caller: Component, request: FsReadToStringRequest): Promise { +export async function readToString(_caller: ComponentRef, request: FsReadToStringRequest): Promise { const protocol = parseUri(request.uri).protocol || "file:"; const object = await self.findFileSystemObject(protocol); return await object.readToString(request); } -export async function writeString(_caller: Component, request: FsWriteStringRequest): Promise { +export async function writeString(_caller: ComponentRef, request: FsWriteStringRequest): Promise { const protocol = parseUri(request.uri).protocol || "file:"; const object = await self.findFileSystemObject(protocol); return await object.writeString(request); } -export async function readDir(_caller: Component, request: FsReadDirRequest): Promise { +export async function readDir(_caller: ComponentRef, request: FsReadDirRequest): Promise { const protocol = parseUri(request).protocol || "file:"; const object = await self.findFileSystemObject(protocol); return await object.readDir(request); } -export async function stat(_caller: Component, request: FsStatRequest): Promise { +export async function stat(_caller: ComponentRef, request: FsStatRequest): Promise { const protocol = parseUri(request).protocol || "file:"; const object = await self.findFileSystemObject(protocol); return await object.stat(request); } -export function getBuiltinResourcesLocation(_caller: Component, _request: FsGetBuiltinResourcesLocationRequest): FsGetBuiltinResourcesLocationResponse { +export function getBuiltinResourcesLocation(_caller: ComponentRef, _request: FsGetBuiltinResourcesLocationRequest): FsGetBuiltinResourcesLocationResponse { if (app.isPackaged && "resourcesPath" in process && typeof process.resourcesPath == "string") { return pathToFileURL(process.resourcesPath).toString(); } @@ -201,11 +201,11 @@ export function getBuiltinResourcesLocation(_caller: Component, _request: FsGetB return encodeURI(path.toURI(nodePath.resolve(import.meta.dirname, ".."))); } -export function getConfigLocation(_caller: Component, _request: FsGetConfigLocationRequest): FsGetConfigLocationResponse { +export function getConfigLocation(_caller: ComponentRef, _request: FsGetConfigLocationRequest): FsGetConfigLocationResponse { return encodeURI(path.toURI(nodePath.dirname(process.execPath))); } -export async function openDirectorySelector(caller: Component, request: FsOpenDirectorySelectorRequest): Promise { +export async function openDirectorySelector(caller: ComponentRef, request: FsOpenDirectorySelectorRequest): Promise { const result = await dialog.showOpenDialog({ properties: [ 'openDirectory', diff --git a/rpcsx-ui/src/fs/server/main.ts b/rpcsx-ui/src/fs/server/main.ts index b994ecd..486b5fe 100644 --- a/rpcsx-ui/src/fs/server/main.ts +++ b/rpcsx-ui/src/fs/server/main.ts @@ -7,33 +7,33 @@ export async function deactivate() { await fs.uninitialize(); } -export async function handleOpen(caller: Component, request: FsOpenRequest) { +export async function handleOpen(caller: ComponentRef, request: FsOpenRequest) { return fs.open(caller, request); } -export async function handleReadToString(caller: Component, request: FsReadToStringRequest) { +export async function handleReadToString(caller: ComponentRef, request: FsReadToStringRequest) { return fs.readToString(caller, request); } -export async function handleWriteString(caller: Component, request: FsWriteStringRequest) { +export async function handleWriteString(caller: ComponentRef, request: FsWriteStringRequest) { return fs.writeString(caller, request); } -export async function handleReadDir(caller: Component, request: FsReadDirRequest) { +export async function handleReadDir(caller: ComponentRef, request: FsReadDirRequest) { return fs.readDir(caller, request); } -export async function handleStat(caller: Component, request: FsStatRequest) { +export async function handleStat(caller: ComponentRef, request: FsStatRequest) { return fs.stat(caller, request); } -export function handleGetBuiltinResourcesLocation(caller: Component, request: FsGetBuiltinResourcesLocationRequest) { +export function handleGetBuiltinResourcesLocation(caller: ComponentRef, request: FsGetBuiltinResourcesLocationRequest) { return fs.getBuiltinResourcesLocation(caller, request); } -export function handleGetConfigLocation(caller: Component, request: FsGetConfigLocationRequest) { +export function handleGetConfigLocation(caller: ComponentRef, request: FsGetConfigLocationRequest) { return fs.getConfigLocation(caller, request); } -export function handleOpenDirectorySelector(caller: Component, request: FsOpenDirectorySelectorRequest) { +export function handleOpenDirectorySelector(caller: ComponentRef, request: FsOpenDirectorySelectorRequest) { return fs.openDirectorySelector(caller, request); } diff --git a/rpcsx-ui/src/github/server/main.ts b/rpcsx-ui/src/github/server/main.ts index ec83e9a..2f44154 100644 --- a/rpcsx-ui/src/github/server/main.ts +++ b/rpcsx-ui/src/github/server/main.ts @@ -1,9 +1,9 @@ import * as github from './github'; -export async function handleReleasesLatest(_caller: Component, params: GithubReleasesLatestRequest): Promise { +export async function handleReleasesLatest(_caller: ComponentRef, params: GithubReleasesLatestRequest): Promise { return github.fetchReleasesLatest(params); } -export async function handleReleases(_caller: Component, params: GithubReleasesRequest): Promise { +export async function handleReleases(_caller: ComponentRef, params: GithubReleasesRequest): Promise { return github.fetchReleases(params); } diff --git a/rpcsx-ui/src/progress/server/main.ts b/rpcsx-ui/src/progress/server/main.ts index 6df5954..6c29e75 100644 --- a/rpcsx-ui/src/progress/server/main.ts +++ b/rpcsx-ui/src/progress/server/main.ts @@ -4,13 +4,13 @@ import * as api from '$'; import { Disposable, IDisposable } from '$core/Disposable'; type ProgressInstance = Omit & { - creator: Component; + creator: ComponentRef; disposable?: IDisposable; }; let nextProgressChannel = 0; let channels: Record = {}; -let subscriptions: Record> = {}; +let subscriptions: Record> = {}; export function deactivate() { nextProgressChannel = 0; @@ -30,7 +30,7 @@ export function deactivate() { subscriptions = {}; } -export function progressCreate(source: Component, params: ProgressCreateRequest): ProgressCreateResponse { +export function progressCreate(source: ComponentRef, params: ProgressCreateRequest): ProgressCreateResponse { const channel = nextProgressChannel++; channels[channel] = { @@ -58,7 +58,7 @@ export function progressCreate(source: Component, params: ProgressCreateRequest) return { channel }; } -export async function progressUpdate(caller: Component, params: ProgressUpdateRequest) { +export async function progressUpdate(caller: ComponentRef, params: ProgressUpdateRequest) { const info = channels[params.channel]; if (!info) { @@ -108,7 +108,7 @@ export async function progressUpdate(caller: Component, params: ProgressUpdateRe } } -export function progressSubscribe(caller: Component, params: ProgressSubscribeRequest) { +export function progressSubscribe(caller: ComponentRef, params: ProgressSubscribeRequest) { if (!(params.channel in channels)) { throw createError(ErrorCode.InvalidParams); } @@ -120,7 +120,7 @@ export function progressSubscribe(caller: Component, params: ProgressSubscribeRe api.sendProgressUpdateEvent(caller, { value: { channel: params.channel, ...info } }); } -export function progressUnsubscribe(caller: Component, params: ProgressUnsubscribeRequest) { +export function progressUnsubscribe(caller: ComponentRef, params: ProgressUnsubscribeRequest) { const channelSubscriptions = subscriptions[params.channel]; if (!channelSubscriptions) { return;