diff --git a/src/apps/components/firstrun/store.ts b/src/apps/components/firstrun/store.ts index 675ee6de..fd88dc1c 100755 --- a/src/apps/components/firstrun/store.ts +++ b/src/apps/components/firstrun/store.ts @@ -131,11 +131,12 @@ export const FirstRunPages = new Map([ }, { caption: "Enable", - action: (process) => { + action: async (process) => { process.userPreferences.update((v) => { v.security.enableThirdParty = true; return v; }); + await Daemon!.serviceHost?.startService("JsExecSvc"); process.switchPage("finish"); }, suggested: true, diff --git a/src/interfaces/IServiceHost.ts b/src/interfaces/IServiceHost.ts index 20263a5b..1062b657 100644 --- a/src/interfaces/IServiceHost.ts +++ b/src/interfaces/IServiceHost.ts @@ -47,5 +47,6 @@ export type ServiceIdentifier = | "IconService" | "LibMgmtSvc" | "MigrationSvc" - | "RecentFilesSvc"; + | "RecentFilesSvc" + | "JsExecSvc"; // !endtpa diff --git a/src/interfaces/services/IJsExec.ts b/src/interfaces/services/IJsExec.ts new file mode 100644 index 00000000..9d1b6b92 --- /dev/null +++ b/src/interfaces/services/IJsExec.ts @@ -0,0 +1,11 @@ +import type { IBaseService } from "$interfaces/IServiceHost"; +import type { JsExecEngineData } from "$types/tpa/engine"; +import type { App } from "$types/apps/app"; + +// !tpa +export interface IJsExecService extends IBaseService { + start(): Promise; + getContents(engine: JsExecEngineData): Promise; + setupEngine(filePath: string, app?: App, metaPath?: string, ...args: any[]): JsExecEngineData; + Invoke(filePath: string, app?: App, metaPath?: string, ...args: any[]): Promise; +} diff --git a/src/ts/apps/tpa/props.ts b/src/ts/apps/tpa/props.ts index a8910999..012ab287 100644 --- a/src/ts/apps/tpa/props.ts +++ b/src/ts/apps/tpa/props.ts @@ -4,7 +4,6 @@ import { ThirdPartyAppProcess } from "$ts/apps/thirdparty"; import { __Console__ } from "$ts/console"; import { Daemon, Env, Fs, Stack } from "$ts/env"; import { getAllImages } from "$ts/images"; -import type { JsExec } from "$ts/jsexec"; import { FilesystemDrive } from "$ts/kernel/mods/fs/drives/generic"; import { Backend } from "$ts/kernel/mods/server/axios"; import { Process } from "$ts/kernel/mods/stack/process/instance"; @@ -32,10 +31,11 @@ import { Store } from "$ts/writable"; import type { ThirdPartyPropMap } from "$types/tpa/thirdparty"; import axios from "axios"; import dayjs from "dayjs"; -import { ThirdPartyProcess } from "./process"; +import { ThirdPartyProcess } from "$ts/apps/tpa/process"; import { SupplementaryThirdPartyPropFunctions } from "./supplementary"; +import type { JsExecEngineData } from "$types/tpa/engine"; -export function ThirdPartyProps(engine: JsExec): ThirdPartyPropMap { +export function ThirdPartyProps(engine: JsExecEngineData): ThirdPartyPropMap { const props = { env: Env, // TEMP handler: Stack, // TEMP diff --git a/src/ts/apps/tpa/supplementary.ts b/src/ts/apps/tpa/supplementary.ts index 412a5e46..1a067d10 100644 --- a/src/ts/apps/tpa/supplementary.ts +++ b/src/ts/apps/tpa/supplementary.ts @@ -1,14 +1,15 @@ import type { Constructs } from "$interfaces/common"; import type { IThirdPartyAppProcess } from "$interfaces/IThirdPartyAppProcess"; import { Daemon, Fs, Stack } from "$ts/env"; -import { JsExec } from "$ts/jsexec"; import { detectJavaScript } from "$ts/util"; import { arrayBufferToText } from "$ts/util/convert"; import { join } from "$ts/util/fs"; import { tryJsonParse } from "$ts/util/json"; -import { ThirdPartyProcess } from "./process"; +import { ThirdPartyProcess } from "$ts/apps/tpa/process"; +import type { JsExecEngineData } from "$types/tpa/engine"; +import type { IJsExecService } from "$interfaces/services/IJsExec"; -export function SupplementaryThirdPartyPropFunctions(engine: JsExec) { +export function SupplementaryThirdPartyPropFunctions(engine: JsExecEngineData) { return { load: async (path: string) => { if (path.startsWith("http")) { @@ -20,19 +21,12 @@ export function SupplementaryThirdPartyPropFunctions(engine: JsExec) { } try { - const subEngine = await Stack.spawn( - JsExec, - undefined, - Daemon?.userInfo?._id, - engine.pid, - join(engine.workingDirectory, path) - ); + const jsExecService = Daemon.serviceHost?.getService("JsExecSvc"); + if (!jsExecService) throw new Error("JsExecSvc is not started. Are TPAs enabled?"); - if (engine.app && engine.metaPath) { - subEngine?.setApp(engine.app, engine.metaPath); - } + const subEngine = await jsExecService.setupEngine(join(engine.workingDirectory, path), engine.app, engine.metaPath); - return await subEngine?.getContents(); + return await jsExecService.getContents(subEngine); } catch (e) { throw e; } diff --git a/src/ts/daemon/contexts/applications.ts b/src/ts/daemon/contexts/applications.ts index 26e26952..3b9ceffb 100644 --- a/src/ts/daemon/contexts/applications.ts +++ b/src/ts/daemon/contexts/applications.ts @@ -128,6 +128,8 @@ export class ApplicationsUserContext extends UserContext implements IApplication v.security.enableThirdParty = true; return v; }); + + await Daemon!.serviceHost?.startService("JsExecSvc"); } async disableThirdParty() { @@ -151,5 +153,7 @@ export class ApplicationsUserContext extends UserContext implements IApplication for (const [pid, proc] of [...store]) { if (!proc._disposed && (proc instanceof ThirdPartyAppProcess || proc instanceof ThirdPartyProcess)) Stack.kill(pid, true); } + + await Daemon!.serviceHost?.stopService("JsExecSvc"); } } diff --git a/src/ts/daemon/contexts/spawn.ts b/src/ts/daemon/contexts/spawn.ts index 16740b59..7a14b9dc 100644 --- a/src/ts/daemon/contexts/spawn.ts +++ b/src/ts/daemon/contexts/spawn.ts @@ -3,10 +3,10 @@ import type { ISpawnUserContext } from "$interfaces/contexts/ISpawnUserContext"; import type { ICommandResult } from "$interfaces/ICommandResult"; import type { IProcess } from "$interfaces/IProcess"; import type { IUserDaemon } from "$interfaces/IUserDaemon"; +import type { IJsExecService } from "$interfaces/services/IJsExec"; import { ThirdPartyAppProcess } from "$ts/apps/thirdparty"; import { ThirdPartyProcess } from "$ts/apps/tpa/process"; import { ArcOSVersion, Daemon, Env, Stack } from "$ts/env"; -import { JsExec } from "$ts/jsexec"; import { CommandResult } from "$ts/result"; import { cloneAppMeta } from "$ts/util/apps"; import { BTN_OKAY_SUG, MessageBox } from "$ts/util/dialog"; @@ -158,9 +158,11 @@ export class SpawnUserContext extends UserContext implements ISpawnUserContext { try { const entrypoint = join(app.workingDirectory, app.entrypoint); - const engine = await JsExec.Invoke(entrypoint, ...args); - engine?.setApp(app, app.tpaPath); - const result = await engine?.getContents(); + + const jsExecService = Daemon.serviceHost?.getService("JsExecSvc"); + if (!jsExecService) throw new Error("JsExecSvc is not started. Are TPAs enabled?"); + + const result = await jsExecService.Invoke(entrypoint, app, app.tpaPath, ...args); gli?.stop?.(); diff --git a/src/ts/kernel/wavekernel.ts b/src/ts/kernel/wavekernel.ts index 3e16f585..8afa90e5 100644 --- a/src/ts/kernel/wavekernel.ts +++ b/src/ts/kernel/wavekernel.ts @@ -4,7 +4,6 @@ import type { IProcessHandler } from "$interfaces/modules/IProcessHandler"; import type { ISystemDispatch } from "$interfaces/modules/ISystemDispatch"; import { __Console__ } from "$ts/console"; import { ArcOSVersion, SetCurrentKernel, SetKernelExports } from "$ts/env"; -import { JsExec } from "$ts/jsexec"; import { getBuild } from "$ts/metadata/build"; import { ChangeLogs } from "$ts/metadata/changelog"; import { getLicense } from "$ts/metadata/license"; @@ -41,7 +40,6 @@ export class WaveKernel implements IWaveKernel { if (import.meta.env.DEV) { const win = window as any; win.kernel = this; - win.JsExec = JsExec; } } diff --git a/src/ts/servicehost/index.ts b/src/ts/servicehost/index.ts index 7fdbed25..25c8eb25 100644 --- a/src/ts/servicehost/index.ts +++ b/src/ts/servicehost/index.ts @@ -14,6 +14,7 @@ import { protoService } from "$ts/servicehost/services/ProtoService"; import { recentFilesService } from "$ts/servicehost/services/RecentFilesSvc"; import { shareService } from "$ts/servicehost/services/ShareMgmt"; import { trashService } from "$ts/servicehost/services/TrashSvc"; +import { jsExecService } from "$ts/servicehost/services/JsExec"; import { MessageBox } from "$ts/util/dialog"; import { Store } from "$ts/writable"; import type { ReadableServiceStore, Service, ServiceChangeResult, ServiceStore } from "$types/services/service"; @@ -127,6 +128,7 @@ export class ServiceHost extends Process implements IServiceHost { ["LibMgmtSvc", { ...libraryManagementService }], ["MigrationSvc", { ...migrationService }], ["RecentFilesSvc", { ...recentFilesService }], + ["JsExecSvc", { ...jsExecService }], ]); public loadStore(store: ServiceStore) { diff --git a/src/ts/jsexec/index.ts b/src/ts/servicehost/services/JsExec/index.ts similarity index 58% rename from src/ts/jsexec/index.ts rename to src/ts/servicehost/services/JsExec/index.ts index d56a34b4..62e8ea74 100644 --- a/src/ts/jsexec/index.ts +++ b/src/ts/servicehost/services/JsExec/index.ts @@ -1,5 +1,5 @@ /** - * ArcOS JavaScript Execution Engine + * ArcOS JavaScript Execution Engine Service * * This file executes JS files in ArcOS under a relatively controlled environment. * It is part of the ArcOS TPA framework: a system for running third-party apps. @@ -8,54 +8,44 @@ * * © IzKuipers 2025 */ +import type { IServiceHost } from "$interfaces/IServiceHost"; import type { ITpaConnector } from "$interfaces/modules/server/ITpaConnector"; import { ThirdPartyAppProcess } from "$ts/apps/thirdparty"; import { ThirdPartyProps } from "$ts/apps/tpa/props"; -import { Daemon, Env, Fs, Stack } from "$ts/env"; -import { Process } from "$ts/kernel/mods/stack/process/instance"; +import { Daemon, Fs } from "$ts/env"; +import { BaseService } from "$ts/servicehost/base"; import { arrayBufferToText } from "$ts/util/convert"; import { getItemNameFromPath, getParentDirectory } from "$ts/util/fs"; import { UUID } from "$ts/util/uuid"; import type { App } from "$types/apps/app"; -import type { ParsedImportStatement, ThirdPartyPropMap } from "$types/tpa/thirdparty"; +import type { Service } from "$types/services/service"; +import type { ParsedImportStatement } from "$types/tpa/thirdparty"; import * as acorn from "acorn"; +import type { JsExecEngineData } from "$types/tpa/engine"; +import type { IJsExecService } from "$interfaces/services/IJsExec"; -export class JsExec extends Process { +export class JsExecService extends BaseService implements IJsExecService { public readonly TPA_REVISION = ThirdPartyAppProcess.TPA_REV; - props?: ThirdPartyPropMap; - app?: App; - args: any[]; - metaPath?: string; - filePath?: string; - workingDirectory: string; - operationId: string; //#region LIFECYCLE - constructor(pid: number, parentPid: number, filePath: string, ...args: any[]) { - super(pid, parentPid); + constructor(pid: number, parentPid: number, name: string, host: IServiceHost, initBroadcast?: (msg: string) => void) { + super(pid, parentPid, name, host, initBroadcast); - this.args = args; - this.filePath = filePath; - this.workingDirectory = getParentDirectory(filePath); - this.name = "JsExec"; this.setSource(__SOURCE__); - this.operationId = UUID(); } async start() { - if (!this.filePath) return false; - - this.props = ThirdPartyProps(this); + this.initBroadcast?.("Starting TPA service"); } //#endregion //#region URL - async getTpaUrl(wrapped: string) { + private async getTpaUrl(engine: JsExecEngineData, wrapped: string) { this.Log(`Getting TPA file URL`); - const { appId, userId, filename } = this.getTpaUrlInfo(); + const { appId, userId, filename } = this.getTpaUrlInfo(engine); try { const urlResult = await Daemon!.GetConnector("TpaConnector").CreateUrl(wrapped, userId, appId, filename); @@ -66,16 +56,16 @@ export class JsExec extends Process { } } - getTpaPostUrl() { - const { appId, userId, filename } = this.getTpaUrlInfo(); + private getTpaPostUrl(engine: JsExecEngineData) { + const { appId, userId, filename } = this.getTpaUrlInfo(engine); return `/tpa/v2/${userId}/${appId}/${filename}`; } - getTpaUrlInfo() { - const appId = this.app?.id || "ArcOS"; + private getTpaUrlInfo(engine: JsExecEngineData) { + const appId = engine.app?.id || "ArcOS"; const userId = Daemon?.userInfo?._id || "SYSTEM"; - const filename = getItemNameFromPath(this.filePath!); + const filename = getItemNameFromPath(engine.filePath!); return { appId, userId, filename }; } @@ -83,59 +73,57 @@ export class JsExec extends Process { //#endregion //#region EXECUTION - async exec(tpaUrl: string) { - this.Log(`Executing ${this.filePath}`); + private async exec(engine: JsExecEngineData, tpaUrl: string) { + this.Log(`Executing ${engine.filePath}`); const code = await import(/* @vite-ignore */ tpaUrl); if (!code.default || !(code.default instanceof Function)) throw new JsExecError("Expected a default function"); try { - const result = await code.default(this.props!); + const result = await code.default(engine.props!); return result; } catch (e) { throw e; - } finally { - await this.killSelf(); } } - async getContents() { + async getContents(engine: JsExecEngineData) { this.Log(`Reading script contents`); - const unwrapped = this.convertImportStatementsToRegex(arrayBufferToText((await Fs.readFile(this.filePath!))!)!); - if (!unwrapped) throw new JsExecError(`Failed to read ${this.filePath}: not found`); + const unwrapped = this.convertImportStatementsToRegex(arrayBufferToText((await Fs.readFile(engine.filePath!))!)!); + if (!unwrapped) throw new JsExecError(`Failed to read ${engine.filePath}: not found`); await this.testFileContents(unwrapped); - const wrapped = this.wrap(unwrapped); - const tpaUrl = await this.getTpaUrl(wrapped); + const wrapped = this.wrap(engine, unwrapped); + const tpaUrl = await this.getTpaUrl(engine, wrapped); - return await this.exec(tpaUrl); + return await this.exec(engine, tpaUrl); } //#endregion //#region HELPERS - setApp(app: App, metaPath?: string) { + private setApp(engine: JsExecEngineData, app: App, metaPath?: string) { this.Log(`Setting app data to ${app.id} (${metaPath ?? ""})`); - if (this.app) return; + if (engine.app) return engine; if (app.tpaRevision && app.tpaRevision > this.TPA_REVISION) throw new JsExecError( `This application expects a newer version of the TPA framework than what ArcOS can supply. Please update your ArcOS version and try again.` ); - this.app = app; - this.metaPath = metaPath; - this.props = ThirdPartyProps(this); + engine.app = app; + engine.metaPath = metaPath; + engine.props = ThirdPartyProps(engine); } - private wrap(contents: string) { - if (!this.props) throw new JsExecError(`No TPA props to use`); + private wrap(engine: JsExecEngineData, contents: string) { + if (!engine.props) throw new JsExecError(`No TPA props to use`); - return `export default async function({${Object.keys(this.props).join(",")}}) {\nconst global = arguments;\n${contents}\n}`; + return `export default async function({${Object.keys(engine.props).join(",")}}) {\nconst global = arguments;\n${contents}\n}`; } private convertImportStatementsToRegex(sourceFile: string) { @@ -157,7 +145,7 @@ export class JsExec extends Process { return sourceFile; } - async testFileContents(unwrapped: string) { + private async testFileContents(unwrapped: string) { try { const ast = acorn.parse(unwrapped, { sourceType: "module", @@ -179,8 +167,24 @@ export class JsExec extends Process { //#endregion - static async Invoke(filePath: string, ...args: any[]) { - return await Stack.spawn(JsExec, undefined, undefined, +Env.get("userdaemon_pid"), filePath, ...args); + setupEngine(filePath: string, app?: App, metaPath?: string, ...args: any[]) { + let engine = {} as JsExecEngineData; + engine.args = args; + engine.filePath = filePath; + engine.workingDirectory = getParentDirectory(filePath); + engine.operationId = UUID(); + + if (app && metaPath) { + this.setApp(engine, app, metaPath); + } + + return engine; + } + + async Invoke(filePath: string, app?: App, metaPath?: string, ...args: any[]) { + let engine = this.setupEngine(filePath, app, metaPath, ...args); + + return this.getContents(engine); } } @@ -191,3 +195,13 @@ export class JsExecError extends Error { this.name = "JsExecError"; } } + +export const jsExecService: Service = { + name: "TPA Service", + description: "Provides the interface to spawn TPAs.", + process: JsExecService, + initialState: "started", + startCondition(daemon) { + return daemon.preferences().security.enableThirdParty; + }, +}; diff --git a/src/ts/servicehost/services/LibMgmtSvc/index.ts b/src/ts/servicehost/services/LibMgmtSvc/index.ts index 6181897b..8bbcfdb0 100644 --- a/src/ts/servicehost/services/LibMgmtSvc/index.ts +++ b/src/ts/servicehost/services/LibMgmtSvc/index.ts @@ -1,7 +1,7 @@ import type { IServiceHost } from "$interfaces/IServiceHost"; +import type { IJsExecService } from "$interfaces/services/IJsExec"; import type { ILibraryManagement } from "$interfaces/services/ILibraryManagement"; import { Daemon, Fs, Stack } from "$ts/env"; -import { JsExec } from "$ts/jsexec"; import { BaseService } from "$ts/servicehost/base"; import { UserPaths } from "$ts/user/store"; import { join } from "$ts/util/fs"; @@ -83,9 +83,11 @@ export class LibraryManagement extends BaseService implements ILibraryManagement try { const filePath = join(UserPaths.Libraries, id, library.entrypoint); - const engine = await Stack.spawn(JsExec, undefined, Daemon?.userInfo._id, Daemon?.pid, filePath); - return (await engine?.getContents()) as T; + const jsExecService = Daemon.serviceHost?.getService("JsExecSvc"); + if (!jsExecService) throw new Error("JsExecSvc is not started. Are TPAs enabled?"); + + return (await jsExecService.Invoke(filePath)) as T; } catch { return defaultReturnValue as T; // TODO: determine } diff --git a/src/types/tpa/engine.ts b/src/types/tpa/engine.ts new file mode 100644 index 00000000..d6aa60d2 --- /dev/null +++ b/src/types/tpa/engine.ts @@ -0,0 +1,12 @@ +import type { App } from "$types/apps/app"; +import type { ThirdPartyPropMap } from "$types/tpa/thirdparty"; + +export interface JsExecEngineData { + props?: ThirdPartyPropMap; + app?: App; + args: any[]; + metaPath?: string; + filePath?: string; + workingDirectory: string; + operationId: string; +}