mobile-debug-mcp 0.24.0 → 0.24.1
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/dist/manage/android.js +27 -4
- package/docs/CHANGELOG.md +3 -0
- package/package.json +1 -1
- package/src/manage/android.ts +27 -3
- package/test/unit/manage/install.test.ts +94 -1
package/dist/manage/android.js
CHANGED
|
@@ -6,6 +6,9 @@ import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } f
|
|
|
6
6
|
import { execAdbWithDiagnostics } from '../utils/diagnostics.js';
|
|
7
7
|
import { detectJavaHome } from '../utils/java.js';
|
|
8
8
|
export class AndroidManage {
|
|
9
|
+
isTestOnlyInstallFailure(output) {
|
|
10
|
+
return typeof output === 'string' && output.includes('INSTALL_FAILED_TEST_ONLY');
|
|
11
|
+
}
|
|
9
12
|
async build(projectPath, _variant) {
|
|
10
13
|
void _variant;
|
|
11
14
|
try {
|
|
@@ -93,6 +96,13 @@ export class AndroidManage {
|
|
|
93
96
|
if (res.code === 0) {
|
|
94
97
|
return { device: deviceInfo, installed: true, output: res.stdout };
|
|
95
98
|
}
|
|
99
|
+
const installOutput = `${res.stdout}\n${res.stderr}`.trim();
|
|
100
|
+
if (this.isTestOnlyInstallFailure(installOutput)) {
|
|
101
|
+
const retryRes = await spawnAdb(['install', '-r', '-t', apkToInstall], deviceId);
|
|
102
|
+
if (retryRes.code === 0) {
|
|
103
|
+
return { device: deviceInfo, installed: true, output: retryRes.stdout };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
96
106
|
}
|
|
97
107
|
catch (e) {
|
|
98
108
|
console.debug('[android-run] adb install failed, attempting push+pm fallback:', e instanceof Error ? e.message : String(e));
|
|
@@ -100,12 +110,25 @@ export class AndroidManage {
|
|
|
100
110
|
const basename = path.basename(apkToInstall);
|
|
101
111
|
const remotePath = `/data/local/tmp/${basename}`;
|
|
102
112
|
await execAdb(['push', apkToInstall, remotePath], deviceId);
|
|
103
|
-
|
|
113
|
+
let finalPmRes = await spawnAdb(['shell', 'pm', 'install', '-r', remotePath], deviceId);
|
|
104
114
|
try {
|
|
105
|
-
|
|
115
|
+
if (finalPmRes.code === 0) {
|
|
116
|
+
return { device: deviceInfo, installed: true, output: finalPmRes.stdout };
|
|
117
|
+
}
|
|
118
|
+
if (this.isTestOnlyInstallFailure(`${finalPmRes.stdout}\n${finalPmRes.stderr}`)) {
|
|
119
|
+
finalPmRes = await spawnAdb(['shell', 'pm', 'install', '-r', '-t', remotePath], deviceId);
|
|
120
|
+
if (finalPmRes.code === 0) {
|
|
121
|
+
return { device: deviceInfo, installed: true, output: finalPmRes.stdout };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw new Error(finalPmRes.stderr || finalPmRes.stdout || 'pm install failed');
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
try {
|
|
128
|
+
await execAdb(['shell', 'rm', remotePath], deviceId);
|
|
129
|
+
}
|
|
130
|
+
catch { }
|
|
106
131
|
}
|
|
107
|
-
catch { }
|
|
108
|
-
return { device: deviceInfo, installed: true, output: pmOut };
|
|
109
132
|
}
|
|
110
133
|
catch (e) {
|
|
111
134
|
// gather diagnostics for attempted adb operations
|
package/docs/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/manage/android.ts
CHANGED
|
@@ -8,6 +8,10 @@ import { detectJavaHome } from '../utils/java.js'
|
|
|
8
8
|
import { InstallAppResponse, StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse } from '../types.js'
|
|
9
9
|
|
|
10
10
|
export class AndroidManage {
|
|
11
|
+
private isTestOnlyInstallFailure(output: string | undefined): boolean {
|
|
12
|
+
return typeof output === 'string' && output.includes('INSTALL_FAILED_TEST_ONLY')
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
async build(projectPath: string, _variant?: string): Promise<{ artifactPath: string, output?: string } | { error: string }> {
|
|
12
16
|
void _variant
|
|
13
17
|
try {
|
|
@@ -92,6 +96,14 @@ export class AndroidManage {
|
|
|
92
96
|
if (res.code === 0) {
|
|
93
97
|
return { device: deviceInfo, installed: true, output: res.stdout }
|
|
94
98
|
}
|
|
99
|
+
|
|
100
|
+
const installOutput = `${res.stdout}\n${res.stderr}`.trim()
|
|
101
|
+
if (this.isTestOnlyInstallFailure(installOutput)) {
|
|
102
|
+
const retryRes = await spawnAdb(['install', '-r', '-t', apkToInstall], deviceId)
|
|
103
|
+
if (retryRes.code === 0) {
|
|
104
|
+
return { device: deviceInfo, installed: true, output: retryRes.stdout }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
95
107
|
} catch (e) {
|
|
96
108
|
console.debug('[android-run] adb install failed, attempting push+pm fallback:', e instanceof Error ? e.message : String(e))
|
|
97
109
|
}
|
|
@@ -99,9 +111,21 @@ export class AndroidManage {
|
|
|
99
111
|
const basename = path.basename(apkToInstall)
|
|
100
112
|
const remotePath = `/data/local/tmp/${basename}`
|
|
101
113
|
await execAdb(['push', apkToInstall, remotePath], deviceId)
|
|
102
|
-
|
|
103
|
-
try {
|
|
104
|
-
|
|
114
|
+
let finalPmRes = await spawnAdb(['shell', 'pm', 'install', '-r', remotePath], deviceId)
|
|
115
|
+
try {
|
|
116
|
+
if (finalPmRes.code === 0) {
|
|
117
|
+
return { device: deviceInfo, installed: true, output: finalPmRes.stdout }
|
|
118
|
+
}
|
|
119
|
+
if (this.isTestOnlyInstallFailure(`${finalPmRes.stdout}\n${finalPmRes.stderr}`)) {
|
|
120
|
+
finalPmRes = await spawnAdb(['shell', 'pm', 'install', '-r', '-t', remotePath], deviceId)
|
|
121
|
+
if (finalPmRes.code === 0) {
|
|
122
|
+
return { device: deviceInfo, installed: true, output: finalPmRes.stdout }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new Error(finalPmRes.stderr || finalPmRes.stdout || 'pm install failed')
|
|
126
|
+
} finally {
|
|
127
|
+
try { await execAdb(['shell', 'rm', remotePath], deviceId) } catch {}
|
|
128
|
+
}
|
|
105
129
|
} catch (e) {
|
|
106
130
|
// gather diagnostics for attempted adb operations
|
|
107
131
|
const basename = path.basename(apkToInstall)
|
|
@@ -69,8 +69,101 @@ exit 0
|
|
|
69
69
|
assert.ok(res2.output && typeof res2.output === 'string', 'Project dir install succeeded with output')
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
const testOnlyAdbPath = path.join(binDir, 'adb-test-only')
|
|
73
|
+
const testOnlyAdbScript = `#!/bin/sh
|
|
74
|
+
if [ "$1" = "-s" ]; then
|
|
75
|
+
shift 2
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
if [ "$1" = "shell" ] && [ "$2" = "getprop" ]; then
|
|
79
|
+
case "$3" in
|
|
80
|
+
ro.build.version.release) echo '16' ;;
|
|
81
|
+
ro.product.model) echo 'sdk_gphone64_arm64' ;;
|
|
82
|
+
ro.kernel.qemu) echo '1' ;;
|
|
83
|
+
esac
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
if [ "$1" = "install" ]; then
|
|
88
|
+
if [ "$2" = "-r" ] && [ "$3" = "-t" ]; then
|
|
89
|
+
echo 'Performing Streamed Install'
|
|
90
|
+
echo 'Success'
|
|
91
|
+
exit 0
|
|
92
|
+
fi
|
|
93
|
+
echo 'Performing Streamed Install'
|
|
94
|
+
echo 'adb: failed to install test.apk: Failure [INSTALL_FAILED_TEST_ONLY: Failed to install test-only apk. Did you forget to add -t?]' 1>&2
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
echo 'Success'
|
|
99
|
+
exit 0
|
|
100
|
+
`
|
|
101
|
+
await fs.writeFile(testOnlyAdbPath, testOnlyAdbScript, { mode: 0o755 })
|
|
102
|
+
process.env.ADB_PATH = testOnlyAdbPath
|
|
103
|
+
|
|
104
|
+
const { dir: d2, file: testOnlyApk } = await makeTempFile('.apk')
|
|
105
|
+
const testOnlyRes = await ai.installApp(testOnlyApk, 'emulator-5554')
|
|
106
|
+
console.log('testOnlyRes', testOnlyRes)
|
|
107
|
+
assert.strictEqual(testOnlyRes.installed, true, 'Test-only APK should retry with -t and install successfully')
|
|
108
|
+
|
|
109
|
+
const cleanupLog = path.join(binDir, 'pm-cleanup.log')
|
|
110
|
+
const pmFallbackAdbPath = path.join(binDir, 'adb-pm-fallback')
|
|
111
|
+
const pmFallbackAdbScript = `#!/bin/sh
|
|
112
|
+
if [ "$1" = "-s" ]; then
|
|
113
|
+
shift 2
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
if [ "$1" = "shell" ] && [ "$2" = "getprop" ]; then
|
|
117
|
+
case "$3" in
|
|
118
|
+
ro.build.version.release) echo '16' ;;
|
|
119
|
+
ro.product.model) echo 'sdk_gphone64_arm64' ;;
|
|
120
|
+
ro.kernel.qemu) echo '1' ;;
|
|
121
|
+
esac
|
|
122
|
+
exit 0
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
if [ "$1" = "install" ]; then
|
|
126
|
+
echo 'adb install failed' 1>&2
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
if [ "$1" = "push" ]; then
|
|
131
|
+
echo 'pushed'
|
|
132
|
+
exit 0
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if [ "$1" = "shell" ] && [ "$2" = "pm" ] && [ "$3" = "install" ]; then
|
|
136
|
+
if [ "$4" = "-r" ] && [ "$5" = "-t" ]; then
|
|
137
|
+
echo 'Failure [INSTALL_FAILED_VERSION_DOWNGRADE]'
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
echo 'Failure [INSTALL_FAILED_TEST_ONLY: Failed to install test-only apk. Did you forget to add -t?]'
|
|
141
|
+
exit 1
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
if [ "$1" = "shell" ] && [ "$2" = "rm" ]; then
|
|
145
|
+
echo cleanup >> "${cleanupLog}"
|
|
146
|
+
exit 0
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
echo 'unexpected args:' "$@" 1>&2
|
|
150
|
+
exit 1
|
|
151
|
+
`
|
|
152
|
+
await fs.writeFile(pmFallbackAdbPath, pmFallbackAdbScript, { mode: 0o755 })
|
|
153
|
+
process.env.ADB_PATH = pmFallbackAdbPath
|
|
154
|
+
|
|
155
|
+
const { dir: d3, file: pmFallbackApk } = await makeTempFile('.apk')
|
|
156
|
+
const pmFallbackRes = await ai.installApp(pmFallbackApk, 'emulator-5554')
|
|
157
|
+
console.log('pmFallbackRes', pmFallbackRes)
|
|
158
|
+
assert.strictEqual(pmFallbackRes.installed, false, 'Failed pm fallback should surface as install failure')
|
|
159
|
+
assert.match(pmFallbackRes.error || '', /INSTALL_FAILED_VERSION_DOWNGRADE/, 'Final pm retry failure should be reported')
|
|
160
|
+
const cleanupCount = (await fs.readFile(cleanupLog, 'utf8')).trim().split('\n').filter(Boolean).length
|
|
161
|
+
assert.strictEqual(cleanupCount, 1, 'pm fallback cleanup should run once')
|
|
162
|
+
|
|
72
163
|
// cleanup
|
|
73
164
|
await fs.rm(d1, { recursive: true, force: true }).catch(() => {})
|
|
165
|
+
await fs.rm(d2, { recursive: true, force: true }).catch(() => {})
|
|
166
|
+
await fs.rm(d3, { recursive: true, force: true }).catch(() => {})
|
|
74
167
|
await fs.rm(dirGradle, { recursive: true, force: true }).catch(() => {})
|
|
75
168
|
|
|
76
169
|
// restore PATH and ADB_PATH
|
|
@@ -87,4 +180,4 @@ exit 0
|
|
|
87
180
|
}
|
|
88
181
|
}
|
|
89
182
|
|
|
90
|
-
run().catch((e) => { console.error(e); process.exit(1) })
|
|
183
|
+
run().catch((e) => { console.error(e); process.exit(1) })
|