dsh-mobilecode 0.4.0 → 0.6.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/README.md +57 -1
- package/lib/android-stream.js +3 -4
- package/lib/client.js +1054 -961
- package/lib/device-build.js +255 -18
- package/lib/index.js +559 -18
- package/lib/uitree.js +4 -4
- package/package.json +2 -2
package/lib/device-build.js
CHANGED
|
@@ -97,17 +97,20 @@ export function exec(command, args, options, onLine) {
|
|
|
97
97
|
return { child, exit }
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
/** Run a command
|
|
101
|
-
export async function
|
|
100
|
+
/** Run a command, capturing stdout AND stderr. Resolves {code, out, err}. */
|
|
101
|
+
export async function captureFull(command, args, options = {}) {
|
|
102
102
|
const out = []
|
|
103
|
+
const err = []
|
|
103
104
|
const running = launch(command, args, {
|
|
104
105
|
cwd: options.cwd,
|
|
105
106
|
env: options.env ? { ...process.env, ...options.env } : undefined,
|
|
106
|
-
stdio: ["ignore", "pipe", "
|
|
107
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
107
108
|
windowsHide: true,
|
|
108
109
|
})
|
|
109
110
|
running.stdout?.setEncoding("utf8")
|
|
111
|
+
running.stderr?.setEncoding("utf8")
|
|
110
112
|
running.stdout?.on("data", (chunk) => out.push(chunk))
|
|
113
|
+
running.stderr?.on("data", (chunk) => err.push(chunk))
|
|
111
114
|
let timer
|
|
112
115
|
if (options.timeoutMs) {
|
|
113
116
|
timer = setTimeout(() => running.kill(), options.timeoutMs)
|
|
@@ -118,9 +121,18 @@ export async function capture(command, args, options = {}) {
|
|
|
118
121
|
running.once("close", (value) => resolve(value ?? -1))
|
|
119
122
|
})
|
|
120
123
|
if (timer) clearTimeout(timer)
|
|
121
|
-
if (code !== 0) return ""
|
|
122
124
|
const text = out.join("")
|
|
123
|
-
return
|
|
125
|
+
return {
|
|
126
|
+
code,
|
|
127
|
+
out: options.maxBytes && Buffer.byteLength(text) > options.maxBytes ? text.slice(0, options.maxBytes) : text,
|
|
128
|
+
err: err.join(""),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Run a command purely for its stdout, e.g. a `-json` query. Empty string on failure. */
|
|
133
|
+
export async function capture(command, args, options = {}) {
|
|
134
|
+
const result = await captureFull(command, args, options)
|
|
135
|
+
return result.code === 0 ? result.out : ""
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
/** Direct child pids of `pid`. POSIX only; returns nothing when pgrep is unavailable. */
|
|
@@ -715,6 +727,231 @@ export function adb() {
|
|
|
715
727
|
return exe ?? "adb"
|
|
716
728
|
}
|
|
717
729
|
|
|
730
|
+
// ── classified adb boundary (Wi-Fi resilience; pattern credited to boheastill/phone-eye) ──
|
|
731
|
+
|
|
732
|
+
/** Wi-Fi adb serials end in `ip:port` (`192.168.1.23:5555`); USB/emulator serials never do. */
|
|
733
|
+
export function isWifiSerial(serial) {
|
|
734
|
+
return /:\d+$/.test(serial)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const ADB_TRANSPORT_RE = /no devices|device (?:'.*?' )?not found|device offline|device unauthorized|device still connecting|error: closed|cannot connect to daemon|failed to start daemon/i
|
|
738
|
+
|
|
739
|
+
/** Why an adb command failed: 'multi-device', 'transport' (transient — a reconnect may fix it), or undefined (real command error). */
|
|
740
|
+
export function classifyAdbFailure(result) {
|
|
741
|
+
const text = `${result.err}\n${result.out}`
|
|
742
|
+
if (/more than one device/i.test(text)) return "multi-device"
|
|
743
|
+
if (ADB_TRANSPORT_RE.test(text)) return "transport"
|
|
744
|
+
return undefined
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Commands that may be auto-replayed after a reconnect: read-only probes only.
|
|
749
|
+
* Anything else (input/am/pm-mutate/install/screencap-to-file) must never
|
|
750
|
+
* replay — a replayed `input tap` could double-tap a payment button.
|
|
751
|
+
*/
|
|
752
|
+
export function replaySafeAdb(args) {
|
|
753
|
+
return /^(exec-out (screencap|cat|uiautomator|logcat|getprop|dumpsys)|shell (screencap|uiautomator|dumpsys|getprop|wm|settings get|pm list|ime list|cat|df|dmesg|logcat|true)|logcat( |$)|emu avd name)/.test(args.join(" "))
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function adbFailTail(result) {
|
|
757
|
+
const text = `${result.err}\n${result.out}`.trim().split(/\r?\n/).filter(Boolean).slice(-3).join(" | ")
|
|
758
|
+
return text.slice(-300)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function adbConnect(serial) {
|
|
762
|
+
const result = await captureFull(adb(), ["connect", serial], { timeoutMs: 15_000 })
|
|
763
|
+
return /connected|already connected/i.test(`${result.out}\n${result.err}`)
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** Attach a Wi-Fi device: `adb connect <ip>:<port>`; true on success. Never throws (returns false with no side effect). */
|
|
767
|
+
export async function adbConnectSerial(serial) {
|
|
768
|
+
return adbConnect(serial)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Pair a Wi-Fi device (Android 11+ requires pairing before the first connect):
|
|
773
|
+
* `adb pair <ip>:<pair-port> <code>`. Resolves when pairing succeeded.
|
|
774
|
+
*/
|
|
775
|
+
export async function adbPair(serial, code) {
|
|
776
|
+
const result = await captureFull(adb(), ["pair", serial, String(code)], { timeoutMs: 20_000 })
|
|
777
|
+
if (/successfully paired/i.test(`${result.out}\n${result.err}`)) return true
|
|
778
|
+
throw new Error(`adb pair ${serial} failed: ${adbFailTail(result) || "no output"}`)
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Parse `adb mdns services` output for the pairing service the device advertises
|
|
783
|
+
* while its "pair using QR code" dialog is open. Returns the `ip:port` the
|
|
784
|
+
* pairing broadcast listens on, or undefined.
|
|
785
|
+
*/
|
|
786
|
+
export function parseMdnsPairing(output) {
|
|
787
|
+
const line = String(output).split(/\r?\n/).find((row) => row.includes("_adb-tls-pairing._tcp"))
|
|
788
|
+
if (line === undefined) return undefined
|
|
789
|
+
const address = /(\d{1,3}(?:\.\d{1,3}){3}:\d+)/.exec(line)?.[1] ?? /(\[[0-9a-f:]+\]:\d+)/.exec(line)?.[1]
|
|
790
|
+
return address ?? undefined
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** A Wi-Fi QR code the device scanner accepts: `WIFI:T:ADB;S:<name>;P:<password>;;`. */
|
|
794
|
+
export function generateQrAdbWifi(name, password) {
|
|
795
|
+
return `WIFI:T:ADB;S:${name};P:${password};;`
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Poll `adb mdns services` until the phone's pairing dialog advertises
|
|
800
|
+
* `_adb-tls-pairing._tcp` (resolves to its `ip:port`), or undefined on timeout.
|
|
801
|
+
* Requires adb with mdns support (>= 31). Non-serial, read-only.
|
|
802
|
+
*/
|
|
803
|
+
export async function waitForMdnsPairing(timeoutMs = 60_000, pollMs = 1_500) {
|
|
804
|
+
const deadline = Date.now() + timeoutMs
|
|
805
|
+
for (;;) {
|
|
806
|
+
const result = await captureFull(adb(), ["mdns", "services"], { timeoutMs: 5_000 }).catch(() => null)
|
|
807
|
+
if (result && result.code === 0) {
|
|
808
|
+
const address = parseMdnsPairing(result.out)
|
|
809
|
+
if (address !== undefined) return address
|
|
810
|
+
}
|
|
811
|
+
if (Date.now() >= deadline) return undefined
|
|
812
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs))
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const QR_NAME_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
817
|
+
/** Random ADB QR name (ADB_WIFI_xxxxxxxxxxxxxx-yyyyyy) + 21-char password, matching Android's own structure. */
|
|
818
|
+
export function randomQrCredentials(prefix = "ADB_WIFI") {
|
|
819
|
+
const pick = (n) => Array.from({ length: n }, () => QR_NAME_CHARS[Math.floor(Math.random() * QR_NAME_CHARS.length)]).join("")
|
|
820
|
+
return { name: `${prefix}_${pick(14)}-${pick(6)}`, password: pick(21) }
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* The single classified boundary for every serial-targeted adb command.
|
|
825
|
+
* On a transient transport failure with a Wi-Fi serial (ip:port) it attempts
|
|
826
|
+
* exactly one `adb connect`; only read-only commands are then replayed, while
|
|
827
|
+
* side-effectful ones raise "reconnected — call again". USB serials get a
|
|
828
|
+
* classified, actionable message instead of an empty result.
|
|
829
|
+
*/
|
|
830
|
+
export async function adbRun(serial, args, options = {}) {
|
|
831
|
+
const argv = ["-s", serial, ...args]
|
|
832
|
+
let result = await captureFull(adb(), argv, options)
|
|
833
|
+
if (result.code === 0) return result.out
|
|
834
|
+
const kind = classifyAdbFailure(result)
|
|
835
|
+
if (kind === "multi-device") throw new Error("adb: more than one device/emulator attached — pass an explicit serial (see adb devices)")
|
|
836
|
+
if (kind !== "transport" || !isWifiSerial(serial)) {
|
|
837
|
+
throw new Error(kind === "transport"
|
|
838
|
+
? `device ${serial} unreachable (${adbFailTail(result)}). For Wi-Fi adb run: adb connect <ip>:5555`
|
|
839
|
+
: `adb ${String(args[0])} failed on ${serial} (exit ${result.code}): ${adbFailTail(result)}`)
|
|
840
|
+
}
|
|
841
|
+
if (!(await adbConnect(serial))) {
|
|
842
|
+
throw new Error(`device ${serial} unreachable — adb connect failed; check the phone's Wi-Fi IP or replug USB once`)
|
|
843
|
+
}
|
|
844
|
+
if (!replaySafeAdb(args)) {
|
|
845
|
+
throw new Error(`device ${serial} reconnected over Wi-Fi; command not auto-replayed (side effects) — call again`)
|
|
846
|
+
}
|
|
847
|
+
result = await captureFull(adb(), argv, options)
|
|
848
|
+
if (result.code !== 0) throw new Error(`device ${serial} still failing after reconnect (exit ${result.code}): ${adbFailTail(result)}`)
|
|
849
|
+
return result.out
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Quote one value for the device shell (`adb shell` rejoins argv through sh -c): '…' with ' → '\''; control chars refused. */
|
|
853
|
+
export function shQuoteDevice(text) {
|
|
854
|
+
if (/[\x00-\x1f]/.test(text)) throw new Error(`refusing control characters in adb shell argument: ${JSON.stringify(text.slice(0, 40))}`)
|
|
855
|
+
return `'${text.replace(/'/g, `'\\''`)}'`
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** Build the `am start` argv from intent parts; values are device-shell quoted so metacharacters stay inert. */
|
|
859
|
+
export function adbIntentArgs({ action, uri, component } = {}) {
|
|
860
|
+
const parts = ["shell", "am", "start"]
|
|
861
|
+
for (const [flag, value] of [["-a", action], ["-d", uri], ["-n", component]]) {
|
|
862
|
+
if (value === undefined || String(value) === "") continue
|
|
863
|
+
parts.push(flag, shQuoteDevice(String(value)))
|
|
864
|
+
}
|
|
865
|
+
return parts
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// ── perf / app-info parsers (pattern credited to newborne/dsh-adb-ultimate) ──
|
|
869
|
+
|
|
870
|
+
/** Parse `cat /proc/meminfo` into {totalKb, availableKb, usedKb, usedPercent}. */
|
|
871
|
+
export function parseMeminfo(output) {
|
|
872
|
+
const grab = (name) => {
|
|
873
|
+
const match = new RegExp(`^${name}:\\s*(\\d+)`, "m").exec(String(output))
|
|
874
|
+
return match ? Number(match[1]) : undefined
|
|
875
|
+
}
|
|
876
|
+
const totalKb = grab("MemTotal")
|
|
877
|
+
const availableKb = grab("MemAvailable")
|
|
878
|
+
if (totalKb === undefined) return undefined
|
|
879
|
+
const usedKb = availableKb === undefined ? undefined : totalKb - availableKb
|
|
880
|
+
return {
|
|
881
|
+
totalKb,
|
|
882
|
+
availableKb,
|
|
883
|
+
usedKb,
|
|
884
|
+
usedPercent: usedKb !== undefined && totalKb > 0 ? Math.round((usedKb / totalKb) * 1000) / 10 : undefined,
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/** Parse `dumpsys battery` into {level, temperatureC, status, health, powered}. */
|
|
889
|
+
export function parseBattery(output) {
|
|
890
|
+
const grab = (name) => {
|
|
891
|
+
const match = new RegExp(`^\\s*${name}:\\s*(.+)`, "m").exec(String(output))
|
|
892
|
+
return match ? match[1].trim() : undefined
|
|
893
|
+
}
|
|
894
|
+
const levelRaw = grab("level")
|
|
895
|
+
const tempRaw = grab("temperature")
|
|
896
|
+
const level = levelRaw ? Number(levelRaw) : undefined
|
|
897
|
+
const tempRawNum = tempRaw ? Number(tempRaw) : undefined
|
|
898
|
+
return {
|
|
899
|
+
level,
|
|
900
|
+
temperatureC: tempRawNum !== undefined && Number.isFinite(tempRawNum) ? Math.round(tempRawNum) / 10 : undefined,
|
|
901
|
+
status: grab("status"),
|
|
902
|
+
health: grab("health"),
|
|
903
|
+
powered: grab("AC powered") === "true" || grab("USB powered") === "true" || grab("Wireless powered") === "true",
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/** Parse `cat /proc/cpuinfo` into {cores, hardware}. */
|
|
908
|
+
export function parseCpuinfo(output) {
|
|
909
|
+
const text = String(output)
|
|
910
|
+
const cores = (text.match(/^processor\s*:/gm) ?? []).length
|
|
911
|
+
const hardware = /^Hardware\s*:\s*(.+)$/m.exec(text)?.[1]?.trim()
|
|
912
|
+
return { cores: cores > 0 ? cores : undefined, hardware }
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/** Parse `dumpsys package <pkg>` into {versionName, versionCode, minSdk, targetSdk, permissions, activities}. */
|
|
916
|
+
export function parseAppInfo(output, packageName) {
|
|
917
|
+
const text = String(output)
|
|
918
|
+
const grab = (name) => {
|
|
919
|
+
const match = new RegExp(`${name}=(\\S+)`, "m").exec(text)
|
|
920
|
+
return match ? match[1] : undefined
|
|
921
|
+
}
|
|
922
|
+
const permissions = []
|
|
923
|
+
const textLines = text.split(/\r?\n/)
|
|
924
|
+
const headerIndex = textLines.findIndex((line) => line.trim() === "requested permissions:")
|
|
925
|
+
if (headerIndex >= 0) {
|
|
926
|
+
for (let i = headerIndex + 1; i < textLines.length; i += 1) {
|
|
927
|
+
const trimmed = textLines[i].trim()
|
|
928
|
+
if (trimmed === "" || !/^(android\.permission\.|com\.[^\s]+\.permission\.)[A-Z0-9_.]+$/.test(trimmed)) break
|
|
929
|
+
permissions.push(trimmed)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const activities = []
|
|
933
|
+
const resolverIndex = textLines.findIndex((line) => line.trim() === "Activity Resolver Table:")
|
|
934
|
+
if (resolverIndex >= 0) {
|
|
935
|
+
// Real resolver-table rows are `<hex> pkg/.Activity filter <hex>` (or `pkg/.Activity (hex)`):
|
|
936
|
+
// 5f60459 com.android.settings/.Settings$ApnEditorActivity filter 4e9281e
|
|
937
|
+
// MIME-type headers (vnd.android.cursor.item/telephony-carrier:) contain a `-` so the
|
|
938
|
+
// component class [\w.$]+ cannot span it, keeping them out of the match.
|
|
939
|
+
for (let i = resolverIndex + 1; i < textLines.length && activities.length < 20; i += 1) {
|
|
940
|
+
const match = /([A-Za-z_][\w.$]*\/[\w.$]+)\s+(?:filter\s+[0-9a-f]+|\([0-9a-f]+\))/i.exec(textLines[i].trim())
|
|
941
|
+
if (match && !activities.includes(match[1])) activities.push(match[1])
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
package: packageName,
|
|
946
|
+
versionName: grab("versionName"),
|
|
947
|
+
versionCode: grab("versionCode"),
|
|
948
|
+
minSdk: grab("minSdk"),
|
|
949
|
+
targetSdk: grab("targetSdk"),
|
|
950
|
+
permissions: [...new Set(permissions)].slice(0, 60),
|
|
951
|
+
activities: activities.slice(0, 20),
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
718
955
|
/** SDK emulator binary (emulator.exe on Windows), or undefined. */
|
|
719
956
|
export function emulatorBinary() {
|
|
720
957
|
const sdk = androidSdk()
|
|
@@ -736,13 +973,13 @@ export async function androidAvds() {
|
|
|
736
973
|
|
|
737
974
|
/** True once the serial has finished booting (sys.boot_completed == 1). */
|
|
738
975
|
export async function androidBooted(serial) {
|
|
739
|
-
const output = await
|
|
976
|
+
const output = await adbRun(serial, ["shell", "getprop", "sys.boot_completed"]).catch(() => "")
|
|
740
977
|
return output.trim() === "1"
|
|
741
978
|
}
|
|
742
979
|
|
|
743
980
|
/** AVD name of an emulator serial (`adb emu avd name`); undefined for physical/offline. */
|
|
744
981
|
export async function avdName(serial) {
|
|
745
|
-
const output = await
|
|
982
|
+
const output = await adbRun(serial, ["emu", "avd", "name"]).catch(() => "")
|
|
746
983
|
const line = output
|
|
747
984
|
.split(/\r?\n/)
|
|
748
985
|
.map((item) => item.trim())
|
|
@@ -771,14 +1008,14 @@ export function bootEmulator(avd) {
|
|
|
771
1008
|
|
|
772
1009
|
/** True when the ADBKeyboard IME is installed (the only way to type non-ASCII over adb). */
|
|
773
1010
|
export async function adbKeyboardReady(serial) {
|
|
774
|
-
const output = await
|
|
1011
|
+
const output = await adbRun(serial, ["shell", "ime", "list", "-s"]).catch(() => "")
|
|
775
1012
|
return /com\.android\.adbkeyboard/i.test(output)
|
|
776
1013
|
}
|
|
777
1014
|
|
|
778
1015
|
/** Type one string via the ADBKeyboard broadcast (base64, so any codepoint survives the shell). */
|
|
779
1016
|
export async function typeViaAdbKeyboard(serial, text) {
|
|
780
1017
|
const msg = Buffer.from(text, "utf8").toString("base64")
|
|
781
|
-
await
|
|
1018
|
+
await adbRun(serial, ["shell", "am", "broadcast", "-a", "ADB_INPUT_B64", "--es", "msg", msg])
|
|
782
1019
|
}
|
|
783
1020
|
|
|
784
1021
|
/** True when every codepoint is safe for `adb shell input text` (printable ASCII, no shell metachars). */
|
|
@@ -819,14 +1056,14 @@ export async function androidDevice() {
|
|
|
819
1056
|
|
|
820
1057
|
/** Primary CPU ABI of a device, e.g. `arm64-v8a`. */
|
|
821
1058
|
export async function androidAbi(serial) {
|
|
822
|
-
const output = await
|
|
1059
|
+
const output = await adbRun(serial, ["shell", "getprop", "ro.product.cpu.abi"]).catch(() => "")
|
|
823
1060
|
const abi = output.trim()
|
|
824
1061
|
return /^[a-z0-9_-]+$/i.test(abi) ? abi : undefined
|
|
825
1062
|
}
|
|
826
1063
|
|
|
827
1064
|
/** Free space on the device's data partition in megabytes, when `df` reports it. */
|
|
828
1065
|
export async function androidFreeMb(serial) {
|
|
829
|
-
return parseFreeMb(await
|
|
1066
|
+
return parseFreeMb(await adbRun(serial, ["shell", "df", "-k", "/data"]).catch(() => ""))
|
|
830
1067
|
}
|
|
831
1068
|
|
|
832
1069
|
/** Second line of `df -k`: Filesystem 1K-blocks Used Available Use% Mounted. */
|
|
@@ -931,7 +1168,7 @@ export async function devices(serial) {
|
|
|
931
1168
|
/** Local path of a fresh screenshot of the serial. undefined on failure. */
|
|
932
1169
|
export async function screenCapture(serial, outDir) {
|
|
933
1170
|
const remote = "/sdcard/dsh-mobilecode-shot.png"
|
|
934
|
-
await
|
|
1171
|
+
await adbRun(serial, ["shell", "screencap", "-p", remote])
|
|
935
1172
|
const name = `screen-${serial}-${Date.now()}.png`
|
|
936
1173
|
const local = path.join(outDir ?? os.tmpdir(), name)
|
|
937
1174
|
const pulled = await new Promise((resolve) => {
|
|
@@ -952,8 +1189,8 @@ export async function screenCapture(serial, outDir) {
|
|
|
952
1189
|
*/
|
|
953
1190
|
export async function uiDump(serial) {
|
|
954
1191
|
const remote = "/sdcard/dsh-mobilecode-ui.xml"
|
|
955
|
-
await
|
|
956
|
-
const xml = await
|
|
1192
|
+
await adbRun(serial, ["shell", "uiautomator", "dump", remote])
|
|
1193
|
+
const xml = await adbRun(serial, ["shell", "cat", remote])
|
|
957
1194
|
const items = []
|
|
958
1195
|
const node = /<node[^>]*>/g
|
|
959
1196
|
for (const match of xml.match(node) ?? []) {
|
|
@@ -975,7 +1212,7 @@ export async function uiDump(serial) {
|
|
|
975
1212
|
|
|
976
1213
|
/** Foreground activity, e.g. "com.foo/.MainActivity", or undefined. */
|
|
977
1214
|
export async function foregroundActivity(serial) {
|
|
978
|
-
const output = await
|
|
1215
|
+
const output = await adbRun(serial, ["shell", "dumpsys", "activity", "activities"]).catch(() => "")
|
|
979
1216
|
const line = output.split("\n").find((item) => /topResumedActivity|mResumedActivity/.test(item))
|
|
980
1217
|
const match = /ActivityRecord\{[^}]*\s([^\s}]+)\}/.exec(line ?? "")
|
|
981
1218
|
return match?.[1] ?? undefined
|
|
@@ -983,14 +1220,14 @@ export async function foregroundActivity(serial) {
|
|
|
983
1220
|
|
|
984
1221
|
/** Kernel log (dmesg). Requires adb root — works on emulators, usually not on real devices. */
|
|
985
1222
|
export async function dmesg(serial) {
|
|
986
|
-
return
|
|
1223
|
+
return adbRun(serial, ["shell", "dmesg"])
|
|
987
1224
|
}
|
|
988
1225
|
|
|
989
1226
|
/** logcat snapshot, filtered by buffer/level/package-like substring. */
|
|
990
1227
|
export async function logcat(serial, { buffer = "main", lines = 200, filter } = {}) {
|
|
991
|
-
const args = ["
|
|
1228
|
+
const args = ["logcat", "-d", "-t", String(lines)]
|
|
992
1229
|
if (buffer && buffer !== "all") args.push("-b", buffer)
|
|
993
|
-
let output = await
|
|
1230
|
+
let output = await adbRun(serial, args)
|
|
994
1231
|
if (filter) output = output.split("\n").filter((line) => line.toLowerCase().includes(filter.toLowerCase())).join("\n")
|
|
995
1232
|
return output
|
|
996
1233
|
}
|