preflite 1.0.1
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/LICENSE +201 -0
- package/README.md +144 -0
- package/dist/adapter-spi/artifact/index.js +1 -0
- package/dist/adapter-spi/command/index.js +1 -0
- package/dist/adapter-spi/install/index.js +1 -0
- package/dist/adapter-spi/lifecycle/index.js +1 -0
- package/dist/adapter-spi/resource/index.js +1 -0
- package/dist/adapter-spi/snapshot/index.js +1 -0
- package/dist/application/agent/AgentCommandPollLoop.js +48 -0
- package/dist/application/agent/AgentRuntimeService.js +27 -0
- package/dist/application/app-package/AppPackageApplicationService.js +97 -0
- package/dist/application/artifact/ArtifactApplicationService.js +13 -0
- package/dist/application/debug/DebugApplicationService.js +117 -0
- package/dist/application/health/HealthMetricsService.js +47 -0
- package/dist/application/lease/LeaseApplicationService.js +79 -0
- package/dist/application/query/ObservationQueryService.js +48 -0
- package/dist/application/reporter/ReporterApplicationService.js +41 -0
- package/dist/application/resource/ResourceOccupationReleaseService.js +49 -0
- package/dist/application/resource/ResourceRegistryService.js +113 -0
- package/dist/application/session/SessionApplicationService.js +39 -0
- package/dist/application/task/TaskApplicationService.js +378 -0
- package/dist/client/agentAppPackageClient.js +91 -0
- package/dist/domain/agent/AgentNode.js +12 -0
- package/dist/domain/agent/AgentRuntime.js +6 -0
- package/dist/domain/artifact/ArtifactPipeline.js +6 -0
- package/dist/domain/artifact/ArtifactRef.js +12 -0
- package/dist/domain/event/AgentEvent.js +10 -0
- package/dist/domain/event/Reporter.js +6 -0
- package/dist/domain/health/HealthMetrics.js +8 -0
- package/dist/domain/lease/Lease.js +28 -0
- package/dist/domain/lease/LeaseManager.js +6 -0
- package/dist/domain/repositories/index.js +1 -0
- package/dist/domain/resource/DeviceDetails.js +23 -0
- package/dist/domain/resource/DeviceResource.js +16 -0
- package/dist/domain/resource/ResourceRegistry.js +6 -0
- package/dist/domain/runtime/interfaces.js +1 -0
- package/dist/domain/session/BaseSession.js +16 -0
- package/dist/domain/session/DebugSession.js +3 -0
- package/dist/domain/session/ExecutionSession.js +3 -0
- package/dist/domain/session/SessionManager.js +8 -0
- package/dist/domain/task/TaskRecord.js +14 -0
- package/dist/domain/task/TaskSpec.js +12 -0
- package/dist/infrastructure/adapters/AdapterRegistry.js +10 -0
- package/dist/infrastructure/adapters/BridgeAdapters.js +6 -0
- package/dist/infrastructure/adapters/android/AndroidResourceAdapter.js +51 -0
- package/dist/infrastructure/adapters/deviceDetailsProbe.js +229 -0
- package/dist/infrastructure/adapters/harmony/HarmonyResourceAdapter.js +40 -0
- package/dist/infrastructure/adapters/ios/IOSResourceAdapter.js +182 -0
- package/dist/infrastructure/adapters/real/ShellCommandAndSnapshot.js +41 -0
- package/dist/infrastructure/airtest/AirtestRuntime.js +168 -0
- package/dist/infrastructure/app-package/AppPackageUrlCache.js +191 -0
- package/dist/infrastructure/app-package/appPackageDownloadDir.js +13 -0
- package/dist/infrastructure/bootstrap/BuildRuntimeContext.js +88 -0
- package/dist/infrastructure/cache/DirCapacityWatchdog.js +150 -0
- package/dist/infrastructure/config/agentConfigFile.js +96 -0
- package/dist/infrastructure/device/DeviceAppPackageOps.js +146 -0
- package/dist/infrastructure/ios/IOSWdaWatchdog.js +207 -0
- package/dist/infrastructure/live-debug/LiveDebugSessionManager.js +74 -0
- package/dist/infrastructure/live-debug/RuntimeLiveDebugAdapters.js +19 -0
- package/dist/infrastructure/midscene/DebugRuntimeImpl.js +533 -0
- package/dist/infrastructure/midscene/MidsceneRuntimeMock.js +22 -0
- package/dist/infrastructure/midscene/MidsceneRuntimeReal.js +552 -0
- package/dist/infrastructure/midscene/executionDumpWatcher.js +219 -0
- package/dist/infrastructure/midscene/videoRecorder.js +365 -0
- package/dist/infrastructure/midscene/zipReportDir.js +36 -0
- package/dist/infrastructure/persistence/InMemoryRepositories.js +94 -0
- package/dist/infrastructure/resilience/DeliveryIdDeduper.js +26 -0
- package/dist/infrastructure/system/CommandRunner.js +128 -0
- package/dist/infrastructure/transport/http/AgentEventHttpIngestClient.js +52 -0
- package/dist/infrastructure/transport/http/CallbackOutboxStore.js +106 -0
- package/dist/infrastructure/transport/http/PlatformCallbackClient.js +113 -0
- package/dist/infrastructure/transport/http/PlatformCommandPollClient.js +89 -0
- package/dist/infrastructure/transport/http/ResilientPlatformCallbackClient.js +117 -0
- package/dist/infrastructure/transport/midscenePaths.js +28 -0
- package/dist/infrastructure/transport/ws/ResilientWsOrHttpEventPublisher.js +29 -0
- package/dist/infrastructure/transport/ws/WsClient.js +182 -0
- package/dist/infrastructure/transport/ws/WsEventPublisher.js +36 -0
- package/dist/interfaces/http/HttpServer.js +227 -0
- package/dist/interfaces/websocket/AgentWsGateway.js +227 -0
- package/dist/main.js +368 -0
- package/dist/mcp/agentHttpClient.js +82 -0
- package/dist/mcp/agentRuntime.js +184 -0
- package/dist/mcp/cli.js +36 -0
- package/dist/mcp/doctor.js +124 -0
- package/dist/mcp/evidence.js +57 -0
- package/dist/mcp/exploration/index.js +129 -0
- package/dist/mcp/exploration/sessionManager.js +122 -0
- package/dist/mcp/exploration/tools-atomic.js +34 -0
- package/dist/mcp/exploration/tools-intelligent.js +33 -0
- package/dist/mcp/exploration/tools-session.js +276 -0
- package/dist/mcp/exploration/types.js +1 -0
- package/dist/mcp/flowStepEvents.js +114 -0
- package/dist/mcp/liveViewer.js +156 -0
- package/dist/mcp/reportReader.js +157 -0
- package/dist/mcp/runManager.js +161 -0
- package/dist/mcp/runSummary.js +72 -0
- package/dist/mcp/runtimeInstall.js +44 -0
- package/dist/mcp/server.js +260 -0
- package/dist/mcp/setup.js +185 -0
- package/dist/mcp/types.js +1 -0
- package/dist/mcp/userConfig.js +45 -0
- package/dist/mcp/visual-flow/codegen.js +576 -0
- package/dist/mcp/visual-flow/index.js +14 -0
- package/dist/mcp/visual-flow/types.js +5 -0
- package/dist/mcp/visual-flow/validate.js +617 -0
- package/dist/protocol-contracts/commands/envelope.js +24 -0
- package/dist/protocol-contracts/commands/index.js +1 -0
- package/dist/protocol-contracts/dto/index.js +1 -0
- package/dist/protocol-contracts/events/index.js +1 -0
- package/dist/protocol-contracts/queries/index.js +1 -0
- package/dist/shared-kernel/enums/index.js +70 -0
- package/dist/shared-kernel/errors/index.js +21 -0
- package/dist/shared-kernel/ids/index.js +18 -0
- package/dist/shared-kernel/time/index.js +3 -0
- package/dist/shared-kernel/value-objects/index.js +1 -0
- package/dist/utils/appPackageLocalPath.js +75 -0
- package/dist/utils/deviceResourceRouting.js +24 -0
- package/dist/utils/harmonyAgentDebugDevice.js +60 -0
- package/dist/utils/harmonyHdcDeviceId.js +134 -0
- package/dist/utils/iosAgentDebugDevice.js +72 -0
- package/dist/utils/iosMjpegCapture.js +90 -0
- package/dist/utils/liveDebugForegroundParse.js +71 -0
- package/dist/utils/midscene-device-session.js +353 -0
- package/dist/utils/midscene-task-cache-env.js +15 -0
- package/dist/utils/midsceneReportConstants.js +49 -0
- package/dist/utils/seedMidsceneTaskCache.js +61 -0
- package/dist/utils/task-runners/context/androidTaskRunnerContext.js +1 -0
- package/dist/utils/task-runners/context/harmonyTaskRunnerContext.js +1 -0
- package/dist/utils/task-runners/context/iosTaskRunnerContext.js +1 -0
- package/dist/utils/task-runners/runAndroidNativeAppTask.js +29 -0
- package/dist/utils/task-runners/runHarmonyNativeAppTask.js +36 -0
- package/dist/utils/task-runners/runIosNativeAppTask.js +30 -0
- package/dist/utils/task-runners/taskAppPackage.js +20 -0
- package/dist/utils/wrapper/resolveTaskRunnerImport.js +11 -0
- package/dist/utils/wrapper/wrapAndroidTaskScript.js +38 -0
- package/dist/utils/wrapper/wrapHarmonyTaskScript.js +42 -0
- package/dist/utils/wrapper/wrapIosTaskScript.js +30 -0
- package/package.json +46 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { ArtifactType, PlatformType } from "../../shared-kernel/enums/index.js";
|
|
6
|
+
import { wrapAndroidTaskScript } from "../../utils/wrapper/wrapAndroidTaskScript.js";
|
|
7
|
+
import { wrapHarmonyTaskScript } from "../../utils/wrapper/wrapHarmonyTaskScript.js";
|
|
8
|
+
import { wrapIosTaskScript } from "../../utils/wrapper/wrapIosTaskScript.js";
|
|
9
|
+
import { getMidsceneReportRootDir, resolveMidsceneRunDir, resolveTaskReportFilePaths } from "../transport/midscenePaths.js";
|
|
10
|
+
import { startExecutionDumpWatcher } from "./executionDumpWatcher.js";
|
|
11
|
+
import { zipDirectoryToFile } from "./zipReportDir.js";
|
|
12
|
+
import { resolveVideoRecorderConfig, startVideoRecording } from "./videoRecorder.js";
|
|
13
|
+
import { AirtestRuntime } from "../airtest/AirtestRuntime.js";
|
|
14
|
+
const WDA_ALREADY_DEBUGGING_EXIT_CODE = 20;
|
|
15
|
+
const DEFAULT_ANDROID_ADB_HOST = "127.0.0.1";
|
|
16
|
+
const DEFAULT_ANDROID_ADB_PORT = 5037;
|
|
17
|
+
const DEFAULT_IOS_WDA_HOST = "127.0.0.1";
|
|
18
|
+
function renderMidsceneCommand(template, resourceId, scriptFile) {
|
|
19
|
+
return template.replaceAll("{resourceId}", resourceId).replaceAll("{scriptFile}", scriptFile);
|
|
20
|
+
}
|
|
21
|
+
function extractDeviceToken(resourceId) {
|
|
22
|
+
const idx = resourceId.indexOf(":");
|
|
23
|
+
return idx >= 0 ? resourceId.slice(idx + 1) : resourceId;
|
|
24
|
+
}
|
|
25
|
+
function pickNonEmptyString(...values) {
|
|
26
|
+
for (const value of values) {
|
|
27
|
+
if (typeof value !== "string")
|
|
28
|
+
continue;
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
if (trimmed)
|
|
31
|
+
return trimmed;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
function toPositivePort(value, name) {
|
|
36
|
+
const parsed = Number(value);
|
|
37
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
38
|
+
throw new Error(`${name} is required and must be a positive number`);
|
|
39
|
+
}
|
|
40
|
+
return Math.floor(parsed);
|
|
41
|
+
}
|
|
42
|
+
function renderTemplate(template, resourceId, wdaPort) {
|
|
43
|
+
const deviceId = extractDeviceToken(resourceId);
|
|
44
|
+
return template
|
|
45
|
+
.replaceAll("{resourceId}", resourceId)
|
|
46
|
+
.replaceAll("{deviceId}", deviceId)
|
|
47
|
+
.replaceAll("{wdaPort}", String(wdaPort));
|
|
48
|
+
}
|
|
49
|
+
function numberFromEnv(env, key, fallback, min, max) {
|
|
50
|
+
const raw = Number(env[key]);
|
|
51
|
+
if (!Number.isFinite(raw))
|
|
52
|
+
return fallback;
|
|
53
|
+
return Math.min(max, Math.max(min, raw));
|
|
54
|
+
}
|
|
55
|
+
function truthyFromEnv(env, key, fallback) {
|
|
56
|
+
const raw = env[key]?.trim().toLowerCase();
|
|
57
|
+
if (!raw)
|
|
58
|
+
return fallback;
|
|
59
|
+
if (["1", "true", "yes", "on"].includes(raw))
|
|
60
|
+
return true;
|
|
61
|
+
if (["0", "false", "no", "off"].includes(raw))
|
|
62
|
+
return false;
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
async function sleep(ms, signal) {
|
|
66
|
+
if (signal?.aborted)
|
|
67
|
+
throw new Error("cancelled");
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
const t = setTimeout(() => {
|
|
70
|
+
signal?.removeEventListener("abort", onAbort);
|
|
71
|
+
resolve();
|
|
72
|
+
}, ms);
|
|
73
|
+
const onAbort = () => {
|
|
74
|
+
clearTimeout(t);
|
|
75
|
+
reject(new Error("cancelled"));
|
|
76
|
+
};
|
|
77
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export class MidsceneRuntimeReal {
|
|
81
|
+
commandRunner;
|
|
82
|
+
runCommandTemplate;
|
|
83
|
+
options;
|
|
84
|
+
resourceWdaPort = new Map();
|
|
85
|
+
preparedSessions = new Map();
|
|
86
|
+
airtestRuntime;
|
|
87
|
+
constructor(commandRunner, runCommandTemplate, options) {
|
|
88
|
+
this.commandRunner = commandRunner;
|
|
89
|
+
this.runCommandTemplate = runCommandTemplate;
|
|
90
|
+
this.options = options;
|
|
91
|
+
this.airtestRuntime = new AirtestRuntime(commandRunner, {
|
|
92
|
+
runCommandTimeoutMs: options.runCommandTimeoutMs,
|
|
93
|
+
androidAdbHost: options.androidAdbHost,
|
|
94
|
+
androidAdbPort: options.androidAdbPort,
|
|
95
|
+
iosWdaHost: options.iosWdaHost,
|
|
96
|
+
harmonyHdcPath: options.harmonyHdcPath,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async canBindPort(port) {
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
const server = net.createServer();
|
|
102
|
+
server.unref();
|
|
103
|
+
server.once("error", () => resolve(false));
|
|
104
|
+
server.listen(port, "127.0.0.1", () => {
|
|
105
|
+
server.close(() => resolve(true));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
normalizePortRange() {
|
|
110
|
+
const fallbackStart = 8200;
|
|
111
|
+
const fallbackEnd = 8399;
|
|
112
|
+
const start = Number.isFinite(this.options.iosWdaPortRangeStart) && this.options.iosWdaPortRangeStart > 0
|
|
113
|
+
? Math.floor(this.options.iosWdaPortRangeStart)
|
|
114
|
+
: fallbackStart;
|
|
115
|
+
const end = Number.isFinite(this.options.iosWdaPortRangeEnd) && this.options.iosWdaPortRangeEnd > 0
|
|
116
|
+
? Math.floor(this.options.iosWdaPortRangeEnd)
|
|
117
|
+
: fallbackEnd;
|
|
118
|
+
return start <= end ? { start, end } : { start: end, end: start };
|
|
119
|
+
}
|
|
120
|
+
async resolveWdaStartupPortCandidate(resourceId) {
|
|
121
|
+
const { start, end } = this.normalizePortRange();
|
|
122
|
+
const usedByOthers = new Set(Array.from(this.resourceWdaPort.entries())
|
|
123
|
+
.filter(([rid]) => rid !== resourceId)
|
|
124
|
+
.map(([, port]) => port));
|
|
125
|
+
for (let port = start; port <= end; port += 1) {
|
|
126
|
+
if (usedByOthers.has(port))
|
|
127
|
+
continue;
|
|
128
|
+
if (!(await this.canBindPort(port)))
|
|
129
|
+
continue;
|
|
130
|
+
return port;
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`no available WDA port in range ${start}-${end}`);
|
|
133
|
+
}
|
|
134
|
+
async isWdaHealthy(resourceId, wdaPort, timeoutMs = 2_000) {
|
|
135
|
+
const url = `http://127.0.0.1:${wdaPort}/status`;
|
|
136
|
+
const curlTimeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1_000));
|
|
137
|
+
const escapedUrl = JSON.stringify(url);
|
|
138
|
+
const command = `curl -fsS --max-time ${curlTimeoutSeconds} ${escapedUrl}`;
|
|
139
|
+
const result = await this.commandRunner.run(command, timeoutMs + 500);
|
|
140
|
+
if (!result.ok)
|
|
141
|
+
return false;
|
|
142
|
+
return result.stdout.trim().length > 0;
|
|
143
|
+
}
|
|
144
|
+
async findHealthyWdaPortInRange(resourceId, signal) {
|
|
145
|
+
const cachedPort = this.resourceWdaPort.get(resourceId);
|
|
146
|
+
if (cachedPort != null && (await this.isWdaHealthy(resourceId, cachedPort))) {
|
|
147
|
+
return cachedPort;
|
|
148
|
+
}
|
|
149
|
+
const { start, end } = this.normalizePortRange();
|
|
150
|
+
for (let port = start; port <= end; port += 1) {
|
|
151
|
+
if (signal?.aborted)
|
|
152
|
+
throw new Error("cancelled");
|
|
153
|
+
if (await this.isWdaHealthy(resourceId, port, 300)) {
|
|
154
|
+
return port;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
async readMappedWdaPort(resourceId) {
|
|
160
|
+
const mapFilePath = this.options.iosWdaPortMapFilePath?.trim();
|
|
161
|
+
if (!mapFilePath)
|
|
162
|
+
return undefined;
|
|
163
|
+
try {
|
|
164
|
+
const raw = await readFile(mapFilePath, "utf8");
|
|
165
|
+
const parsed = JSON.parse(raw);
|
|
166
|
+
const udid = extractDeviceToken(resourceId);
|
|
167
|
+
const portRaw = parsed.portsByUdid?.[udid];
|
|
168
|
+
const port = Number(portRaw);
|
|
169
|
+
if (!Number.isFinite(port) || port <= 0)
|
|
170
|
+
return undefined;
|
|
171
|
+
return Math.floor(port);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
getIosWdaHost() {
|
|
178
|
+
const host = this.options.iosWdaHost?.trim();
|
|
179
|
+
return host ? host : DEFAULT_IOS_WDA_HOST;
|
|
180
|
+
}
|
|
181
|
+
getAndroidAdbHost() {
|
|
182
|
+
const host = this.options.androidAdbHost?.trim();
|
|
183
|
+
return host ? host : DEFAULT_ANDROID_ADB_HOST;
|
|
184
|
+
}
|
|
185
|
+
getAndroidAdbPort() {
|
|
186
|
+
const port = this.options.androidAdbPort;
|
|
187
|
+
if (Number.isFinite(port) && port > 0)
|
|
188
|
+
return Math.floor(port);
|
|
189
|
+
return DEFAULT_ANDROID_ADB_PORT;
|
|
190
|
+
}
|
|
191
|
+
buildIosRuntimeEnv(resourceId, wdaPort) {
|
|
192
|
+
const deviceId = extractDeviceToken(resourceId);
|
|
193
|
+
const wdaHost = this.getIosWdaHost();
|
|
194
|
+
return {
|
|
195
|
+
MIDSCENE_PLATFORM: "ios",
|
|
196
|
+
MIDSCENE_DEVICE_ID: deviceId,
|
|
197
|
+
MIDSCENE_IOS_DEVICE_ID: deviceId,
|
|
198
|
+
MIDSCENE_IOS_WDA_HOST: wdaHost,
|
|
199
|
+
MIDSCENE_IOS_WDA_PORT: String(wdaPort),
|
|
200
|
+
IOS_WDA_PORT: String(wdaPort),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
buildAndroidRuntimeEnv(resourceId) {
|
|
204
|
+
const serial = extractDeviceToken(resourceId);
|
|
205
|
+
return {
|
|
206
|
+
MIDSCENE_PLATFORM: "android",
|
|
207
|
+
MIDSCENE_DEVICE_ID: serial,
|
|
208
|
+
MIDSCENE_ANDROID_SERIAL: serial,
|
|
209
|
+
MIDSCENE_ANDROID_ADB_HOST: this.getAndroidAdbHost(),
|
|
210
|
+
MIDSCENE_ANDROID_ADB_PORT: String(this.getAndroidAdbPort()),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
buildHarmonyRuntimeEnv(resourceId) {
|
|
214
|
+
const deviceId = extractDeviceToken(resourceId);
|
|
215
|
+
const env = {
|
|
216
|
+
MIDSCENE_PLATFORM: "harmony",
|
|
217
|
+
MIDSCENE_DEVICE_ID: deviceId,
|
|
218
|
+
MIDSCENE_HARMONY_DEVICE_ID: deviceId,
|
|
219
|
+
};
|
|
220
|
+
const hdcPath = this.options.harmonyHdcPath?.trim();
|
|
221
|
+
const hdcHost = this.options.harmonyHdcHost?.trim();
|
|
222
|
+
if (hdcPath)
|
|
223
|
+
env.MIDSCENE_HARMONY_HDC_PATH = hdcPath;
|
|
224
|
+
if (hdcHost)
|
|
225
|
+
env.MIDSCENE_HARMONY_HDC_HOST = hdcHost;
|
|
226
|
+
if (Number.isFinite(this.options.harmonyHdcPort) && this.options.harmonyHdcPort > 0) {
|
|
227
|
+
env.MIDSCENE_HARMONY_HDC_PORT = String(Math.floor(this.options.harmonyHdcPort));
|
|
228
|
+
}
|
|
229
|
+
return env;
|
|
230
|
+
}
|
|
231
|
+
buildWrappedScript(task, resourceId, runtimeEnv) {
|
|
232
|
+
if (task.requiredPlatform === PlatformType.IOS) {
|
|
233
|
+
const wdaHost = pickNonEmptyString(runtimeEnv.MIDSCENE_IOS_WDA_HOST, this.getIosWdaHost());
|
|
234
|
+
const wdaPortRaw = pickNonEmptyString(runtimeEnv.MIDSCENE_IOS_WDA_PORT, runtimeEnv.IOS_WDA_PORT);
|
|
235
|
+
const wdaPort = toPositivePort(wdaPortRaw, "MIDSCENE_IOS_WDA_PORT");
|
|
236
|
+
const deviceId = pickNonEmptyString(runtimeEnv.MIDSCENE_IOS_DEVICE_ID, runtimeEnv.MIDSCENE_DEVICE_ID, extractDeviceToken(resourceId));
|
|
237
|
+
return wrapIosTaskScript(task.script, {
|
|
238
|
+
wdaHost: wdaHost ?? this.getIosWdaHost(),
|
|
239
|
+
wdaPort,
|
|
240
|
+
deviceId,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (task.requiredPlatform === PlatformType.ANDROID) {
|
|
244
|
+
const adbHost = pickNonEmptyString(runtimeEnv.MIDSCENE_ANDROID_ADB_HOST, this.getAndroidAdbHost());
|
|
245
|
+
const adbPortRaw = pickNonEmptyString(runtimeEnv.MIDSCENE_ANDROID_ADB_PORT, String(this.getAndroidAdbPort()));
|
|
246
|
+
const serial = pickNonEmptyString(runtimeEnv.MIDSCENE_ANDROID_SERIAL, runtimeEnv.MIDSCENE_DEVICE_ID, extractDeviceToken(resourceId));
|
|
247
|
+
return wrapAndroidTaskScript(task.script, {
|
|
248
|
+
adbHost: adbHost ?? this.getAndroidAdbHost(),
|
|
249
|
+
adbPort: toPositivePort(adbPortRaw, "MIDSCENE_ANDROID_ADB_PORT"),
|
|
250
|
+
serial,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (task.requiredPlatform === PlatformType.HARMONY) {
|
|
254
|
+
const deviceId = pickNonEmptyString(runtimeEnv.MIDSCENE_HARMONY_DEVICE_ID, runtimeEnv.MIDSCENE_DEVICE_ID, extractDeviceToken(resourceId));
|
|
255
|
+
if (!deviceId) {
|
|
256
|
+
throw new Error("MIDSCENE_HARMONY_DEVICE_ID is required");
|
|
257
|
+
}
|
|
258
|
+
const hdcHost = pickNonEmptyString(runtimeEnv.MIDSCENE_HARMONY_HDC_HOST);
|
|
259
|
+
const hdcPortRaw = pickNonEmptyString(runtimeEnv.MIDSCENE_HARMONY_HDC_PORT);
|
|
260
|
+
if ((hdcHost && !hdcPortRaw) || (!hdcHost && hdcPortRaw)) {
|
|
261
|
+
throw new Error("MIDSCENE_HARMONY_HDC_HOST and MIDSCENE_HARMONY_HDC_PORT must be provided together");
|
|
262
|
+
}
|
|
263
|
+
return wrapHarmonyTaskScript(task.script, {
|
|
264
|
+
deviceId,
|
|
265
|
+
hdcPath: pickNonEmptyString(runtimeEnv.MIDSCENE_HARMONY_HDC_PATH),
|
|
266
|
+
hdcTconnHost: hdcHost,
|
|
267
|
+
hdcTconnPort: hdcPortRaw ? toPositivePort(hdcPortRaw, "MIDSCENE_HARMONY_HDC_PORT") : undefined,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
throw new Error(`unsupported platform for wrapping script: ${task.requiredPlatform}`);
|
|
271
|
+
}
|
|
272
|
+
async waitForWdaHealthy(resourceId, wdaPort, signal) {
|
|
273
|
+
const deadline = Date.now() + this.options.iosWdaStartupTimeoutMs;
|
|
274
|
+
while (Date.now() < deadline) {
|
|
275
|
+
if (await this.isWdaHealthy(resourceId, wdaPort))
|
|
276
|
+
return true;
|
|
277
|
+
await sleep(1_000, signal);
|
|
278
|
+
}
|
|
279
|
+
return this.isWdaHealthy(resourceId, wdaPort);
|
|
280
|
+
}
|
|
281
|
+
async prepareIosSession(resourceId, signal) {
|
|
282
|
+
const cachedPort = this.resourceWdaPort.get(resourceId);
|
|
283
|
+
if (cachedPort != null && (await this.isWdaHealthy(resourceId, cachedPort))) {
|
|
284
|
+
return {
|
|
285
|
+
platform: PlatformType.IOS,
|
|
286
|
+
runtimeEnv: this.buildIosRuntimeEnv(resourceId, cachedPort),
|
|
287
|
+
preparedAt: Date.now(),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
const mappedPort = await this.readMappedWdaPort(resourceId);
|
|
291
|
+
if (mappedPort != null && (await this.isWdaHealthy(resourceId, mappedPort))) {
|
|
292
|
+
this.resourceWdaPort.set(resourceId, mappedPort);
|
|
293
|
+
console.info(`[MidsceneRuntimeReal] reuse WDA mapped port resourceId=${resourceId} wdaPort=${mappedPort}`);
|
|
294
|
+
return {
|
|
295
|
+
platform: PlatformType.IOS,
|
|
296
|
+
runtimeEnv: this.buildIosRuntimeEnv(resourceId, mappedPort),
|
|
297
|
+
preparedAt: Date.now(),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (mappedPort == null) {
|
|
301
|
+
const existingPort = await this.findHealthyWdaPortInRange(resourceId, signal);
|
|
302
|
+
if (existingPort != null) {
|
|
303
|
+
this.resourceWdaPort.set(resourceId, existingPort);
|
|
304
|
+
console.info(`[MidsceneRuntimeReal] reuse WDA scanned port resourceId=${resourceId} wdaPort=${existingPort}`);
|
|
305
|
+
return {
|
|
306
|
+
platform: PlatformType.IOS,
|
|
307
|
+
runtimeEnv: this.buildIosRuntimeEnv(resourceId, existingPort),
|
|
308
|
+
preparedAt: Date.now(),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const startTpl = this.options.iosWdaStartCommandTemplate?.trim();
|
|
313
|
+
if (!startTpl) {
|
|
314
|
+
throw new Error(`iOS resource ${resourceId} WDA is not healthy and IOS_WDA_START_COMMAND_TEMPLATE is not configured`);
|
|
315
|
+
}
|
|
316
|
+
const wdaPort = mappedPort ?? (await this.resolveWdaStartupPortCandidate(resourceId));
|
|
317
|
+
const command = renderTemplate(startTpl, resourceId, wdaPort);
|
|
318
|
+
for (let attempt = 1; attempt <= this.options.iosWdaStartupRetry; attempt += 1) {
|
|
319
|
+
console.info(`[MidsceneRuntimeReal] WDA bootstrap start resourceId=${resourceId} wdaPort=${wdaPort} attempt=${attempt}/${this.options.iosWdaStartupRetry}`);
|
|
320
|
+
const started = await this.commandRunner.run(command, this.options.iosWdaStartupTimeoutMs, signal);
|
|
321
|
+
const alreadyDebugging = started.exitCode === WDA_ALREADY_DEBUGGING_EXIT_CODE;
|
|
322
|
+
const shouldWaitForHealth = started.ok || alreadyDebugging;
|
|
323
|
+
if (!shouldWaitForHealth)
|
|
324
|
+
continue;
|
|
325
|
+
if (!started.ok) {
|
|
326
|
+
console.warn(`[MidsceneRuntimeReal] WDA bootstrap command failed resourceId=${resourceId} wdaPort=${wdaPort} attempt=${attempt} stdout=${started.stdout.slice(0, 200)} stderr=${started.stderr.slice(0, 400)}`);
|
|
327
|
+
}
|
|
328
|
+
const ready = await this.waitForWdaHealthy(resourceId, wdaPort, signal);
|
|
329
|
+
if (ready) {
|
|
330
|
+
this.resourceWdaPort.set(resourceId, wdaPort);
|
|
331
|
+
console.info(`[MidsceneRuntimeReal] WDA bootstrap ready resourceId=${resourceId} wdaPort=${wdaPort} attempt=${attempt}`);
|
|
332
|
+
return {
|
|
333
|
+
platform: PlatformType.IOS,
|
|
334
|
+
runtimeEnv: this.buildIosRuntimeEnv(resourceId, wdaPort),
|
|
335
|
+
preparedAt: Date.now(),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
throw new Error(`WDA bootstrap failed for resource ${resourceId} wdaPort=${wdaPort}`);
|
|
340
|
+
}
|
|
341
|
+
async prepareSession(task, resourceId, signal) {
|
|
342
|
+
if (task.requiredPlatform === PlatformType.IOS) {
|
|
343
|
+
return this.prepareIosSession(resourceId, signal);
|
|
344
|
+
}
|
|
345
|
+
if (task.requiredPlatform === PlatformType.ANDROID) {
|
|
346
|
+
return {
|
|
347
|
+
platform: PlatformType.ANDROID,
|
|
348
|
+
runtimeEnv: this.buildAndroidRuntimeEnv(resourceId),
|
|
349
|
+
preparedAt: Date.now(),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (task.requiredPlatform === PlatformType.HARMONY) {
|
|
353
|
+
return {
|
|
354
|
+
platform: PlatformType.HARMONY,
|
|
355
|
+
runtimeEnv: this.buildHarmonyRuntimeEnv(resourceId),
|
|
356
|
+
preparedAt: Date.now(),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
throw new Error(`unsupported platform for midscene execution: ${task.requiredPlatform}`);
|
|
360
|
+
}
|
|
361
|
+
async ensurePreparedSession(task, resourceId, signal) {
|
|
362
|
+
const cached = this.preparedSessions.get(resourceId);
|
|
363
|
+
if (cached && cached.platform === task.requiredPlatform) {
|
|
364
|
+
if (cached.platform !== PlatformType.IOS)
|
|
365
|
+
return cached;
|
|
366
|
+
const wdaPort = Number(cached.runtimeEnv.MIDSCENE_IOS_WDA_PORT);
|
|
367
|
+
if (Number.isFinite(wdaPort) && wdaPort > 0 && (await this.isWdaHealthy(resourceId, wdaPort))) {
|
|
368
|
+
return cached;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const prepared = await this.prepareSession(task, resourceId, signal);
|
|
372
|
+
this.preparedSessions.set(resourceId, prepared);
|
|
373
|
+
return prepared;
|
|
374
|
+
}
|
|
375
|
+
async prepare(task, resourceId, signal) {
|
|
376
|
+
const prepared = await this.prepareSession(task, resourceId, signal);
|
|
377
|
+
this.preparedSessions.set(resourceId, prepared);
|
|
378
|
+
}
|
|
379
|
+
async execute(task, resourceId, signal, context) {
|
|
380
|
+
const prepared = await this.ensurePreparedSession(task, resourceId, signal);
|
|
381
|
+
const runtimeEnv = {
|
|
382
|
+
...prepared.runtimeEnv,
|
|
383
|
+
...(context?.runtimeEnv ?? {}),
|
|
384
|
+
};
|
|
385
|
+
if (task.scriptKind === "airtest") {
|
|
386
|
+
return this.airtestRuntime.execute(task, resourceId, signal, {
|
|
387
|
+
...context,
|
|
388
|
+
runtimeEnv,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
runtimeEnv.MIDSCENE_RUN_DIR = resolveMidsceneRunDir(process.cwd(), { ...process.env, ...runtimeEnv });
|
|
392
|
+
const wrappedScript = this.buildWrappedScript(task, resourceId, runtimeEnv);
|
|
393
|
+
const baseDir = path.join(tmpdir(), "automation-agent-runs", String(Date.now()));
|
|
394
|
+
await mkdir(baseDir, { recursive: true });
|
|
395
|
+
const scriptFile = path.join(baseDir, "task-script.ts");
|
|
396
|
+
await writeFile(scriptFile, wrappedScript, "utf8");
|
|
397
|
+
const startedAt = Date.now();
|
|
398
|
+
const command = renderMidsceneCommand(this.runCommandTemplate, resourceId, scriptFile);
|
|
399
|
+
const reportStem = String(runtimeEnv.MIDSCENE_FLOW_REPORT_STEM ?? "").trim();
|
|
400
|
+
const outputFormat = runtimeEnv.MIDSCENE_OUTPUT_FORMAT === "html-and-external-assets" ? "html-and-external-assets" : "single-html";
|
|
401
|
+
const reportRoot = getMidsceneReportRootDir(process.cwd(), { ...process.env, ...runtimeEnv });
|
|
402
|
+
const reportPaths = reportStem.length > 0 ? resolveTaskReportFilePaths(reportRoot, reportStem, outputFormat) : null;
|
|
403
|
+
let lastReportBytes = 0;
|
|
404
|
+
const pollReport = async () => {
|
|
405
|
+
if (!context?.onReportProgress || !reportPaths)
|
|
406
|
+
return;
|
|
407
|
+
try {
|
|
408
|
+
const st = await stat(reportPaths.reportHtmlPath);
|
|
409
|
+
if (st.size <= lastReportBytes)
|
|
410
|
+
return;
|
|
411
|
+
lastReportBytes = st.size;
|
|
412
|
+
const html = await readFile(reportPaths.reportHtmlPath, "utf8");
|
|
413
|
+
context.onReportProgress({
|
|
414
|
+
reportHtml: html,
|
|
415
|
+
reportFormat: outputFormat,
|
|
416
|
+
partial: true,
|
|
417
|
+
reportName: reportPaths.reportName,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
/* 报告尚未创建 */
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
const htmlPollMs = reportPaths?.bundleDir && context?.onReportProgress ? 5000 : 2000;
|
|
425
|
+
const reportPoll = reportPaths && context?.onReportProgress ? setInterval(() => void pollReport(), htmlPollMs) : null;
|
|
426
|
+
let stopExecutionDumpWatch;
|
|
427
|
+
if (reportPaths?.bundleDir && context?.onReportProgress) {
|
|
428
|
+
await mkdir(reportPaths.bundleDir, { recursive: true });
|
|
429
|
+
const allEnv = { ...process.env, ...runtimeEnv };
|
|
430
|
+
stopExecutionDumpWatch = startExecutionDumpWatcher(reportPaths.bundleDir, async (payload) => {
|
|
431
|
+
let reportHtml = "";
|
|
432
|
+
try {
|
|
433
|
+
reportHtml = await readFile(reportPaths.reportHtmlPath, "utf8");
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
/* index.html 尚未写出时仍上传 dump / 资源,HTML 留空 */
|
|
437
|
+
}
|
|
438
|
+
context.onReportProgress({
|
|
439
|
+
reportHtml,
|
|
440
|
+
reportFormat: outputFormat,
|
|
441
|
+
partial: true,
|
|
442
|
+
reportName: reportPaths.reportName,
|
|
443
|
+
executionDumpJson: payload.executionDumpJson,
|
|
444
|
+
executionDumpRevision: payload.executionDumpRevision,
|
|
445
|
+
reportAssetFiles: payload.reportAssetFiles,
|
|
446
|
+
});
|
|
447
|
+
}, {
|
|
448
|
+
debounceMs: 450,
|
|
449
|
+
imageCompression: {
|
|
450
|
+
quality: Math.floor(numberFromEnv(allEnv, "MIDSCENE_REPORT_IMAGE_QUALITY", 100, 1, 100)),
|
|
451
|
+
maxWidth: Math.floor(numberFromEnv(allEnv, "MIDSCENE_REPORT_IMAGE_MAX_WIDTH", 0, 0, 4096)) || undefined,
|
|
452
|
+
overwriteFiles: truthyFromEnv(allEnv, "MIDSCENE_REPORT_IMAGE_OVERWRITE", true),
|
|
453
|
+
},
|
|
454
|
+
}).stop;
|
|
455
|
+
}
|
|
456
|
+
const recordingReportDir = reportPaths?.bundleDir ?? (reportPaths ? path.dirname(reportPaths.reportHtmlPath) : null);
|
|
457
|
+
const recorder = recordingReportDir
|
|
458
|
+
? await startVideoRecording(recordingReportDir, resolveVideoRecorderConfig(process.env, {
|
|
459
|
+
platform: task.requiredPlatform,
|
|
460
|
+
resourceId,
|
|
461
|
+
taskId: context?.taskId,
|
|
462
|
+
runtimeEnv,
|
|
463
|
+
})).catch((err) => {
|
|
464
|
+
console.warn(`[MidsceneRuntimeReal] video recorder start failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
465
|
+
return null;
|
|
466
|
+
})
|
|
467
|
+
: null;
|
|
468
|
+
const result = await this.commandRunner.run(command, this.options.runCommandTimeoutMs, signal, {
|
|
469
|
+
env: runtimeEnv,
|
|
470
|
+
onStdoutChunk: context?.onLogChunk
|
|
471
|
+
? (t) => {
|
|
472
|
+
context.onLogChunk(t, "stdout");
|
|
473
|
+
}
|
|
474
|
+
: undefined,
|
|
475
|
+
onStderrChunk: context?.onLogChunk
|
|
476
|
+
? (t) => {
|
|
477
|
+
context.onLogChunk(t, "stderr");
|
|
478
|
+
}
|
|
479
|
+
: undefined,
|
|
480
|
+
});
|
|
481
|
+
const recordingResult = recorder ? await recorder.stop() : null;
|
|
482
|
+
if (reportPoll) {
|
|
483
|
+
clearInterval(reportPoll);
|
|
484
|
+
await pollReport();
|
|
485
|
+
}
|
|
486
|
+
if (stopExecutionDumpWatch) {
|
|
487
|
+
await new Promise((resolveDelay) => {
|
|
488
|
+
setTimeout(resolveDelay, 550);
|
|
489
|
+
});
|
|
490
|
+
stopExecutionDumpWatch();
|
|
491
|
+
}
|
|
492
|
+
const endedAt = Date.now();
|
|
493
|
+
const logFile = path.join(baseDir, "runtime.log");
|
|
494
|
+
const traceFile = path.join(baseDir, "runtime-trace.json");
|
|
495
|
+
const screenshotFile = path.join(baseDir, "runtime-shot.png");
|
|
496
|
+
await writeFile(logFile, [`command: ${command}`, `exitCode: ${result.exitCode}`, result.stdout, result.stderr].join("\n"), "utf8");
|
|
497
|
+
await writeFile(traceFile, JSON.stringify({
|
|
498
|
+
startedAt,
|
|
499
|
+
endedAt,
|
|
500
|
+
durationMs: endedAt - startedAt,
|
|
501
|
+
exitCode: result.exitCode,
|
|
502
|
+
ok: result.ok,
|
|
503
|
+
}, null, 2), "utf8");
|
|
504
|
+
await writeFile(screenshotFile, "", "utf8");
|
|
505
|
+
const artifacts = [
|
|
506
|
+
{ type: ArtifactType.LOG, uri: `file://${logFile}` },
|
|
507
|
+
{ type: ArtifactType.TRACE, uri: `file://${traceFile}` },
|
|
508
|
+
{ type: ArtifactType.SCREENSHOT, uri: `file://${screenshotFile}` },
|
|
509
|
+
];
|
|
510
|
+
if (recordingResult?.ok && recordingResult.outputPath) {
|
|
511
|
+
artifacts.push({ type: ArtifactType.VIDEO, uri: `file://${recordingResult.outputPath}` });
|
|
512
|
+
}
|
|
513
|
+
let reportInfo;
|
|
514
|
+
if (reportPaths) {
|
|
515
|
+
try {
|
|
516
|
+
const html = await readFile(reportPaths.reportHtmlPath, "utf8");
|
|
517
|
+
let reportBundleZipPath;
|
|
518
|
+
if (outputFormat === "html-and-external-assets" && reportPaths.bundleDir) {
|
|
519
|
+
reportBundleZipPath = await zipDirectoryToFile(reportPaths.bundleDir, reportStem);
|
|
520
|
+
}
|
|
521
|
+
const ri = {
|
|
522
|
+
reportHtmlPath: reportPaths.reportHtmlPath,
|
|
523
|
+
reportName: reportPaths.reportName,
|
|
524
|
+
reportFormat: outputFormat,
|
|
525
|
+
reportBundleDir: reportPaths.bundleDir,
|
|
526
|
+
reportBundleZipPath,
|
|
527
|
+
};
|
|
528
|
+
reportInfo = ri;
|
|
529
|
+
context?.onReportProgress?.({
|
|
530
|
+
reportHtml: html,
|
|
531
|
+
reportFormat: outputFormat,
|
|
532
|
+
partial: false,
|
|
533
|
+
reportName: reportPaths.reportName,
|
|
534
|
+
reportBundleZipUri: reportBundleZipPath ? `file://${reportBundleZipPath}` : undefined,
|
|
535
|
+
});
|
|
536
|
+
artifacts.push({ type: ArtifactType.REPORT, uri: `file://${reportPaths.reportHtmlPath}` });
|
|
537
|
+
if (reportBundleZipPath) {
|
|
538
|
+
artifacts.push({ type: ArtifactType.REPORT, uri: `file://${reportBundleZipPath}` });
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
console.warn(`[MidsceneRuntimeReal] report artifact missing or unreadable: ${err instanceof Error ? err.message : String(err)}`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
ok: result.ok,
|
|
547
|
+
message: result.ok ? "midscene real execution success" : result.stderr || "midscene execution failed",
|
|
548
|
+
artifacts,
|
|
549
|
+
reportInfo,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
}
|