fix(adb): keep the device's refusal when a package change exits nonzero (#2344)
Squash merge of https://github.com/morluto/rea/pull/2344. Co-authored-by: Hui <a3239737781@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
911894d758
commit
554ea7611e
|
|
@ -1127,6 +1127,68 @@ exit 1`,
|
||||||
expect(projected.message).toContain("not installed for 0");
|
expect(projected.message).toContain("not installed for 0");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// adb exits 1 on a refused package change: `adb install` after printing the
|
||||||
|
// status line to stderr, `cmd package uninstall` after printing it to stdout.
|
||||||
|
it("preserves the device's install refusal when adb exits nonzero", async () => {
|
||||||
|
const { binary, root } = await writeAdbStub(
|
||||||
|
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||||
|
if [ "$1" = "-s" ] && [ "$3" = "install" ]; then
|
||||||
|
printf 'Performing Streamed Install\\n'
|
||||||
|
printf 'adb: failed to install app.apk: Failure [INSTALL_FAILED_ALREADY_EXISTS: Attempt to re-install com.example without first uninstalling.]\\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 1`,
|
||||||
|
);
|
||||||
|
const apk = join(root, "app.apk");
|
||||||
|
await writeFile(apk, "apk");
|
||||||
|
const projected = await refusedBy(providerOver(binary), {
|
||||||
|
operation: "install_adb_package",
|
||||||
|
input: { serial: "emulator-5554", apk_path: apk, replace: false },
|
||||||
|
});
|
||||||
|
expect(projected).toMatchObject({
|
||||||
|
code: "unsupported_target",
|
||||||
|
details: {
|
||||||
|
reason:
|
||||||
|
"INSTALL_FAILED_ALREADY_EXISTS: Attempt to re-install com.example without first uninstalling.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(projected.message).toContain(
|
||||||
|
"INSTALL_FAILED_ALREADY_EXISTS: Attempt to re-install com.example without first uninstalling.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves the device's uninstall refusal when adb exits nonzero", async () => {
|
||||||
|
const { binary } = await writeAdbStub(
|
||||||
|
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||||
|
if [ "$1" = "-s" ] && [ "$3" = "uninstall" ]; then printf 'Failure [not installed for 0]\\n'; exit 1; fi
|
||||||
|
exit 1`,
|
||||||
|
);
|
||||||
|
const projected = await refusedBy(providerOver(binary), {
|
||||||
|
operation: "uninstall_adb_package",
|
||||||
|
input: { serial: "emulator-5554", package: "com.absent" },
|
||||||
|
});
|
||||||
|
expect(projected).toMatchObject({
|
||||||
|
code: "unsupported_target",
|
||||||
|
details: { reason: "not installed for 0" },
|
||||||
|
});
|
||||||
|
expect(projected.message).toContain("not installed for 0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a nonzero install without a device verdict as an unreadable result", async () => {
|
||||||
|
const { binary, root } = await writeAdbStub(
|
||||||
|
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
|
||||||
|
if [ "$1" = "-s" ] && [ "$3" = "install" ]; then printf 'adb: failed to install app.apk: closed\\n' >&2; exit 1; fi
|
||||||
|
exit 1`,
|
||||||
|
);
|
||||||
|
const apk = join(root, "app.apk");
|
||||||
|
await writeFile(apk, "apk");
|
||||||
|
const projected = await refusedBy(providerOver(binary), {
|
||||||
|
operation: "install_adb_package",
|
||||||
|
input: { serial: "emulator-5554", apk_path: apk, replace: false },
|
||||||
|
});
|
||||||
|
expect(projected).toMatchObject({ code: "unreadable_output" });
|
||||||
|
});
|
||||||
|
|
||||||
it("starts an app through resolved launcher component and force-stops it", async () => {
|
it("starts an app through resolved launcher component and force-stops it", async () => {
|
||||||
const { binary } = await writeAdbStub(
|
const { binary } = await writeAdbStub(
|
||||||
shellStub({
|
shellStub({
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ import {
|
||||||
parseBugreportCopiedPath,
|
parseBugreportCopiedPath,
|
||||||
parseDumpsysPackageOutput,
|
parseDumpsysPackageOutput,
|
||||||
parseFeatureListOutput,
|
parseFeatureListOutput,
|
||||||
|
packageChangeFailureReason,
|
||||||
parsePackageChangeOutput,
|
parsePackageChangeOutput,
|
||||||
parseProcessListOutput,
|
parseProcessListOutput,
|
||||||
parsePngDimensions,
|
parsePngDimensions,
|
||||||
|
|
@ -248,6 +249,31 @@ const runAdb = async (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* adb exits nonzero when the device refuses a package change, printing the
|
||||||
|
* `Failure [...]` verdict to stderr (install) or stdout (uninstall). Read the
|
||||||
|
* refusal from that failed run so it is not reported as unreadable output.
|
||||||
|
*/
|
||||||
|
const packageChangeRefusal = (
|
||||||
|
operation: string,
|
||||||
|
serial: string,
|
||||||
|
error: AnalysisError,
|
||||||
|
): Result<never, AnalysisError> | undefined => {
|
||||||
|
if (!(error instanceof AnalysisProtocolError)) return undefined;
|
||||||
|
const failure = execFileOutputFailure(error.cause);
|
||||||
|
if (failure === undefined) return undefined;
|
||||||
|
const reason = packageChangeFailureReason(
|
||||||
|
`${failure.stdout}\n${failure.stderr}`,
|
||||||
|
);
|
||||||
|
return reason === null
|
||||||
|
? undefined
|
||||||
|
: err(
|
||||||
|
new AnalysisUnsupportedTargetError(operation, serial, reason, {
|
||||||
|
cause: error,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const clientIdentityResult = (
|
const clientIdentityResult = (
|
||||||
selection: AdbBinarySelection,
|
selection: AdbBinarySelection,
|
||||||
identity: AdbClientIdentity,
|
identity: AdbClientIdentity,
|
||||||
|
|
@ -1754,7 +1780,11 @@ export class AdbProvider {
|
||||||
{ timeoutMs: PULL_TIMEOUT_MS, maxOutputBytes: PULL_MAX_OUTPUT_BYTES },
|
{ timeoutMs: PULL_TIMEOUT_MS, maxOutputBytes: PULL_MAX_OUTPUT_BYTES },
|
||||||
signal,
|
signal,
|
||||||
);
|
);
|
||||||
if (!installed.ok) return installed;
|
if (!installed.ok)
|
||||||
|
return (
|
||||||
|
packageChangeRefusal(operation, input.serial, installed.error) ??
|
||||||
|
installed
|
||||||
|
);
|
||||||
const verdict = parsePackageChangeOutput(installed.value.stdout);
|
const verdict = parsePackageChangeOutput(installed.value.stdout);
|
||||||
if (verdict.status === "failure")
|
if (verdict.status === "failure")
|
||||||
return err(
|
return err(
|
||||||
|
|
@ -1803,7 +1833,11 @@ export class AdbProvider {
|
||||||
},
|
},
|
||||||
signal,
|
signal,
|
||||||
);
|
);
|
||||||
if (!uninstalled.ok) return uninstalled;
|
if (!uninstalled.ok)
|
||||||
|
return (
|
||||||
|
packageChangeRefusal(operation, input.serial, uninstalled.error) ??
|
||||||
|
uninstalled
|
||||||
|
);
|
||||||
const verdict = parsePackageChangeOutput(uninstalled.value.stdout);
|
const verdict = parsePackageChangeOutput(uninstalled.value.stdout);
|
||||||
if (verdict.status === "failure")
|
if (verdict.status === "failure")
|
||||||
return err(
|
return err(
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,10 @@ export const parseBugreportCopiedPath = (stdout: string): string | null => {
|
||||||
return match === null ? null : match[1]!;
|
return match === null ? null : match[1]!;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Read the device's `Failure [REASON]` verdict from package-change output. */
|
||||||
|
export const packageChangeFailureReason = (output: string): string | null =>
|
||||||
|
/Failure \[([^\]]+)\]/u.exec(output)?.[1] ?? null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse `adb install`/`adb uninstall` output. adb can print `Failure [...]`
|
* Parse `adb install`/`adb uninstall` output. adb can print `Failure [...]`
|
||||||
* with a zero exit code, so the text decides, not the exit status.
|
* with a zero exit code, so the text decides, not the exit status.
|
||||||
|
|
@ -305,10 +309,11 @@ export const parsePackageChangeOutput = (
|
||||||
} => {
|
} => {
|
||||||
const text = stdout.trim();
|
const text = stdout.trim();
|
||||||
if (/^Success$/mu.test(text)) return { status: "success", reason: null };
|
if (/^Success$/mu.test(text)) return { status: "success", reason: null };
|
||||||
const failure = /Failure \[([^\]]+)\]/u.exec(text);
|
|
||||||
return {
|
return {
|
||||||
status: "failure",
|
status: "failure",
|
||||||
reason: failure?.[1] ?? (text === "" ? null : text.slice(0, 512)),
|
reason:
|
||||||
|
packageChangeFailureReason(text) ??
|
||||||
|
(text === "" ? null : text.slice(0, 512)),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue