Skip to content

Commit 8fe10c0

Browse files
authored
refactor(apps): prepare deno-runtime for Deno upgrade (RocketChat#39701)
1 parent 61d3d9a commit 8fe10c0

7 files changed

Lines changed: 80 additions & 33 deletions

File tree

packages/apps-engine/deno-runtime/deno.jsonc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"stack-trace": "npm:stack-trace@0.0.10",
1313
"uuid": "npm:uuid@8.3.2"
1414
},
15+
"unstable": ["detect-cjs"],
1516
"tasks": {
1617
"test": "deno test --no-check --allow-read=../../../,/tmp --allow-write=/tmp"
1718
},

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

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as child_process from 'child_process';
2+
import * as fs from 'fs';
23
import * as path from 'path';
34
import { type Readable, EventEmitter } from 'stream';
45
import { inspect as utilInspect } from 'util';
@@ -11,7 +12,7 @@ import { ProcessMessenger } from './ProcessMessenger';
1112
import { bundleLegacyApp } from './bundler';
1213
import { newDecoder } from './codec';
1314
import { AppStatus, AppStatusUtils } from '../../../definition/AppStatus';
14-
import { AppInterface, AppMethod } from '../../../definition/metadata';
15+
import { AppMethod } from '../../../definition/metadata';
1516
import type { AppManager } from '../../AppManager';
1617
import type { AppBridges } from '../../bridges';
1718
import type { IParseAppPackageResult } from '../../compiler';
@@ -75,13 +76,13 @@ export function isValidOrigin(accessor: string): accessor is (typeof ALLOWED_ACC
7576
return ALLOWED_ACCESSOR_METHODS.includes(accessor as any);
7677
}
7778

78-
export function getDenoWrapperPath(): string {
79+
export function getDenoConfigPath(): string {
7980
try {
8081
// This path is relative to the compiled version of the Apps-Engine source
81-
return require.resolve('../../../deno-runtime/main.ts');
82+
return require.resolve('../../../deno-runtime/deno.jsonc');
8283
} catch {
83-
// This path is relative to the original Apps-Engine files
84-
return require.resolve('../../../../deno-runtime/main.ts');
84+
// This path is relative to the original Apps-Engine files - used during tests
85+
return require.resolve('../../../../deno-runtime/deno.jsonc');
8586
}
8687
}
8788

@@ -117,14 +118,38 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
117118

118119
private readonly tempFilePath: string;
119120

120-
// We need to keep the appSource around in case the Deno process needs to be restarted
121+
private readonly denoRuntimePath: string;
122+
123+
private readonly denoConfigPath: string;
124+
121125
constructor(
122126
manager: AppManager,
127+
// We need to keep the appSource around in case the Deno process needs to be restarted
123128
private readonly appPackage: IParseAppPackageResult,
124129
private readonly storageItem: IAppStorageItem,
125130
) {
126131
super();
127132

133+
this.tempFilePath = manager.getTempFilePath();
134+
this.denoRuntimePath = path.join(this.tempFilePath, 'deno-runtime', 'main.ts');
135+
this.denoConfigPath = getDenoConfigPath();
136+
137+
/**
138+
* 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
139+
* The temp directory is the same we are given by the host to store temporary upload files
140+
*/
141+
try {
142+
fs.symlinkSync(
143+
path.dirname(this.denoConfigPath),
144+
path.dirname(this.denoRuntimePath),
145+
'dir'
146+
);
147+
} catch (reason: unknown) {
148+
if ((reason as NodeJS.ErrnoException).code !== 'EEXIST') {
149+
throw reason;
150+
}
151+
}
152+
128153
this.debug = baseDebug.extend(appPackage.info.id);
129154
this.messenger = new ProcessMessenger();
130155
this.livenessManager = new LivenessManager({
@@ -139,31 +164,26 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
139164
this.api = manager.getApiManager();
140165
this.logStorage = manager.getLogStorage();
141166
this.bridges = manager.getBridges();
142-
this.tempFilePath = manager.getTempFilePath();
143167
}
144168

145169
public spawnProcess(): void {
146170
try {
147171
const denoExePath = 'deno';
148172

149-
const denoWrapperPath = getDenoWrapperPath();
173+
const denoWrapperPath = this.denoRuntimePath;
150174
// During development, the appsEngineDir is enough to run the deno process
151-
const appsEngineDir = path.dirname(path.join(denoWrapperPath, '..'));
175+
const appsEngineDir = path.dirname(path.join(this.denoConfigPath, '..'));
152176
const DENO_DIR = process.env.DENO_DIR ?? path.join(appsEngineDir, '.deno-cache');
153177
// When running in production, we're likely inside a node_modules which the Deno
154178
// process must be able to read in order to include files that use NPM packages
155179
const parentNodeModulesDir = path.dirname(path.join(appsEngineDir, '..'));
156180

157-
const allowedDirs = [appsEngineDir, parentNodeModulesDir];
158-
159-
// If the app handles file upload events, it needs to be able to read the temp dir
160-
if (this.appPackage.implemented.doesImplement(AppInterface.IPreFileUpload)) {
161-
allowedDirs.push(this.tempFilePath);
162-
}
181+
const allowedDirs = [appsEngineDir, parentNodeModulesDir, this.tempFilePath];
163182

164183
const options = [
165184
'run',
166185
'--cached-only',
186+
`--config=${this.denoConfigPath}`,
167187
`--allow-read=${allowedDirs.join(',')}`,
168188
`--allow-env=${ALLOWED_ENVIRONMENT_VARIABLES.join(',')}`,
169189
denoWrapperPath,
@@ -188,6 +208,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter implements IRu
188208
},
189209
};
190210

211+
// SECURITY: We control the command, the arguments and the script that will be executed.
191212
this.deno = child_process.spawn(denoExePath, options, environment);
192213
this.messenger.setReceiver(this.deno);
193214
this.livenessManager.attach(this.deno);

packages/apps-engine/tests/server/AppManager.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export class AppManagerTestFixture {
3636
logStorage: this.testingInfastructure.getLogStorage(),
3737
bridges: this.testingInfastructure.getAppBridges(),
3838
sourceStorage: this.testingInfastructure.getSourceStorage(),
39+
tempFilePath: this.testingInfastructure.getTempFilePath(),
3940
});
4041

4142
Expect(manager.getStorage()).toBe(this.testingInfastructure.getAppStorage());
@@ -51,6 +52,7 @@ export class AppManagerTestFixture {
5152
logStorage: {} as AppLogStorage,
5253
bridges: {} as AppBridges,
5354
sourceStorage: {} as AppSourceStorage,
55+
tempFilePath: 'temp-file-path',
5456
}),
5557
).toThrowError(Error, 'There is already a valid AppManager instance');
5658
}
@@ -66,6 +68,7 @@ export class AppManagerTestFixture {
6668
logStorage: invalid as any,
6769
bridges: invalid as any,
6870
sourceStorage: invalid as any,
71+
tempFilePath: 'temp-file-path',
6972
}),
7073
).toThrowError(Error, 'Invalid instance of the AppMetadataStorage');
7174

@@ -76,6 +79,7 @@ export class AppManagerTestFixture {
7679
logStorage: invalid as any,
7780
bridges: invalid as any,
7881
sourceStorage: invalid as any,
82+
tempFilePath: 'temp-file-path',
7983
}),
8084
).toThrowError(Error, 'Invalid instance of the AppLogStorage');
8185

@@ -86,6 +90,7 @@ export class AppManagerTestFixture {
8690
logStorage: this.testingInfastructure.getLogStorage(),
8791
bridges: invalid as any,
8892
sourceStorage: invalid as any,
93+
tempFilePath: 'temp-file-path',
8994
}),
9095
).toThrowError(Error, 'Invalid instance of the AppBridges');
9196

@@ -96,6 +101,7 @@ export class AppManagerTestFixture {
96101
logStorage: this.testingInfastructure.getLogStorage(),
97102
bridges: this.testingInfastructure.getAppBridges(),
98103
sourceStorage: invalid as any,
104+
tempFilePath: this.testingInfastructure.getTempFilePath(),
99105
}),
100106
).toThrowError(Error, 'Invalid instance of the AppSourceStorage');
101107
}
@@ -107,6 +113,7 @@ export class AppManagerTestFixture {
107113
logStorage: this.testingInfastructure.getLogStorage(),
108114
bridges: this.testingInfastructure.getAppBridges(),
109115
sourceStorage: this.testingInfastructure.getSourceStorage(),
116+
tempFilePath: this.testingInfastructure.getTempFilePath(),
110117
});
111118

112119
Expect(manager.getParser() instanceof AppPackageParser).toBe(true);
@@ -129,6 +136,7 @@ export class AppManagerTestFixture {
129136
logStorage: this.testingInfastructure.getLogStorage(),
130137
bridges: this.testingInfastructure.getAppBridges(),
131138
sourceStorage: this.testingInfastructure.getSourceStorage(),
139+
tempFilePath: this.testingInfastructure.getTempFilePath(),
132140
});
133141

134142
const appsOverview = TestData.getAppsOverview();
@@ -153,6 +161,7 @@ export class AppManagerTestFixture {
153161
logStorage: this.testingInfastructure.getLogStorage(),
154162
bridges: this.testingInfastructure.getAppBridges(),
155163
sourceStorage: this.testingInfastructure.getSourceStorage(),
164+
tempFilePath: this.testingInfastructure.getTempFilePath(),
156165
});
157166

158167
const appsOverview = TestData.getAppsOverview();
@@ -177,6 +186,7 @@ export class AppManagerTestFixture {
177186
logStorage: this.testingInfastructure.getLogStorage(),
178187
bridges: this.testingInfastructure.getAppBridges(),
179188
sourceStorage: this.testingInfastructure.getSourceStorage(),
189+
tempFilePath: this.testingInfastructure.getTempFilePath(),
180190
});
181191

182192
const sameLicenseData = 'same-license-data';
@@ -217,6 +227,7 @@ export class AppManagerTestFixture {
217227
logStorage: this.testingInfastructure.getLogStorage(),
218228
bridges: this.testingInfastructure.getAppBridges(),
219229
sourceStorage: this.testingInfastructure.getSourceStorage(),
230+
tempFilePath: this.testingInfastructure.getTempFilePath(),
220231
});
221232

222233
const existingSubscriptionInfo = TestData.getMarketplaceSubscriptionInfo({

packages/apps-engine/tests/server/managers/AppRuntimeManager.spec.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { IParseAppPackageResult } from '../../../src/server/compiler';
66
import { AppRuntimeManager } from '../../../src/server/managers/AppRuntimeManager';
77
import type { IRuntimeController } from '../../../src/server/runtime/IRuntimeController';
88
import type { IAppStorageItem } from '../../../src/server/storage';
9+
import { TestInfastructureSetup } from '../../test-data/utilities';
910

1011
export class AppRuntimeManagerTestFixture {
1112
private mockManager: AppManager;
@@ -20,20 +21,9 @@ export class AppRuntimeManagerTestFixture {
2021

2122
@SetupFixture
2223
public setupFixture() {
23-
this.mockManager = {
24-
getAccessorManager() {
25-
return {} as any;
26-
},
27-
getApiManager() {
28-
return {} as any;
29-
},
30-
getLogStorage() {
31-
return {} as any;
32-
},
33-
getBridges() {
34-
return {} as any;
35-
},
36-
} as AppManager;
24+
const testInfrastructure = new TestInfastructureSetup();
25+
26+
this.mockManager = testInfrastructure.getMockManager();
3727

3828
this.mockAppPackage = {
3929
info: {

packages/apps-engine/tests/server/runtime/DenoRuntimeSubprocessController.spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as fs from 'fs/promises';
2+
import * as os from 'os';
23
import * as path from 'path';
34

4-
import { TestFixture, Setup, Expect, AsyncTest, SpyOn, Any, AsyncSetupFixture, Teardown } from 'alsatian';
5+
import { TestFixture, Setup, Expect, AsyncTest, SpyOn, Any, AsyncSetupFixture, Teardown, TeardownFixture } from 'alsatian';
56
import type { SuccessObject } from 'jsonrpc-lite';
67

78
import { AppStatus } from '../../../src/definition/AppStatus';
@@ -57,6 +58,13 @@ export class DenuRuntimeSubprocessControllerTestFixture {
5758
this.controller.stopApp();
5859
}
5960

61+
@TeardownFixture
62+
public async teardownFixture() {
63+
await fs.unlink(path.join(os.tmpdir(), 'deno-runtime')).catch((reason) => {
64+
console.warn('Failed to delete temporary Deno runtime symlink', reason);
65+
});
66+
}
67+
6068
@AsyncTest('correctly identifies a call to the HTTP accessor')
6169
public async testHttpAccessor() {
6270
const spy = SpyOn(this.manager.getBridges().getHttpBridge(), 'doCall');

packages/apps-engine/tests/test-data/storage/storage.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,17 @@ const Datastore = require('@seald-io/nedb') as typeof import('@seald-io/nedb').d
1010
export class TestsAppStorage extends AppMetadataStorage {
1111
private db: InstanceType<typeof Datastore>;
1212

13-
constructor() {
13+
private static instance: TestsAppStorage;
14+
15+
public static getInstance(): TestsAppStorage {
16+
if (!TestsAppStorage.instance) {
17+
TestsAppStorage.instance = new TestsAppStorage();
18+
}
19+
20+
return TestsAppStorage.instance;
21+
}
22+
23+
private constructor() {
1424
super('nedb');
1525
this.db = new Datastore({ filename: 'tests/test-data/dbs/apps.nedb', autoload: true });
1626
this.db.ensureIndex({ fieldName: 'id', unique: true });

packages/apps-engine/tests/test-data/utilities.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import * as os from 'os';
2+
13
import { TestsAppBridges } from './bridges/appBridges';
24
import { TestSourceStorage } from './storage/TestSourceStorage';
35
import { TestsAppLogStorage } from './storage/logStorage';
@@ -73,7 +75,7 @@ export class TestInfastructureSetup {
7375
private runtimeManager: AppRuntimeManager;
7476

7577
constructor() {
76-
this.appStorage = new TestsAppStorage();
78+
this.appStorage = TestsAppStorage.getInstance();
7779
this.logStorage = new TestsAppLogStorage();
7880
this.bridges = new TestsAppBridges();
7981
this.sourceStorage = new TestSourceStorage();
@@ -130,10 +132,14 @@ export class TestInfastructureSetup {
130132
getRuntime: () => {
131133
return this.runtimeManager;
132134
},
133-
getTempFilePath: () => 'temp-file-path',
135+
getTempFilePath: this.getTempFilePath,
134136
} as unknown as AppManager;
135137
}
136138

139+
public getTempFilePath(): string {
140+
return os.tmpdir();
141+
}
142+
137143
public getAppStorage(): AppMetadataStorage {
138144
return this.appStorage;
139145
}

0 commit comments

Comments
 (0)