From 5b68aef97963c3f0b3a2aa8bb420f1bebdb80839 Mon Sep 17 00:00:00 2001 From: qiyinxi Date: Mon, 9 Nov 2026 17:17:41 +0100 Subject: [PATCH] =?UTF-8?q?=E9=97=AE=E9=A2=98=E5=8C=85=E8=A1=A5=E6=94=B6?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=99=A8=E6=97=A5=E5=BF=97=E3=80=81adb=20?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E4=B8=8E=20adb=20server=20=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E3=80=81Windows=20=E4=BF=9D=E7=95=99=E7=AB=AF=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 有用户新建实例开机超时,问题包里缺了排查要用的三样东西,只能另外问对方要: 管理器自己的日志、机器上的 adb.exe 和 adb server 的日志、Windows 保留的端口。 - 管理器日志:logs\modavd-ui.log、modavd-bootwatch.log 和各自的 .1,各取最后 5 MB - adb.exe 进程与 adb server 日志:列出所有 adb.exe(没在监听的也列),复制 ANDROID_ADB_LOG_PATH 或临时目录下 adb.log 的最后 2 MB - Windows 保留的 TCP 端口:原样列出 netsh excludedportrange 的输出,并指出本目录的 adb server 端口和各实例的端口有没有落在里面 这三节脚本里没有,不计时;和脚本逐行对照的测试跳过这三节和它们的文件。 --- README.en.md | 2 +- README.md | 2 +- internal/report/collect.go | 3 + internal/report/extra.go | 223 ++++++++++++++++++++++++ internal/report/extra_windows_test.go | 117 +++++++++++++ internal/report/parity_windows_test.go | 38 +++- internal/report/scenario_test.go | 7 + internal/report/testdata/scenarios.json | 5 +- 8 files changed, 391 insertions(+), 6 deletions(-) create mode 100644 internal/report/extra.go create mode 100644 internal/report/extra_windows_test.go diff --git a/README.en.md b/README.en.md index 4ffb23b..0855937 100644 --- a/README.en.md +++ b/README.en.md @@ -74,7 +74,7 @@ modavd [--root ] [--json] [--quiet] [--verbose] [args] | `disk compact` | Compact a stopped instance's data disk, or skip rewriting when estimated savings are too small. Verify before atomically replacing the original, and report host usage before and after, including growth. Large disks have budgets of many hours and effectively do not time out in normal use; cancel to stop an operation. Requires NTFS/ReFS, no snapshots and room for the temporary image. See [budgets and result fields](internal/emu/COMPACT.md) | | `check [--memory M]` | Computer check before starting | | `report-config [--no-clipboard]` | Copy the computer configuration and the game info from the running instances (read-only) | -| `collect-info [--out-dir D] [--stage-dir D] [--keep-stage] [--no-explorer]` | Collect a problem report and zip it (read-only). Ctrl+C before ZIP completion returns `canceled`, deletes the unfinished ZIP and removes staging; `--keep-stage` preserves staging and includes its path in the error | +| `collect-info [--out-dir D] [--stage-dir D] [--keep-stage] [--no-explorer]` | Collect a problem report and zip it (read-only). Besides the emulator logs it takes the manager's and the boot watch's logs (`modavd-ui.log`, `modavd-bootwatch.log` and their `.1`, the last 5 MB of each), every adb.exe process and the adb server log (the last 2 MB of `%TEMP%\adb.log`), and the TCP ports Windows reserves (netsh), saying whether a port this root uses is among them. Ctrl+C before ZIP completion returns `canceled`, deletes the unfinished ZIP and removes staging; `--keep-stage` preserves staging and includes its path in the error | | `components [status]`, `components source [auto\|googledownloads_cn\|tencent\|google]`, `components fetch [--accept-license] [--mirror ID] [--mirror-url URL] [--keep-downloads] [--play-store]`, `components import-playstore ...` | View, choose, or fetch the Google components (adb and the system image; see "Fetching the Google components"); `--play-store` and `import-playstore` add the optional Google Play Store (see below) | | `window show\|hide`, `install `, `shell `, `adb -- ` | Window, install an APK, guest command, adb passthrough | | `update [status\|check\|prepare\|install\|recover] [--automas-version V]` | Update the whole ModAVD package in the root folder, using the update source from "Settings" (`设置`) (see "Updating ModAVD") | diff --git a/README.md b/README.md index ecf5311..13ca14c 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ modavd [--root <目录>] [--json] [--quiet] [--verbose] <命令> [参数] | `disk compact` | 整理已关机实例的数据盘;预计收益不足时跳过重写;转换后先校验再原子替换,原盘在替换成功前不动;报告前后占用和增减;大盘预算可达数十小时,正常整理基本不会超时,停止请取消;仅支持 NTFS/ReFS,拒绝快照或重写空间不足,详见[预算与结果契约](internal/emu/COMPACT.md) | | `check [--memory M]` | 开机前的电脑检查 | | `report-config [--no-clipboard]` | 复制电脑配置和开着实例里的游戏信息(只读) | -| `collect-info [--out-dir D] [--stage-dir D] [--keep-stage] [--no-explorer]` | 收集问题信息并打包(只读);ZIP 完成前的 Ctrl+C 返回 `canceled`,删除未完成 ZIP 并清理暂存目录;`--keep-stage` 则保留暂存目录并在错误中给出路径 | +| `collect-info [--out-dir D] [--stage-dir D] [--keep-stage] [--no-explorer]` | 收集问题信息并打包(只读)。除模拟器日志外,还收管理器和开机守护的日志(`modavd-ui.log`、`modavd-bootwatch.log` 及其 `.1`,各取最后 5 MB)、所有 adb.exe 进程和 adb server 日志(`%TEMP%\adb.log` 最后 2 MB)、Windows 保留的 TCP 端口(netsh),并指出本目录要用的端口有没有落在里面;ZIP 完成前的 Ctrl+C 返回 `canceled`,删除未完成 ZIP 并清理暂存目录;`--keep-stage` 则保留暂存目录并在错误中给出路径 | | `components [status]`、`components source [auto\|googledownloads_cn\|tencent\|google]`、`components fetch [--accept-license] [--mirror ID] [--mirror-url URL] [--keep-downloads] [--play-store]`、`components import-playstore <文件或文件夹>…` | 查看、选择或补全 Google 组件(adb 和系统镜像;见「补全 Google 组件」);`--play-store`、`import-playstore` 补全可选的 Google Play 商店(见下) | | `window show\|hide`、`install `、`shell <命令>`、`adb -- <参数>` | 窗口、装 APK、客体命令、adb 透传 | | `update [status\|check\|prepare\|install\|recover] [--automas-version V]` | 更新根目录里的整个 ModAVD 包,按「设置」里的更新源(见「更新 ModAVD」) | diff --git a/internal/report/collect.go b/internal/report/collect.go index cdb35b2..3455b8f 100644 --- a/internal/report/collect.go +++ b/internal/report/collect.go @@ -260,7 +260,10 @@ func collect(ctx context.Context, rootDir string, h Host, opt CollectOptions) (r c.step("杀毒软件", c.antivirus) c.step("实例列表与实例文件", c.instanceFiles) c.step("正在运行的模拟器与 adb", c.runningAndAdb) + c.extra(sectionAdbProcesses, c.adbProcesses) + c.extra(sectionPortExcluded, c.portExclusions) c.step("模拟器日志", c.emulatorLogs) + c.extra(sectionManagerLogs, c.managerLogFiles) c.step("模拟器崩溃转储", c.crashDumps) if err := c.diagnosticDumps(); err != nil { msg := "诊断崩溃转储:" + errText(err) diff --git a/internal/report/extra.go b/internal/report/extra.go new file mode 100644 index 0000000..241df67 --- /dev/null +++ b/internal/report/extra.go @@ -0,0 +1,223 @@ +package report + +import ( + "fmt" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "time" + + "modavd/internal/adb" + "modavd/internal/root" +) + +// Sections collect-info.ps1 never had, so that whoever sends the problem package is not asked for these separately: +// what the manager and the boot watch logged, every adb.exe with the adb server's own log, and the TCP ports Windows +// keeps for itself. The parity tests leave these sections and their files out (goOnlySections, goOnlyEntry). +const ( + sectionManagerLogs = "管理器日志" + sectionAdbProcesses = "adb.exe 进程与 adb server 日志" + sectionPortExcluded = "Windows 保留的 TCP 端口(netsh)" +) + +var ( + // managerLogTailBytes: modavd-ui.log starts over at 5 MB (one .1 backup), the boot watch's log at 4 MB. + managerLogTailBytes int64 = 5 << 20 + // adbLogTailBytes: adb.log is shared by every adb server of the Windows user and only ever appended to. + adbLogTailBytes int64 = 2 << 20 +) + +const netshTimeout = 15 * time.Second + +// managerLogs are the manager's own logs in logs\; a missing .1 backup is not mentioned. +var managerLogs = []struct{ name, what string }{ + {"modavd-ui.log", "管理器日志"}, + {"modavd-ui.log.1", "管理器日志(上一份)"}, + {"modavd-bootwatch.log", "开机守护日志"}, + {"modavd-bootwatch.log.1", "开机守护日志(上一份)"}, +} + +// extra runs one of these sections like step (a failure or a panic is a line and a problem), without a timing line. +func (c *collector) extra(name string, fn func() error) { + if c.ctx.Err() != nil { + return + } + c.progress("正在收集:" + name) + err := func() (err error) { + defer func() { + if p := recover(); p != nil { + err = fmt.Errorf("内部错误:%v", p) + } + }() + return fn() + }() + if err != nil { + msg := errText(err) + c.L("[本节出错] " + name + ":" + msg) + c.problems = append(c.problems, name+":"+msg) + } +} + +// managerLogFiles copies modavd-ui.log, modavd-bootwatch.log and their .1 backups (the last managerLogTailBytes of +// each) into logs\ of the package. +func (c *collector) managerLogFiles() error { + c.section(sectionManagerLogs) + logDir := filepath.Join(c.root, root.LogsDir) + for _, f := range managerLogs { + src := filepath.Join(logDir, f.name) + if !isFile(src) { + if !strings.HasSuffix(f.name, ".1") { + c.L(f.what + "(" + f.name + "):没有") + } + continue + } + c.copyTail(f.what+"("+f.name+")", src, filepath.Join(c.stage, root.LogsDir, f.name), managerLogTailBytes) + } + return nil +} + +// copyTail copies the last tail bytes of src to dst and says how much in one line. +func (c *collector) copyTail(label, src, dst string, tail int64) { + n, err := trueLength(src) + var got int64 + if err == nil { + got, err = copyShared(src, dst, tail) + } + switch { + case err != nil: + c.L(label + ":复制失败:" + errText(err)) + case n > tail: + c.L(fmt.Sprintf("%s:%s,只取了最后 %s", label, formatSize(float64(n)), formatSize(float64(got)))) + default: + c.L(fmt.Sprintf("%s:%s", label, formatSize(float64(got)))) + } +} + +// adbProcesses lists every adb.exe (a server that never got to listen, another program's adb) and copies the end of +// the adb server log: ANDROID_ADB_LOG_PATH, else adb.log in the temporary directory (GetTempPath: TMP, then TEMP), +// where every adb server of this Windows user writes why it started, stopped or could not listen. +func (c *collector) adbProcesses() error { + c.section(sectionAdbProcesses) + procs, err := c.h.Processes(func(name string) bool { return strings.EqualFold(name, "adb.exe") }) + if err != nil { + // the same process table as 正在运行的模拟器与 adb, which already counts the failure; the log still matters + c.L("adb.exe 进程:读不到(" + errText(err) + ")") + procs = nil + } + sort.SliceStable(procs, func(i, j int) bool { + if !procs[i].Started.Equal(procs[j].Started) { + return procs[i].Started.Before(procs[j].Started) + } + return procs[i].PID < procs[j].PID + }) + if err == nil && len(procs) == 0 { + c.L("adb.exe 进程:没有") + } else if len(procs) > 0 { + c.L(fmt.Sprintf("adb.exe 进程:%d 个", len(procs))) + } + for _, p := range procs { + started, path := "", p.ExePath + if !p.Started.IsZero() { + started = p.Started.Local().Format("2006-01-02 15:04:05") + } + if path == "" { + path = "(路径读不到)" + } + c.L(fmt.Sprintf(" pid %d 启动于 %s %s", p.PID, started, path)) + if p.CommandLine != "" { + c.L(" " + p.CommandLine) + } + } + + log := c.opt.getenv("ANDROID_ADB_LOG_PATH") + if log == "" { + tmp := c.opt.getenv("TMP") + if tmp == "" { + tmp = c.opt.getenv("TEMP") + } + if tmp == "" { + c.L("adb server 日志:找不到临时目录(TMP、TEMP 都没有)") + return nil + } + log = filepath.Join(tmp, "adb.log") + } + if !isFile(log) { + c.L("adb server 日志(" + log + "):没有") + return nil + } + c.copyTail("adb server 日志("+log+")", log, filepath.Join(c.stage, "adb", "adb.log"), adbLogTailBytes) + return nil +} + +// excludedRange is one line of `netsh interface ipv4 show excludedportrange`: start port, end port. +var excludedRange = regexp.MustCompile(`^\s*([0-9]+)\s+([0-9]+)\b`) + +// portUse is a port this root needs and what for. +type portUse struct { + port int + what string +} + +// portExclusions shows the TCP port ranges Windows keeps for itself (Hyper-V, WinNAT, Docker take ranges at boot; +// nothing else can listen there) and says which ports of this root fall into one: the two adb servers and the +// console, adb and gRPC ports of every instance. +func (c *collector) portExclusions() error { + c.section(sectionPortExcluded) + sys := c.opt.getenv("SystemRoot") + if sys == "" { + sys = c.opt.getenv("windir") + } + if sys == "" { + sys = `C:\Windows` + } + r, err := c.h.Run(c.ctx, filepath.Join(sys, "System32", "netsh.exe"), + []string{"interface", "ipv4", "show", "excludedportrange", "protocol=tcp"}, netshTimeout) + if err != nil { + return err + } + if r.TimedOut { + c.L("netsh 超时(15 秒),已放弃") + return nil + } + var ranges [][2]int + for _, line := range splitCRLF(adb.DecodeOutput(r.Stdout) + "\n" + adb.DecodeOutput(r.Stderr)) { + if netTrim(line) == "" { + continue + } + c.L(" " + netTrimEnd(line)) + if m := excludedRange.FindStringSubmatch(line); m != nil { + lo, _ := strconv.Atoi(m[1]) + hi, _ := strconv.Atoi(m[2]) + ranges = append(ranges, [2]int{lo, hi}) + } + } + if r.ExitCode != 0 { + c.L(fmt.Sprintf("netsh 退出码 %d,看不出保留了哪些端口", r.ExitCode)) + return nil + } + rt := &root.Root{Dir: c.root} + uses := []portUse{{rt.AdbServerPort(), "私有 adb server"}, {rt.ScriptAdbServerPort(), "脚本 adb server"}} + for _, n := range c.instances { + if i, ok := diagnosticIndex(n); ok { + uses = append(uses, portUse{c.plan.Console(i), n + " 控制台"}, portUse{c.plan.Adb(i), n + " adb"}, + portUse{c.plan.Grpc(i), n + " gRPC"}) + } + } + var hits []string + for _, u := range uses { + for _, rg := range ranges { + if u.port >= rg[0] && u.port <= rg[1] { + hits = append(hits, fmt.Sprintf("%d(%s)在 %d–%d 里", u.port, u.what, rg[0], rg[1])) + break + } + } + } + if len(hits) == 0 { + c.L("本目录要用的端口都不在保留范围里") + } else { + c.L("警告:本目录要用的端口被 Windows 保留了,adb server 或模拟器在这些端口上监听不了:" + strings.Join(hits, ";")) + } + return nil +} diff --git a/internal/report/extra_windows_test.go b/internal/report/extra_windows_test.go new file mode 100644 index 0000000..3c9d249 --- /dev/null +++ b/internal/report/extra_windows_test.go @@ -0,0 +1,117 @@ +package report + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" +) + +// setNetsh makes netsh.exe answer stdout, or fail to start with startErr. +func setNetsh(f *fixture, stdout, startErr string) { + if f.sc.Run == nil { + f.sc.Run = map[string]struct { + Stdout, Stderr, StartErr string + Exit int + Timeout bool + }{} + } + f.sc.Run["netsh.exe"] = struct { + Stdout, Stderr, StartErr string + Exit int + Timeout bool + }{Stdout: stdout, StartErr: startErr} +} + +// The sections the scripts never had: the manager's logs, every adb.exe with the adb server log, the reserved ports. +func TestCollectManagerLogsAdbAndPorts(t *testing.T) { + useGoldenZone(t, "full") + f := newFixture(t, "full", testTempDir(t), "") + guardDials(t, f.adbPort) + logs := filepath.Join(f.root, "logs") + if err := os.WriteFile(filepath.Join(logs, "modavd-ui.log.1"), []byte("older manager log\n"), 0o644); err != nil { + t.Fatal(err) + } + res, names, files := runCollect(t, f) + s := summaryOf(t, files) + + wantSection := "\r\n== 管理器日志 ==\r\n管理器日志(modavd-ui.log):12 B\r\n管理器日志(上一份)(modavd-ui.log.1):18 B\r\n" + + "开机守护日志(modavd-bootwatch.log):15 B\r\n" + if !strings.Contains(s, wantSection) { + t.Errorf("manager logs section:\n%s", s) + } + for name, want := range map[string]string{ + "logs/modavd-ui.log": "manager log\n", "logs/modavd-ui.log.1": "older manager log\n", + "logs/modavd-bootwatch.log": "boot watch log\n", "adb/adb.log": "--- adb starting (pid 4242) ---\n", + } { + if got, ok := files[name]; !ok || string(got) != want { + t.Errorf("%s: %q (in zip %v), want %q", name, got, ok, want) + } + } + if hasEntryPrefix(names, "logs/modavd-bootwatch.log.1") { + t.Error("a missing .1 backup in the zip") + } + + adbLog := filepath.Join(f.env["TEMP"], "adb.log") + if !strings.Contains(s, "\r\n== adb.exe 进程与 adb server 日志 ==\r\nadb.exe 进程:2 个\r\n") || + !strings.Contains(s, " pid 4242 启动于 2026-01-15 11:57:00 (路径读不到)\r\n adb -L tcp:fake fork-server server\r\n") || + !strings.Contains(s, ` X:\fake\other\adb.exe`+"\r\n") || + !strings.Contains(s, "adb server 日志("+adbLog+"):32 B\r\n") { + t.Errorf("adb section:\n%s", s) + } + + if !strings.Contains(s, "\r\n== Windows 保留的 TCP 端口(netsh) ==\r\n") || + !strings.Contains(s, "\r\n 50000 50059 *\r\n") || !strings.Contains(s, "\r\n本目录要用的端口都不在保留范围里\r\n") { + t.Errorf("ports section:\n%s", s) + } + // none of them failed or has a timing line + for _, name := range []string{sectionManagerLogs, sectionAdbProcesses, sectionPortExcluded} { + if strings.Contains(s, "\r\n "+name+":") || strings.Contains(strings.Join(res.Problems, "\n"), name+":") { + t.Errorf("%s: timing line or problem %q", name, res.Problems) + } + } +} + +// Only the end of a long log is taken, and the summary says how much of how much. +func TestCollectManagerLogTail(t *testing.T) { + useGoldenZone(t, "full") + f := newFixture(t, "full", testTempDir(t), "") + guardDials(t, f.adbPort) + old := managerLogTailBytes + managerLogTailBytes = 4 + t.Cleanup(func() { managerLogTailBytes = old }) + _, _, files := runCollect(t, f) + if got := string(files["logs/modavd-ui.log"]); got != "log\n" { + t.Errorf("tail %q", got) + } + if s := summaryOf(t, files); !strings.Contains(s, "管理器日志(modavd-ui.log):12 B,只取了最后 4 B\r\n") { + t.Errorf("summary:\n%s", s) + } +} + +// A port of this root inside a reserved range is named; netsh failing to start is a failed section, not a stop. +func TestCollectPortExclusions(t *testing.T) { + useGoldenZone(t, "full") + f := newFixture(t, "full", testTempDir(t), "") + guardDials(t, f.adbPort) + lo, hi := f.adbPort-3, f.adbPort+3 + setNetsh(f, fmt.Sprintf("\r\n协议 tcp 端口排除范围\r\n\r\n开始端口 结束端口\r\n---------- --------\r\n %d %d\r\n\r\n", lo, hi), "") + _, _, files := runCollect(t, f) + want := fmt.Sprintf("警告:本目录要用的端口被 Windows 保留了,adb server 或模拟器在这些端口上监听不了:%d(私有 adb server)在 %d–%d 里\r\n", + f.adbPort, lo, hi) + if s := summaryOf(t, files); !strings.Contains(s, want) { + t.Errorf("want %q in:\n%s", want, s) + } + + f = newFixture(t, "full", testTempDir(t), "") + guardDials(t, f.adbPort) + setNetsh(f, "", "netsh is gone") + res, _, files := runCollect(t, f) + s := summaryOf(t, files) + if !strings.Contains(s, "\r\n== Windows 保留的 TCP 端口(netsh) ==\r\n[本节出错] Windows 保留的 TCP 端口(netsh):netsh is gone\r\n") || + !strings.Contains(strings.Join(res.Problems, "\n"), "Windows 保留的 TCP 端口(netsh):netsh is gone") || + !strings.Contains(s, "\r\n== 模拟器日志(logs\\) ==\r\n") { + t.Errorf("problems %q, summary:\n%s", res.Problems, s) + } +} diff --git a/internal/report/parity_windows_test.go b/internal/report/parity_windows_test.go index 7dff44f..43eed2e 100644 --- a/internal/report/parity_windows_test.go +++ b/internal/report/parity_windows_test.go @@ -270,7 +270,13 @@ func TestCollectMatchesScript(t *testing.T) { res, names, files := runCollect(t, f) want, _ := f.golden(t, name, "collect-entries.txt") - compareLines(t, "zip entries", names, lines(want)) + var scriptNames []string + for _, n := range names { + if !goOnlyEntry(n) { + scriptNames = append(scriptNames, n) + } + } + compareLines(t, "zip entries", scriptNames, lines(want)) // summary.txt: the script's lines, but for modavdLine's wantSummary, bom := f.golden(t, name, "collect", "summary.txt") @@ -285,7 +291,7 @@ func TestCollectMatchesScript(t *testing.T) { // ModAVD adds a diagnostic state line for each canonical instance. var scriptLines []string var stateLines []string - for _, line := range lines(strings.TrimPrefix(got, "\ufeff")) { + for _, line := range withoutGoOnly(lines(strings.TrimPrefix(got, "\ufeff"))) { if strings.HasSuffix(line, ":诊断模式 关") { stateLines = append(stateLines, line) continue @@ -390,6 +396,34 @@ func TestCollectMatchesScript(t *testing.T) { } } +// goOnlySections are the summary sections collect-info.ps1 does not have (extra.go). +var goOnlySections = map[string]bool{sectionManagerLogs: true, sectionAdbProcesses: true, sectionPortExcluded: true} + +// withoutGoOnly drops goOnlySections from summary lines: each with the empty line before its header and its lines. +func withoutGoOnly(ls []string) []string { + var out []string + skip := false + for i, l := range ls { + if l == "" && i+1 < len(ls) && strings.HasPrefix(ls[i+1], "== ") && strings.HasSuffix(ls[i+1], " ==") { + skip = goOnlySections[strings.TrimSuffix(strings.TrimPrefix(ls[i+1], "== "), " ==")] + } + if !skip { + out = append(out, l) + } + } + return out +} + +// goOnlyEntry: a file of goOnlySections (the manager's logs, the adb server log). +func goOnlyEntry(name string) bool { + for _, f := range managerLogs { + if strings.EqualFold(name, "logs/"+f.name) { + return true + } + } + return strings.HasPrefix(name, "adb/") +} + func TestReportConfigMatchesScript(t *testing.T) { for _, name := range []string{"full", "bare", "shortname"} { t.Run(name, func(t *testing.T) { diff --git a/internal/report/scenario_test.go b/internal/report/scenario_test.go index be00688..4677ad4 100644 --- a/internal/report/scenario_test.go +++ b/internal/report/scenario_test.go @@ -481,6 +481,10 @@ func (h *fakeHost) Run(_ context.Context, exe string, args []string, _ time.Dura h.runs = append(h.runs, exe) h.mu.Unlock() r, ok := h.sc.Run[strings.ToLower(filepath.Base(exe))] + if !ok && strings.EqualFold(filepath.Base(exe), "netsh.exe") { + // a computer whose only reserved range is far from ModAVD's ports (the scripts never ran netsh) + return RunResult{Stdout: []byte(fakeNetshOut)}, nil + } if !ok { return RunResult{}, fmt.Errorf("test: no program %s in the scenario", exe) } @@ -493,6 +497,9 @@ func (h *fakeHost) Run(_ context.Context, exe string, args []string, _ time.Dura return RunResult{ExitCode: r.Exit, Stdout: []byte(r.Stdout), Stderr: []byte(r.Stderr)}, nil } +// fakeNetshOut is `netsh interface ipv4 show excludedportrange protocol=tcp` of a Chinese Windows with one range. +const fakeNetshOut = "\r\n协议 tcp 端口排除范围\r\n\r\n开始端口 结束端口\r\n---------- --------\r\n 50000 50059 *\r\n\r\n* - 管理的端口排除。\r\n\r\n" + func (h *fakeHost) Drive(root string) (Drive, error) { if err := failIf(h.sc.DriveErr); err != nil { return Drive{}, err diff --git a/internal/report/testdata/scenarios.json b/internal/report/testdata/scenarios.json index e99a9c9..506e696 100644 --- a/internal/report/testdata/scenarios.json +++ b/internal/report/testdata/scenarios.json @@ -172,14 +172,15 @@ {"path": "root/logs/logcat-mas_1-20260113-080000.txt", "size": 2000}, {"path": "root/logs/mas_10-20260114-090000.log", "size": 200}, {"path": "root/logs/modavd-bridge-1-20260113-080100.log", "size": 100}, - {"path": "root/logs/modavd-bootwatch.log", "text": "not collected\n"}, - {"path": "root/logs/modavd-ui.log", "text": "not collected\n"}, + {"path": "root/logs/modavd-bootwatch.log", "text": "boot watch log\n"}, + {"path": "root/logs/modavd-ui.log", "text": "manager log\n"}, {"path": "root/logs/startup-mas_1-20260110-080000-hang-threads.txt", "text": "threads 0110\n"}, {"path": "root/logs/startup-mas_1-20260111-080000-hang-threads.txt", "text": "threads 0111\n"}, {"path": "root/logs/startup-mas_1-20260112-080000-hang-threads.txt", "text": "threads 0112\n"}, {"path": "root/logs/startup-mas_10-20260109-080000-hang-threads.txt", "text": "threads 10\n"}, {"path": "root/logs/startup-odd-hang-threads.txt", "text": "no stamp in the name\n", "mtime": "2026-01-14 10:00:00"}, {"path": "root/logs/fetch-components.log", "text": "fetch log\n"}, + {"path": "local/Temp/adb.log", "text": "--- adb starting (pid 4242) ---\n"}, {"path": "local/Temp/AndroidEmulator/emu-crash-37.2.10-mas.db/reports/r1.dmp", "size": 1000, "mtime": "2026-01-14 09:00:00"}, {"path": "local/Temp/AndroidEmulator/emu-crash-37.2.10-mas.db/reports/r2.dmp", "size": 5000, "mtime": "2026-01-13 09:00:00"}, {"path": "local/Temp/AndroidEmulator/emu-crash-37.2.11-mas.db/pending/r3.dmp", "size": 800, "mtime": "2026-01-12 09:00:00"},