@androperator/emulator 0.1.0
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 +202 -0
- package/NOTICE +5 -0
- package/README.md +110 -0
- package/dist/adbClient.d.ts +10 -0
- package/dist/adbClient.js +15 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +112 -0
- package/dist/configuredAvds.d.ts +7 -0
- package/dist/configuredAvds.js +135 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +5 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +16 -0
- package/dist/hostRequirements.d.ts +9 -0
- package/dist/hostRequirements.js +28 -0
- package/dist/hostToolClient.d.ts +10 -0
- package/dist/hostToolClient.js +23 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/lifecycle.d.ts +22 -0
- package/dist/lifecycle.js +209 -0
- package/dist/processRunner.d.ts +33 -0
- package/dist/processRunner.js +65 -0
- package/dist/runningEmulators.d.ts +8 -0
- package/dist/runningEmulators.js +55 -0
- package/dist/runtimeConfig.d.ts +15 -0
- package/dist/runtimeConfig.js +28 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.js +2 -0
- package/dist/validation.d.ts +3 -0
- package/dist/validation.js +12 -0
- package/docs/provenance.md +47 -0
- package/package.json +50 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type AndroidSdkTool } from "./hostToolClient.js";
|
|
2
|
+
import type { RuntimeConfig } from "./runtimeConfig.js";
|
|
3
|
+
export interface HostToolAvailability {
|
|
4
|
+
tool: AndroidSdkTool;
|
|
5
|
+
available: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function checkRequiredEmulatorTools(config: RuntimeConfig): Promise<HostToolAvailability[]>;
|
|
8
|
+
export declare function assertRequiredEmulatorTools(config: RuntimeConfig): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=hostRequirements.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Ported from Androperator; adaptations recorded in docs/provenance.md.
|
|
2
|
+
import { isAndroidSdkToolAvailable } from "./hostToolClient.js";
|
|
3
|
+
import { ERROR_CODES } from "./errors.js";
|
|
4
|
+
export async function checkRequiredEmulatorTools(config) {
|
|
5
|
+
const tools = ["adb", "emulator", "sdkmanager", "avdmanager"];
|
|
6
|
+
const results = [];
|
|
7
|
+
for (const tool of tools) {
|
|
8
|
+
results.push({
|
|
9
|
+
tool,
|
|
10
|
+
available: await isAndroidSdkToolAvailable(config, tool),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return results;
|
|
14
|
+
}
|
|
15
|
+
export async function assertRequiredEmulatorTools(config) {
|
|
16
|
+
const results = await checkRequiredEmulatorTools(config);
|
|
17
|
+
const missing = results.filter((result) => !result.available).map((result) => result.tool);
|
|
18
|
+
if (missing.length === 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const error = {
|
|
22
|
+
code: ERROR_CODES.ANDROID_SDK_TOOL_MISSING,
|
|
23
|
+
message: `Required Android SDK tools are missing: ${missing.join(", ")}`,
|
|
24
|
+
details: { missingTools: missing },
|
|
25
|
+
};
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=hostRequirements.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProcessResult } from "./processRunner.js";
|
|
2
|
+
import type { RuntimeConfig } from "./runtimeConfig.js";
|
|
3
|
+
export type AndroidSdkTool = "adb" | "emulator" | "sdkmanager" | "avdmanager";
|
|
4
|
+
export declare function runAndroidSdkTool(config: RuntimeConfig, tool: AndroidSdkTool, args: string[], options?: {
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
input?: string;
|
|
8
|
+
}): Promise<ProcessResult>;
|
|
9
|
+
export declare function isAndroidSdkToolAvailable(config: RuntimeConfig, tool: AndroidSdkTool): Promise<boolean>;
|
|
10
|
+
//# sourceMappingURL=hostToolClient.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function getToolPath(config, tool) {
|
|
2
|
+
switch (tool) {
|
|
3
|
+
case "adb":
|
|
4
|
+
return config.adbPath;
|
|
5
|
+
case "emulator":
|
|
6
|
+
return config.emulatorPath;
|
|
7
|
+
case "sdkmanager":
|
|
8
|
+
return config.sdkmanagerPath;
|
|
9
|
+
case "avdmanager":
|
|
10
|
+
return config.avdmanagerPath;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function runAndroidSdkTool(config, tool, args, options) {
|
|
14
|
+
return config.runner.run(getToolPath(config, tool), args, options);
|
|
15
|
+
}
|
|
16
|
+
export async function isAndroidSdkToolAvailable(config, tool) {
|
|
17
|
+
const result = await runAndroidSdkTool(config, tool, ["--help"], { timeoutMs: 5_000 });
|
|
18
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return result.code === 0 || result.code === 1 || result.stdout.length > 0 || result.stderr.length > 0;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=hostToolClient.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./errors.js";
|
|
3
|
+
export * from "./runtimeConfig.js";
|
|
4
|
+
export * from "./processRunner.js";
|
|
5
|
+
export * from "./hostToolClient.js";
|
|
6
|
+
export * from "./hostRequirements.js";
|
|
7
|
+
export * from "./configuredAvds.js";
|
|
8
|
+
export * from "./runningEmulators.js";
|
|
9
|
+
export * from "./lifecycle.js";
|
|
10
|
+
export * from "./validation.js";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./errors.js";
|
|
3
|
+
export * from "./runtimeConfig.js";
|
|
4
|
+
export * from "./processRunner.js";
|
|
5
|
+
export * from "./hostToolClient.js";
|
|
6
|
+
export * from "./hostRequirements.js";
|
|
7
|
+
export * from "./configuredAvds.js";
|
|
8
|
+
export * from "./runningEmulators.js";
|
|
9
|
+
export * from "./lifecycle.js";
|
|
10
|
+
export * from "./validation.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RuntimeConfig } from "./runtimeConfig.js";
|
|
2
|
+
export declare function normalizeEmulatorDataPartitionSize(size: string): string;
|
|
3
|
+
export declare function setAvdDataPartitionSize(name: string, size: string): Promise<void>;
|
|
4
|
+
export declare function isSystemImageInstalled(config: RuntimeConfig, systemImage: string): Promise<boolean>;
|
|
5
|
+
export declare function acceptAndroidSdkLicenses(config: RuntimeConfig): Promise<void>;
|
|
6
|
+
export declare function ensureSystemImageInstalled(config: RuntimeConfig, systemImage: string, options?: {
|
|
7
|
+
acceptLicenses?: boolean;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
export declare function createAvd(config: RuntimeConfig, options: {
|
|
10
|
+
name: string;
|
|
11
|
+
systemImage: string;
|
|
12
|
+
deviceProfile: string;
|
|
13
|
+
dataPartitionSize: string;
|
|
14
|
+
replace?: boolean;
|
|
15
|
+
acceptLicenses?: boolean;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
export declare function startAvd(config: RuntimeConfig, name: string, extraArgs?: string[]): Promise<void>;
|
|
18
|
+
export declare function waitForEmulatorRegistration(config: RuntimeConfig, name: string, timeoutMs?: number): Promise<string>;
|
|
19
|
+
export declare function waitForBootCompletion(config: RuntimeConfig, serial: string, timeoutMs?: number): Promise<void>;
|
|
20
|
+
export declare function stopAvd(config: RuntimeConfig, name: string): Promise<void>;
|
|
21
|
+
export declare function deleteAvd(config: RuntimeConfig, name: string): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=lifecycle.d.ts.map
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { validateAvdName, validateSystemImage } from "./validation.js";
|
|
2
|
+
// Ported from Androperator; adaptations recorded in docs/provenance.md.
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { runAdb, listDevices } from "./adbClient.js";
|
|
6
|
+
import { runAndroidSdkTool } from "./hostToolClient.js";
|
|
7
|
+
import { ERROR_CODES } from "./errors.js";
|
|
8
|
+
import { ADB_REGISTRATION_TIMEOUT_MS, BOOT_POLL_INTERVAL_MS, EMULATOR_DATA_PARTITION_SIZE_PATTERN, EMULATOR_BOOT_TIMEOUT_MS, } from "./constants.js";
|
|
9
|
+
import { getAvdConfigPath, inspectConfiguredAvd } from "./configuredAvds.js";
|
|
10
|
+
import { isEmulatorBooted, resolveRunningEmulatorByName } from "./runningEmulators.js";
|
|
11
|
+
function buildError(code, message, details) {
|
|
12
|
+
return { code, message, details };
|
|
13
|
+
}
|
|
14
|
+
export function normalizeEmulatorDataPartitionSize(size) {
|
|
15
|
+
const normalized = size.trim().toUpperCase();
|
|
16
|
+
const match = normalized.match(EMULATOR_DATA_PARTITION_SIZE_PATTERN);
|
|
17
|
+
if (!match) {
|
|
18
|
+
throw buildError(ERROR_CODES.ANDROID_AVD_CREATE_FAILED, "Emulator data partition size must be a positive integer followed by G or GB", { value: size, expectedFormat: "<positive_integer>G|GB" });
|
|
19
|
+
}
|
|
20
|
+
return `${match[1]}G`;
|
|
21
|
+
}
|
|
22
|
+
async function setAvdConfigValue(path, key, value) {
|
|
23
|
+
let contents;
|
|
24
|
+
try {
|
|
25
|
+
contents = await readFile(path, "utf8");
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw buildError(ERROR_CODES.ANDROID_AVD_CREATE_FAILED, `Failed to read created AVD config at ${path}`, { path, cause: error instanceof Error ? error.message : String(error) });
|
|
29
|
+
}
|
|
30
|
+
const line = `${key}=${value}`;
|
|
31
|
+
const lines = contents.length > 0 ? contents.split("\n") : [];
|
|
32
|
+
const index = lines.findIndex((existing) => existing.trimStart().startsWith(`${key}=`));
|
|
33
|
+
if (index >= 0) {
|
|
34
|
+
lines[index] = line;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
38
|
+
lines[lines.length - 1] = line;
|
|
39
|
+
lines.push("");
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
lines.push(line);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await writeFile(path, lines.join("\n"), "utf8");
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw buildError(ERROR_CODES.ANDROID_AVD_CREATE_FAILED, `Failed to write AVD config at ${path}`, { path, key, value, cause: error instanceof Error ? error.message : String(error) });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function setAvdDataPartitionSize(name, size) {
|
|
53
|
+
await setAvdConfigValue(await getAvdConfigPath(name), "disk.dataPartition.size", normalizeEmulatorDataPartitionSize(size));
|
|
54
|
+
}
|
|
55
|
+
async function deleteCreatedAvdAfterFailedConfigUpdate(config, name, cause) {
|
|
56
|
+
const result = await runAndroidSdkTool(config, "avdmanager", ["delete", "avd", "--name", name], {
|
|
57
|
+
timeoutMs: 60_000,
|
|
58
|
+
});
|
|
59
|
+
const typedCause = cause;
|
|
60
|
+
throw buildError(ERROR_CODES.ANDROID_AVD_CREATE_FAILED, typedCause.message ?? `Failed to configure Android Virtual Device ${name}`, {
|
|
61
|
+
...(typedCause.details ?? {}),
|
|
62
|
+
name,
|
|
63
|
+
cleanup: {
|
|
64
|
+
attempted: true,
|
|
65
|
+
succeeded: result.code === 0,
|
|
66
|
+
stderr: result.stderr,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export async function isSystemImageInstalled(config, systemImage) {
|
|
71
|
+
const result = await runAndroidSdkTool(config, "sdkmanager", ["--list_installed"], { timeoutMs: 30_000 });
|
|
72
|
+
if (result.code !== 0) {
|
|
73
|
+
throw buildError(ERROR_CODES.ANDROID_SYSTEM_IMAGE_INSTALL_FAILED, result.stderr || "Failed to query installed Android system images", { systemImage });
|
|
74
|
+
}
|
|
75
|
+
return result.stdout.split("\n").some((line) => {
|
|
76
|
+
// Legacy sdkmanager uses semicolon IDs and pipes; the Android CLI shim
|
|
77
|
+
// uses slash IDs and whitespace columns. Compare the entire first token.
|
|
78
|
+
const packageId = line.trim().split(/[\s|]+/, 1)[0].replaceAll("/", ";");
|
|
79
|
+
return packageId === systemImage;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export async function acceptAndroidSdkLicenses(config) {
|
|
83
|
+
const result = await runAndroidSdkTool(config, "sdkmanager", ["--licenses"], {
|
|
84
|
+
timeoutMs: 120_000,
|
|
85
|
+
input: "y\n".repeat(100),
|
|
86
|
+
});
|
|
87
|
+
if (result.code !== 0) {
|
|
88
|
+
throw buildError(ERROR_CODES.ANDROID_SYSTEM_IMAGE_INSTALL_FAILED, result.stderr || "Failed to accept Android SDK licenses");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function ensureSystemImageInstalled(config, systemImage, options = {}) {
|
|
92
|
+
validateSystemImage(systemImage);
|
|
93
|
+
if (await isSystemImageInstalled(config, systemImage)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (options.acceptLicenses)
|
|
97
|
+
await acceptAndroidSdkLicenses(config);
|
|
98
|
+
const result = await runAndroidSdkTool(config, "sdkmanager", [systemImage], { timeoutMs: 300_000 });
|
|
99
|
+
if (result.code !== 0) {
|
|
100
|
+
throw buildError(ERROR_CODES.ANDROID_SYSTEM_IMAGE_INSTALL_FAILED, result.stderr || "Failed to install Android system image", { systemImage });
|
|
101
|
+
}
|
|
102
|
+
if (!(await isSystemImageInstalled(config, systemImage))) {
|
|
103
|
+
throw buildError(ERROR_CODES.ANDROID_SYSTEM_IMAGE_INSTALL_FAILED, "System image remains uninstalled; accept its SDK license before retrying, or explicitly set acceptLicenses", { systemImage });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export async function createAvd(config, options) {
|
|
107
|
+
const systemImage = options.systemImage;
|
|
108
|
+
const deviceProfile = options.deviceProfile;
|
|
109
|
+
const dataPartitionSize = normalizeEmulatorDataPartitionSize(options.dataPartitionSize);
|
|
110
|
+
validateAvdName(options.name);
|
|
111
|
+
const existedBeforeCreate = (await inspectConfiguredAvd(options.name)).exists;
|
|
112
|
+
validateSystemImage(systemImage);
|
|
113
|
+
if (!deviceProfile.trim() || deviceProfile.startsWith("-")) {
|
|
114
|
+
throw buildError(ERROR_CODES.INVALID_ARGUMENT, "deviceProfile must be a nonblank hardware profile ID");
|
|
115
|
+
}
|
|
116
|
+
if (existedBeforeCreate) {
|
|
117
|
+
if (!options.replace)
|
|
118
|
+
throw buildError(ERROR_CODES.ANDROID_AVD_ALREADY_EXISTS, `AVD ${options.name} already exists; set replace only to discard its data`, { name: options.name });
|
|
119
|
+
await assertAvdStopped(config, options.name);
|
|
120
|
+
}
|
|
121
|
+
await ensureSystemImageInstalled(config, systemImage, { acceptLicenses: options.acceptLicenses });
|
|
122
|
+
const result = await runAndroidSdkTool(config, "avdmanager", ["create", "avd", ...(options.replace ? ["--force"] : []), "--name", options.name, "--package", systemImage, "--device", deviceProfile], { timeoutMs: 120_000, input: "no\n" });
|
|
123
|
+
if (result.code !== 0) {
|
|
124
|
+
throw buildError(ERROR_CODES.ANDROID_AVD_CREATE_FAILED, result.stderr || "Failed to create Android Virtual Device", { name: options.name, systemImage, deviceProfile });
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
await setAvdDataPartitionSize(options.name, dataPartitionSize);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (!existedBeforeCreate) {
|
|
131
|
+
await deleteCreatedAvdAfterFailedConfigUpdate(config, options.name, error);
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export async function startAvd(config, name, extraArgs = []) {
|
|
137
|
+
validateAvdName(name);
|
|
138
|
+
const args = [`@${name}`, "-no-snapshot-load", "-no-boot-anim", ...extraArgs];
|
|
139
|
+
const child = config.runner.spawn(config.emulatorPath, args, {
|
|
140
|
+
detached: true,
|
|
141
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
142
|
+
shell: false,
|
|
143
|
+
});
|
|
144
|
+
if (child && typeof child.once === "function") {
|
|
145
|
+
await new Promise((resolve, reject) => {
|
|
146
|
+
child.once("spawn", resolve);
|
|
147
|
+
child.once("error", (error) => reject(buildError(ERROR_CODES.EMULATOR_START_FAILED, error.message, { name })));
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (child && typeof child.unref === "function") {
|
|
151
|
+
child.unref();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export async function waitForEmulatorRegistration(config, name, timeoutMs = ADB_REGISTRATION_TIMEOUT_MS) {
|
|
155
|
+
const startedAt = Date.now();
|
|
156
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
157
|
+
const running = await resolveRunningEmulatorByName(config, name);
|
|
158
|
+
if (running) {
|
|
159
|
+
return running.serial;
|
|
160
|
+
}
|
|
161
|
+
await delay(BOOT_POLL_INTERVAL_MS);
|
|
162
|
+
}
|
|
163
|
+
throw buildError(ERROR_CODES.EMULATOR_START_FAILED, `Timed out waiting for emulator ${name} to appear in adb`, { name, timeoutMs });
|
|
164
|
+
}
|
|
165
|
+
export async function waitForBootCompletion(config, serial, timeoutMs = EMULATOR_BOOT_TIMEOUT_MS) {
|
|
166
|
+
const startedAt = Date.now();
|
|
167
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
168
|
+
if (await isEmulatorBooted(config, serial)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
await delay(BOOT_POLL_INTERVAL_MS);
|
|
172
|
+
}
|
|
173
|
+
throw buildError(ERROR_CODES.EMULATOR_BOOT_TIMEOUT, `Timed out waiting for emulator ${serial} to finish booting`, { serial, timeoutMs });
|
|
174
|
+
}
|
|
175
|
+
export async function stopAvd(config, name) {
|
|
176
|
+
const running = await resolveRunningEmulatorByName(config, name);
|
|
177
|
+
if (!running) {
|
|
178
|
+
throw buildError(ERROR_CODES.EMULATOR_NOT_RUNNING, `Emulator ${name} is not running`, { name });
|
|
179
|
+
}
|
|
180
|
+
const result = await runAdb({ ...config, deviceId: running.serial }, ["emu", "kill"]);
|
|
181
|
+
if (result.code !== 0) {
|
|
182
|
+
throw buildError(ERROR_CODES.EMULATOR_STOP_FAILED, result.stderr || `Failed to stop emulator ${name}`, { name, serial: running.serial });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export async function deleteAvd(config, name) {
|
|
186
|
+
validateAvdName(name);
|
|
187
|
+
await assertAvdStopped(config, name);
|
|
188
|
+
const existing = await inspectConfiguredAvd(name);
|
|
189
|
+
if (!existing.exists) {
|
|
190
|
+
throw buildError(ERROR_CODES.EMULATOR_NOT_FOUND, `AVD ${name} does not exist`, { name });
|
|
191
|
+
}
|
|
192
|
+
const result = await runAndroidSdkTool(config, "avdmanager", ["delete", "avd", "--name", name], {
|
|
193
|
+
timeoutMs: 60_000,
|
|
194
|
+
});
|
|
195
|
+
if (result.code !== 0) {
|
|
196
|
+
throw buildError(ERROR_CODES.EMULATOR_DELETE_FAILED, result.stderr || `Failed to delete emulator ${name}`, { name });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function assertAvdStopped(config, name) {
|
|
200
|
+
const devices = await listDevices(config);
|
|
201
|
+
if (devices.some((device) => device.serial.startsWith("emulator-") && device.state !== "device")) {
|
|
202
|
+
throw buildError(ERROR_CODES.EMULATOR_ALREADY_RUNNING, "Cannot safely modify an AVD while an emulator is offline or unauthorized");
|
|
203
|
+
}
|
|
204
|
+
const running = await resolveRunningEmulatorByName(config, name, devices);
|
|
205
|
+
if (running) {
|
|
206
|
+
throw buildError(ERROR_CODES.EMULATOR_ALREADY_RUNNING, `Cannot modify running emulator ${name}`, { name, serial: running.serial });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ProcessResult {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
code: number | null;
|
|
5
|
+
error?: Error;
|
|
6
|
+
}
|
|
7
|
+
export interface ProcessRunner {
|
|
8
|
+
run(command: string, args: string[], options?: {
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
input?: string;
|
|
12
|
+
}): Promise<ProcessResult>;
|
|
13
|
+
spawn(command: string, args: string[], options?: {
|
|
14
|
+
detached?: boolean;
|
|
15
|
+
stdio?: any;
|
|
16
|
+
shell?: boolean;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
}): any;
|
|
19
|
+
}
|
|
20
|
+
export declare class NodeProcessRunner implements ProcessRunner {
|
|
21
|
+
run(command: string, args: string[], options?: {
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
cwd?: string;
|
|
24
|
+
input?: string;
|
|
25
|
+
}): Promise<ProcessResult>;
|
|
26
|
+
spawn(command: string, args: string[], options?: {
|
|
27
|
+
detached?: boolean;
|
|
28
|
+
stdio?: any;
|
|
29
|
+
shell?: boolean;
|
|
30
|
+
env?: NodeJS.ProcessEnv;
|
|
31
|
+
}): any;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=processRunner.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Ported from Androperator; adaptations recorded in docs/provenance.md.
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
export class NodeProcessRunner {
|
|
4
|
+
async run(command, args, options) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const stdin = options?.input !== undefined ? "pipe" : "ignore";
|
|
7
|
+
const proc = spawn(command, args, {
|
|
8
|
+
cwd: options?.cwd,
|
|
9
|
+
detached: process.platform !== "win32",
|
|
10
|
+
stdio: [stdin, "pipe", "pipe"],
|
|
11
|
+
shell: false,
|
|
12
|
+
});
|
|
13
|
+
let stdout = "";
|
|
14
|
+
let stderr = "";
|
|
15
|
+
proc.stdout?.on("data", (d) => (stdout += d.toString()));
|
|
16
|
+
proc.stderr?.on("data", (d) => (stderr += d.toString()));
|
|
17
|
+
proc.stdin?.on("error", () => { });
|
|
18
|
+
if (options?.input !== undefined) {
|
|
19
|
+
proc.stdin?.write(options.input);
|
|
20
|
+
proc.stdin?.end();
|
|
21
|
+
}
|
|
22
|
+
const timeoutMs = options?.timeoutMs ?? 30_000;
|
|
23
|
+
let timedOut = false;
|
|
24
|
+
const t = setTimeout(() => {
|
|
25
|
+
timedOut = true;
|
|
26
|
+
// SDK scripts can launch children that inherit the output pipes.
|
|
27
|
+
if (process.platform !== "win32" && proc.pid !== undefined) {
|
|
28
|
+
try {
|
|
29
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code !== "ESRCH")
|
|
33
|
+
proc.kill("SIGKILL");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
proc.kill("SIGKILL");
|
|
38
|
+
}
|
|
39
|
+
proc.stdout?.destroy();
|
|
40
|
+
proc.stderr?.destroy();
|
|
41
|
+
proc.stdin?.destroy();
|
|
42
|
+
resolve({ stdout, stderr: `${stderr}\nProcess timed out after ${timeoutMs}ms`, code: null });
|
|
43
|
+
}, timeoutMs);
|
|
44
|
+
proc.on("error", (err) => {
|
|
45
|
+
clearTimeout(t);
|
|
46
|
+
resolve({ stdout, stderr, code: err.code === "ENOENT" ? 127 : 1, error: err });
|
|
47
|
+
});
|
|
48
|
+
proc.on("close", (code) => {
|
|
49
|
+
clearTimeout(t);
|
|
50
|
+
resolve({ stdout, stderr: timedOut ? `${stderr}\nProcess timed out after ${timeoutMs}ms` : stderr, code: timedOut ? null : code ?? null });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
spawn(command, args, options) {
|
|
55
|
+
const detached = options?.detached ?? false;
|
|
56
|
+
const stdio = options?.stdio ?? (detached ? ["ignore", "ignore", "ignore"] : ["ignore", "pipe", "pipe"]);
|
|
57
|
+
return spawn(command, args, {
|
|
58
|
+
detached,
|
|
59
|
+
stdio,
|
|
60
|
+
shell: options?.shell ?? false,
|
|
61
|
+
env: options?.env,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=processRunner.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RuntimeConfig } from "./runtimeConfig.js";
|
|
2
|
+
import { type DeviceInfo } from "./adbClient.js";
|
|
3
|
+
import type { RunningEmulator } from "./types.js";
|
|
4
|
+
export declare function getRunningEmulatorAvdName(config: RuntimeConfig, serial: string): Promise<string>;
|
|
5
|
+
export declare function isEmulatorBooted(config: RuntimeConfig, serial: string): Promise<boolean>;
|
|
6
|
+
export declare function listRunningEmulators(config: RuntimeConfig, devices?: DeviceInfo[]): Promise<RunningEmulator[]>;
|
|
7
|
+
export declare function resolveRunningEmulatorByName(config: RuntimeConfig, name: string, devices?: DeviceInfo[]): Promise<RunningEmulator | undefined>;
|
|
8
|
+
//# sourceMappingURL=runningEmulators.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Ported from Androperator; adaptations recorded in docs/provenance.md.
|
|
2
|
+
import { runAdb } from "./adbClient.js";
|
|
3
|
+
import { listDevices } from "./adbClient.js";
|
|
4
|
+
function isEmulatorSerial(serial) {
|
|
5
|
+
return serial.startsWith("emulator-");
|
|
6
|
+
}
|
|
7
|
+
function parseEmulatorAvdName(stdout) {
|
|
8
|
+
const lines = stdout
|
|
9
|
+
.split("\n")
|
|
10
|
+
.map((line) => line.trim())
|
|
11
|
+
.filter((line) => line.length > 0 && line !== "OK");
|
|
12
|
+
if (lines.length === 0) {
|
|
13
|
+
throw new Error("Emulator did not return an AVD name");
|
|
14
|
+
}
|
|
15
|
+
return lines[0];
|
|
16
|
+
}
|
|
17
|
+
async function isBootPropComplete(config, serial, prop) {
|
|
18
|
+
const result = await runAdb({ ...config, deviceId: serial }, ["shell", "getprop", prop]);
|
|
19
|
+
return result.code === 0 && result.stdout.trim() === "1";
|
|
20
|
+
}
|
|
21
|
+
export async function getRunningEmulatorAvdName(config, serial) {
|
|
22
|
+
const result = await runAdb({ ...config, deviceId: serial }, ["emu", "avd", "name"]);
|
|
23
|
+
if (result.code !== 0) {
|
|
24
|
+
throw new Error(result.stderr || `Failed to resolve AVD name for ${serial}`);
|
|
25
|
+
}
|
|
26
|
+
return parseEmulatorAvdName(result.stdout);
|
|
27
|
+
}
|
|
28
|
+
export async function isEmulatorBooted(config, serial) {
|
|
29
|
+
const [sysBootCompleted, devBootComplete] = await Promise.all([
|
|
30
|
+
isBootPropComplete(config, serial, "sys.boot_completed"),
|
|
31
|
+
isBootPropComplete(config, serial, "dev.bootcomplete"),
|
|
32
|
+
]);
|
|
33
|
+
return sysBootCompleted && devBootComplete;
|
|
34
|
+
}
|
|
35
|
+
export async function listRunningEmulators(config, devices) {
|
|
36
|
+
devices ??= await listDevices(config);
|
|
37
|
+
const emulators = devices.filter((device) => device.state === "device" && isEmulatorSerial(device.serial));
|
|
38
|
+
const running = [];
|
|
39
|
+
for (const device of emulators) {
|
|
40
|
+
const avdName = await getRunningEmulatorAvdName(config, device.serial);
|
|
41
|
+
const booted = await isEmulatorBooted(config, device.serial);
|
|
42
|
+
running.push({
|
|
43
|
+
type: "emulator",
|
|
44
|
+
avdName,
|
|
45
|
+
serial: device.serial,
|
|
46
|
+
booted,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return running;
|
|
50
|
+
}
|
|
51
|
+
export async function resolveRunningEmulatorByName(config, name, devices) {
|
|
52
|
+
const emulators = await listRunningEmulators(config, devices);
|
|
53
|
+
return emulators.find((emulator) => emulator.avdName === name);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=runningEmulators.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ProcessRunner } from "./processRunner.js";
|
|
2
|
+
export interface RuntimeConfig {
|
|
3
|
+
adbPath: string;
|
|
4
|
+
emulatorPath: string;
|
|
5
|
+
sdkmanagerPath: string;
|
|
6
|
+
avdmanagerPath: string;
|
|
7
|
+
runner: ProcessRunner;
|
|
8
|
+
/** Optional consumer ADB adapter, for diagnostics and logging. */
|
|
9
|
+
adb?: (args: string[], options?: {
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
}) => ReturnType<ProcessRunner["run"]>;
|
|
12
|
+
deviceId?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function getDefaultRuntimeConfig(overrides?: Partial<RuntimeConfig>): RuntimeConfig;
|
|
15
|
+
//# sourceMappingURL=runtimeConfig.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { NodeProcessRunner } from "./processRunner.js";
|
|
5
|
+
export function getDefaultRuntimeConfig(overrides = {}) {
|
|
6
|
+
const sdkRoots = [process.env.ANDROID_HOME, process.env.ANDROID_SDK_ROOT,
|
|
7
|
+
join(homedir(), "Library", "Android", "sdk"), join(homedir(), "Android", "Sdk")]
|
|
8
|
+
.filter((root) => root !== undefined && root.length > 0);
|
|
9
|
+
function tool(name, relativePath, env) {
|
|
10
|
+
const explicit = process.env[env];
|
|
11
|
+
if (explicit !== undefined) {
|
|
12
|
+
if (!explicit.trim())
|
|
13
|
+
throw new Error(`${env} must not be blank`);
|
|
14
|
+
return explicit;
|
|
15
|
+
}
|
|
16
|
+
return sdkRoots.map((root) => join(root, relativePath)).find(existsSync) ?? name;
|
|
17
|
+
}
|
|
18
|
+
const defined = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== undefined));
|
|
19
|
+
return {
|
|
20
|
+
adbPath: tool("adb", "platform-tools/adb", "ADB_PATH"),
|
|
21
|
+
emulatorPath: tool("emulator", "emulator/emulator", "EMULATOR_PATH"),
|
|
22
|
+
sdkmanagerPath: tool("sdkmanager", "cmdline-tools/latest/bin/sdkmanager", "SDKMANAGER_PATH"),
|
|
23
|
+
avdmanagerPath: tool("avdmanager", "cmdline-tools/latest/bin/avdmanager", "AVDMANAGER_PATH"),
|
|
24
|
+
runner: new NodeProcessRunner(),
|
|
25
|
+
...defined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=runtimeConfig.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ConfiguredAvd {
|
|
2
|
+
name: string;
|
|
3
|
+
exists: boolean;
|
|
4
|
+
running: boolean;
|
|
5
|
+
apiLevel: number | null;
|
|
6
|
+
abi: string | null;
|
|
7
|
+
playStore: boolean;
|
|
8
|
+
deviceProfile: string | null;
|
|
9
|
+
systemImage: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface RunningEmulator {
|
|
12
|
+
type: "emulator";
|
|
13
|
+
avdName: string;
|
|
14
|
+
serial: string;
|
|
15
|
+
booted: boolean;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ERROR_CODES } from "./errors.js";
|
|
2
|
+
export function validateAvdName(name) {
|
|
3
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(name)) {
|
|
4
|
+
throw { code: ERROR_CODES.ANDROID_AVD_CREATE_FAILED, message: "AVD name must start with a letter or number and contain only letters, numbers, dots, underscores or hyphens", details: { name } };
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function validateSystemImage(systemImage) {
|
|
8
|
+
if (!/^system-images;android-[A-Za-z0-9_.-]+;[A-Za-z0-9_.-]+;[A-Za-z0-9_.-]+$/.test(systemImage)) {
|
|
9
|
+
throw { code: ERROR_CODES.INVALID_ARGUMENT, message: "Use a system image package ID, for example system-images;android-35;google_apis;arm64-v8a", details: { systemImage } };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Extraction provenance
|
|
2
|
+
|
|
3
|
+
Source: `https://github.com/androperator/androperator`, revision
|
|
4
|
+
`43210a51315ad9e0badd80311bf686596a46b154`. Source license: Apache-2.0, retained
|
|
5
|
+
in `LICENSE`; attribution retained in `NOTICE`. This is primarily ported code.
|
|
6
|
+
|
|
7
|
+
Paths in the first column are relative to the source `apps/node/src/`.
|
|
8
|
+
|
|
9
|
+
| Original module/test | Destination | Adaptation |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `domain/android-emulators/configuredAvds.ts` | `src/configuredAvds.ts` | Remove support policy; validate names; follow locator paths; propagate read errors; recognize empty locator files |
|
|
12
|
+
| `domain/android-emulators/runningEmulators.ts` | `src/runningEmulators.ts` | Remove support annotation; retain serial/name and both boot-property checks |
|
|
13
|
+
| `domain/android-emulators/lifecycle.ts` | `src/lifecycle.ts` | Explicit inputs/license/replacement policy; early size validation; exact image matching and post-install verification; async launch errors; fail-closed delete/replacement |
|
|
14
|
+
| `domain/android-emulators/hostRequirements.ts` | `src/hostRequirements.ts` | Generic runtime/error imports |
|
|
15
|
+
| `domain/android-emulators/constants.ts`, `types.ts` | `src/constants.ts`, `src/types.ts` | Keep only timing/size mechanics and generic metadata |
|
|
16
|
+
| `adapters/android-sdk/hostToolClient.ts` | `src/hostToolClient.ts` | Generic imports |
|
|
17
|
+
| `adapters/android-bridge/processRunner.ts` | `src/processRunner.ts` | Remove shell execution; handle stdin closure; kill timed-out SDK process group and report timeout |
|
|
18
|
+
| `adapters/android-bridge/adbClient.ts`, `domain/devices/listDevices.ts` | `src/adbClient.ts` | Preserve serial selection; inject consumer logging; check failed/malformed device discovery |
|
|
19
|
+
| `adapters/android-bridge/runtimeConfig.ts` | `src/runtimeConfig.ts` | New minimal runtime contract and SDK discovery adapted from source |
|
|
20
|
+
| `contracts/errors.ts` | `src/errors.ts` | Retain emulator error strings; add boundary errors |
|
|
21
|
+
| `cli/commands/emulator.ts` | `src/cli.ts` | New parser and versioned JSON envelope; orchestration adapted from source |
|
|
22
|
+
| `test/unit/configuredAvds.test.ts` | `src/test/configuredAvds.test.ts` | Port parsing/discovery checks; remove consumer compatibility assertions |
|
|
23
|
+
| `test/unit/runningEmulators.test.ts` | `src/test/runningEmulators.test.ts` | Port console, boot and serial checks; remove compatibility assertions |
|
|
24
|
+
| `test/unit/emulatorLifecycle.test.ts` | `src/test/emulatorLifecycle.test.ts` | Port creation/storage/cleanup/start/wait/stop/delete checks with explicit inputs; developer-settings test remains in consumer |
|
|
25
|
+
| `test/unit/androidEmulatorsFoundation.test.ts` | `src/test/androidEmulatorsFoundation.test.ts` | Port tool availability tests; defaults tests remain in consumer |
|
|
26
|
+
| `test/unit/fakes/FakeProcessRunner.ts` | `src/test/fakes/FakeProcessRunner.ts` | Port deterministic runner |
|
|
27
|
+
| No source equivalent | `src/test/boundaries.test.ts` | New boundary, process-error and CLI protocol checks |
|
|
28
|
+
|
|
29
|
+
`compatibility.ts`, `provision.ts`, defaults, default-name generation and guest
|
|
30
|
+
developer settings remain Androperator policy. Its `emulatorCli.test.ts` and
|
|
31
|
+
`emulatorProvision.test.ts` stay in the consumer as integration regressions.
|
|
32
|
+
|
|
33
|
+
## Intentional corrections and limits
|
|
34
|
+
|
|
35
|
+
- Generic creation never silently replaces data or accepts licenses. The consumer
|
|
36
|
+
can explicitly retain its former policy. Replacement refuses a running target.
|
|
37
|
+
- Invalid capacity fails before SDK mutation. Failed post-create configuration
|
|
38
|
+
still attempts cleanup only for a newly created AVD, as in the source.
|
|
39
|
+
- Installed-image matching uses the exact package column instead of substring
|
|
40
|
+
matching. Installation is re-queried because license refusal can exit zero. No remote image catalog or recommendation algorithm was added.
|
|
41
|
+
- Config paths follow AVD metadata instead of assuming `<name>.avd` always owns
|
|
42
|
+
the data. Unexpected filesystem errors are no longer silently treated as absent.
|
|
43
|
+
- ADB query errors and offline emulators cannot be interpreted as safe deletion.
|
|
44
|
+
- Start reports asynchronous spawn failure; callers must await it. Process timeout
|
|
45
|
+
is reported even if a child exits successfully after the deadline.
|
|
46
|
+
- Storage configuration remains virtual capacity only. No host disk cap, resizing,
|
|
47
|
+
qcow2 conversion, cancellation or snapshot-save policy is claimed.
|