Skip to content

Commit ca338ca

Browse files
d-gubertCopilot
andcommitted
refactor: DenoRuntimeSubprocessController setup and path resolutions
Co-authored-by: Copilot <copilot@github.com>
1 parent 75fd4b7 commit ca338ca

2 files changed

Lines changed: 67 additions & 41 deletions

File tree

packages/apps/src/server/runtime/deno/AppsEngineDenoRuntime.ts

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as child_process from 'child_process';
22
import * as fs from 'fs';
3+
import os from 'os';
34
import * as path from 'path';
45
import { type Readable, EventEmitter } from 'stream';
56
import { inspect as utilInspect } from 'util';
@@ -20,6 +21,7 @@ import { AppConsole, type ILoggerStorageEntry } from '../../logging';
2021
import type { AppAccessorManager, AppApiManager } from '../../managers';
2122
import type { AppLogStorage, IAppStorageItem } from '../../storage';
2223
import type { IRuntimeController } from '../IRuntimeController';
24+
import type { DenoConfigurationFileSchema } from './typings';
2325

2426
const baseDebug = debugFactory('appsEngine:runtime:deno');
2527

@@ -58,7 +60,7 @@ const COMMAND_PONG = '_zPONG';
5860

5961
export const JSONRPC_METHOD_NOT_FOUND = -32601;
6062

61-
export function getRuntimeTimeout() {
63+
function getRuntimeTimeout() {
6264
const defaultTimeout = 30000;
6365
const envValue = isFinite(process.env.APPS_ENGINE_RUNTIME_TIMEOUT as any)
6466
? Number(process.env.APPS_ENGINE_RUNTIME_TIMEOUT)
@@ -72,28 +74,27 @@ export function getRuntimeTimeout() {
7274
return envValue;
7375
}
7476

75-
export function isValidOrigin(accessor: string): accessor is (typeof ALLOWED_ACCESSOR_METHODS)[number] {
77+
function isValidOrigin(accessor: string): accessor is (typeof ALLOWED_ACCESSOR_METHODS)[number] {
7678
return ALLOWED_ACCESSOR_METHODS.includes(accessor as any);
7779
}
7880

79-
export function getDenoConfigPath(): string {
80-
try {
81-
// This path is relative to the compiled version of the Apps-Engine source
82-
return require.resolve('../../../deno-runtime/deno.jsonc');
83-
} catch {
84-
// This path is relative to the original Apps-Engine files - used during tests
85-
return require.resolve('../../../../deno-runtime/deno.jsonc');
86-
}
81+
function getDenoConfigPath(): string {
82+
return require.resolve('../../../../deno-runtime/deno.jsonc');
83+
}
84+
85+
function getDenoConfig(): DenoConfigurationFileSchema {
86+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-return, import/no-dynamic-require
87+
return require(getDenoConfigPath());
8788
}
8889

8990
/**
9091
* Resolves the absolute path to @rocket.chat/apps-engine's src/ directory.
9192
* Uses require.resolve so it works regardless of the runtime environment
9293
* (monorepo dev, Meteor bundle, standalone node_modules).
9394
*/
94-
export function getAppsEngineSourceDir(): string {
95+
function getAppsEngineDir(): string {
9596
const pkgJsonPath = require.resolve('@rocket.chat/apps-engine/package.json');
96-
return path.join(path.dirname(pkgJsonPath), 'src');
97+
return path.dirname(pkgJsonPath);
9798
}
9899

99100
/**
@@ -104,22 +105,20 @@ export function getAppsEngineSourceDir(): string {
104105
*
105106
* Returns the path to the generated config file.
106107
*/
107-
export function generateRuntimeDenoConfig(staticConfigPath: string, tempDir: string): string {
108-
const staticConfig = JSON.parse(fs.readFileSync(staticConfigPath, 'utf8'));
109-
const appsEngineSrcUrl = `file://${getAppsEngineSourceDir()}/`;
108+
function generateEphemeralDenoConfig(targetPath: string, appsEnginePath: string, staticConfig: DenoConfigurationFileSchema): void {
109+
if (!targetPath.startsWith(os.tmpdir())) {
110+
throw new Error(`Temp directory "${targetPath}" is not inside the system temp directory`);
111+
}
110112

111113
const runtimeConfig = {
112114
...staticConfig,
113115
imports: {
114116
...staticConfig.imports,
115-
'@rocket.chat/apps-engine/': appsEngineSrcUrl,
117+
'@rocket.chat/apps-engine/': appsEnginePath,
116118
},
117119
};
118120

119-
const runtimeConfigPath = path.join(tempDir, 'deno_runtime.jsonc');
120-
fs.writeFileSync(runtimeConfigPath, JSON.stringify(runtimeConfig, null, '\t'));
121-
122-
return runtimeConfigPath;
121+
fs.writeFileSync(targetPath, JSON.stringify(runtimeConfig, null, '\t'));
123122
}
124123

125124
type AbortFunction = (reason?: any) => void;
@@ -154,10 +153,20 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
154153

155154
private readonly tempFilePath: string;
156155

156+
private readonly denoBin = 'deno';
157+
157158
private readonly denoRuntimePath: string;
158159

159160
private readonly denoConfigPath: string;
160161

162+
private readonly denoEphemeralConfigPath: string;
163+
164+
private readonly denoDir: string;
165+
166+
private readonly appsEnginePath: string;
167+
168+
private readonly packagePath: string;
169+
161170
constructor(
162171
manager: AppManager,
163172
// We need to keep the appSource around in case the Deno process needs to be restarted
@@ -167,8 +176,13 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
167176
super();
168177

169178
this.tempFilePath = manager.getTempFilePath();
170-
this.denoRuntimePath = path.join(this.tempFilePath, 'deno-runtime', 'main.ts');
171179
this.denoConfigPath = getDenoConfigPath();
180+
this.appsEnginePath = getAppsEngineDir();
181+
this.denoEphemeralConfigPath = path.join(this.tempFilePath, 'deno_ephemeral.jsonc');
182+
183+
this.denoRuntimePath = path.join(this.tempFilePath, 'deno-runtime', 'main.ts');
184+
this.denoDir = process.env.DENO_DIR ?? path.join(this.denoConfigPath, '..', '.deno-cache');
185+
this.packagePath = path.dirname(this.denoConfigPath);
172186

173187
/**
174188
* Deno 2.x refuses to run scripts inside the node_modules, so we create a symlink to the deno runtime files in the temp directory
@@ -182,6 +196,9 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
182196
}
183197
}
184198

199+
// Generate a runtime config with the resolved absolute path for @rocket.chat/apps-engine/
200+
generateEphemeralDenoConfig(this.denoEphemeralConfigPath, this.appsEnginePath, getDenoConfig());
201+
185202
this.debug = baseDebug.extend(appPackage.info.id);
186203
this.messenger = new ProcessMessenger();
187204
this.livenessManager = new LivenessManager({
@@ -200,30 +217,15 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
200217

201218
public spawnProcess(): void {
202219
try {
203-
const denoExePath = 'deno';
204-
205-
const denoWrapperPath = this.denoRuntimePath;
206-
// The apps package dir (where deno-runtime/ and .deno-cache/ live)
207-
const appsEngineDir = path.dirname(path.join(this.denoConfigPath, '..'));
208-
const DENO_DIR = process.env.DENO_DIR ?? path.join(appsEngineDir, '.deno-cache');
209-
// When running in production, we're likely inside a node_modules which the Deno
210-
// process must be able to read in order to include files that use NPM packages
211-
const parentNodeModulesDir = path.dirname(path.join(appsEngineDir, '..'));
212-
// The definition/ source files from @rocket.chat/apps-engine that deno-runtime imports
213-
const appsEngineSrcDir = getAppsEngineSourceDir();
214-
215-
const allowedDirs = [appsEngineDir, parentNodeModulesDir, appsEngineSrcDir, this.tempFilePath];
216-
217-
// Generate a runtime config with the resolved absolute path for @rocket.chat/apps-engine/
218-
const runtimeConfigPath = generateRuntimeDenoConfig(this.denoConfigPath, this.tempFilePath);
220+
const allowedDirs = [this.appsEnginePath, this.packagePath, this.tempFilePath];
219221

220222
const options = [
221223
'run',
222224
'--cached-only',
223-
`--config=${runtimeConfigPath}`,
225+
`--config=${this.denoEphemeralConfigPath}`,
224226
`--allow-read=${allowedDirs.join(',')}`,
225227
`--allow-env=${ALLOWED_ENVIRONMENT_VARIABLES.join(',')}`,
226-
denoWrapperPath,
228+
this.denoRuntimePath,
227229
'--subprocess',
228230
this.appPackage.info.id,
229231
'--spawnId',
@@ -241,12 +243,12 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
241243
// We need to pass the PATH, otherwise the shell won't find the deno executable
242244
// But the runtime itself won't have access to the env var because of the parameters
243245
PATH: process.env.PATH,
244-
DENO_DIR,
246+
DENO_DIR: this.denoDir,
245247
},
246248
};
247249

248250
// SECURITY: We control the command, the arguments and the script that will be executed.
249-
this.deno = child_process.spawn(denoExePath, options, environment);
251+
this.deno = child_process.spawn(this.denoBin, options, environment);
250252
this.messenger.setReceiver(this.deno);
251253
this.livenessManager.attach(this.deno);
252254

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export type DenoConfigurationFileSchema = {
2+
compilerOptions?: Record<string, string | Array<string | Record<string, any>> | Record<string, any>>;
3+
importMap?: string;
4+
imports?: Record<string, string>;
5+
scopes?: Record<string, Record<string, string>>;
6+
exclude?: string[];
7+
lint?: Record<string, string | Array<string | Record<string, any>> | Record<string, any>>;
8+
fmt?: Record<string, string | Array<string | Record<string, any>> | Record<string, any>>;
9+
minimumDependencyAge?: any;
10+
nodeModulesDir?: any;
11+
vendor?: boolean;
12+
tasks?: Record<string, string>;
13+
test?: Record<string, string | Array<string | Record<string, any>> | Record<string, any>>;
14+
publish?: any;
15+
bench?: Record<string, string | Array<string | Record<string, any>> | Record<string, any>>;
16+
lock?: string | boolean | { path: string; frozen?: boolean };
17+
unstable?: string[];
18+
name?: string;
19+
version?: string;
20+
exports?: string | Record<string, string>;
21+
permissions?: Record<string, string>;
22+
patch?: string[];
23+
links?: string[];
24+
};

0 commit comments

Comments
 (0)