adb-sqlite-viewer 1.0.7 → 1.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 +20 -20
- package/README.md +86 -86
- package/bridge/package.json +13 -13
- package/bridge/server.js +357 -301
- package/cli/bin.cjs +25 -25
- package/cli/ensure-adb.cjs +150 -150
- package/cli/server.cjs +122 -122
- package/dist/assets/index-B63KZ0IL.js +62 -0
- package/dist/assets/sql-wasm-UFUCzYNW.wasm +0 -0
- package/dist/index.html +239 -140
- package/electron/main.cjs +270 -114
- package/package.json +97 -85
- package/dist/assets/index-CIcR1CkF.js +0 -61
package/bridge/server.js
CHANGED
|
@@ -1,301 +1,357 @@
|
|
|
1
|
-
const http = require("http");
|
|
2
|
-
const { execFile, spawn } = require("child_process");
|
|
3
|
-
const path = require("path");
|
|
4
|
-
const fs = require("fs");
|
|
5
|
-
|
|
6
|
-
const TIMEOUT = 30_000;
|
|
7
|
-
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
8
|
-
|
|
9
|
-
// ── Helpers ────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
function cors(res) {
|
|
12
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
13
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
14
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function json(res, status, data) {
|
|
18
|
-
cors(res);
|
|
19
|
-
res.writeHead(status, { "Content-Type": "application/json" });
|
|
20
|
-
res.end(JSON.stringify(data));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function readBody(req) {
|
|
24
|
-
return new Promise((resolve, reject) => {
|
|
25
|
-
const chunks = [];
|
|
26
|
-
let size = 0;
|
|
27
|
-
req.on("data", (chunk) => {
|
|
28
|
-
size += chunk.length;
|
|
29
|
-
if (size > 1_000_000) {
|
|
30
|
-
reject(new Error("Request body too large"));
|
|
31
|
-
req.destroy();
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
chunks.push(chunk);
|
|
35
|
-
});
|
|
36
|
-
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
37
|
-
req.on("error", reject);
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Run adb with simple args (devices, version). Rejects on non-zero exit. */
|
|
42
|
-
function adb(args) {
|
|
43
|
-
return new Promise((resolve, reject) => {
|
|
44
|
-
execFile("adb", args, { timeout: TIMEOUT, maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
|
|
45
|
-
if (err) {
|
|
46
|
-
reject(new Error(stderr?.trim() || err.message));
|
|
47
|
-
} else {
|
|
48
|
-
resolve(stdout);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Run a shell command on device via stdin piping.
|
|
56
|
-
* Avoids Windows argument quoting issues with complex shell commands.
|
|
57
|
-
* Returns stdout even on non-zero exit codes (common for probe scripts).
|
|
58
|
-
*/
|
|
59
|
-
function adbShell(command, serial) {
|
|
60
|
-
return new Promise((resolve, reject) => {
|
|
61
|
-
const args = [];
|
|
62
|
-
if (serial) args.push("-s", serial);
|
|
63
|
-
args.push("shell");
|
|
64
|
-
|
|
65
|
-
const proc = spawn("adb", args, { windowsHide: true });
|
|
66
|
-
let stdout = "";
|
|
67
|
-
let stderr = "";
|
|
68
|
-
let settled = false;
|
|
69
|
-
|
|
70
|
-
const timer = setTimeout(() => {
|
|
71
|
-
if (!settled) {
|
|
72
|
-
settled = true;
|
|
73
|
-
proc.kill();
|
|
74
|
-
reject(new Error("Command timed out"));
|
|
75
|
-
}
|
|
76
|
-
}, TIMEOUT);
|
|
77
|
-
|
|
78
|
-
proc.stdout.on("data", (data) => { stdout += data; });
|
|
79
|
-
proc.stderr.on("data", (data) => { stderr += data; });
|
|
80
|
-
|
|
81
|
-
proc.on("close", () => {
|
|
82
|
-
clearTimeout(timer);
|
|
83
|
-
if (settled) return;
|
|
84
|
-
settled = true;
|
|
85
|
-
// Always resolve with stdout — device commands often exit non-zero
|
|
86
|
-
// (e.g. probe scripts where some iterations fail) but still produce
|
|
87
|
-
// valid output. Only reject if we got nothing and stderr has content.
|
|
88
|
-
if (!stdout && stderr.trim()) {
|
|
89
|
-
reject(new Error(stderr.trim()));
|
|
90
|
-
} else {
|
|
91
|
-
resolve(stdout);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
proc.on("error", (err) => {
|
|
96
|
-
clearTimeout(timer);
|
|
97
|
-
if (settled) return;
|
|
98
|
-
settled = true;
|
|
99
|
-
reject(err);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Send command through stdin — bypasses Windows command-line quoting entirely
|
|
103
|
-
proc.stdin.write(command + "\n");
|
|
104
|
-
proc.stdin.end();
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ── Routes ─────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
async function handlePing(_req, res) {
|
|
111
|
-
json(res, 200, { ok: true });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function handleDevices(_req, res) {
|
|
115
|
-
try {
|
|
116
|
-
const raw = await adb(["devices", "-l"]);
|
|
117
|
-
const lines = raw.split("\n").slice(1); // skip header
|
|
118
|
-
const devices = [];
|
|
119
|
-
for (const line of lines) {
|
|
120
|
-
const trimmed = line.trim();
|
|
121
|
-
if (!trimmed || trimmed.startsWith("*")) continue;
|
|
122
|
-
const parts = trimmed.split(/\s+/);
|
|
123
|
-
const serial = parts[0];
|
|
124
|
-
const state = parts[1];
|
|
125
|
-
if (state !== "device") continue;
|
|
126
|
-
|
|
127
|
-
// Extract model from "model:<value>" token
|
|
128
|
-
let model = "";
|
|
129
|
-
for (const p of parts.slice(2)) {
|
|
130
|
-
if (p.startsWith("model:")) {
|
|
131
|
-
model = p.slice(6);
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
devices.push({
|
|
136
|
-
serial,
|
|
137
|
-
display_name: model ? `${model} (${serial})` : serial,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
json(res, 200, { devices });
|
|
141
|
-
} catch (err) {
|
|
142
|
-
json(res, 500, { error: err.message });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function handleShell(req, res) {
|
|
147
|
-
let body;
|
|
148
|
-
try {
|
|
149
|
-
body = JSON.parse(await readBody(req));
|
|
150
|
-
} catch {
|
|
151
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const { command, serial } = body;
|
|
156
|
-
if (!command || typeof command !== "string") {
|
|
157
|
-
json(res, 400, { error: "Missing 'command' string in body" });
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
const output = await adbShell(command, serial);
|
|
163
|
-
json(res, 200, { output });
|
|
164
|
-
} catch (err) {
|
|
165
|
-
json(res, 500, { error: err.message });
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async function handlePushSqlite3(req, res) {
|
|
170
|
-
let body;
|
|
171
|
-
try {
|
|
172
|
-
body = JSON.parse(await readBody(req));
|
|
173
|
-
} catch {
|
|
174
|
-
body = {};
|
|
175
|
-
}
|
|
176
|
-
const { serial } = body;
|
|
177
|
-
|
|
178
|
-
// Locate bundled sqlite3-arm64 binary (check multiple locations)
|
|
179
|
-
const candidates = [
|
|
180
|
-
path.join(__dirname, "..", "sqlite3-arm64"), // npm package / dev
|
|
181
|
-
path.join(path.dirname(process.execPath), "sqlite3-arm64"), // next to pkg exe
|
|
182
|
-
];
|
|
183
|
-
const binaryPath = candidates.find((p) => fs.existsSync(p));
|
|
184
|
-
if (!binaryPath) {
|
|
185
|
-
json(res, 404, { error: "Bundled sqlite3-arm64 binary not found. Place it next to adb-bridge.exe." });
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
// Push to /data/local/tmp/
|
|
191
|
-
const pushArgs = [];
|
|
192
|
-
if (serial) pushArgs.push("-s", serial);
|
|
193
|
-
pushArgs.push("push", binaryPath, "/data/local/tmp/sqlite3");
|
|
194
|
-
await adb(pushArgs);
|
|
195
|
-
|
|
196
|
-
// Make executable
|
|
197
|
-
const chmodArgs = [];
|
|
198
|
-
if (serial) chmodArgs.push("-s", serial);
|
|
199
|
-
chmodArgs.push("shell", "chmod", "755", "/data/local/tmp/sqlite3");
|
|
200
|
-
await adb(chmodArgs);
|
|
201
|
-
|
|
202
|
-
// Verify
|
|
203
|
-
const verifyArgs = [];
|
|
204
|
-
if (serial) verifyArgs.push("-s", serial);
|
|
205
|
-
verifyArgs.push("shell", "/data/local/tmp/sqlite3", "-version");
|
|
206
|
-
const version = await adb(verifyArgs);
|
|
207
|
-
|
|
208
|
-
json(res, 200, { ok: true, version: version.trim() });
|
|
209
|
-
} catch (err) {
|
|
210
|
-
json(res, 500, { error: err.message });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
}
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const { execFile, spawn } = require("child_process");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
const TIMEOUT = 30_000;
|
|
7
|
+
const MAX_BUFFER = 10 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
// ── Helpers ────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
function cors(res) {
|
|
12
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
13
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
14
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function json(res, status, data) {
|
|
18
|
+
cors(res);
|
|
19
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
20
|
+
res.end(JSON.stringify(data));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readBody(req) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let size = 0;
|
|
27
|
+
req.on("data", (chunk) => {
|
|
28
|
+
size += chunk.length;
|
|
29
|
+
if (size > 1_000_000) {
|
|
30
|
+
reject(new Error("Request body too large"));
|
|
31
|
+
req.destroy();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
chunks.push(chunk);
|
|
35
|
+
});
|
|
36
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString()));
|
|
37
|
+
req.on("error", reject);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Run adb with simple args (devices, version). Rejects on non-zero exit. */
|
|
42
|
+
function adb(args) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
execFile("adb", args, { timeout: TIMEOUT, maxBuffer: MAX_BUFFER }, (err, stdout, stderr) => {
|
|
45
|
+
if (err) {
|
|
46
|
+
reject(new Error(stderr?.trim() || err.message));
|
|
47
|
+
} else {
|
|
48
|
+
resolve(stdout);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run a shell command on device via stdin piping.
|
|
56
|
+
* Avoids Windows argument quoting issues with complex shell commands.
|
|
57
|
+
* Returns stdout even on non-zero exit codes (common for probe scripts).
|
|
58
|
+
*/
|
|
59
|
+
function adbShell(command, serial) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const args = [];
|
|
62
|
+
if (serial) args.push("-s", serial);
|
|
63
|
+
args.push("shell");
|
|
64
|
+
|
|
65
|
+
const proc = spawn("adb", args, { windowsHide: true });
|
|
66
|
+
let stdout = "";
|
|
67
|
+
let stderr = "";
|
|
68
|
+
let settled = false;
|
|
69
|
+
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
if (!settled) {
|
|
72
|
+
settled = true;
|
|
73
|
+
proc.kill();
|
|
74
|
+
reject(new Error("Command timed out"));
|
|
75
|
+
}
|
|
76
|
+
}, TIMEOUT);
|
|
77
|
+
|
|
78
|
+
proc.stdout.on("data", (data) => { stdout += data; });
|
|
79
|
+
proc.stderr.on("data", (data) => { stderr += data; });
|
|
80
|
+
|
|
81
|
+
proc.on("close", () => {
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
if (settled) return;
|
|
84
|
+
settled = true;
|
|
85
|
+
// Always resolve with stdout — device commands often exit non-zero
|
|
86
|
+
// (e.g. probe scripts where some iterations fail) but still produce
|
|
87
|
+
// valid output. Only reject if we got nothing and stderr has content.
|
|
88
|
+
if (!stdout && stderr.trim()) {
|
|
89
|
+
reject(new Error(stderr.trim()));
|
|
90
|
+
} else {
|
|
91
|
+
resolve(stdout);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
proc.on("error", (err) => {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
if (settled) return;
|
|
98
|
+
settled = true;
|
|
99
|
+
reject(err);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Send command through stdin — bypasses Windows command-line quoting entirely
|
|
103
|
+
proc.stdin.write(command + "\n");
|
|
104
|
+
proc.stdin.end();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Routes ─────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
async function handlePing(_req, res) {
|
|
111
|
+
json(res, 200, { ok: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function handleDevices(_req, res) {
|
|
115
|
+
try {
|
|
116
|
+
const raw = await adb(["devices", "-l"]);
|
|
117
|
+
const lines = raw.split("\n").slice(1); // skip header
|
|
118
|
+
const devices = [];
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
const trimmed = line.trim();
|
|
121
|
+
if (!trimmed || trimmed.startsWith("*")) continue;
|
|
122
|
+
const parts = trimmed.split(/\s+/);
|
|
123
|
+
const serial = parts[0];
|
|
124
|
+
const state = parts[1];
|
|
125
|
+
if (state !== "device") continue;
|
|
126
|
+
|
|
127
|
+
// Extract model from "model:<value>" token
|
|
128
|
+
let model = "";
|
|
129
|
+
for (const p of parts.slice(2)) {
|
|
130
|
+
if (p.startsWith("model:")) {
|
|
131
|
+
model = p.slice(6);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
devices.push({
|
|
136
|
+
serial,
|
|
137
|
+
display_name: model ? `${model} (${serial})` : serial,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
json(res, 200, { devices });
|
|
141
|
+
} catch (err) {
|
|
142
|
+
json(res, 500, { error: err.message });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function handleShell(req, res) {
|
|
147
|
+
let body;
|
|
148
|
+
try {
|
|
149
|
+
body = JSON.parse(await readBody(req));
|
|
150
|
+
} catch {
|
|
151
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const { command, serial } = body;
|
|
156
|
+
if (!command || typeof command !== "string") {
|
|
157
|
+
json(res, 400, { error: "Missing 'command' string in body" });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const output = await adbShell(command, serial);
|
|
163
|
+
json(res, 200, { output });
|
|
164
|
+
} catch (err) {
|
|
165
|
+
json(res, 500, { error: err.message });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function handlePushSqlite3(req, res) {
|
|
170
|
+
let body;
|
|
171
|
+
try {
|
|
172
|
+
body = JSON.parse(await readBody(req));
|
|
173
|
+
} catch {
|
|
174
|
+
body = {};
|
|
175
|
+
}
|
|
176
|
+
const { serial } = body;
|
|
177
|
+
|
|
178
|
+
// Locate bundled sqlite3-arm64 binary (check multiple locations)
|
|
179
|
+
const candidates = [
|
|
180
|
+
path.join(__dirname, "..", "sqlite3-arm64"), // npm package / dev
|
|
181
|
+
path.join(path.dirname(process.execPath), "sqlite3-arm64"), // next to pkg exe
|
|
182
|
+
];
|
|
183
|
+
const binaryPath = candidates.find((p) => fs.existsSync(p));
|
|
184
|
+
if (!binaryPath) {
|
|
185
|
+
json(res, 404, { error: "Bundled sqlite3-arm64 binary not found. Place it next to adb-bridge.exe." });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
// Push to /data/local/tmp/
|
|
191
|
+
const pushArgs = [];
|
|
192
|
+
if (serial) pushArgs.push("-s", serial);
|
|
193
|
+
pushArgs.push("push", binaryPath, "/data/local/tmp/sqlite3");
|
|
194
|
+
await adb(pushArgs);
|
|
195
|
+
|
|
196
|
+
// Make executable
|
|
197
|
+
const chmodArgs = [];
|
|
198
|
+
if (serial) chmodArgs.push("-s", serial);
|
|
199
|
+
chmodArgs.push("shell", "chmod", "755", "/data/local/tmp/sqlite3");
|
|
200
|
+
await adb(chmodArgs);
|
|
201
|
+
|
|
202
|
+
// Verify
|
|
203
|
+
const verifyArgs = [];
|
|
204
|
+
if (serial) verifyArgs.push("-s", serial);
|
|
205
|
+
verifyArgs.push("shell", "/data/local/tmp/sqlite3", "-version");
|
|
206
|
+
const version = await adb(verifyArgs);
|
|
207
|
+
|
|
208
|
+
json(res, 200, { ok: true, version: version.trim() });
|
|
209
|
+
} catch (err) {
|
|
210
|
+
json(res, 500, { error: err.message });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function handlePullDb(req, res) {
|
|
215
|
+
let body;
|
|
216
|
+
try {
|
|
217
|
+
body = JSON.parse(await readBody(req));
|
|
218
|
+
} catch {
|
|
219
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const { serial, packageName, dbPath } = body;
|
|
224
|
+
if (!packageName || !dbPath) {
|
|
225
|
+
json(res, 400, { error: "Missing packageName or dbPath" });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const tmpFile = path.join(
|
|
230
|
+
require("os").tmpdir(),
|
|
231
|
+
`adb-pull-${Date.now()}.db`
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
// Copy from app sandbox to /data/local/tmp/ (run-as required)
|
|
236
|
+
await adbShell(
|
|
237
|
+
`run-as ${packageName} cat ${dbPath} > /data/local/tmp/_pull_db.tmp`,
|
|
238
|
+
serial
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Pull to host
|
|
242
|
+
const pullArgs = [];
|
|
243
|
+
if (serial) pullArgs.push("-s", serial);
|
|
244
|
+
pullArgs.push("pull", "/data/local/tmp/_pull_db.tmp", tmpFile);
|
|
245
|
+
await adb(pullArgs);
|
|
246
|
+
|
|
247
|
+
// Stream the file back
|
|
248
|
+
const stat = fs.statSync(tmpFile);
|
|
249
|
+
const fileName = dbPath.split("/").pop() || "database.db";
|
|
250
|
+
cors(res);
|
|
251
|
+
res.writeHead(200, {
|
|
252
|
+
"Content-Type": "application/octet-stream",
|
|
253
|
+
"Content-Disposition": `attachment; filename="${fileName}"`,
|
|
254
|
+
"Content-Length": stat.size,
|
|
255
|
+
});
|
|
256
|
+
const stream = fs.createReadStream(tmpFile);
|
|
257
|
+
stream.pipe(res);
|
|
258
|
+
stream.on("end", () => {
|
|
259
|
+
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
|
|
260
|
+
});
|
|
261
|
+
} catch (err) {
|
|
262
|
+
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
|
|
263
|
+
json(res, 500, { error: err.message });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Exports (for use by cli/server.cjs and electron) ───
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Dispatch an incoming request to the appropriate API handler.
|
|
271
|
+
* Returns true if a route was matched, false otherwise.
|
|
272
|
+
*/
|
|
273
|
+
async function handleRequest(req, res) {
|
|
274
|
+
const url = new URL(req.url, "http://localhost");
|
|
275
|
+
const p = url.pathname;
|
|
276
|
+
|
|
277
|
+
if (req.method === "OPTIONS") {
|
|
278
|
+
cors(res);
|
|
279
|
+
res.writeHead(204);
|
|
280
|
+
res.end();
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
if (p === "/api/ping" && req.method === "GET") {
|
|
286
|
+
await handlePing(req, res);
|
|
287
|
+
return true;
|
|
288
|
+
} else if (p === "/api/devices" && req.method === "GET") {
|
|
289
|
+
await handleDevices(req, res);
|
|
290
|
+
return true;
|
|
291
|
+
} else if (p === "/api/shell" && req.method === "POST") {
|
|
292
|
+
await handleShell(req, res);
|
|
293
|
+
return true;
|
|
294
|
+
} else if (p === "/api/push-sqlite3" && req.method === "POST") {
|
|
295
|
+
await handlePushSqlite3(req, res);
|
|
296
|
+
return true;
|
|
297
|
+
} else if (p === "/api/pull-db" && req.method === "POST") {
|
|
298
|
+
await handlePullDb(req, res);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
json(res, 500, { error: err.message });
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Verify adb is in PATH. Returns the version string, or rejects.
|
|
311
|
+
*/
|
|
312
|
+
function checkAdb() {
|
|
313
|
+
return new Promise((resolve, reject) => {
|
|
314
|
+
execFile("adb", ["version"], { timeout: 5000 }, (err, stdout) => {
|
|
315
|
+
if (err) {
|
|
316
|
+
reject(new Error("'adb' not found in PATH. Install Android SDK Platform-Tools."));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
resolve(stdout.split("\n")[0].trim());
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
module.exports = { handleRequest, checkAdb };
|
|
325
|
+
|
|
326
|
+
// ── Standalone startup (node bridge/server.js) ─────────
|
|
327
|
+
|
|
328
|
+
if (require.main === module) {
|
|
329
|
+
const PORT = 15555;
|
|
330
|
+
const HOST = "127.0.0.1";
|
|
331
|
+
|
|
332
|
+
const server = http.createServer(async (req, res) => {
|
|
333
|
+
const handled = await handleRequest(req, res);
|
|
334
|
+
if (!handled) {
|
|
335
|
+
json(res, 404, { error: "Not found" });
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
checkAdb()
|
|
340
|
+
.then((versionLine) => {
|
|
341
|
+
console.log(`Found: ${versionLine}`);
|
|
342
|
+
server.listen(PORT, HOST, () => {
|
|
343
|
+
console.log(`ADB Bridge listening on http://${HOST}:${PORT}`);
|
|
344
|
+
console.log("Endpoints:");
|
|
345
|
+
console.log(" GET /api/ping — health check");
|
|
346
|
+
console.log(" GET /api/devices — list connected devices");
|
|
347
|
+
console.log(" POST /api/shell — run adb shell command");
|
|
348
|
+
console.log("\nPress Ctrl+C to stop.");
|
|
349
|
+
if (process.send) process.send({ type: "ready" });
|
|
350
|
+
});
|
|
351
|
+
})
|
|
352
|
+
.catch((err) => {
|
|
353
|
+
console.error("ERROR:", err.message);
|
|
354
|
+
if (process.send) process.send({ type: "error", message: err.message });
|
|
355
|
+
process.exit(1);
|
|
356
|
+
});
|
|
357
|
+
}
|