Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/apps/components/firstrun/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,12 @@ export const FirstRunPages = new Map<string, FirstRunPage>([
},
{
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,
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/IServiceHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ export type ServiceIdentifier =
| "IconService"
| "LibMgmtSvc"
| "MigrationSvc"
| "RecentFilesSvc";
| "RecentFilesSvc"
| "JsExecSvc";
// !endtpa
11 changes: 11 additions & 0 deletions src/interfaces/services/IJsExec.ts
Original file line number Diff line number Diff line change
@@ -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<any>;
getContents(engine: JsExecEngineData): Promise<any>;
setupEngine(filePath: string, app?: App, metaPath?: string, ...args: any[]): JsExecEngineData;
Invoke(filePath: string, app?: App, metaPath?: string, ...args: any[]): Promise<any>;
}
6 changes: 3 additions & 3 deletions src/ts/apps/tpa/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down
22 changes: 8 additions & 14 deletions src/ts/apps/tpa/supplementary.ts
Original file line number Diff line number Diff line change
@@ -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")) {
Expand All @@ -20,19 +21,12 @@ export function SupplementaryThirdPartyPropFunctions(engine: JsExec) {
}

try {
const subEngine = await Stack.spawn<JsExec>(
JsExec,
undefined,
Daemon?.userInfo?._id,
engine.pid,
join(engine.workingDirectory, path)
);
const jsExecService = Daemon.serviceHost?.getService<IJsExecService>("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;
}
Expand Down
4 changes: 4 additions & 0 deletions src/ts/daemon/contexts/applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export class ApplicationsUserContext extends UserContext implements IApplication
v.security.enableThirdParty = true;
return v;
});

await Daemon!.serviceHost?.startService("JsExecSvc");
}

async disableThirdParty() {
Expand All @@ -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");
}
}
10 changes: 6 additions & 4 deletions src/ts/daemon/contexts/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<IJsExecService>("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?.();

Expand Down
2 changes: 0 additions & 2 deletions src/ts/kernel/wavekernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/ts/servicehost/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -127,6 +128,7 @@ export class ServiceHost extends Process implements IServiceHost {
["LibMgmtSvc", { ...libraryManagementService }],
["MigrationSvc", { ...migrationService }],
["RecentFilesSvc", { ...recentFilesService }],
["JsExecSvc", { ...jsExecService }],
]);

public loadStore(store: ServiceStore) {
Expand Down
116 changes: 65 additions & 51 deletions src/ts/jsexec/index.ts → src/ts/servicehost/services/JsExec/index.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<ITpaConnector>("TpaConnector").CreateUrl(wrapped, userId, appId, filename);

Expand All @@ -66,76 +56,74 @@ 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 };
}

//#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 ?? "<unknown meta>"})`);

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) {
Expand All @@ -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",
Expand All @@ -179,8 +167,24 @@ export class JsExec extends Process {

//#endregion

static async Invoke(filePath: string, ...args: any[]) {
return await Stack.spawn<JsExec>(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);
}
}

Expand All @@ -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;
},
};
Loading
Loading