@efffrida/vitest-pool 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/frida/agent.js +19 -74
- package/dist/frida/agent.js.map +1 -1
- package/dist/src/index.d.ts +38 -70
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +146 -316
- package/dist/src/index.js.map +1 -1
- package/package.json +19 -20
- package/src/index.ts +215 -382
package/src/index.ts
CHANGED
|
@@ -1,141 +1,95 @@
|
|
|
1
|
-
import type * as
|
|
2
|
-
import type * as FridaDeviceAcquisitionError from "@efffrida/frida-tools/FridaDeviceAcquisitionError";
|
|
3
|
-
import type * as FridaSessionError from "@efffrida/frida-tools/FridaSessionError";
|
|
4
|
-
import type * as Option from "effect/Option";
|
|
5
|
-
import type * as ParseResult from "effect/ParseResult";
|
|
6
|
-
import type * as Runtime from "effect/Runtime";
|
|
7
|
-
import type * as VitestNode from "vitest/node";
|
|
1
|
+
import type * as Context from "effect/Context";
|
|
8
2
|
|
|
9
|
-
import * as
|
|
10
|
-
import * as Command from "@effect/platform/Command";
|
|
11
|
-
import * as CommandExecutor from "@effect/platform/CommandExecutor";
|
|
12
|
-
import * as FileSystem from "@effect/platform/FileSystem";
|
|
13
|
-
import * as Path from "@effect/platform/Path";
|
|
14
|
-
import * as FridaDevice from "@efffrida/frida-tools/FridaDevice";
|
|
15
|
-
import * as FridaScript from "@efffrida/frida-tools/FridaScript";
|
|
16
|
-
import * as FridaSession from "@efffrida/frida-tools/FridaSession";
|
|
3
|
+
import * as Array from "effect/Array";
|
|
17
4
|
import * as Cause from "effect/Cause";
|
|
18
5
|
import * as Deferred from "effect/Deferred";
|
|
19
6
|
import * as Duration from "effect/Duration";
|
|
20
7
|
import * as Effect from "effect/Effect";
|
|
21
8
|
import * as Exit from "effect/Exit";
|
|
9
|
+
import * as FileSystem from "effect/FileSystem";
|
|
10
|
+
import * as Function from "effect/Function";
|
|
22
11
|
import * as Layer from "effect/Layer";
|
|
23
|
-
import * as ManagedRuntime from "effect/ManagedRuntime";
|
|
24
12
|
import * as Match from "effect/Match";
|
|
13
|
+
import * as Path from "effect/Path";
|
|
25
14
|
import * as Schema from "effect/Schema";
|
|
26
15
|
import * as Scope from "effect/Scope";
|
|
27
|
-
import * as Sink from "effect/Sink";
|
|
28
16
|
import * as Stream from "effect/Stream";
|
|
29
|
-
import * as
|
|
17
|
+
import * as ChildProcess from "effect/unstable/process/ChildProcess";
|
|
18
|
+
|
|
19
|
+
import type * as VitestNode from "vitest/node";
|
|
20
|
+
|
|
21
|
+
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
22
|
+
import * as FridaDevice from "@efffrida/frida-tools/FridaDevice";
|
|
23
|
+
import * as FridaScript from "@efffrida/frida-tools/FridaScript";
|
|
24
|
+
import * as FridaSession from "@efffrida/frida-tools/FridaSession";
|
|
30
25
|
import * as Flatted from "flatted";
|
|
31
26
|
import * as Frida from "frida";
|
|
32
27
|
|
|
33
28
|
// First, pick your device
|
|
34
|
-
|
|
29
|
+
const DeviceSchema = Schema.Union([
|
|
35
30
|
Schema.Struct({
|
|
36
|
-
|
|
31
|
+
connection: Schema.Literal("local"),
|
|
37
32
|
}),
|
|
38
33
|
Schema.Struct({
|
|
39
|
-
|
|
40
|
-
timeout: Schema.
|
|
34
|
+
connection: Schema.Literal("usb"),
|
|
35
|
+
timeout: Schema.optional(Schema.DurationFromMillis),
|
|
41
36
|
}),
|
|
42
37
|
Schema.Struct({
|
|
43
38
|
address: Schema.String,
|
|
44
|
-
|
|
45
|
-
token: Schema.
|
|
46
|
-
origin: Schema.
|
|
47
|
-
keepaliveInterval: Schema.
|
|
39
|
+
connection: Schema.Literal("remote"),
|
|
40
|
+
token: Schema.optional(Schema.String),
|
|
41
|
+
origin: Schema.optional(Schema.String),
|
|
42
|
+
keepaliveInterval: Schema.optional(Schema.DurationFromMillis),
|
|
48
43
|
}),
|
|
49
44
|
Schema.Struct({
|
|
50
45
|
emulatorName: Schema.String,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
fridaExecutable: Schema.
|
|
55
|
-
emulatorExecutable: Schema.
|
|
56
|
-
})
|
|
57
|
-
)
|
|
46
|
+
hidden: Schema.optional(Schema.Boolean),
|
|
47
|
+
adbExecutable: Schema.optional(Schema.String),
|
|
48
|
+
connection: Schema.Literal("android-emulator"),
|
|
49
|
+
fridaExecutable: Schema.optional(Schema.String),
|
|
50
|
+
emulatorExecutable: Schema.optional(Schema.String),
|
|
51
|
+
}),
|
|
52
|
+
]);
|
|
58
53
|
|
|
59
54
|
// Second, pick your session
|
|
60
|
-
|
|
55
|
+
const AttachSchema = Schema.Union([
|
|
61
56
|
Schema.Struct({
|
|
62
|
-
|
|
57
|
+
pid: Schema.Number,
|
|
63
58
|
}),
|
|
64
59
|
Schema.Struct({
|
|
65
|
-
spawn: Schema.
|
|
66
|
-
preSpawn: Schema.
|
|
60
|
+
spawn: Schema.NonEmptyArray(Schema.String),
|
|
61
|
+
preSpawn: Schema.optional(Schema.Boolean),
|
|
67
62
|
}),
|
|
68
63
|
Schema.Struct({
|
|
69
64
|
attachFrontmost: Schema.Literal(true),
|
|
70
|
-
frontmostScope: Schema.
|
|
71
|
-
})
|
|
72
|
-
)
|
|
65
|
+
frontmostScope: Schema.optional(Schema.Literals(["minimal", "metadata", "full"])),
|
|
66
|
+
}),
|
|
67
|
+
]);
|
|
73
68
|
|
|
74
69
|
// Third, pick your runtime and platform
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.
|
|
70
|
+
const ConfigSchema = Schema.Struct({
|
|
71
|
+
device: DeviceSchema,
|
|
72
|
+
attach: AttachSchema,
|
|
73
|
+
runtime: Schema.optional(Schema.Literals(["default", "qjs", "v8"])),
|
|
74
|
+
platform: Schema.optional(Schema.Literals(["gum", "browser", "neutral"])),
|
|
75
|
+
});
|
|
80
76
|
|
|
81
77
|
/**
|
|
82
78
|
* @since 1.0.0
|
|
83
79
|
* @category Tests
|
|
84
80
|
*/
|
|
85
81
|
export class FridaPoolWorker implements VitestNode.PoolWorker {
|
|
86
|
-
readonly name = "frida-pool";
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
private readonly poolOptions: VitestNode.PoolOptions;
|
|
90
|
-
private readonly customOptions: Schema.Schema.Type<ConfigSchema>;
|
|
91
|
-
|
|
92
|
-
private readonly cancelables: Map<
|
|
93
|
-
(arg: any) => void,
|
|
94
|
-
Runtime.Cancel<
|
|
95
|
-
unknown,
|
|
96
|
-
| FridaDeviceAcquisitionError.FridaDeviceAcquisitionError
|
|
97
|
-
| PlatformError.PlatformError
|
|
98
|
-
| FridaSessionError.FridaSessionError
|
|
99
|
-
>
|
|
100
|
-
>;
|
|
101
|
-
|
|
102
|
-
private modifiedAgentScope: Scope.CloseableScope;
|
|
103
|
-
private managedRuntime:
|
|
104
|
-
| ManagedRuntime.ManagedRuntime<
|
|
105
|
-
FridaScript.FridaScript,
|
|
106
|
-
| FridaDeviceAcquisitionError.FridaDeviceAcquisitionError
|
|
107
|
-
| PlatformError.PlatformError
|
|
108
|
-
| FridaSessionError.FridaSessionError
|
|
109
|
-
>
|
|
110
|
-
| undefined;
|
|
111
|
-
private sends: Array<
|
|
112
|
-
Promise<
|
|
113
|
-
Exit.Exit<
|
|
114
|
-
void,
|
|
115
|
-
| FridaDeviceAcquisitionError.FridaDeviceAcquisitionError
|
|
116
|
-
| PlatformError.PlatformError
|
|
117
|
-
| FridaSessionError.FridaSessionError
|
|
118
|
-
| ParseResult.ParseError
|
|
119
|
-
>
|
|
120
|
-
>
|
|
121
|
-
> = [];
|
|
122
|
-
|
|
123
|
-
constructor(poolOptions: VitestNode.PoolOptions, customOptions: Schema.Schema.Type<ConfigSchema>) {
|
|
124
|
-
this.poolOptions = poolOptions;
|
|
125
|
-
this.customOptions = customOptions;
|
|
126
|
-
this.cancelables = new Map();
|
|
127
|
-
this.managedRuntime = undefined;
|
|
128
|
-
this.modifiedAgentScope = Effect.runSync(Scope.make());
|
|
129
|
-
}
|
|
82
|
+
public readonly name = "frida-pool";
|
|
83
|
+
private static initQueue: Promise<void> = Promise.resolve();
|
|
130
84
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
Effect.provide(NodeContext.layer),
|
|
135
|
-
Effect.runPromise
|
|
136
|
-
);
|
|
85
|
+
private readonly scope: Scope.Closeable;
|
|
86
|
+
private readonly scriptContext: Promise<Context.Context<FridaScript.FridaScript>>;
|
|
87
|
+
private readonly cancelables: Map<(arg: any) => void, (interrupter?: number) => void> = new Map();
|
|
137
88
|
|
|
138
|
-
|
|
89
|
+
private sends: Array<Promise<unknown>> = [];
|
|
90
|
+
|
|
91
|
+
constructor(poolOptions: VitestNode.PoolOptions, customOptions: Schema.Schema.Type<typeof ConfigSchema>) {
|
|
92
|
+
const FridaRuntime = Match.value(customOptions.runtime).pipe(
|
|
139
93
|
Match.when(undefined, () => undefined),
|
|
140
94
|
Match.when("v8", () => Frida.ScriptRuntime.V8),
|
|
141
95
|
Match.when("qjs", () => Frida.ScriptRuntime.QJS),
|
|
@@ -143,7 +97,7 @@ export class FridaPoolWorker implements VitestNode.PoolWorker {
|
|
|
143
97
|
Match.exhaustive
|
|
144
98
|
);
|
|
145
99
|
|
|
146
|
-
const FridaPlatform = Match.value(
|
|
100
|
+
const FridaPlatform = Match.value(customOptions.platform).pipe(
|
|
147
101
|
Match.when(undefined, () => undefined),
|
|
148
102
|
Match.when("gum", () => Frida.JsPlatform.Gum),
|
|
149
103
|
Match.when("browser", () => Frida.JsPlatform.Browser),
|
|
@@ -151,137 +105,211 @@ export class FridaPoolWorker implements VitestNode.PoolWorker {
|
|
|
151
105
|
Match.exhaustive
|
|
152
106
|
);
|
|
153
107
|
|
|
154
|
-
const DeviceLive = Match.value(
|
|
155
|
-
Match.when({
|
|
156
|
-
Match.when({
|
|
157
|
-
FridaDevice.layerUsbDevice({
|
|
158
|
-
timeout: timeout ? Duration.toMillis(timeout) : undefined,
|
|
159
|
-
} as Frida.GetDeviceOptions)
|
|
108
|
+
const DeviceLive = Match.value(customOptions.device).pipe(
|
|
109
|
+
Match.when({ connection: "local" }, () => FridaDevice.layerLocalDevice),
|
|
110
|
+
Match.when({ connection: "usb" }, ({ timeout }) =>
|
|
111
|
+
FridaDevice.layerUsbDevice(timeout !== undefined ? { timeout: Duration.toMillis(timeout) } : {})
|
|
160
112
|
),
|
|
161
|
-
Match.when({
|
|
113
|
+
Match.when({ connection: "remote" }, ({ address, keepaliveInterval, origin, token }) =>
|
|
162
114
|
FridaDevice.layerRemoteDevice(address, {
|
|
163
|
-
token,
|
|
164
|
-
origin,
|
|
165
|
-
|
|
166
|
-
|
|
115
|
+
...(token !== undefined ? { token } : {}),
|
|
116
|
+
...(origin !== undefined ? { origin } : {}),
|
|
117
|
+
...(keepaliveInterval !== undefined
|
|
118
|
+
? { keepaliveInterval: Duration.toMillis(keepaliveInterval) }
|
|
119
|
+
: {}),
|
|
120
|
+
})
|
|
167
121
|
),
|
|
168
122
|
Match.when(
|
|
169
|
-
{
|
|
123
|
+
{ connection: "android-emulator" },
|
|
170
124
|
({ adbExecutable, emulatorExecutable, emulatorName, fridaExecutable, hidden }) =>
|
|
171
125
|
FridaDevice.layerAndroidEmulatorDevice(emulatorName, {
|
|
172
|
-
hidden,
|
|
173
|
-
adbExecutable,
|
|
174
|
-
fridaExecutable,
|
|
175
|
-
emulatorExecutable,
|
|
126
|
+
hidden: hidden ?? undefined,
|
|
127
|
+
adbExecutable: adbExecutable ?? undefined,
|
|
128
|
+
fridaExecutable: fridaExecutable ?? undefined,
|
|
129
|
+
emulatorExecutable: emulatorExecutable ?? undefined,
|
|
176
130
|
})
|
|
177
131
|
),
|
|
178
132
|
Match.exhaustive
|
|
179
133
|
);
|
|
180
134
|
|
|
181
|
-
const SessionLive = Match.value(
|
|
182
|
-
Match.when({
|
|
183
|
-
Match.when({ attachFrontmost: true }, ({ frontmostScope }) =>
|
|
184
|
-
FridaSession.layerFrontmost(
|
|
185
|
-
|
|
135
|
+
const SessionLive = Match.value(customOptions.attach).pipe(
|
|
136
|
+
Match.when({ pid: Match.number }, ({ pid }) => FridaSession.layer(pid)),
|
|
137
|
+
Match.when({ attachFrontmost: true }, ({ frontmostScope }) => {
|
|
138
|
+
return FridaSession.layerFrontmost(
|
|
139
|
+
frontmostScope !== undefined
|
|
140
|
+
? {
|
|
141
|
+
scope: Match.value(frontmostScope).pipe(
|
|
142
|
+
Match.when("minimal", () => Frida.Scope.Minimal),
|
|
143
|
+
Match.when("metadata", () => Frida.Scope.Metadata),
|
|
144
|
+
Match.when("full", () => Frida.Scope.Full),
|
|
145
|
+
Match.exhaustive
|
|
146
|
+
),
|
|
147
|
+
}
|
|
148
|
+
: {}
|
|
149
|
+
);
|
|
150
|
+
}),
|
|
186
151
|
Match.when({ preSpawn: true }, ({ spawn }) =>
|
|
187
|
-
Layer.
|
|
152
|
+
Layer.unwrap(
|
|
188
153
|
Effect.gen(function* () {
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
return FridaSession.layer(process.pid);
|
|
154
|
+
const [command, ...args] = spawn;
|
|
155
|
+
const handle = yield* ChildProcess.make(command, args);
|
|
156
|
+
return FridaSession.layer(handle.pid);
|
|
193
157
|
})
|
|
194
158
|
)
|
|
195
159
|
),
|
|
196
160
|
Match.orElse(({ spawn }) => FridaSession.layer(spawn))
|
|
197
161
|
);
|
|
198
162
|
|
|
199
|
-
const FridaLive = Layer.provide(SessionLive, DeviceLive)
|
|
200
|
-
const ScriptLive =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
163
|
+
const FridaLive = Layer.provide(SessionLive, DeviceLive);
|
|
164
|
+
const ScriptLive = Effect.gen(function* () {
|
|
165
|
+
const path = yield* Path.Path;
|
|
166
|
+
const fs = yield* FileSystem.FileSystem;
|
|
167
|
+
|
|
168
|
+
const tempDir = path.join(poolOptions.project.config.root, "temp");
|
|
169
|
+
yield* fs.makeDirectory(tempDir, { recursive: true });
|
|
170
|
+
|
|
171
|
+
const agentUrl = yield* path.fromFileUrl(new URL("../frida/agent.ts", import.meta.url));
|
|
172
|
+
const baseAgent = yield* fs.readFileString(agentUrl);
|
|
173
|
+
const tempFile = yield* fs.makeTempFileScoped({
|
|
174
|
+
directory: tempDir,
|
|
175
|
+
prefix: ".vitest-frida-pool-agent-",
|
|
176
|
+
suffix: ".ts",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const setupFiles = poolOptions.project.config.setupFiles;
|
|
180
|
+
const testFiles = yield* Effect.map(
|
|
181
|
+
Effect.promise(() => poolOptions.project.globTestFiles()),
|
|
182
|
+
({ testFiles }) => testFiles
|
|
183
|
+
);
|
|
184
|
+
const globalSetupFiles = Array.isArray(poolOptions.project.config.globalSetup)
|
|
185
|
+
? poolOptions.project.config.globalSetup
|
|
186
|
+
: Array.make(poolOptions.project.config.globalSetup);
|
|
187
|
+
|
|
188
|
+
const marker = "// @efffrida/vitest-pool/agent/file-map";
|
|
189
|
+
const allFiles = Array.flatten([setupFiles, testFiles, globalSetupFiles]);
|
|
190
|
+
const newContent = Function.pipe(
|
|
191
|
+
allFiles,
|
|
192
|
+
Array.map(
|
|
193
|
+
(file) => `
|
|
194
|
+
if (_file === "${file}") {
|
|
195
|
+
// @ts-ignore
|
|
196
|
+
return await import("${file}")
|
|
197
|
+
}`
|
|
198
|
+
),
|
|
199
|
+
Array.join("\n")
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
yield* fs.writeFileString(tempFile, baseAgent.replace(marker, newContent));
|
|
203
|
+
return yield* path.toFileUrl(tempFile);
|
|
204
|
+
}).pipe(
|
|
205
|
+
Effect.map(
|
|
206
|
+
FridaScript.layer({
|
|
207
|
+
...(FridaRuntime !== undefined ? { runtime: FridaRuntime } : {}),
|
|
208
|
+
...(FridaPlatform !== undefined ? { platform: FridaPlatform } : {}),
|
|
209
|
+
})
|
|
210
|
+
),
|
|
211
|
+
Layer.unwrap,
|
|
212
|
+
Layer.provide(FridaLive),
|
|
213
|
+
Layer.provide(NodeServices.layer),
|
|
214
|
+
Layer.satisfiesSuccessType<FridaScript.FridaScript>()
|
|
215
|
+
);
|
|
205
216
|
|
|
206
|
-
this.
|
|
217
|
+
this.scope = Scope.makeUnsafe();
|
|
218
|
+
const prev = FridaPoolWorker.initQueue;
|
|
219
|
+
const runInit = () => ScriptLive.pipe(Layer.buildWithScope(this.scope), Effect.runPromise);
|
|
220
|
+
this.scriptContext = prev.then(runInit, runInit);
|
|
221
|
+
FridaPoolWorker.initQueue = this.scriptContext.then(
|
|
222
|
+
() => undefined,
|
|
223
|
+
() => undefined
|
|
224
|
+
);
|
|
225
|
+
}
|
|
207
226
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const prettyError = Cause.prettyErrors(exit.cause);
|
|
211
|
-
throw prettyError[0];
|
|
227
|
+
async start(): Promise<void> {
|
|
228
|
+
await this.scriptContext;
|
|
212
229
|
}
|
|
213
230
|
|
|
214
231
|
async stop(): Promise<void> {
|
|
232
|
+
await Promise.allSettled(this.sends);
|
|
215
233
|
for (const cancelable of this.cancelables.values()) cancelable();
|
|
234
|
+
await Scope.close(this.scope, Exit.void).pipe(Effect.runPromise);
|
|
216
235
|
this.cancelables.clear();
|
|
217
|
-
// await Promise.allSettled(this.sends);
|
|
218
|
-
await this.managedRuntime!.dispose();
|
|
219
|
-
await Effect.runPromise(Scope.close(this.modifiedAgentScope, Exit.void));
|
|
220
236
|
}
|
|
221
237
|
|
|
222
238
|
async send(message: VitestNode.WorkerRequest): Promise<void> {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
239
|
+
const context = await this.scriptContext;
|
|
240
|
+
let sendPromise: Promise<unknown> = undefined!;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
sendPromise = Effect.flatMap(FridaScript.FridaScript, (fridaScript) =>
|
|
244
|
+
fridaScript.callExport("onMessage")(message)
|
|
245
|
+
).pipe(Effect.runPromiseWith(context));
|
|
246
|
+
this.sends.push(sendPromise);
|
|
247
|
+
await sendPromise;
|
|
248
|
+
} finally {
|
|
249
|
+
this.sends = this.sends.filter((p) => p !== sendPromise);
|
|
250
|
+
}
|
|
235
251
|
}
|
|
236
252
|
|
|
237
253
|
on(event: string, callback: (arg: any) => void): void {
|
|
238
|
-
let cancelable!: Runtime.Cancel<
|
|
239
|
-
unknown,
|
|
240
|
-
| FridaDeviceAcquisitionError.FridaDeviceAcquisitionError
|
|
241
|
-
| PlatformError.PlatformError
|
|
242
|
-
| FridaSessionError.FridaSessionError
|
|
243
|
-
>;
|
|
244
|
-
|
|
245
|
-
const sink = Sink.forEach<
|
|
246
|
-
{
|
|
247
|
-
message: unknown;
|
|
248
|
-
data: Option.Option<Buffer<ArrayBufferLike>>;
|
|
249
|
-
},
|
|
250
|
-
void,
|
|
251
|
-
never,
|
|
252
|
-
never
|
|
253
|
-
>((input) =>
|
|
254
|
-
Effect.sync(() => {
|
|
255
|
-
callback(input.message);
|
|
256
|
-
})
|
|
257
|
-
);
|
|
258
|
-
|
|
259
254
|
switch (event) {
|
|
260
|
-
case "message":
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
255
|
+
case "message": {
|
|
256
|
+
this.scriptContext.then((ctx) => {
|
|
257
|
+
this.cancelables.set(
|
|
258
|
+
callback,
|
|
259
|
+
Effect.runCallbackWith(ctx)(
|
|
260
|
+
Effect.flatMap(FridaScript.FridaScript, (fridaScript) =>
|
|
261
|
+
Stream.runForEach(fridaScript.stream, (input) =>
|
|
262
|
+
Effect.sync(() => callback(input.message))
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
264
269
|
break;
|
|
270
|
+
}
|
|
265
271
|
|
|
266
272
|
case "error":
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
273
|
+
this.scriptContext.then((ctx) => {
|
|
274
|
+
this.cancelables.set(
|
|
275
|
+
callback,
|
|
276
|
+
Effect.runCallbackWith(ctx)(
|
|
277
|
+
Effect.flatMap(FridaScript.FridaScript, (fridaScript) =>
|
|
278
|
+
Deferred.await(fridaScript.scriptError)
|
|
279
|
+
),
|
|
280
|
+
{
|
|
281
|
+
onExit: (exit) => {
|
|
282
|
+
if (Exit.isSuccess(exit)) {
|
|
283
|
+
callback(exit.value);
|
|
284
|
+
} else if (!Cause.hasInterruptsOnly(exit.cause)) {
|
|
285
|
+
callback(exit.cause);
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
271
293
|
break;
|
|
272
294
|
|
|
273
295
|
case "exit":
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
296
|
+
this.scriptContext.then((ctx) => {
|
|
297
|
+
this.cancelables.set(
|
|
298
|
+
callback,
|
|
299
|
+
Effect.runCallbackWith(ctx)(
|
|
300
|
+
Effect.flatMap(FridaScript.FridaScript, (fridaScript) =>
|
|
301
|
+
Deferred.await(fridaScript.destroyed)
|
|
302
|
+
),
|
|
303
|
+
{ onExit: () => callback(void 0) }
|
|
304
|
+
)
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
278
308
|
break;
|
|
279
309
|
|
|
280
310
|
default:
|
|
281
311
|
throw new Error(`Event ${event} not supported in FridaPoolWorker`);
|
|
282
312
|
}
|
|
283
|
-
|
|
284
|
-
this.cancelables.set(callback, cancelable);
|
|
285
313
|
}
|
|
286
314
|
|
|
287
315
|
off(_event: string, callback: (arg: any) => void): void {
|
|
@@ -307,206 +335,11 @@ export class FridaPoolWorker implements VitestNode.PoolWorker {
|
|
|
307
335
|
* @category Tests
|
|
308
336
|
*/
|
|
309
337
|
export const createFridaPool = (
|
|
310
|
-
customOptions: Schema.
|
|
338
|
+
customOptions: Schema.Codec.Encoded<typeof ConfigSchema>
|
|
311
339
|
): VitestNode.PoolRunnerInitializer => {
|
|
312
|
-
const decoded = Schema.decodeUnknownSync(ConfigSchema)(customOptions);
|
|
313
340
|
return {
|
|
314
341
|
name: "frida-pool",
|
|
315
|
-
createPoolWorker: (options: VitestNode.PoolOptions) =>
|
|
342
|
+
createPoolWorker: (options: VitestNode.PoolOptions) =>
|
|
343
|
+
new FridaPoolWorker(options, Schema.decodeUnknownSync(ConfigSchema)(customOptions)),
|
|
316
344
|
};
|
|
317
345
|
};
|
|
318
|
-
|
|
319
|
-
/** @internal */
|
|
320
|
-
const vitestGlobalsPlugin: Esbuild.Plugin = {
|
|
321
|
-
name: "vitest-globals",
|
|
322
|
-
setup(build) {
|
|
323
|
-
// Handle vitest and @vitest/* imports
|
|
324
|
-
build.onResolve({ filter: /^(vitest|@vitest\/.*)$/ }, (args) => ({
|
|
325
|
-
path: args.path,
|
|
326
|
-
namespace: "vitest-globals",
|
|
327
|
-
}));
|
|
328
|
-
|
|
329
|
-
build.onLoad({ filter: /.*/, namespace: "vitest-globals" }, (args) => {
|
|
330
|
-
if (args.path === "vitest") {
|
|
331
|
-
return {
|
|
332
|
-
loader: "js",
|
|
333
|
-
contents: "export * from 'VITEST_GLOBAL';\nexport { default } from 'VITEST_GLOBAL';",
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
if (args.path === "@vitest/runner") {
|
|
337
|
-
return {
|
|
338
|
-
loader: "js",
|
|
339
|
-
contents: "export * from 'VITEST_RUNNER_GLOBAL';\nexport { default } from 'VITEST_RUNNER_GLOBAL';",
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
return {
|
|
343
|
-
loader: "js",
|
|
344
|
-
contents: "export * from 'VITEST_GLOBAL';\nexport { default } from 'VITEST_GLOBAL';",
|
|
345
|
-
};
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
build.onResolve({ filter: /^VITEST_(GLOBAL|RUNNER_GLOBAL)$/ }, (args) => ({
|
|
349
|
-
path: args.path,
|
|
350
|
-
namespace: "vitest-synthetic",
|
|
351
|
-
}));
|
|
352
|
-
|
|
353
|
-
build.onLoad({ filter: /.*/, namespace: "vitest-synthetic" }, (args) => {
|
|
354
|
-
const globalName = args.path === "VITEST_GLOBAL" ? "__vitest" : "__vitest_runner";
|
|
355
|
-
return {
|
|
356
|
-
loader: "js",
|
|
357
|
-
contents: `
|
|
358
|
-
const g = globalThis.${globalName};
|
|
359
|
-
export default g;
|
|
360
|
-
export const {
|
|
361
|
-
describe, it, test, expect, vi, beforeAll, afterAll,
|
|
362
|
-
beforeEach, afterEach, suite, bench, assert
|
|
363
|
-
} = g;
|
|
364
|
-
`,
|
|
365
|
-
};
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
// Handle Node.js built-in modules - redirect to globals set up by the agent
|
|
369
|
-
// The agent imports these from frida-compile's shims and exposes them as globals
|
|
370
|
-
const nodeModuleMap: Record<string, string> = {
|
|
371
|
-
"node:assert": "__node_assert",
|
|
372
|
-
"node:buffer": "__node_buffer",
|
|
373
|
-
"node:crypto": "__node_crypto",
|
|
374
|
-
"node:diagnostics_channel": "__node_diagnosticsChannel",
|
|
375
|
-
"node:events": "__node_events",
|
|
376
|
-
"node:fs": "__node_fs",
|
|
377
|
-
"node:net": "__node_net",
|
|
378
|
-
"node:os": "__node_os",
|
|
379
|
-
"node:path": "__node_path",
|
|
380
|
-
"node:process": "__node_process",
|
|
381
|
-
"node:stream": "__node_stream",
|
|
382
|
-
"node:timers": "__node_timers",
|
|
383
|
-
"node:tty": "__node_tty",
|
|
384
|
-
"node:url": "__node_url",
|
|
385
|
-
"node:util": "__node_util",
|
|
386
|
-
"node:vm": "__node_vm",
|
|
387
|
-
assert: "__node_assert",
|
|
388
|
-
buffer: "__node_buffer",
|
|
389
|
-
crypto: "__node_crypto",
|
|
390
|
-
diagnostics_channel: "__node_diagnosticsChannel",
|
|
391
|
-
events: "__node_events",
|
|
392
|
-
fs: "__node_fs",
|
|
393
|
-
net: "__node_net",
|
|
394
|
-
os: "__node_os",
|
|
395
|
-
path: "__node_path",
|
|
396
|
-
process: "__node_process",
|
|
397
|
-
stream: "__node_stream",
|
|
398
|
-
timers: "__node_timers",
|
|
399
|
-
tty: "__node_tty",
|
|
400
|
-
url: "__node_url",
|
|
401
|
-
util: "__node_util",
|
|
402
|
-
vm: "__node_vm",
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
build.onResolve(
|
|
406
|
-
{
|
|
407
|
-
filter: /^(node:)?(assert|buffer|crypto|diagnostics_channel|events|fs|net|os|path|process|stream|timers|tty|url|util|vm)$/,
|
|
408
|
-
},
|
|
409
|
-
(args) => ({
|
|
410
|
-
path: args.path,
|
|
411
|
-
namespace: "node-globals",
|
|
412
|
-
})
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
build.onLoad({ filter: /.*/, namespace: "node-globals" }, (args) => {
|
|
416
|
-
const globalName = nodeModuleMap[args.path];
|
|
417
|
-
if (!globalName) {
|
|
418
|
-
return { loader: "js", contents: "export default {};" };
|
|
419
|
-
}
|
|
420
|
-
return {
|
|
421
|
-
loader: "js",
|
|
422
|
-
contents: `
|
|
423
|
-
const m = globalThis.${globalName};
|
|
424
|
-
export default m;
|
|
425
|
-
export const { Buffer } = m.Buffer ? m : { Buffer: m.default?.Buffer };
|
|
426
|
-
export * from 'NODE_MODULE_REEXPORT_${globalName}';
|
|
427
|
-
`,
|
|
428
|
-
};
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
// Handle re-exports from node modules
|
|
432
|
-
build.onResolve({ filter: /^NODE_MODULE_REEXPORT_/ }, (args) => ({
|
|
433
|
-
path: args.path,
|
|
434
|
-
namespace: "node-reexport",
|
|
435
|
-
}));
|
|
436
|
-
|
|
437
|
-
build.onLoad({ filter: /.*/, namespace: "node-reexport" }, (args) => {
|
|
438
|
-
const globalName = args.path.replace("NODE_MODULE_REEXPORT_", "");
|
|
439
|
-
return {
|
|
440
|
-
loader: "js",
|
|
441
|
-
contents: `
|
|
442
|
-
const m = globalThis.${globalName};
|
|
443
|
-
const mod = m.default || m;
|
|
444
|
-
for (const key in mod) {
|
|
445
|
-
if (key !== 'default') {
|
|
446
|
-
Object.defineProperty(exports, key, {
|
|
447
|
-
enumerable: true,
|
|
448
|
-
get: () => mod[key]
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
`,
|
|
453
|
-
};
|
|
454
|
-
});
|
|
455
|
-
},
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
/** @internal */
|
|
459
|
-
const compileTestFiles = Effect.fnUntraced(function* (
|
|
460
|
-
agentTemplatePath: URL,
|
|
461
|
-
poolOptions: VitestNode.PoolOptions,
|
|
462
|
-
customOptions?: Schema.Schema.Type<ConfigSchema> | undefined
|
|
463
|
-
): Effect.fn.Return<URL, PlatformError.PlatformError, Path.Path | FileSystem.FileSystem | Scope.Scope> {
|
|
464
|
-
const path = yield* Path.Path;
|
|
465
|
-
const fs = yield* FileSystem.FileSystem;
|
|
466
|
-
const url = yield* path.fromFileUrl(agentTemplatePath);
|
|
467
|
-
|
|
468
|
-
const testFiles: Array<string> = (poolOptions.project as any).testFilesList ?? [];
|
|
469
|
-
const testFilesMap: Record<string, string> = {};
|
|
470
|
-
|
|
471
|
-
const esbuildPlatform = Match.value(customOptions?.platform).pipe(
|
|
472
|
-
Match.when("browser", () => "browser" as const),
|
|
473
|
-
Match.when("neutral", () => "neutral" as const),
|
|
474
|
-
Match.whenOr("gum", undefined, () => "node" as const),
|
|
475
|
-
Match.orElseAbsurd
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
for (const testFile of testFiles) {
|
|
479
|
-
const result = yield* Effect.promise(() =>
|
|
480
|
-
Esbuild.build({
|
|
481
|
-
bundle: true,
|
|
482
|
-
write: false,
|
|
483
|
-
format: "esm",
|
|
484
|
-
platform: esbuildPlatform,
|
|
485
|
-
target: "es2020",
|
|
486
|
-
entryPoints: [testFile],
|
|
487
|
-
plugins: [vitestGlobalsPlugin],
|
|
488
|
-
})
|
|
489
|
-
);
|
|
490
|
-
|
|
491
|
-
if (!result.outputFiles || result.outputFiles.length === 0) {
|
|
492
|
-
return yield* Effect.dieMessage(`esbuild produced no output for ${testFile}`);
|
|
493
|
-
} else {
|
|
494
|
-
testFilesMap[testFile] = Buffer.from(result.outputFiles[0].text).toString("base64");
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const agentTemplateContents = yield* fs.readFileString(url);
|
|
499
|
-
const modifiedAgentContents = agentTemplateContents.replace(
|
|
500
|
-
/^const testFiles: Record<string, string> = \{\};$/m,
|
|
501
|
-
`const testFiles: Record<string, string> = ${JSON.stringify(testFilesMap, null, 4)};`
|
|
502
|
-
);
|
|
503
|
-
|
|
504
|
-
const tempAgentPath = yield* fs.makeTempFileScoped({
|
|
505
|
-
suffix: ".ts",
|
|
506
|
-
prefix: ".agent-",
|
|
507
|
-
directory: path.dirname(url),
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
yield* fs.writeFileString(tempAgentPath, modifiedAgentContents);
|
|
511
|
-
return yield* path.toFileUrl(tempAgentPath);
|
|
512
|
-
});
|