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/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
- // ── Exports (for use by cli/server.cjs and electron) ───
215
-
216
- /**
217
- * Dispatch an incoming request to the appropriate API handler.
218
- * Returns true if a route was matched, false otherwise.
219
- */
220
- async function handleRequest(req, res) {
221
- const url = new URL(req.url, "http://localhost");
222
- const p = url.pathname;
223
-
224
- if (req.method === "OPTIONS") {
225
- cors(res);
226
- res.writeHead(204);
227
- res.end();
228
- return true;
229
- }
230
-
231
- try {
232
- if (p === "/api/ping" && req.method === "GET") {
233
- await handlePing(req, res);
234
- return true;
235
- } else if (p === "/api/devices" && req.method === "GET") {
236
- await handleDevices(req, res);
237
- return true;
238
- } else if (p === "/api/shell" && req.method === "POST") {
239
- await handleShell(req, res);
240
- return true;
241
- } else if (p === "/api/push-sqlite3" && req.method === "POST") {
242
- await handlePushSqlite3(req, res);
243
- return true;
244
- }
245
- } catch (err) {
246
- json(res, 500, { error: err.message });
247
- return true;
248
- }
249
-
250
- return false;
251
- }
252
-
253
- /**
254
- * Verify adb is in PATH. Returns the version string, or rejects.
255
- */
256
- function checkAdb() {
257
- return new Promise((resolve, reject) => {
258
- execFile("adb", ["version"], { timeout: 5000 }, (err, stdout) => {
259
- if (err) {
260
- reject(new Error("'adb' not found in PATH. Install Android SDK Platform-Tools."));
261
- return;
262
- }
263
- resolve(stdout.split("\n")[0].trim());
264
- });
265
- });
266
- }
267
-
268
- module.exports = { handleRequest, checkAdb };
269
-
270
- // ── Standalone startup (node bridge/server.js) ─────────
271
-
272
- if (require.main === module) {
273
- const PORT = 15555;
274
- const HOST = "127.0.0.1";
275
-
276
- const server = http.createServer(async (req, res) => {
277
- const handled = await handleRequest(req, res);
278
- if (!handled) {
279
- json(res, 404, { error: "Not found" });
280
- }
281
- });
282
-
283
- checkAdb()
284
- .then((versionLine) => {
285
- console.log(`Found: ${versionLine}`);
286
- server.listen(PORT, HOST, () => {
287
- console.log(`ADB Bridge listening on http://${HOST}:${PORT}`);
288
- console.log("Endpoints:");
289
- console.log(" GET /api/ping — health check");
290
- console.log(" GET /api/devices — list connected devices");
291
- console.log(" POST /api/shell run adb shell command");
292
- console.log("\nPress Ctrl+C to stop.");
293
- if (process.send) process.send({ type: "ready" });
294
- });
295
- })
296
- .catch((err) => {
297
- console.error("ERROR:", err.message);
298
- if (process.send) process.send({ type: "error", message: err.message });
299
- process.exit(1);
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
+ }