deepadb 1.0.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.
Files changed (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +631 -0
  3. package/build/bridge/adb-bridge.d.ts +65 -0
  4. package/build/bridge/adb-bridge.d.ts.map +1 -0
  5. package/build/bridge/adb-bridge.js +164 -0
  6. package/build/bridge/adb-bridge.js.map +1 -0
  7. package/build/bridge/device-manager.d.ts +41 -0
  8. package/build/bridge/device-manager.d.ts.map +1 -0
  9. package/build/bridge/device-manager.js +109 -0
  10. package/build/bridge/device-manager.js.map +1 -0
  11. package/build/bridge/local-bridge.d.ts +92 -0
  12. package/build/bridge/local-bridge.d.ts.map +1 -0
  13. package/build/bridge/local-bridge.js +345 -0
  14. package/build/bridge/local-bridge.js.map +1 -0
  15. package/build/config/config.d.ts +39 -0
  16. package/build/config/config.d.ts.map +1 -0
  17. package/build/config/config.js +84 -0
  18. package/build/config/config.js.map +1 -0
  19. package/build/graphql-api.d.ts +36 -0
  20. package/build/graphql-api.d.ts.map +1 -0
  21. package/build/graphql-api.js +296 -0
  22. package/build/graphql-api.js.map +1 -0
  23. package/build/http-transport.d.ts +26 -0
  24. package/build/http-transport.d.ts.map +1 -0
  25. package/build/http-transport.js +105 -0
  26. package/build/http-transport.js.map +1 -0
  27. package/build/index.d.ts +14 -0
  28. package/build/index.d.ts.map +1 -0
  29. package/build/index.js +66 -0
  30. package/build/index.js.map +1 -0
  31. package/build/middleware/chipset.d.ts +29 -0
  32. package/build/middleware/chipset.d.ts.map +1 -0
  33. package/build/middleware/chipset.js +123 -0
  34. package/build/middleware/chipset.js.map +1 -0
  35. package/build/middleware/cleanup.d.ts +24 -0
  36. package/build/middleware/cleanup.d.ts.map +1 -0
  37. package/build/middleware/cleanup.js +53 -0
  38. package/build/middleware/cleanup.js.map +1 -0
  39. package/build/middleware/fetch-utils.d.ts +19 -0
  40. package/build/middleware/fetch-utils.d.ts.map +1 -0
  41. package/build/middleware/fetch-utils.js +64 -0
  42. package/build/middleware/fetch-utils.js.map +1 -0
  43. package/build/middleware/logger.d.ts +17 -0
  44. package/build/middleware/logger.d.ts.map +1 -0
  45. package/build/middleware/logger.js +38 -0
  46. package/build/middleware/logger.js.map +1 -0
  47. package/build/middleware/output-processor.d.ts +57 -0
  48. package/build/middleware/output-processor.d.ts.map +1 -0
  49. package/build/middleware/output-processor.js +162 -0
  50. package/build/middleware/output-processor.js.map +1 -0
  51. package/build/middleware/sanitize.d.ts +30 -0
  52. package/build/middleware/sanitize.d.ts.map +1 -0
  53. package/build/middleware/sanitize.js +46 -0
  54. package/build/middleware/sanitize.js.map +1 -0
  55. package/build/middleware/security.d.ts +52 -0
  56. package/build/middleware/security.d.ts.map +1 -0
  57. package/build/middleware/security.js +123 -0
  58. package/build/middleware/security.js.map +1 -0
  59. package/build/middleware/ui-dump.d.ts +23 -0
  60. package/build/middleware/ui-dump.d.ts.map +1 -0
  61. package/build/middleware/ui-dump.js +59 -0
  62. package/build/middleware/ui-dump.js.map +1 -0
  63. package/build/server.d.ts +18 -0
  64. package/build/server.d.ts.map +1 -0
  65. package/build/server.js +133 -0
  66. package/build/server.js.map +1 -0
  67. package/build/tool-context.d.ts +22 -0
  68. package/build/tool-context.d.ts.map +1 -0
  69. package/build/tool-context.js +9 -0
  70. package/build/tool-context.js.map +1 -0
  71. package/build/tools/accessibility.d.ts +10 -0
  72. package/build/tools/accessibility.d.ts.map +1 -0
  73. package/build/tools/accessibility.js +259 -0
  74. package/build/tools/accessibility.js.map +1 -0
  75. package/build/tools/at-commands.d.ts +20 -0
  76. package/build/tools/at-commands.d.ts.map +1 -0
  77. package/build/tools/at-commands.js +378 -0
  78. package/build/tools/at-commands.js.map +1 -0
  79. package/build/tools/baseband.d.ts +15 -0
  80. package/build/tools/baseband.d.ts.map +1 -0
  81. package/build/tools/baseband.js +323 -0
  82. package/build/tools/baseband.js.map +1 -0
  83. package/build/tools/build.d.ts +6 -0
  84. package/build/tools/build.d.ts.map +1 -0
  85. package/build/tools/build.js +80 -0
  86. package/build/tools/build.js.map +1 -0
  87. package/build/tools/ci.d.ts +9 -0
  88. package/build/tools/ci.d.ts.map +1 -0
  89. package/build/tools/ci.js +163 -0
  90. package/build/tools/ci.js.map +1 -0
  91. package/build/tools/control.d.ts +10 -0
  92. package/build/tools/control.d.ts.map +1 -0
  93. package/build/tools/control.js +197 -0
  94. package/build/tools/control.js.map +1 -0
  95. package/build/tools/device-farm.d.ts +10 -0
  96. package/build/tools/device-farm.d.ts.map +1 -0
  97. package/build/tools/device-farm.js +140 -0
  98. package/build/tools/device-farm.js.map +1 -0
  99. package/build/tools/device-profiles.d.ts +16 -0
  100. package/build/tools/device-profiles.d.ts.map +1 -0
  101. package/build/tools/device-profiles.js +272 -0
  102. package/build/tools/device-profiles.js.map +1 -0
  103. package/build/tools/device.d.ts +6 -0
  104. package/build/tools/device.d.ts.map +1 -0
  105. package/build/tools/device.js +72 -0
  106. package/build/tools/device.js.map +1 -0
  107. package/build/tools/diagnostics.d.ts +7 -0
  108. package/build/tools/diagnostics.d.ts.map +1 -0
  109. package/build/tools/diagnostics.js +153 -0
  110. package/build/tools/diagnostics.js.map +1 -0
  111. package/build/tools/emulator.d.ts +9 -0
  112. package/build/tools/emulator.d.ts.map +1 -0
  113. package/build/tools/emulator.js +223 -0
  114. package/build/tools/emulator.js.map +1 -0
  115. package/build/tools/files.d.ts +6 -0
  116. package/build/tools/files.d.ts.map +1 -0
  117. package/build/tools/files.js +78 -0
  118. package/build/tools/files.js.map +1 -0
  119. package/build/tools/firmware-analysis.d.ts +24 -0
  120. package/build/tools/firmware-analysis.d.ts.map +1 -0
  121. package/build/tools/firmware-analysis.js +623 -0
  122. package/build/tools/firmware-analysis.js.map +1 -0
  123. package/build/tools/forwarding.d.ts +7 -0
  124. package/build/tools/forwarding.d.ts.map +1 -0
  125. package/build/tools/forwarding.js +64 -0
  126. package/build/tools/forwarding.js.map +1 -0
  127. package/build/tools/health.d.ts +7 -0
  128. package/build/tools/health.d.ts.map +1 -0
  129. package/build/tools/health.js +112 -0
  130. package/build/tools/health.js.map +1 -0
  131. package/build/tools/logcat-watch.d.ts +11 -0
  132. package/build/tools/logcat-watch.d.ts.map +1 -0
  133. package/build/tools/logcat-watch.js +209 -0
  134. package/build/tools/logcat-watch.js.map +1 -0
  135. package/build/tools/logs.d.ts +6 -0
  136. package/build/tools/logs.d.ts.map +1 -0
  137. package/build/tools/logs.js +83 -0
  138. package/build/tools/logs.js.map +1 -0
  139. package/build/tools/mirroring.d.ts +14 -0
  140. package/build/tools/mirroring.d.ts.map +1 -0
  141. package/build/tools/mirroring.js +243 -0
  142. package/build/tools/mirroring.js.map +1 -0
  143. package/build/tools/multi-device.d.ts +9 -0
  144. package/build/tools/multi-device.d.ts.map +1 -0
  145. package/build/tools/multi-device.js +138 -0
  146. package/build/tools/multi-device.js.map +1 -0
  147. package/build/tools/network-capture.d.ts +10 -0
  148. package/build/tools/network-capture.d.ts.map +1 -0
  149. package/build/tools/network-capture.js +143 -0
  150. package/build/tools/network-capture.js.map +1 -0
  151. package/build/tools/network-discovery.d.ts +21 -0
  152. package/build/tools/network-discovery.d.ts.map +1 -0
  153. package/build/tools/network-discovery.js +284 -0
  154. package/build/tools/network-discovery.js.map +1 -0
  155. package/build/tools/ota-monitor.d.ts +16 -0
  156. package/build/tools/ota-monitor.d.ts.map +1 -0
  157. package/build/tools/ota-monitor.js +211 -0
  158. package/build/tools/ota-monitor.js.map +1 -0
  159. package/build/tools/packages.d.ts +6 -0
  160. package/build/tools/packages.d.ts.map +1 -0
  161. package/build/tools/packages.js +237 -0
  162. package/build/tools/packages.js.map +1 -0
  163. package/build/tools/plugins.d.ts +22 -0
  164. package/build/tools/plugins.d.ts.map +1 -0
  165. package/build/tools/plugins.js +118 -0
  166. package/build/tools/plugins.js.map +1 -0
  167. package/build/tools/prompts.d.ts +9 -0
  168. package/build/tools/prompts.d.ts.map +1 -0
  169. package/build/tools/prompts.js +94 -0
  170. package/build/tools/prompts.js.map +1 -0
  171. package/build/tools/qemu.d.ts +18 -0
  172. package/build/tools/qemu.d.ts.map +1 -0
  173. package/build/tools/qemu.js +791 -0
  174. package/build/tools/qemu.js.map +1 -0
  175. package/build/tools/registry.d.ts +13 -0
  176. package/build/tools/registry.d.ts.map +1 -0
  177. package/build/tools/registry.js +221 -0
  178. package/build/tools/registry.js.map +1 -0
  179. package/build/tools/regression.d.ts +10 -0
  180. package/build/tools/regression.d.ts.map +1 -0
  181. package/build/tools/regression.js +215 -0
  182. package/build/tools/regression.js.map +1 -0
  183. package/build/tools/resources.d.ts +10 -0
  184. package/build/tools/resources.d.ts.map +1 -0
  185. package/build/tools/resources.js +77 -0
  186. package/build/tools/resources.js.map +1 -0
  187. package/build/tools/ril-intercept.d.ts +24 -0
  188. package/build/tools/ril-intercept.d.ts.map +1 -0
  189. package/build/tools/ril-intercept.js +273 -0
  190. package/build/tools/ril-intercept.js.map +1 -0
  191. package/build/tools/screen-record.d.ts +9 -0
  192. package/build/tools/screen-record.d.ts.map +1 -0
  193. package/build/tools/screen-record.js +95 -0
  194. package/build/tools/screen-record.js.map +1 -0
  195. package/build/tools/screenshot-diff.d.ts +13 -0
  196. package/build/tools/screenshot-diff.d.ts.map +1 -0
  197. package/build/tools/screenshot-diff.js +370 -0
  198. package/build/tools/screenshot-diff.js.map +1 -0
  199. package/build/tools/selinux-audit.d.ts +17 -0
  200. package/build/tools/selinux-audit.d.ts.map +1 -0
  201. package/build/tools/selinux-audit.js +301 -0
  202. package/build/tools/selinux-audit.js.map +1 -0
  203. package/build/tools/shell.d.ts +6 -0
  204. package/build/tools/shell.d.ts.map +1 -0
  205. package/build/tools/shell.js +63 -0
  206. package/build/tools/shell.js.map +1 -0
  207. package/build/tools/snapshot.d.ts +9 -0
  208. package/build/tools/snapshot.d.ts.map +1 -0
  209. package/build/tools/snapshot.js +192 -0
  210. package/build/tools/snapshot.js.map +1 -0
  211. package/build/tools/split-apk.d.ts +13 -0
  212. package/build/tools/split-apk.d.ts.map +1 -0
  213. package/build/tools/split-apk.js +229 -0
  214. package/build/tools/split-apk.js.map +1 -0
  215. package/build/tools/test-gen.d.ts +14 -0
  216. package/build/tools/test-gen.d.ts.map +1 -0
  217. package/build/tools/test-gen.js +252 -0
  218. package/build/tools/test-gen.js.map +1 -0
  219. package/build/tools/testing.d.ts +9 -0
  220. package/build/tools/testing.d.ts.map +1 -0
  221. package/build/tools/testing.js +144 -0
  222. package/build/tools/testing.js.map +1 -0
  223. package/build/tools/thermal-power.d.ts +19 -0
  224. package/build/tools/thermal-power.d.ts.map +1 -0
  225. package/build/tools/thermal-power.js +330 -0
  226. package/build/tools/thermal-power.js.map +1 -0
  227. package/build/tools/ui.d.ts +6 -0
  228. package/build/tools/ui.d.ts.map +1 -0
  229. package/build/tools/ui.js +266 -0
  230. package/build/tools/ui.js.map +1 -0
  231. package/build/tools/wireless.d.ts +7 -0
  232. package/build/tools/wireless.d.ts.map +1 -0
  233. package/build/tools/wireless.js +78 -0
  234. package/build/tools/wireless.js.map +1 -0
  235. package/build/tools/workflow-market.d.ts +17 -0
  236. package/build/tools/workflow-market.d.ts.map +1 -0
  237. package/build/tools/workflow-market.js +237 -0
  238. package/build/tools/workflow-market.js.map +1 -0
  239. package/build/tools/workflow.d.ts +32 -0
  240. package/build/tools/workflow.d.ts.map +1 -0
  241. package/build/tools/workflow.js +374 -0
  242. package/build/tools/workflow.js.map +1 -0
  243. package/build/ws-transport.d.ts +30 -0
  244. package/build/ws-transport.d.ts.map +1 -0
  245. package/build/ws-transport.js +133 -0
  246. package/build/ws-transport.js.map +1 -0
  247. package/package.json +37 -0
@@ -0,0 +1,791 @@
1
+ /**
2
+ * QEMU/KVM Tools — On-device virtual machine management.
3
+ *
4
+ * Provides QEMU VM lifecycle management for on-device mode (Termux).
5
+ * Uses KVM hardware acceleration when available (/dev/kvm) for near-native
6
+ * performance. Manages disk images, VM boot/shutdown, and ADB port forwarding
7
+ * to guest VMs.
8
+ *
9
+ * These tools complement the existing AVD emulator tools:
10
+ * - AVD tools (emulator.ts): manage Android SDK emulators on desktop
11
+ * - QEMU tools (this file): manage hardware-accelerated VMs on-device
12
+ *
13
+ * Images are stored in {tempDir}/qemu-images/.
14
+ * QEMU must be installed via Termux: pkg install qemu-system-aarch64-headless
15
+ */
16
+ import { z } from "zod";
17
+ import { join, resolve, basename } from "path";
18
+ import { mkdirSync, existsSync, readdirSync, statSync, unlinkSync } from "fs";
19
+ import { spawn, spawnSync } from "child_process";
20
+ import { OutputProcessor } from "../middleware/output-processor.js";
21
+ import { isOnDevice } from "../config/config.js";
22
+ import { registerCleanup, unregisterCleanup } from "../middleware/cleanup.js";
23
+ /** Maximum concurrent QEMU VMs to prevent resource exhaustion. */
24
+ const MAX_VMS = 3;
25
+ /**
26
+ * Maximum percentage of physical RAM that a VM may use.
27
+ * Reserves 35% for the host OS, Android services, Termux, and DeepADB itself.
28
+ */
29
+ const MAX_MEMORY_PERCENT = 65;
30
+ /**
31
+ * Detect available CPU cores on the device.
32
+ * Returns total cores minus 1 for the guest (minimum 1), leaving one core
33
+ * reserved for the host OS. On single-core devices, the VM gets the one core.
34
+ */
35
+ async function detectMaxCpus(bridge) {
36
+ try {
37
+ const result = await bridge.shell("nproc 2>/dev/null || grep -c ^processor /proc/cpuinfo 2>/dev/null", {
38
+ ignoreExitCode: true, timeout: 5000,
39
+ });
40
+ const total = parseInt(result.stdout.trim(), 10) || 1;
41
+ // Reserve 1 core for host; minimum 1 for guest
42
+ const forGuest = total > 1 ? total - 1 : 1;
43
+ return { total, forGuest };
44
+ }
45
+ catch {
46
+ return { total: 1, forGuest: 1 };
47
+ }
48
+ }
49
+ /**
50
+ * Detect total physical RAM in MB.
51
+ * Returns total system memory and the maximum amount allocatable to a VM
52
+ * (MAX_MEMORY_PERCENT of total), leaving headroom for the host.
53
+ */
54
+ async function detectMaxMemoryMb(bridge) {
55
+ try {
56
+ const result = await bridge.shell("grep MemTotal /proc/meminfo 2>/dev/null", {
57
+ ignoreExitCode: true, timeout: 5000,
58
+ });
59
+ const match = result.stdout.match(/MemTotal:\s+(\d+)\s+kB/i);
60
+ if (match) {
61
+ const totalMb = Math.floor(parseInt(match[1], 10) / 1024);
62
+ const forGuestMb = Math.floor(totalMb * MAX_MEMORY_PERCENT / 100);
63
+ return { totalMb, forGuestMb };
64
+ }
65
+ }
66
+ catch { /* fallback */ }
67
+ // Conservative fallback: 2GB total, ~1.3GB for guest
68
+ return { totalMb: 2048, forGuestMb: Math.floor(2048 * MAX_MEMORY_PERCENT / 100) };
69
+ }
70
+ /**
71
+ * Detect big.LITTLE CPU topology by parsing /proc/cpuinfo.
72
+ * On heterogeneous SoCs (e.g., Tensor G1: X1 + A78 + A55), KVM's `-cpu host`
73
+ * fails if QEMU threads migrate between core types with different feature sets.
74
+ * Returns a `taskset` hex mask pinning to the LITTLE (efficiency) cores,
75
+ * which are homogeneous and safe for KVM passthrough.
76
+ *
77
+ * Returns null if the CPU topology is homogeneous (no pinning needed).
78
+ */
79
+ async function detectLittleCoresMask(bridge) {
80
+ try {
81
+ const result = await bridge.shell("grep -E '^processor|^CPU part' /proc/cpuinfo 2>/dev/null", { ignoreExitCode: true, timeout: 5000 });
82
+ const lines = result.stdout.split("\n").map(l => l.trim()).filter(l => l.length > 0);
83
+ const cores = [];
84
+ let currentId = -1;
85
+ for (const line of lines) {
86
+ const procMatch = line.match(/^processor\s*:\s*(\d+)/);
87
+ if (procMatch) {
88
+ currentId = parseInt(procMatch[1], 10);
89
+ continue;
90
+ }
91
+ const partMatch = line.match(/^CPU part\s*:\s*(0x[0-9a-fA-F]+)/);
92
+ if (partMatch && currentId >= 0) {
93
+ cores.push({ id: currentId, part: partMatch[1].toLowerCase() });
94
+ currentId = -1;
95
+ }
96
+ }
97
+ if (cores.length === 0) {
98
+ return { mask: null, isHeterogeneous: false, littleCores: [], totalCores: 0, description: "unknown topology" };
99
+ }
100
+ // Check if all cores have the same CPU part
101
+ const uniqueParts = new Set(cores.map(c => c.part));
102
+ if (uniqueParts.size <= 1) {
103
+ return {
104
+ mask: null,
105
+ isHeterogeneous: false,
106
+ littleCores: cores.map(c => c.id),
107
+ totalCores: cores.length,
108
+ description: `homogeneous (${cores.length} cores, part ${cores[0].part})`,
109
+ };
110
+ }
111
+ // Heterogeneous: find the LITTLE cores (smallest CPU part number = efficiency cores)
112
+ // ARM convention: smaller part numbers = smaller/efficiency cores
113
+ // A55=0xd05, A78=0xd41, X1=0xd44, A76=0xd0b, etc.
114
+ const partCounts = new Map();
115
+ for (const core of cores) {
116
+ const list = partCounts.get(core.part) ?? [];
117
+ list.push(core.id);
118
+ partCounts.set(core.part, list);
119
+ }
120
+ // Sort parts numerically — smallest is LITTLE
121
+ const sortedParts = [...partCounts.entries()].sort(([a], [b]) => parseInt(a, 16) - parseInt(b, 16));
122
+ const littleCores = sortedParts[0][1].sort((a, b) => a - b);
123
+ // Build hex mask for taskset
124
+ let maskBits = 0;
125
+ for (const coreId of littleCores) {
126
+ maskBits |= (1 << coreId);
127
+ }
128
+ const mask = maskBits.toString(16);
129
+ const partLabels = sortedParts.map(([part, ids]) => `${part}×${ids.length}`).join(" + ");
130
+ return {
131
+ mask,
132
+ isHeterogeneous: true,
133
+ littleCores,
134
+ totalCores: cores.length,
135
+ description: `heterogeneous big.LITTLE (${partLabels}), LITTLE cores: [${littleCores.join(",")}]`,
136
+ };
137
+ }
138
+ catch {
139
+ return { mask: null, isHeterogeneous: false, littleCores: [], totalCores: 0, description: "detection failed" };
140
+ }
141
+ }
142
+ const runningVms = new Map();
143
+ function getImageDir(tempDir) {
144
+ return join(tempDir, "qemu-images");
145
+ }
146
+ /** Sanitize a VM/image name for safe filesystem use. */
147
+ function sanitizeName(name) {
148
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_").substring(0, 64);
149
+ }
150
+ /**
151
+ * Verify that a path is contained within the image directory.
152
+ * Prevents path traversal attacks via image name manipulation.
153
+ */
154
+ function verifyContainment(filePath, imageDir) {
155
+ const resolved = resolve(filePath);
156
+ const resolvedDir = resolve(imageDir);
157
+ return resolved.startsWith(resolvedDir + "/") || resolved.startsWith(resolvedDir + "\\");
158
+ }
159
+ /** Register cleanup to kill all QEMU VMs on process exit. */
160
+ function updateCleanup() {
161
+ if (runningVms.size > 0) {
162
+ registerCleanup("qemu-vms", () => {
163
+ for (const [, vm] of runningVms) {
164
+ try {
165
+ vm.process.kill("SIGKILL");
166
+ }
167
+ catch { /* ignore */ }
168
+ // Root-elevated: kill actual QEMU child by stored PID
169
+ if (vm.rootElevated && vm.qemuPid) {
170
+ try {
171
+ spawnSync("su", ["-c", `kill -9 ${vm.qemuPid} 2>/dev/null; true`], { timeout: 3000 });
172
+ }
173
+ catch { /* best-effort */ }
174
+ }
175
+ if (vm.pidFile) {
176
+ try {
177
+ spawnSync("su", ["-c", `rm -f '${vm.pidFile.replace(/'/g, "'\\''")}' 2>/dev/null; true`], { timeout: 2000 });
178
+ }
179
+ catch { /* ignore */ }
180
+ }
181
+ }
182
+ });
183
+ }
184
+ else {
185
+ unregisterCleanup("qemu-vms");
186
+ }
187
+ }
188
+ export function registerQemuTools(ctx) {
189
+ ctx.server.tool("adb_qemu_setup", "Check and install QEMU for on-device virtualization. Verifies KVM availability, checks if QEMU is installed, reports version info, and can install QEMU via Termux package manager. Only available in on-device mode.", {
190
+ install: z.boolean().optional().default(false)
191
+ .describe("If true, install QEMU via pkg if not already present"),
192
+ }, async ({ install }) => {
193
+ try {
194
+ if (!isOnDevice()) {
195
+ return {
196
+ content: [{ type: "text", text: "QEMU tools are only available in on-device mode (Termux). Use adb_emulator_start for AVD management on desktop." }],
197
+ isError: true,
198
+ };
199
+ }
200
+ const sections = [];
201
+ sections.push("=== QEMU/KVM Setup ===");
202
+ // Check KVM
203
+ const kvmAvailable = existsSync("/dev/kvm");
204
+ sections.push(`\nKVM: ${kvmAvailable ? "✓ /dev/kvm available — hardware acceleration enabled" : "✗ /dev/kvm not found — VMs will be very slow"}`);
205
+ // Check QEMU installation
206
+ let qemuVersion = "";
207
+ let qemuInstalled = false;
208
+ try {
209
+ const result = await ctx.bridge.shell("qemu-system-aarch64 --version 2>&1 | head -1", {
210
+ ignoreExitCode: true, timeout: 5000,
211
+ });
212
+ if (result.stdout.includes("QEMU")) {
213
+ qemuInstalled = true;
214
+ qemuVersion = result.stdout.trim();
215
+ }
216
+ }
217
+ catch { /* not installed */ }
218
+ if (qemuInstalled) {
219
+ sections.push(`QEMU: ✓ ${qemuVersion}`);
220
+ }
221
+ else if (install) {
222
+ sections.push("QEMU: not installed — installing...");
223
+ try {
224
+ const installResult = await ctx.bridge.shell("pkg install -y qemu-system-aarch64-headless qemu-utils 2>&1", { timeout: 120000, ignoreExitCode: true });
225
+ if (installResult.stdout.includes("Setting up") || installResult.stdout.includes("already the newest")) {
226
+ sections.push("✓ QEMU installed successfully.");
227
+ // Verify
228
+ const verResult = await ctx.bridge.shell("qemu-system-aarch64 --version 2>&1 | head -1", {
229
+ ignoreExitCode: true, timeout: 5000,
230
+ });
231
+ if (verResult.stdout.includes("QEMU")) {
232
+ sections.push(`Version: ${verResult.stdout.trim()}`);
233
+ }
234
+ }
235
+ else {
236
+ sections.push(`Installation output:\n${installResult.stdout.substring(0, 500)}`);
237
+ }
238
+ }
239
+ catch (e) {
240
+ sections.push(`✗ Installation failed: ${e instanceof Error ? e.message : String(e)}`);
241
+ }
242
+ }
243
+ else {
244
+ sections.push("QEMU: ✗ not installed");
245
+ sections.push("Install with: adb_qemu_setup install=true");
246
+ sections.push(" or manually: pkg install qemu-system-aarch64-headless qemu-utils");
247
+ }
248
+ // Check qemu-img tool
249
+ try {
250
+ const imgResult = await ctx.bridge.shell("qemu-img --version 2>&1 | head -1", {
251
+ ignoreExitCode: true, timeout: 5000,
252
+ });
253
+ if (imgResult.stdout.includes("qemu-img")) {
254
+ sections.push(`qemu-img: ✓ ${imgResult.stdout.trim()}`);
255
+ }
256
+ }
257
+ catch { /* not available */ }
258
+ // Image directory
259
+ const imageDir = getImageDir(ctx.config.tempDir);
260
+ if (!existsSync(imageDir))
261
+ mkdirSync(imageDir, { recursive: true });
262
+ sections.push(`\nImage directory: ${imageDir}`);
263
+ // Host resource detection for VM allocation guidance
264
+ const cpuInfo = await detectMaxCpus(ctx.bridge);
265
+ const memInfo = await detectMaxMemoryMb(ctx.bridge);
266
+ sections.push(`\nHost resources:`);
267
+ sections.push(` CPU cores: ${cpuInfo.total} total → ${cpuInfo.forGuest} available for VMs (1 reserved for host)`);
268
+ sections.push(` Memory: ${memInfo.totalMb} MB total → ${memInfo.forGuestMb} MB available for VMs (${MAX_MEMORY_PERCENT}% limit)`);
269
+ // CPU topology (big.LITTLE detection)
270
+ const topology = await detectLittleCoresMask(ctx.bridge);
271
+ sections.push(` CPU topology: ${topology.description}`);
272
+ if (topology.isHeterogeneous && topology.mask) {
273
+ sections.push(` KVM strategy: pin to LITTLE cores [${topology.littleCores.join(",")}] via taskset 0x${topology.mask}`);
274
+ }
275
+ // Running VMs
276
+ sections.push(`Running VMs: ${runningVms.size}`);
277
+ return { content: [{ type: "text", text: sections.join("\n") }] };
278
+ }
279
+ catch (error) {
280
+ return { content: [{ type: "text", text: OutputProcessor.formatError(error) }], isError: true };
281
+ }
282
+ });
283
+ ctx.server.tool("adb_qemu_images", "Manage QEMU disk images for virtual machines. List available images, create new qcow2/raw disk images, or delete existing ones. Images are stored in the DeepADB image directory.", {
284
+ action: z.enum(["list", "create", "delete"]).describe("Action to perform"),
285
+ name: z.string().optional().describe("Image name (for create/delete). Alphanumeric, hyphens, underscores only."),
286
+ sizeMb: z.number().min(64).max(65536).optional().default(8192)
287
+ .describe("Image size in MB (for create). Default 8192 (8GB). Range: 64-65536."),
288
+ format: z.enum(["qcow2", "raw"]).optional().default("qcow2")
289
+ .describe("Image format. qcow2 is recommended (sparse, snapshots). raw for maximum I/O performance."),
290
+ }, async ({ action, name, sizeMb, format }) => {
291
+ try {
292
+ if (!isOnDevice()) {
293
+ return { content: [{ type: "text", text: "QEMU tools are only available in on-device mode." }], isError: true };
294
+ }
295
+ const imageDir = getImageDir(ctx.config.tempDir);
296
+ if (!existsSync(imageDir))
297
+ mkdirSync(imageDir, { recursive: true });
298
+ if (action === "list") {
299
+ const files = readdirSync(imageDir)
300
+ .filter(f => f.endsWith(".qcow2") || f.endsWith(".img") || f.endsWith(".raw"))
301
+ .sort();
302
+ if (files.length === 0) {
303
+ return { content: [{ type: "text", text: `No disk images found.\nCreate one with: adb_qemu_images action="create" name="myvm"\nImage directory: ${imageDir}` }] };
304
+ }
305
+ const lines = [`${files.length} disk image(s):\n`];
306
+ for (const file of files) {
307
+ try {
308
+ const stat = statSync(join(imageDir, file));
309
+ const sizeMbStr = (stat.size / (1024 * 1024)).toFixed(1);
310
+ lines.push(` ${file} — ${sizeMbStr} MB on disk`);
311
+ }
312
+ catch {
313
+ lines.push(` ${file} — (stat failed)`);
314
+ }
315
+ }
316
+ // Show running VMs
317
+ if (runningVms.size > 0) {
318
+ lines.push(`\nRunning VMs: ${Array.from(runningVms.keys()).join(", ")}`);
319
+ }
320
+ lines.push(`\nImage directory: ${imageDir}`);
321
+ return { content: [{ type: "text", text: lines.join("\n") }] };
322
+ }
323
+ if (!name) {
324
+ return { content: [{ type: "text", text: "Image name is required for create/delete actions." }], isError: true };
325
+ }
326
+ const safeName = sanitizeName(name);
327
+ const ext = format === "raw" ? ".raw" : ".qcow2";
328
+ const imagePath = join(imageDir, safeName + ext);
329
+ // Path containment check
330
+ if (!verifyContainment(imagePath, imageDir)) {
331
+ return { content: [{ type: "text", text: "Invalid image name — path traversal detected." }], isError: true };
332
+ }
333
+ if (action === "create") {
334
+ if (existsSync(imagePath)) {
335
+ return { content: [{ type: "text", text: `Image already exists: ${safeName}${ext}` }], isError: true };
336
+ }
337
+ // Use qemu-img to create the image
338
+ const createResult = await ctx.bridge.shell(`qemu-img create -f ${format} '${imagePath.replace(/'/g, "'\\''")}' ${sizeMb}M 2>&1`, { timeout: 30000, ignoreExitCode: true });
339
+ if (existsSync(imagePath)) {
340
+ const stat = statSync(imagePath);
341
+ return {
342
+ content: [{
343
+ type: "text",
344
+ text: `✓ Image created: ${safeName}${ext}\nFormat: ${format}\nVirtual size: ${sizeMb} MB\nActual size: ${(stat.size / (1024 * 1024)).toFixed(1)} MB\nPath: ${imagePath}`,
345
+ }],
346
+ };
347
+ }
348
+ else {
349
+ return {
350
+ content: [{ type: "text", text: `Failed to create image: ${createResult.stdout || createResult.stderr}\nIs qemu-utils installed? Run: adb_qemu_setup install=true` }],
351
+ isError: true,
352
+ };
353
+ }
354
+ }
355
+ if (action === "delete") {
356
+ // Check both extensions
357
+ const candidates = [
358
+ join(imageDir, safeName + ".qcow2"),
359
+ join(imageDir, safeName + ".raw"),
360
+ join(imageDir, safeName + ".img"),
361
+ ];
362
+ const found = candidates.find(c => existsSync(c));
363
+ if (!found) {
364
+ return { content: [{ type: "text", text: `Image not found: ${safeName}` }], isError: true };
365
+ }
366
+ // Don't delete if a VM is using it
367
+ for (const [vmName, vm] of runningVms) {
368
+ if (resolve(vm.imagePath) === resolve(found)) {
369
+ return { content: [{ type: "text", text: `Cannot delete — image is in use by running VM '${vmName}'.` }], isError: true };
370
+ }
371
+ }
372
+ if (!verifyContainment(found, imageDir)) {
373
+ return { content: [{ type: "text", text: "Invalid path — containment check failed." }], isError: true };
374
+ }
375
+ unlinkSync(found);
376
+ return { content: [{ type: "text", text: `✓ Deleted: ${basename(found)}` }] };
377
+ }
378
+ return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
379
+ }
380
+ catch (error) {
381
+ return { content: [{ type: "text", text: OutputProcessor.formatError(error) }], isError: true };
382
+ }
383
+ });
384
+ ctx.server.tool("adb_qemu_start", "Boot a QEMU virtual machine with KVM hardware acceleration. Auto-detects optimal resource allocation: uses total cores minus 1 for the VM (reserving one for the host OS), and up to 65% of physical RAM. Custom values are accepted but capped at safe limits to prevent host starvation.", {
385
+ name: z.string().describe("VM name (used to track and stop the VM)"),
386
+ image: z.string().describe("Disk image filename (from adb_qemu_images) or absolute path"),
387
+ memoryMb: z.number().min(128).max(65536).optional()
388
+ .describe("RAM in MB. Auto-detected if omitted (65% of physical RAM). Capped at safe limit."),
389
+ cpus: z.number().min(1).max(64).optional()
390
+ .describe("Virtual CPUs. Auto-detected if omitted (total cores minus 1). Capped at safe limit."),
391
+ adbPort: z.number().min(1024).max(65535).optional().default(5556)
392
+ .describe("Host port to forward to guest ADB (port 5555). Default 5556."),
393
+ kernel: z.string().optional()
394
+ .describe("Path to kernel image (for Android boot). If omitted, QEMU uses the disk image's bootloader."),
395
+ initrd: z.string().optional()
396
+ .describe("Path to initrd/ramdisk image (for Android boot)."),
397
+ append: z.string().optional()
398
+ .describe("Kernel command line arguments (for Android boot)."),
399
+ display: z.enum(["none", "vnc"]).optional().default("none")
400
+ .describe("Display output. 'none' for headless (default). 'vnc' starts a VNC server on port 5900."),
401
+ }, async ({ name, image, memoryMb, cpus, adbPort, kernel, initrd, append, display }) => {
402
+ try {
403
+ if (!isOnDevice()) {
404
+ return { content: [{ type: "text", text: "QEMU tools are only available in on-device mode." }], isError: true };
405
+ }
406
+ const safeName = sanitizeName(name);
407
+ if (runningVms.has(safeName)) {
408
+ return { content: [{ type: "text", text: `VM '${safeName}' is already running. Stop it first with adb_qemu_stop.` }], isError: true };
409
+ }
410
+ if (runningVms.size >= MAX_VMS) {
411
+ return { content: [{ type: "text", text: `Maximum ${MAX_VMS} concurrent VMs reached. Stop a VM first.` }], isError: true };
412
+ }
413
+ // ── Auto-detect and cap resources ──
414
+ const cpuInfo = await detectMaxCpus(ctx.bridge);
415
+ const memInfo = await detectMaxMemoryMb(ctx.bridge);
416
+ // Account for resources already consumed by running VMs
417
+ let usedCpus = 0;
418
+ let usedMemMb = 0;
419
+ for (const [, vm] of runningVms) {
420
+ usedCpus += vm.cpus;
421
+ usedMemMb += vm.memoryMb;
422
+ }
423
+ // Remaining pool: host ALWAYS keeps 1 core no matter what.
424
+ // If the pool is exhausted, refuse to start rather than starving the host.
425
+ const availableCpus = cpuInfo.forGuest - usedCpus;
426
+ const availableMemMb = memInfo.forGuestMb - usedMemMb;
427
+ if (availableCpus <= 0) {
428
+ return {
429
+ content: [{ type: "text", text: `Cannot start VM — all allocatable CPU cores are in use.\nHost has ${cpuInfo.total} cores, 1 reserved for host OS, ${usedCpus} allocated to running VMs.\nStop a VM first with adb_qemu_stop.` }],
430
+ isError: true,
431
+ };
432
+ }
433
+ if (availableMemMb < 128) {
434
+ return {
435
+ content: [{ type: "text", text: `Cannot start VM — insufficient memory.\n${memInfo.forGuestMb} MB allocatable (${MAX_MEMORY_PERCENT}% of ${memInfo.totalMb} MB), ${usedMemMb} MB in use by running VMs.\nStop a VM first with adb_qemu_stop.` }],
436
+ isError: true,
437
+ };
438
+ }
439
+ // Check for ADB port conflicts with running VMs
440
+ for (const [vmName, vm] of runningVms) {
441
+ if (vm.adbPort === adbPort) {
442
+ return {
443
+ content: [{ type: "text", text: `Port ${adbPort} is already in use by VM '${vmName}'.\nSpecify a different port: adb_qemu_start adbPort=${adbPort + 2}` }],
444
+ isError: true,
445
+ };
446
+ }
447
+ }
448
+ // Apply auto-detection or cap user-provided values.
449
+ // INVARIANT: sum of all VM CPUs must never exceed (total cores - 1).
450
+ let actualCpus;
451
+ let cpuNote = "";
452
+ if (cpus !== undefined) {
453
+ if (cpus > availableCpus) {
454
+ actualCpus = availableCpus;
455
+ cpuNote = ` (requested ${cpus}, capped to ${availableCpus} — ${cpuInfo.total} total cores, 1 reserved for host${usedCpus > 0 ? `, ${usedCpus} used by other VMs` : ""})`;
456
+ }
457
+ else {
458
+ actualCpus = cpus;
459
+ }
460
+ }
461
+ else {
462
+ actualCpus = availableCpus;
463
+ cpuNote = ` (auto: ${cpuInfo.total} total, 1 reserved for host${usedCpus > 0 ? `, ${usedCpus} used by other VMs` : ""})`;
464
+ }
465
+ // INVARIANT: sum of all VM memory must never exceed MAX_MEMORY_PERCENT of physical RAM.
466
+ let actualMemMb;
467
+ let memNote = "";
468
+ if (memoryMb !== undefined) {
469
+ if (memoryMb > availableMemMb) {
470
+ actualMemMb = availableMemMb;
471
+ memNote = ` (requested ${memoryMb} MB, capped to ${availableMemMb} MB — ${MAX_MEMORY_PERCENT}% of ${memInfo.totalMb} MB${usedMemMb > 0 ? `, ${usedMemMb} MB used by other VMs` : ""})`;
472
+ }
473
+ else {
474
+ actualMemMb = memoryMb;
475
+ }
476
+ }
477
+ else {
478
+ // Auto: use the lesser of available pool and 2048 MB (sensible single-VM default)
479
+ actualMemMb = Math.min(availableMemMb, 2048);
480
+ memNote = ` (auto: ${MAX_MEMORY_PERCENT}% of ${memInfo.totalMb} MB${usedMemMb > 0 ? `, ${usedMemMb} MB used by other VMs` : ""}${availableMemMb > 2048 ? ", capped at 2048 MB" : ""})`;
481
+ }
482
+ // Resolve image path — check image directory first, then treat as absolute
483
+ const imageDir = getImageDir(ctx.config.tempDir);
484
+ let resolvedImage = image;
485
+ if (!image.startsWith("/")) {
486
+ // Relative name — look in image directory
487
+ const candidates = [
488
+ join(imageDir, image),
489
+ join(imageDir, image + ".qcow2"),
490
+ join(imageDir, image + ".raw"),
491
+ join(imageDir, image + ".img"),
492
+ ];
493
+ const found = candidates.find(c => existsSync(c));
494
+ if (!found) {
495
+ return { content: [{ type: "text", text: `Image not found: ${image}\nUse adb_qemu_images action="list" to see available images.` }], isError: true };
496
+ }
497
+ resolvedImage = found;
498
+ }
499
+ if (!existsSync(resolvedImage)) {
500
+ return { content: [{ type: "text", text: `Image file not found: ${resolvedImage}` }], isError: true };
501
+ }
502
+ // Check KVM
503
+ const useKvm = existsSync("/dev/kvm");
504
+ // Detect image format from extension.
505
+ // Images created by adb_qemu_images have correct extensions (.qcow2 or .raw).
506
+ // Manually placed .img files are assumed raw — use .qcow2 extension for qcow2 images.
507
+ const fmt = resolvedImage.endsWith(".qcow2") ? "qcow2" : "raw";
508
+ // Build QEMU command
509
+ // Use -pidfile so QEMU writes its own PID — needed for reliable cleanup
510
+ // when spawned via su (the su wrapper PID differs from the QEMU child PID).
511
+ const pidFilePath = join(ctx.config.tempDir, `qemu-${safeName}.pid`);
512
+ const qemuArgs = [
513
+ "-M", "virt,gic-version=max",
514
+ "-m", String(actualMemMb),
515
+ "-smp", String(actualCpus),
516
+ "-drive", `file=${resolvedImage},format=${fmt},if=virtio`,
517
+ "-netdev", `user,id=net0,hostfwd=tcp::${adbPort}-:5555`,
518
+ "-device", "virtio-net-pci,netdev=net0",
519
+ "-pidfile", pidFilePath,
520
+ ];
521
+ // ── big.LITTLE / KVM handling ──
522
+ // On heterogeneous SoCs (Tensor G1, Snapdragon 8 Gen series, Dimensity, etc.)
523
+ // KVM's `-cpu host` fails when QEMU threads migrate between core types with
524
+ // different feature registers. We pin to LITTLE (efficiency) cores via taskset.
525
+ // SELinux on Android blocks untrusted_app → kvm_device access, so KVM always
526
+ // requires root elevation via `su`.
527
+ let cpuTopologyNote = "";
528
+ let tasksetPrefix = "";
529
+ const topology = useKvm ? await detectLittleCoresMask(ctx.bridge) : null;
530
+ if (useKvm) {
531
+ qemuArgs.unshift("-enable-kvm");
532
+ qemuArgs.push("-cpu", "host");
533
+ if (topology?.isHeterogeneous && topology.mask) {
534
+ tasksetPrefix = `taskset ${topology.mask} `;
535
+ cpuTopologyNote = `\nCPU topology: ${topology.description}\nTaskset: pinned to LITTLE cores [${topology.littleCores.join(",")}] (mask 0x${topology.mask})`;
536
+ }
537
+ else if (topology) {
538
+ cpuTopologyNote = `\nCPU topology: ${topology.description}`;
539
+ }
540
+ }
541
+ else {
542
+ qemuArgs.push("-cpu", "cortex-a72");
543
+ }
544
+ // Display
545
+ if (display === "vnc") {
546
+ qemuArgs.push("-vnc", ":0");
547
+ }
548
+ else {
549
+ qemuArgs.push("-nographic");
550
+ }
551
+ // Kernel/initrd/append for Android boot
552
+ if (kernel) {
553
+ if (!existsSync(kernel)) {
554
+ return { content: [{ type: "text", text: `Kernel not found: ${kernel}` }], isError: true };
555
+ }
556
+ qemuArgs.push("-kernel", kernel);
557
+ }
558
+ if (initrd) {
559
+ if (!existsSync(initrd)) {
560
+ return { content: [{ type: "text", text: `Initrd not found: ${initrd}` }], isError: true };
561
+ }
562
+ qemuArgs.push("-initrd", initrd);
563
+ }
564
+ if (append) {
565
+ qemuArgs.push("-append", append);
566
+ }
567
+ // Serial console for headless mode
568
+ if (display === "none") {
569
+ qemuArgs.push("-serial", "mon:stdio");
570
+ }
571
+ ctx.logger.info(`[qemu:${safeName}] Starting: ${tasksetPrefix}qemu-system-aarch64 ${qemuArgs.join(" ")}`);
572
+ // KVM requires root (SELinux blocks untrusted_app → kvm_device).
573
+ // Spawn via `su -c` with full Termux PATH/LD_LIBRARY_PATH.
574
+ // Without KVM, spawn directly as the current user.
575
+ let proc;
576
+ const termuxBin = "/data/data/com.termux/files/usr/bin";
577
+ const termuxLib = "/data/data/com.termux/files/usr/lib";
578
+ if (useKvm) {
579
+ // Build a shell command string for su -c.
580
+ // Single-quote each arg that could contain special chars (file paths).
581
+ const escapedArgs = qemuArgs.map(arg => {
582
+ if (arg.includes("=") || arg.includes("/") || arg.includes(",") || arg.includes(":")) {
583
+ return `'${arg.replace(/'/g, "'\\''")}'`;
584
+ }
585
+ return arg;
586
+ }).join(" ");
587
+ const suCmd = `PATH=${termuxBin}:$PATH LD_LIBRARY_PATH=${termuxLib} ${tasksetPrefix}qemu-system-aarch64 ${escapedArgs}`;
588
+ proc = spawn("su", ["-c", suCmd], {
589
+ stdio: ["ignore", "pipe", "pipe"],
590
+ });
591
+ }
592
+ else {
593
+ proc = spawn("qemu-system-aarch64", qemuArgs, {
594
+ stdio: ["ignore", "pipe", "pipe"],
595
+ env: { ...process.env, PATH: `${process.env.PATH}:${termuxBin}` },
596
+ });
597
+ }
598
+ const vmInfo = {
599
+ process: proc,
600
+ name: safeName,
601
+ imagePath: resolvedImage,
602
+ adbPort,
603
+ memoryMb: actualMemMb,
604
+ cpus: actualCpus,
605
+ startTime: Date.now(),
606
+ pid: proc.pid ?? null,
607
+ qemuPid: null,
608
+ pidFile: pidFilePath,
609
+ rootElevated: useKvm,
610
+ };
611
+ runningVms.set(safeName, vmInfo);
612
+ updateCleanup();
613
+ let stderrOutput = "";
614
+ proc.stderr?.on("data", (chunk) => {
615
+ stderrOutput += chunk.toString();
616
+ // Cap stderr accumulation
617
+ if (stderrOutput.length > 4096)
618
+ stderrOutput = stderrOutput.substring(stderrOutput.length - 4096);
619
+ });
620
+ proc.on("error", (err) => {
621
+ ctx.logger.error(`[qemu:${safeName}] Spawn error: ${err.message}`);
622
+ runningVms.delete(safeName);
623
+ updateCleanup();
624
+ });
625
+ proc.on("exit", (code) => {
626
+ ctx.logger.info(`[qemu:${safeName}] Exited (code: ${code})`);
627
+ runningVms.delete(safeName);
628
+ updateCleanup();
629
+ });
630
+ // Wait briefly for startup errors
631
+ await new Promise(r => setTimeout(r, 2000));
632
+ if (!runningVms.has(safeName)) {
633
+ const errMsg = stderrOutput.trim().split("\n").slice(0, 5).join("\n");
634
+ return {
635
+ content: [{ type: "text", text: `VM '${safeName}' failed to start.${errMsg ? "\n" + errMsg : ""}\n\nIs QEMU installed? Run: adb_qemu_setup install=true` }],
636
+ isError: true,
637
+ };
638
+ }
639
+ // Read the QEMU PID from the pidfile (written by QEMU's -pidfile option).
640
+ // The pidfile is owned by root (QEMU runs elevated), so read it via su.
641
+ // Store in vmInfo for reliable stop/cleanup without filesystem access at stop time.
642
+ if (vmInfo.rootElevated && vmInfo.pidFile) {
643
+ try {
644
+ const catResult = spawnSync("su", ["-c", `cat '${vmInfo.pidFile.replace(/'/g, "'\\''")}' 2>/dev/null`], { timeout: 3000 });
645
+ const qemuPid = parseInt(catResult.stdout?.toString().trim(), 10);
646
+ if (qemuPid > 0) {
647
+ vmInfo.qemuPid = qemuPid;
648
+ ctx.logger.info(`[qemu:${safeName}] QEMU PID from pidfile: ${qemuPid} (su wrapper: ${vmInfo.pid})`);
649
+ }
650
+ }
651
+ catch { /* pidfile may not be written yet — non-critical */ }
652
+ }
653
+ const sections = [];
654
+ sections.push(`✓ VM '${safeName}' started`);
655
+ sections.push(`PID: ${vmInfo.pid}`);
656
+ sections.push(`Image: ${basename(resolvedImage)}`);
657
+ sections.push(`CPUs: ${actualCpus}${cpuNote}`);
658
+ sections.push(`Memory: ${actualMemMb} MB${memNote}`);
659
+ sections.push(`KVM: ${useKvm ? "enabled (root-elevated)" : "disabled (software emulation)"}`);
660
+ if (cpuTopologyNote)
661
+ sections.push(cpuTopologyNote.trimStart());
662
+ sections.push(`ADB port: ${adbPort} → guest:5555`);
663
+ if (display === "vnc")
664
+ sections.push("VNC: :0 (port 5900)");
665
+ sections.push(`\nConnect to guest ADB: adb connect localhost:${adbPort}`);
666
+ sections.push("Stop with: adb_qemu_stop name=\"" + safeName + "\"");
667
+ return { content: [{ type: "text", text: sections.join("\n") }] };
668
+ }
669
+ catch (error) {
670
+ return { content: [{ type: "text", text: OutputProcessor.formatError(error) }], isError: true };
671
+ }
672
+ });
673
+ ctx.server.tool("adb_qemu_stop", "Stop a running QEMU virtual machine. Sends SIGTERM for graceful shutdown, with force kill option.", {
674
+ name: z.string().optional().describe("VM name to stop. If omitted, lists running VMs."),
675
+ force: z.boolean().optional().default(false).describe("Use SIGKILL instead of SIGTERM for immediate termination."),
676
+ }, async ({ name, force }) => {
677
+ try {
678
+ if (!name) {
679
+ if (runningVms.size === 0) {
680
+ return { content: [{ type: "text", text: "No QEMU VMs running." }] };
681
+ }
682
+ const lines = ["Running QEMU VMs:\n"];
683
+ for (const [vmName, vm] of runningVms) {
684
+ const uptime = Math.round((Date.now() - vm.startTime) / 1000);
685
+ lines.push(` ${vmName} — PID ${vm.pid}, ${vm.memoryMb}MB, port ${vm.adbPort}, uptime ${uptime}s`);
686
+ }
687
+ lines.push("\nProvide name to stop a specific VM.");
688
+ return { content: [{ type: "text", text: lines.join("\n") }] };
689
+ }
690
+ const safeName = sanitizeName(name);
691
+ const vm = runningVms.get(safeName);
692
+ if (!vm) {
693
+ return { content: [{ type: "text", text: `VM '${safeName}' is not running.` }], isError: true };
694
+ }
695
+ try {
696
+ vm.process.kill(force ? "SIGKILL" : "SIGTERM");
697
+ }
698
+ catch { /* already dead */ }
699
+ // Root-elevated VMs: kill the actual QEMU child by stored PID from pidfile.
700
+ // The su wrapper kill above orphans QEMU — this kills the real process.
701
+ if (vm.rootElevated && vm.qemuPid) {
702
+ try {
703
+ spawnSync("su", ["-c", `kill -9 ${vm.qemuPid} 2>/dev/null; true`], { timeout: 5000 });
704
+ await new Promise(r => setTimeout(r, 500));
705
+ }
706
+ catch { /* best-effort */ }
707
+ }
708
+ // Clean up pidfile
709
+ if (vm.pidFile) {
710
+ try {
711
+ spawnSync("su", ["-c", `rm -f '${vm.pidFile.replace(/'/g, "'\\''")}' 2>/dev/null; true`], { timeout: 2000 });
712
+ }
713
+ catch { /* ignore */ }
714
+ }
715
+ runningVms.delete(safeName);
716
+ updateCleanup();
717
+ return {
718
+ content: [{ type: "text", text: `VM '${safeName}' ${force ? "killed" : "stop signal sent"} (was PID ${vm.pid}).` }],
719
+ };
720
+ }
721
+ catch (error) {
722
+ return { content: [{ type: "text", text: OutputProcessor.formatError(error) }], isError: true };
723
+ }
724
+ });
725
+ ctx.server.tool("adb_qemu_status", "Show status of QEMU virtual machines — running VMs with resource usage and port mappings, plus KVM and QEMU availability.", {}, async () => {
726
+ try {
727
+ if (!isOnDevice()) {
728
+ return { content: [{ type: "text", text: "QEMU tools are only available in on-device mode." }], isError: true };
729
+ }
730
+ const sections = [];
731
+ sections.push("=== QEMU VM Status ===");
732
+ // KVM
733
+ sections.push(`KVM: ${existsSync("/dev/kvm") ? "✓ available" : "✗ unavailable"}`);
734
+ // QEMU binary
735
+ let qemuAvailable = false;
736
+ try {
737
+ const result = await ctx.bridge.shell("qemu-system-aarch64 --version 2>&1 | head -1", {
738
+ ignoreExitCode: true, timeout: 5000,
739
+ });
740
+ qemuAvailable = result.stdout.includes("QEMU");
741
+ if (qemuAvailable)
742
+ sections.push(`QEMU: ✓ ${result.stdout.trim()}`);
743
+ }
744
+ catch { /* not installed */ }
745
+ if (!qemuAvailable)
746
+ sections.push("QEMU: ✗ not installed");
747
+ // Host resource detection
748
+ const cpuInfo = await detectMaxCpus(ctx.bridge);
749
+ const memInfo = await detectMaxMemoryMb(ctx.bridge);
750
+ let usedCpus = 0, usedMemMb = 0;
751
+ for (const [, vm] of runningVms) {
752
+ usedCpus += vm.cpus;
753
+ usedMemMb += vm.memoryMb;
754
+ }
755
+ sections.push(`\nHost resources:`);
756
+ sections.push(` CPU cores: ${cpuInfo.total} total, ${cpuInfo.forGuest} allocatable (1 reserved for host)`);
757
+ sections.push(` Memory: ${memInfo.totalMb} MB total, ${memInfo.forGuestMb} MB allocatable (${MAX_MEMORY_PERCENT}% limit)`);
758
+ if (usedCpus > 0 || usedMemMb > 0) {
759
+ sections.push(` In use by VMs: ${usedCpus} CPUs, ${usedMemMb} MB`);
760
+ sections.push(` Available: ${Math.max(0, cpuInfo.forGuest - usedCpus)} CPUs, ${Math.max(0, memInfo.forGuestMb - usedMemMb)} MB`);
761
+ }
762
+ // Running VMs
763
+ sections.push(`\nRunning VMs: ${runningVms.size}/${MAX_VMS}`);
764
+ if (runningVms.size > 0) {
765
+ for (const [vmName, vm] of runningVms) {
766
+ const uptimeMs = Date.now() - vm.startTime;
767
+ const uptimeMin = Math.floor(uptimeMs / 60000);
768
+ const uptimeSec = Math.round((uptimeMs % 60000) / 1000);
769
+ sections.push(`\n── ${vmName} ──`);
770
+ sections.push(` PID: ${vm.pid}`);
771
+ sections.push(` Image: ${basename(vm.imagePath)}`);
772
+ sections.push(` Resources: ${vm.memoryMb} MB RAM, ${vm.cpus} CPU(s)`);
773
+ sections.push(` ADB: localhost:${vm.adbPort} → guest:5555`);
774
+ sections.push(` Uptime: ${uptimeMin}m ${uptimeSec}s`);
775
+ }
776
+ }
777
+ // Image inventory
778
+ const imageDir = getImageDir(ctx.config.tempDir);
779
+ if (existsSync(imageDir)) {
780
+ const images = readdirSync(imageDir)
781
+ .filter(f => f.endsWith(".qcow2") || f.endsWith(".img") || f.endsWith(".raw"));
782
+ sections.push(`\nDisk images: ${images.length} in ${imageDir}`);
783
+ }
784
+ return { content: [{ type: "text", text: sections.join("\n") }] };
785
+ }
786
+ catch (error) {
787
+ return { content: [{ type: "text", text: OutputProcessor.formatError(error) }], isError: true };
788
+ }
789
+ });
790
+ }
791
+ //# sourceMappingURL=qemu.js.map