Skip to content

Commit 3c4559d

Browse files
author
刘威
committed
feat: bundle adb platform-tools into packaged app
ADB runtime logic now works as follows: - App window shows first, then ADB initializes asynchronously in the background - If an ADB server is already listening on 127.0.0.1:5037, the app reuses it and does not start its own server - If no server is running, the app starts ADB with the bundled binary and then initializes device tracking This change adds: - CI step to download platform-tools before packaging - electron-builder extraResources to embed platform-tools into the app resources directory - runtime lookup for packaged/dev/downloaded platform-tools locations - prepare-platform-tools script and tracked placeholder directory - clearer ADB startup error reporting and adbkit module compatibility fallback
1 parent ecb3e95 commit 3c4559d

8 files changed

Lines changed: 171 additions & 43 deletions

File tree

.github/workflows/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ jobs:
9090
npm_config_fund: false
9191
npm_config_loglevel: warn
9292

93+
- name: Prepare bundled platform-tools
94+
run: npm run prepare:platform-tools
95+
9396
# Rebuild native modules after cache restore
9497
- name: Rebuild native modules
9598
if: steps.npm-cache.outputs.cache-hit == 'true'

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ node_modules/
44
# Build output
55
dist/
66
out/
7+
bundled-tools/*
8+
!bundled-tools/platform-tools/
9+
bundled-tools/platform-tools/*
10+
!bundled-tools/platform-tools/.gitkeep
711

812
# Electron
913
*.log
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

electron-builder.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ files:
66
- src/**/*
77
- assets/**/*
88
- package.json
9+
extraResources:
10+
- from: bundled-tools/platform-tools
11+
to: platform-tools
12+
filter:
13+
- '**/*'
914
win:
1015
target:
1116
- target: zip

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"main": "src/main/index.js",
66
"scripts": {
77
"start": "electron .",
8+
"prepare:platform-tools": "node scripts/prepare-platform-tools.js",
89
"build": "electron-builder",
910
"build:win": "electron-builder --win",
1011
"build:mac": "electron-builder --mac",

scripts/prepare-platform-tools.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const path = require('path');
2+
const { downloadPlatformTools } = require('../src/main/adb/download');
3+
4+
async function main() {
5+
const targetDir = path.resolve(__dirname, '../bundled-tools/platform-tools');
6+
7+
console.log(`[CI] Preparing platform-tools in ${targetDir}`);
8+
9+
const adbPath = await downloadPlatformTools((status, progress) => {
10+
const suffix = typeof progress === 'number' ? ` ${progress}%` : '';
11+
console.log(`[CI] platform-tools ${status}${suffix}`);
12+
}, { targetDir });
13+
14+
console.log(`[CI] ADB prepared at ${adbPath}`);
15+
}
16+
17+
main().catch((err) => {
18+
console.error('[CI] Failed to prepare platform-tools:', err);
19+
process.exit(1);
20+
});

src/main/adb/device.js

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const net = require('net');
44
const { spawn } = require('child_process');
55
const path = require('path');
66
const fs = require('fs');
7-
const { getBundledAdbPath, hasBundledAdb } = require('./download');
7+
const { getBundledAdbPath } = require('./download');
88

99
const ADB_SERVER_PORT = 5037;
1010
const HEALTH_CHECK_INTERVAL = 5000;
@@ -79,9 +79,12 @@ class DeviceManager extends EventEmitter {
7979

8080
if (!isRunning) {
8181
console.log('[ADB] ADB server not running, starting...');
82-
const started = await this._startAdbServer();
83-
if (!started) {
84-
this.emit('server:error', { message: 'Failed to start ADB server' });
82+
const startResult = await this._startAdbServer();
83+
if (!startResult.ok) {
84+
this.emit('server:error', {
85+
message: startResult.message || 'Failed to start ADB server',
86+
help: startResult.help || 'Please install Android Platform Tools or start ADB manually.'
87+
});
8588
return;
8689
}
8790
} else {
@@ -90,7 +93,22 @@ class DeviceManager extends EventEmitter {
9093

9194
// Initialize adbkit client
9295
try {
93-
const adbkit = require('@devicefarmer/adbkit').default || require('@devicefarmer/adbkit');
96+
// Prefer adbkit from package.json; keep compatibility with devicefarmer fork if present.
97+
let adbkit;
98+
try {
99+
adbkit = require('adbkit');
100+
} catch (errPrimary) {
101+
const missingPrimary = errPrimary && errPrimary.code === 'MODULE_NOT_FOUND';
102+
if (!missingPrimary) throw errPrimary;
103+
try {
104+
const fallback = require('@devicefarmer/adbkit');
105+
adbkit = fallback.default || fallback;
106+
} catch (errFallback) {
107+
throw new Error(
108+
`Unable to load ADB library. Tried "adbkit" and "@devicefarmer/adbkit". ${errPrimary.message}`
109+
);
110+
}
111+
}
94112
this.client = adbkit.createClient();
95113

96114
// Start device tracking
@@ -144,36 +162,68 @@ class DeviceManager extends EventEmitter {
144162

145163
let stdout = '';
146164
let stderr = '';
165+
let settled = false;
166+
const finish = (result) => {
167+
if (settled) return;
168+
settled = true;
169+
clearTimeout(timeout);
170+
resolve(result);
171+
};
147172

148173
proc.stdout.on('data', (data) => { stdout += data.toString(); });
149174
proc.stderr.on('data', (data) => { stderr += data.toString(); });
150175

151-
proc.on('close', (code) => {
176+
proc.on('close', async (code) => {
152177
if (code === 0) {
153178
console.log('[ADB] Server started successfully');
154-
resolve(true);
179+
finish({ ok: true });
155180
} else {
156-
console.error(`[ADB] Server start failed with code ${code}: ${stderr}`);
157-
resolve(false);
181+
// Some adb variants return non-zero while server is actually up.
182+
const running = await this._checkServerRunning();
183+
if (running) {
184+
console.warn(`[ADB] start-server exited with code ${code}, but server is reachable`);
185+
finish({ ok: true });
186+
return;
187+
}
188+
189+
const details = (stderr || stdout || '').trim();
190+
const message = details
191+
? `Failed to start ADB server (${details})`
192+
: `Failed to start ADB server (exit code ${code})`;
193+
console.error(`[ADB] ${message}`);
194+
finish({
195+
ok: false,
196+
message,
197+
help: 'Run "adb start-server" to verify ADB works, or install Android Platform Tools.'
198+
});
158199
}
159200
});
160201

161202
proc.on('error', (err) => {
162203
console.error(`[ADB] Failed to spawn adb: ${err.message}`);
163-
this.emit('adb:error', {
164-
message: 'ADB not found. Please install Android Platform Tools.',
165-
help: 'Run "adb install-platform-tools" or install Android SDK.'
204+
const isMissingAdb = err.code === 'ENOENT';
205+
finish({
206+
ok: false,
207+
message: isMissingAdb
208+
? 'ADB binary not found. Please install Android Platform Tools.'
209+
: `Failed to launch ADB: ${err.message}`,
210+
help: isMissingAdb
211+
? 'Use "Auto Download" in the app or install Android Platform Tools manually.'
212+
: 'Please verify adb executable is accessible and retry.'
166213
});
167-
resolve(false);
168214
});
169215

170216
// Timeout after 10 seconds
171-
setTimeout(() => {
217+
const timeout = setTimeout(() => {
172218
if (!proc.killed) {
173219
proc.kill();
174220
console.log('[ADB] Server start timed out');
175-
resolve(false);
176221
}
222+
finish({
223+
ok: false,
224+
message: 'Failed to start ADB server (timeout)',
225+
help: 'Please check whether adb can start from terminal via "adb start-server".'
226+
});
177227
}, 10000);
178228
});
179229
}

src/main/adb/download.js

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,55 @@ const PLATFORM_TOOLS_URLS = {
1111
linux: 'https://dl.google.com/android/repository/platform-tools-latest-linux.zip'
1212
};
1313

14-
// Get platform tools directory
15-
function getPlatformToolsDir() {
14+
function getAdbExecutableName() {
15+
return process.platform === 'win32' ? 'adb.exe' : 'adb';
16+
}
17+
18+
function getDownloadedPlatformToolsDir() {
1619
return path.join(os.homedir(), '.adb-proxy-browser', 'platform-tools');
1720
}
1821

22+
function getDevBundledPlatformToolsDir() {
23+
return path.resolve(__dirname, '../../../bundled-tools/platform-tools');
24+
}
25+
26+
function getPackagedPlatformToolsDir() {
27+
if (!process.resourcesPath) {
28+
return null;
29+
}
30+
return path.join(process.resourcesPath, 'platform-tools');
31+
}
32+
33+
function getPlatformToolsDirCandidates() {
34+
const candidates = [];
35+
const packagedDir = getPackagedPlatformToolsDir();
36+
const devBundledDir = getDevBundledPlatformToolsDir();
37+
const downloadedDir = getDownloadedPlatformToolsDir();
38+
39+
if (packagedDir) candidates.push(packagedDir);
40+
candidates.push(devBundledDir);
41+
candidates.push(downloadedDir);
42+
43+
return candidates;
44+
}
45+
46+
function getExistingPlatformToolsDir() {
47+
return getPlatformToolsDirCandidates().find((dir) => fs.existsSync(path.join(dir, getAdbExecutableName()))) || null;
48+
}
49+
50+
// Backward-compatible name used by the rest of the app.
51+
function getPlatformToolsDir() {
52+
return getExistingPlatformToolsDir() || getDownloadedPlatformToolsDir();
53+
}
54+
1955
// Get adb path
2056
function getBundledAdbPath() {
21-
const dir = getPlatformToolsDir();
22-
const adbName = process.platform === 'win32' ? 'adb.exe' : 'adb';
23-
return path.join(dir, adbName);
57+
return path.join(getPlatformToolsDir(), getAdbExecutableName());
2458
}
2559

2660
// Check if bundled adb exists
2761
function hasBundledAdb() {
28-
const adbPath = getBundledAdbPath();
29-
return fs.existsSync(adbPath);
62+
return !!getExistingPlatformToolsDir();
3063
}
3164

3265
// Download file
@@ -35,10 +68,9 @@ function downloadFile(url, dest) {
3568
console.log(`[ADB] Downloading from ${url}`);
3669
const file = fs.createWriteStream(dest);
3770

38-
const request = (url) => {
39-
https.get(url, (response) => {
71+
const request = (nextUrl) => {
72+
https.get(nextUrl, (response) => {
4073
if (response.statusCode === 302 || response.statusCode === 301) {
41-
// Follow redirect
4274
request(response.headers.location);
4375
return;
4476
}
@@ -53,7 +85,7 @@ function downloadFile(url, dest) {
5385

5486
response.on('data', (chunk) => {
5587
downloaded += chunk.length;
56-
const percent = Math.round((downloaded / totalSize) * 100);
88+
const percent = totalSize ? Math.round((downloaded / totalSize) * 100) : 0;
5789
process.stdout.write(`\r[ADB] Downloading: ${percent}%`);
5890
});
5991

@@ -79,20 +111,18 @@ async function extractZip(zipPath, destDir) {
79111
return new Promise((resolve, reject) => {
80112
console.log(`[ADB] Extracting ${zipPath} to ${destDir}`);
81113

82-
// Create dest directory
83114
if (!fs.existsSync(destDir)) {
84115
fs.mkdirSync(destDir, { recursive: true });
85116
}
86117

87118
let cmd;
88119
if (process.platform === 'win32') {
89-
// Use PowerShell to extract
90120
cmd = `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${path.dirname(destDir)}' -Force"`;
91121
} else {
92122
cmd = `unzip -o "${zipPath}" -d "${path.dirname(destDir)}"`;
93123
}
94124

95-
exec(cmd, (error, stdout, stderr) => {
125+
exec(cmd, (error) => {
96126
if (error) {
97127
console.error(`[ADB] Extract error: ${error.message}`);
98128
reject(error);
@@ -104,39 +134,50 @@ async function extractZip(zipPath, destDir) {
104134
});
105135
}
106136

137+
function ensureAdbExecutable(targetDir) {
138+
if (process.platform === 'win32') {
139+
return;
140+
}
141+
142+
const adbPath = path.join(targetDir, getAdbExecutableName());
143+
if (fs.existsSync(adbPath)) {
144+
fs.chmodSync(adbPath, '755');
145+
}
146+
}
147+
148+
function ensureAdbExists(targetDir) {
149+
const adbPath = path.join(targetDir, getAdbExecutableName());
150+
if (!fs.existsSync(adbPath)) {
151+
throw new Error(`ADB binary not found after extraction: ${adbPath}`);
152+
}
153+
return adbPath;
154+
}
155+
107156
// Download and setup platform tools
108-
async function downloadPlatformTools(onProgress) {
157+
async function downloadPlatformTools(onProgress, options = {}) {
109158
const url = PLATFORM_TOOLS_URLS[process.platform];
110159
if (!url) {
111160
throw new Error(`Unsupported platform: ${process.platform}`);
112161
}
113162

114-
const platformToolsDir = getPlatformToolsDir();
115-
const zipPath = path.join(os.tmpdir(), 'platform-tools.zip');
163+
const platformToolsDir = options.targetDir || getDownloadedPlatformToolsDir();
164+
const zipPath = options.zipPath || path.join(os.tmpdir(), `platform-tools-${process.platform}.zip`);
116165

117166
try {
118167
if (onProgress) onProgress('downloading', 0);
119168

120-
// Download
121169
await downloadFile(url, zipPath);
122170
if (onProgress) onProgress('extracting', 50);
123171

124-
// Extract
125172
await extractZip(zipPath, platformToolsDir);
173+
ensureAdbExecutable(platformToolsDir);
174+
const adbPath = ensureAdbExists(platformToolsDir);
126175
if (onProgress) onProgress('complete', 100);
127176

128-
// Make executable on Unix
129-
if (process.platform !== 'win32') {
130-
const adbPath = getBundledAdbPath();
131-
fs.chmodSync(adbPath, '755');
132-
}
133-
134-
// Cleanup
135177
fs.unlinkSync(zipPath);
136178

137-
return getBundledAdbPath();
179+
return adbPath;
138180
} catch (err) {
139-
// Cleanup on error
140181
try {
141182
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
142183
} catch (e) {}
@@ -146,6 +187,9 @@ async function downloadPlatformTools(onProgress) {
146187

147188
module.exports = {
148189
getPlatformToolsDir,
190+
getDownloadedPlatformToolsDir,
191+
getDevBundledPlatformToolsDir,
192+
getPackagedPlatformToolsDir,
149193
getBundledAdbPath,
150194
hasBundledAdb,
151195
downloadPlatformTools

0 commit comments

Comments
 (0)