diff --git a/internal/emu/afterboot.go b/internal/emu/afterboot.go index b4f579d..3773e88 100644 --- a/internal/emu/afterboot.go +++ b/internal/emu/afterboot.go @@ -86,7 +86,6 @@ func (m *Manager) afterBoot(ctx context.Context, inst *avd.Instance, mode bootSe i := inst.Index ports := m.Root.Ports() port := ports.Console(i) - var imeDefault bool cores, ok := inst.CPU() if !ok || cores <= 0 { cores = avd.DefaultCPU @@ -101,6 +100,18 @@ func (m *Manager) afterBoot(ctx context.Context, inst *avd.Instance, mode bootSe } steps := []func(){ + func() { + // 0a) the guest library fixes (Vulkan driver, ARM translation layer). A launched boot of an initialized + // instance has mounted them during the boot animation already (earlyGuestFixes): this only finds them + // mounted. Otherwise mounting them restarts zygote, system_server and every app, so it comes first: + // everything below then runs on the framework that stays, and a change the package manager writes + // with a delay (the stock launcher removed by the first-boot setup) is not lost with the restart. + // The progress step keeps its name vulkan_fix (API, GUI, create) + prog.emit("vulkan_fix", fmt.Sprintf("实例 %d 客体库补丁(Vulkan 驱动、ARM 转译层)", i), progressVulkanFix) + restarted, pending := m.ensureGuestFixes(ctx, inst, mode, until) + r.SetupPending = pending + r.ZygoteRestarted = restarted + }, func() { if err := m.applyRegional(ctx, i, mode); err != nil { m.Log.Warnf("实例 %d 语言设置未完成,下次继续: %v", i, err) @@ -169,7 +180,7 @@ func (m *Manager) afterBoot(ctx context.Context, inst *avd.Instance, mode bootSe func() { // 7b) NetworkWatchlistService hashes every APK in full once a day has passed since the last boot (about // 4 GB into the page cache, the whole guest RAM then stays resident on the host). Read when - // system_server starts: effective from the next boot, also after the zygote restart of step 11 + // system_server starts: effective from the next boot res := m.shell(ctx, i, "settings put global network_watchlist_enabled 0", 0) if res.Code != 0 { m.Log.Warnf("实例 %d 关闭 NetworkWatchlistService 失败(下次开机仍会把安装包整个读一遍): %s", @@ -189,7 +200,7 @@ func (m *Manager) afterBoot(ctx context.Context, inst *avd.Instance, mode bootSe }, // 7d) host input method: install, enable, make it the default by the rules (every boot) // (logPath is this boot's emulator log unless the instance was simply adopted) - func() { imeDefault = m.setupHostIme(ctx, i, mode != setupAdopted, logPath, prog) }, + func() { r.HostIME = m.setupHostIme(ctx, i, mode != setupAdopted, logPath, prog) }, // 8) network: Wi-Fi always on (games then skip "not on Wi-Fi" prompts); fastpath sends bulk traffic over // eth0 (the virtual Wi-Fi alone does about 6 MB/s). The Wi-Fi switch persists in /data: set every boot func() { m.setupNetwork(ctx, i) }, @@ -207,35 +218,19 @@ func (m *Manager) afterBoot(ctx context.Context, inst *avd.Instance, mode bootSe prog.emit("first_boot", fmt.Sprintf("实例 %d 首次开机初始化", i), progressFirstBoot) m.firstBootInit(ctx, i) r.FirstBoot = true + return + } + // 9b) the stock launcher stays removed (pixellauncher.go): an older start lost its removal to a restart + if mode != setupAdopted { + m.ensurePixelLauncherRemoved(ctx, i) } }, // 10) orientation: the display stays landscape (persistent; set every boot so old instances get it) func() { m.setupOrientation(ctx, i) }, - // 10b) hand the clusters the guest freed back to the host, at most once a day (best effort, see autotrim.go); - // before the guest library fixes, whose zygote restart is deliberately the last step + // 10b) hand the clusters the guest freed back to the host, at most once a day (best effort, see autotrim.go) func() { m.autoTrim(ctx, i, mode) }, - func() { - // 11) the guest library fixes (Vulkan driver, ARM translation layer). Must be last: mounting them restarts - // zygote, system_server and every app. The progress step keeps its name vulkan_fix (API, GUI, create) - prog.emit("vulkan_fix", fmt.Sprintf("实例 %d 客体库补丁(Vulkan 驱动、ARM 转译层)", i), progressVulkanFix) - restarted, pending := m.ensureGuestFixes(ctx, inst, mode, until) - r.SetupPending = pending - if !restarted { - return - } - r.ZygoteRestarted = true - if ctx.Err() != nil { - return - } - // the letterbox education switch lives in system_server's memory and went with the restart - m.setupOrientation(ctx, i) - // the default input method lives in settings and should survive; it was ours after 7d, so check it - if imeDefault { - m.recheckHostIme(i, m.guestIO(ctx, i)) - } - }, - // 12) download disks (dldisk.go). After 11: the framework restart there remounts the shared storage, and a - // disk mounted on it before would be gone with it + // 12) download disks (dldisk.go). After the guest library fixes (0a): the framework restart there remounts the + // shared storage, and a disk mounted on it before would be gone with it func() { m.setupDownloadDisk(ctx, i) }, // 13) the guest's clock source into the log, its kernel log's clock lines saved when an x86_64 guest is not on // tsc (clocksource.go, ModAVD#110). Last: the later the one read, the more of a fallback during boot it sees @@ -247,7 +242,6 @@ func (m *Manager) afterBoot(ctx context.Context, inst *avd.Instance, mode bootSe } step() } - r.HostIME = imeDefault return r, ctxErr(ctx) } diff --git a/internal/emu/bootadb_windows_test.go b/internal/emu/bootadb_windows_test.go index 5662120..5987b12 100644 --- a/internal/emu/bootadb_windows_test.go +++ b/internal/emu/bootadb_windows_test.go @@ -41,7 +41,7 @@ func TestWaitBootAdbUnusableAfterBoot(t *testing.T) { tries := bootAdbFakes(t, q, true) adbAfterBootWait, bootRegisterEvery = 1500*time.Millisecond, 500*time.Millisecond t0 := time.Now() - _, err := m.waitBoot(context.Background(), 3, time.Hour, &q, nil, "", &progressor{}, nil, nil) + _, err := m.waitBoot(context.Background(), 3, time.Hour, &q, nil, "", &progressor{}, nil, nil, nil) if errs.CodeOf(err) != errs.Timeout || !strings.Contains(err.Error(), "ModAVD 实例 3 的安卓已经开好了(模拟器报告开机完成),但之后") || !strings.Contains(err.Error(), "私有 adb(端口 20391)一直用不了") { t.Fatalf("err %v", err) @@ -61,7 +61,7 @@ func TestWaitBootAdbUnusableAfterBoot(t *testing.T) { t.Fatal(err) } adbAfterBootWait = time.Hour - _, err = m.waitBoot(context.Background(), 3, 1500*time.Millisecond, &q, nil, "", &progressor{}, nil, nil) + _, err = m.waitBoot(context.Background(), 3, 1500*time.Millisecond, &q, nil, "", &progressor{}, nil, nil, nil) if errs.CodeOf(err) != errs.Timeout || !strings.Contains(err.Error(), "安卓已经开好了") || !strings.Contains(err.Error(), ";adb 自己的日志最后一行:fatal: boom。日志见 ") { t.Fatalf("at the timeout: %v", err) @@ -73,7 +73,7 @@ func TestWaitBootNoBootReport(t *testing.T) { m, _ := opsTestManager(t) q, _ := startSleeper(t) tries := bootAdbFakes(t, q, false) - _, err := m.waitBoot(context.Background(), 3, 1500*time.Millisecond, &q, nil, "", &progressor{}, nil, nil) + _, err := m.waitBoot(context.Background(), 3, 1500*time.Millisecond, &q, nil, "", &progressor{}, nil, nil, nil) if errs.CodeOf(err) != errs.Timeout || !strings.Contains(err.Error(), "启动超时(2 秒内安卓没有启动完成)") { t.Fatalf("err %v", err) } diff --git a/internal/emu/createinit.go b/internal/emu/createinit.go index 8a0aeba..b59f6f4 100644 --- a/internal/emu/createinit.go +++ b/internal/emu/createinit.go @@ -139,7 +139,9 @@ type createStage struct { // the tuning 1–2 s, the host IME and the steps after it 2–3 s, the first-boot setup 3 s, the guest Vulkan fix 13–14 s // (most of it waiting for the framework after the zygote restart), the shutdown 5–8 s. The bridge on a new instance // takes two rounds with a cold boot each, then a check: the first remount turns verity off (27 s with its -// shutdown), the install (12 s with its shutdown), the check that finds it installed (1 s, no boot). +// shutdown), the install (12 s with its shutdown), the check that finds it installed (1 s, no boot). Since 11-15 a cold +// boot mounts the guest library fixes during the boot (earlyGuestFixes, the boot itself still 17 s): only the check +// after it is left (stageFixCheck); the first boot of a new instance mounts them right after the boot as before. const ( stagePrepare = 2.0 // the checks before the launch (and starting the private adb server) stageBoot = 35.0 // from the launch until Android has booted, the first boot of a new instance @@ -148,6 +150,7 @@ const ( stageIme = 2.5 stageFirstBoot = 3.0 stageVulkanFix = 13.5 + stageFixCheck = 0.5 // the guest library fixes after a cold boot: found mounted already stageRemount = 27.0 // the bridge's first round: the first remount and the shutdown stageBridge = 12.0 // its second: the install and the shutdown stageBridgeCheck = 1.0 // its last: the check that finds it installed @@ -167,13 +170,19 @@ func createPlan(boot float64, firstBoot, bridge bool) []createStage { return append(plan, createStage{"create_stop", stageShutdown}) } -// bootStages are the stages of one boot of the setup, by the steps Start reports; boot is the wait for Android. +// bootStages are the stages of one boot of the setup, by the steps Start reports; boot is the wait for Android. The +// guest library fixes come first after the boot (afterBoot); a boot with the first-boot setup is the only one that +// mounts them there, the others did during the boot. func bootStages(boot float64, firstBoot bool) []createStage { - s := []createStage{{"launch", boot}, {"tune", stageTune}, {"ime", stageIme}} + fix := stageFixCheck + if firstBoot { + fix = stageVulkanFix + } + s := []createStage{{"launch", boot}, {"vulkan_fix", fix}, {"tune", stageTune}, {"ime", stageIme}} if firstBoot { s = append(s, createStage{"first_boot", stageFirstBoot}) } - return append(s, createStage{"vulkan_fix", stageVulkanFix}) + return s } // bridgeRound is a round of the bridge install that changes the guest: the script (and the shutdown), then a cold diff --git a/internal/emu/createinit_test.go b/internal/emu/createinit_test.go index 1f10e8c..fd3e04f 100644 --- a/internal/emu/createinit_test.go +++ b/internal/emu/createinit_test.go @@ -541,15 +541,20 @@ func TestCreateCanceledReportsFinalShutdown(t *testing.T) { } // oneBoot is what Start reports for one boot of the setup: the boot wait (polled by the clock), then the steps after -// it. firstBoot adds the first-boot setup. +// it, the guest library fixes first. firstBoot adds the first-boot setup; a later boot mounts the fixes during the +// boot wait already (earlyGuestFixes), and the step after it only finds them mounted. func oneBoot(firstBoot bool) []Progress { - evs := []Progress{{Step: "launch", Done: progressLaunch}, {Step: "boot", Done: 150}, {Step: "boot", Done: 200}, - {Step: "boot", Done: progressBooted}, {Step: "tune", Done: progressTune}, {Step: "ime", Done: progressIme}} + evs := []Progress{{Step: "launch", Done: progressLaunch}, {Step: "boot", Done: 150}} + if !firstBoot { + evs = append(evs, Progress{Step: "vulkan_fix", Done: 151}) + } + evs = append(evs, Progress{Step: "boot", Done: 200}, Progress{Step: "boot", Done: progressBooted}, + Progress{Step: "vulkan_fix", Done: progressVulkanFix}, Progress{Step: "tune", Done: progressTune}, + Progress{Step: "ime", Done: progressIme}) if firstBoot { evs = append(evs, Progress{Step: "first_boot", Done: progressFirstBoot}) } - return append(evs, Progress{Step: "vulkan_fix", Done: progressVulkanFix}, - Progress{Step: "done", Message: "实例 6 已就绪(共 45.7 秒)", Done: progressTotal}) + return append(evs, Progress{Step: "done", Message: "实例 6 已就绪(共 45.7 秒)", Done: progressTotal}) } // runCreateProgress feeds a setup's events into a createProgress with plan and returns what it sent. bridgeRounds @@ -621,8 +626,9 @@ func TestCreateProgress(t *testing.T) { want float64 // seconds done when it starts }{ {"launch", stagePrepare}, - {"tune", stagePrepare + stageBoot}, - {"vulkan_fix", stagePrepare + stageBoot + stageTune + stageIme + stageFirstBoot}, + {"vulkan_fix", stagePrepare + stageBoot}, + {"tune", stagePrepare + stageBoot + stageVulkanFix}, + {"first_boot", stagePrepare + stageBoot + stageVulkanFix + stageTune + stageIme}, {"create_stop", total - stageShutdown}, } { if d, want := doneAt(t, got, c.step), int(float64(progressTotal)*c.want/total); d != want { @@ -652,13 +658,13 @@ func TestCreateProgressBridge(t *testing.T) { var cold, rounds []int for _, e := range planned[slices.IndexFunc(planned, func(e Progress) bool { return e.Step == "bridge" }):] { switch e.Step { - case "launch", "tune", "vulkan_fix": + case "launch", "tune": cold = append(cold, e.Done) case "bridge": rounds = append(rounds, e.Done) } } - increasing := len(cold) == 6 + increasing := len(cold) == 4 for k := 1; k < len(cold); k++ { increasing = increasing && cold[k] > cold[k-1] } @@ -711,9 +717,14 @@ func TestCreatePlanOfSetup(t *testing.T) { } return doneAt(t, sent, "tune") } + // the fixes after the boot: mounted there on a first boot, only checked after a later one want := func(boot, firstBoot float64) int { - total := stagePrepare + boot + stageTune + stageIme + firstBoot + stageVulkanFix + stageShutdown - return int(float64(progressTotal) * (stagePrepare + boot) / total) + fix := stageFixCheck + if firstBoot > 0 { + fix = stageVulkanFix + } + total := stagePrepare + boot + fix + stageTune + stageIme + firstBoot + stageShutdown + return int(float64(progressTotal) * (stagePrepare + boot + fix) / total) } i := 6 if d, w := tuneAt(CreateRequest{Index: &i, NoBridge: true}), want(stageBoot, stageFirstBoot); d != w { @@ -792,8 +803,8 @@ func TestCreateProgressBootLog(t *testing.T) { t.Fatalf("unreadable log: %d, was %d", d, late) } write("x is operational\nAdvertising in: x\nINFO | Initialized handle: videmulator20060\nINFO | Boot completed in 19637 ms\n") - // the log is only read in the boot wait: the tuning starts its own stage - p.boot(Progress{Step: "tune", Done: progressTune}) + // the log is only read in the boot wait: the guest library fixes start their own stage + p.boot(Progress{Step: "vulkan_fix", Done: progressVulkanFix}) p.boot(Progress{Step: "boot", Done: 150}) if d := got[len(got)-1].Done; d != booted { t.Fatalf("after the boot: %d, want %d", d, booted) diff --git a/internal/emu/early_test.go b/internal/emu/early_test.go new file mode 100644 index 0000000..b021cb1 --- /dev/null +++ b/internal/emu/early_test.go @@ -0,0 +1,104 @@ +package emu + +import ( + "strings" + "testing" + "time" + + "modavd/internal/adb" +) + +func TestEarlyBootState(t *testing.T) { + for _, c := range []struct { + name string + res adb.Result + want bootState + ok bool + }{ + {"booting", adb.Result{Out: "zygote=running booted=\r\n"}, bootState{zygote: true}, true}, + {"booted", adb.Result{Out: "zygote=running booted=1\n"}, bootState{zygote: true, booted: true}, true}, + {"zygote not yet", adb.Result{Out: "zygote= booted=\n"}, bootState{}, true}, + {"zygote restarting", adb.Result{Out: "zygote=restarting booted=\n"}, bootState{}, true}, + {"noise before", adb.Result{Out: "WARNING: linker\nzygote=running booted=\n"}, bootState{zygote: true}, true}, + {"adb failed", adb.Result{Code: 1, Out: "error: device offline"}, bootState{}, false}, + {"no line", adb.Result{Out: "something else\n"}, bootState{}, false}, + } { + got, ok := earlyBootState(c.res) + if got != c.want || ok != c.ok { + t.Errorf("%s: %+v %v, want %+v %v", c.name, got, ok, c.want, c.ok) + } + } + if !strings.Contains(earlyBootProbe, "getprop init.svc.zygote") || !strings.Contains(earlyBootProbe, "getprop sys.boot_completed") { + t.Errorf("probe %q", earlyBootProbe) + } +} + +func TestPixelLauncherListed(t *testing.T) { + for out, want := range map[string]bool{ + "package:com.google.android.apps.nexuslauncher\r\n": true, + "package:com.google.android.apps.nexuslauncher.extra\n": false, + "": false, + "package:de.jrpie.android.launcher\npackage:com.google.android.apps.nexuslauncher": true, + } { + if got := pixelLauncherListed(out); got != want { + t.Errorf("%q: %v", out, got) + } + } +} + +func TestPixelLauncherRemoveResult(t *testing.T) { + for _, c := range []struct { + res adb.Result + ok, saved bool + }{ + {adb.Result{Out: "Success\r\nSAVED\r\n"}, true, true}, + {adb.Result{Out: "Success\nUNSAVED\n"}, true, false}, + {adb.Result{Code: 1, Out: "Failure [not installed for 0]\n"}, false, false}, + {adb.Result{Code: -1, Out: "adb shell 超时(45 秒)\nSAVED"}, false, false}, + {adb.Result{Out: "Success\n"}, false, false}, + } { + if ok, saved := pixelLauncherRemoveResult(c.res); ok != c.ok || saved != c.saved { + t.Errorf("%+v: %v %v", c.res, ok, saved) + } + } +} + +func TestPixelLauncherRemoveReport(t *testing.T) { + for _, c := range []struct { + ok, saved bool + out string + warn bool + has string + }{ + {true, true, "Success\nSAVED\n", false, "实例 2 做:已卸载并写盘"}, + {true, false, "Success\nUNSAVED\n", true, "实例 2 做:已卸载,但 20 秒内没见系统写盘,下次开机会再检查"}, + {false, false, "Failure [DELETE_FAILED_INTERNAL_ERROR]\n", true, "实例 2 做失败,下次开机再试: Failure [DELETE_FAILED_INTERNAL_ERROR]"}, + {false, false, "", true, "失败,下次开机再试: 无输出"}, + } { + r := pixelLauncherRemoveReport(2, "做", c.ok, c.saved, c.out) + if r.warn != c.warn || !strings.Contains(r.msg, c.has) { + t.Errorf("%+v: %+v", c, r) + } + } +} + +// The removal runs inside `su 0 sh -c '...'` (no single quote), uninstalls for user 0 only, and waits on the file +// the package manager writes the per-user state to. +func TestPixelLauncherRemoveScript(t *testing.T) { + s := pixelLauncherRemoveScript + for _, want := range []string{"pm uninstall --user 0 " + pixelLauncherPackage + " || exit 1", + "/data/system/users/0/package-restrictions.xml", "stat -c %y", "echo SAVED", "echo UNSAVED", "-lt 40", "sleep 0.5"} { + if !strings.Contains(s, want) { + t.Errorf("script lacks %q: %s", want, s) + } + } + if strings.Contains(s, "'") { + t.Error("script has a single quote") + } + if strings.Index(s, "b=$(stat") < strings.Index(s, "pm uninstall") { + t.Error("the time before the write is taken before pm returns") + } + if pixelLauncherRemoveTimeout <= 20*time.Second { + t.Errorf("timeout %v does not cover the 20 s wait", pixelLauncherRemoveTimeout) + } +} diff --git a/internal/emu/early_windows_test.go b/internal/emu/early_windows_test.go new file mode 100644 index 0000000..316bed0 --- /dev/null +++ b/internal/emu/early_windows_test.go @@ -0,0 +1,51 @@ +//go:build windows + +package emu + +import ( + "context" + "testing" + "time" +) + +// TestWaitBootEarly: the boot wait hands each poll where adb answers before the boot has completed to early, until +// early says done; a poll where adb does not answer does not call it; the boot completing ends the wait. +func TestWaitBootEarly(t *testing.T) { + m, _ := opsTestManager(t) + q, _ := startSleeper(t) + bootAdbFakes(t, q, false) + saved := waitBootProbe + t.Cleanup(func() { waitBootProbe = saved }) + // polls: adb not up, up (not done), up (done), up, booted + probes := []struct{ booted, answered bool }{{false, false}, {false, true}, {false, true}, {false, true}, {true, true}} + n := 0 + waitBootProbe = func(_ *Manager, _ context.Context, _ int, _ time.Duration, _ bool) (bool, bool) { + p := probes[min(n, len(probes)-1)] + n++ + return p.booted, p.answered + } + var calls []int // the probe number each early call followed + early := func() bool { + calls = append(calls, n) + return len(calls) == 2 + } + got, err := m.waitBoot(context.Background(), 3, time.Minute, &q, nil, "", &progressor{}, nil, nil, early) + if err != nil || got.PID != q.PID { + t.Fatalf("waitBoot: %v %+v", err, got) + } + if len(calls) != 2 || calls[0] != 2 || calls[1] != 3 { + t.Errorf("early after probes %v, want [2 3]", calls) + } + if n != 5 { + t.Errorf("%d probes, want 5", n) + } + + // booted at once: early never runs + n, calls = len(probes)-1, nil + if _, err := m.waitBoot(context.Background(), 3, time.Minute, &q, nil, "", &progressor{}, nil, nil, early); err != nil { + t.Fatal(err) + } + if len(calls) != 0 { + t.Errorf("early ran on a booted guest: %v", calls) + } +} diff --git a/internal/emu/firstboot.go b/internal/emu/firstboot.go index 0868f78..51e819a 100644 --- a/internal/emu/firstboot.go +++ b/internal/emu/firstboot.go @@ -169,16 +169,20 @@ func (m *Manager) installLauncher(ctx context.Context, i int) string { // The stock launcher is uninstalled for user 0 (the system partition keeps it; `cmd package install-existing` // restores it), not disabled: disabling sends PACKAGE_CHANGED, SystemUI's OverviewProxyService then looks for // the stock launcher's recents service and crashes on null, which breaks the home switch transition and leaves - // only the wallpaper on the first boot. Uninstalling sends PACKAGE_REMOVED, which it ignores (10-08). - cmds := []string{ - "cmd package set-home-activity " + lightLauncherHome, - "pm uninstall --user 0 " + pixelLauncherPackage, - "input keyevent KEYCODE_HOME", - } - if res := m.shell(ctx, i, strings.Join(cmds, " ; "), 0); res.Code != 0 { + // only the wallpaper on the first boot. Uninstalling sends PACKAGE_REMOVED, which it ignores (10-08). The removal + // is waited for until it is on disk (pixelLauncherRemoveScript); one that fails is retried at the next boot + // (ensurePixelLauncherRemoved), the light launcher is home all the same. + if res := m.shell(ctx, i, "cmd package set-home-activity "+lightLauncherHome, 0); res.Code != 0 { m.Log.Warnf("实例 %d 设置轻量桌面失败: %s", i, res.Out) return "pixel" } + ok, saved, out := m.removePixelLauncher(ctx, i) + if r := pixelLauncherRemoveReport(i, "卸载原生 Pixel 桌面", ok, saved, out); r.warn { + m.logReport(r) + } + if res := m.shell(ctx, i, "input keyevent KEYCODE_HOME", 0); res.Code != 0 { + m.Log.Warnf("实例 %d 回到轻量桌面失败: %s", i, res.Out) + } return lightLauncherPackage } diff --git a/internal/emu/guestfix.go b/internal/emu/guestfix.go index 6e36fff..9aaf184 100644 --- a/internal/emu/guestfix.go +++ b/internal/emu/guestfix.go @@ -105,12 +105,84 @@ func (m *Manager) ensureGuestFixes(ctx context.Context, inst *avd.Instance, mode }, profiles, mode == setupOwed, until) } +// earlyGuestFixes mounts the guest library fixes of a launched boot while that boot is still on its way: adb answers, +// zygote runs, sys.boot_completed is not 1 yet. The zygote restart then cuts short a framework start nobody has seen, +// and a window keeps showing the boot animation until the restarted framework is up, instead of the home screen going +// away and coming back after the boot (11-15). Measured on a 1080p instance: adb answers about 11 s after the launch, +// the boot completes about 5 s later. +// +// done=false: the guest is not that far yet (adb error, zygote not running): the boot wait asks again at its next +// poll. Everything else is done, also a failure or a boot that completed meanwhile: afterBoot's guest fix step then +// mounts what is still missing, after the boot as before. restarted: zygote was restarted here. +func (m *Manager) earlyGuestFixes(ctx context.Context, inst *avd.Instance, prog *progressor) (done, restarted bool) { + i := inst.Index + profiles, err := m.guestLibFixes(inst) + if err != nil { + return true, false // afterBoot's step says why + } + state, ok := earlyBootState(m.shell(ctx, i, earlyBootProbe, statusAdbTimeout)) + switch { + case !ok: + return false, false + case state.booted: + return true, false + case !state.zygote: + return false, false + } + remote, ok := m.pushGuestScript(ctx, i, guest.LibFix) + if !ok { + return true, false + } + prog.emit("vulkan_fix", fmt.Sprintf("实例 %d 开机过程中挂客体库补丁(Vulkan 驱动、ARM 转译层)", i), 0) + g := m.guestIO(ctx, i) + restarted, _ = m.mountGuestFixes(ctx, i, guestFixIO{ + guestIO: g, + remote: remote, + library: func(p guestpatch.Profile) string { return m.patchedGuestLibrary(ctx, i, p) }, + waitBack: func(uiBefore string) bool { + // The boot may have completed right before the restart: sys.boot_completed then stays 1 while the + // framework comes back, and the boot wait would go on at once. Otherwise it waits for the restarted one. + if m.bootCompleted(ctx, i, statusAdbTimeout, false) { + return m.waitFrameworkBack(ctx, g, uiBefore, frameworkBackTimeout) + } + return true + }, + early: true, + }, profiles, false, time.Time{}) + return true, restarted +} + +// earlyBootProbe prints the zygote service state and sys.boot_completed (no root needed). +const earlyBootProbe = `echo "zygote=$(getprop init.svc.zygote) booted=$(getprop sys.boot_completed)"` + +// bootState is earlyBootProbe's answer: zygote runs, the boot has completed. +type bootState struct{ zygote, booted bool } + +// earlyBootState reads earlyBootProbe's output; ok=false when adb failed or the line is missing. +func earlyBootState(res adb.Result) (bootState, bool) { + if res.Code != 0 { + return bootState{}, false + } + for _, line := range outputLines(res.Out) { + f := strings.Fields(line) + if len(f) == 0 || !strings.HasPrefix(f[0], "zygote=") { + continue + } + var s bootState + s.zygote = f[0] == "zygote=running" + s.booted = len(f) > 1 && f[1] == "booted=1" + return s, true + } + return bootState{}, false +} + // guestFixIO is what mountGuestFixes works through: the instance's adb in Manager, a fake guest in tests. type guestFixIO struct { guestIO remote string // libfix.sh in the guest (pushed already) library func(p guestpatch.Profile) string // the host's patched copy (patchedGuestLibrary); "" when it cannot be had waitBack func(uiBefore string) bool // after the zygote restart (waitFrameworkBack) + early bool // during the boot (earlyGuestFixes): the messages say so } // report is one log line: a warning or an info. @@ -293,6 +365,11 @@ func (m *Manager) mountGuestFixes(ctx context.Context, i int, fx guestFixIO, pro if ctx.Err() != nil { return true, false // canceled while waiting: zygote did restart, but nothing is known about the framework } + if fx.early && back { + m.Log.Infof("实例 %d 客体库补丁已在开机过程中挂上(%s),zygote 已重启,接着等安卓开机完成", i, + strings.Join(mounted, "、")) + return true, false + } how := "超时未回来" if back { how = "已回来" diff --git a/internal/emu/guestfix_test.go b/internal/emu/guestfix_test.go index 8d94a78..de7bbd9 100644 --- a/internal/emu/guestfix_test.go +++ b/internal/emu/guestfix_test.go @@ -420,6 +420,7 @@ func TestMountGuestFixes(t *testing.T) { cases := []struct { name string owed bool + early bool // during the boot (earlyGuestFixes) installing bool // an install holds the guest lock from the start installDuringPush bool // an install takes it while a library is uploaded pushFail []string @@ -436,6 +437,12 @@ func TestMountGuestFixes(t *testing.T) { {name: "launched, cached in the guest", fixes: []string{mountedBoth}, commands: []string{"pidof", "fix", "wait"}, runs: [][]string{bothRuns}, locked: []bool{true}, restarted: true, level: logx.Info, has: []string{"客体库补丁已挂上(" + both + "),zygote 重启后系统界面已回来"}}, + {name: "launched early, cached in the guest", early: true, fixes: []string{mountedBoth}, + commands: []string{"pidof", "fix", "wait"}, runs: [][]string{bothRuns}, locked: []bool{true}, restarted: true, + level: logx.Info, has: []string{"客体库补丁已在开机过程中挂上(" + both + "),zygote 已重启,接着等安卓开机完成"}}, + {name: "launched early, nothing to mount", early: true, fixes: []string{gfLine(gfVK, "IMAGE_PATCHED") + + gfLine(gfNDK, "ABSENT") + "UNCHANGED\r\n"}, commands: []string{"pidof", "fix"}, runs: [][]string{bothRuns}, + locked: []bool{true}, level: logx.Info, has: []string{"已是补丁版,不用挂"}}, {name: "launched, both uploaded", fixes: []string{uploadBoth, mountedBoth}, commands: []string{"pidof", "fix", "push", "push", "fix", "wait"}, runs: [][]string{bothRuns, bothRuns}, pushes: []string{gfVK.UploadPath, gfNDK.UploadPath}, locked: []bool{true, true}, restarted: true, @@ -532,7 +539,9 @@ func TestMountGuestFixes(t *testing.T) { f.onPush = takeInstall } - restarted, pending := m.mountGuestFixes(context.Background(), 3, f.io(), gfAll, c.owed, until) + fx := f.io() + fx.early = c.early + restarted, pending := m.mountGuestFixes(context.Background(), 3, fx, gfAll, c.owed, until) install.Unlock() if restarted != c.restarted || pending != c.pending { diff --git a/internal/emu/ime.go b/internal/emu/ime.go index 8adcb53..6e9dfa4 100644 --- a/internal/emu/ime.go +++ b/internal/emu/ime.go @@ -392,26 +392,3 @@ func installFailure(res adb.Result) string { } return fmt.Sprintf("退出码 %d", res.Code) } - -// recheckHostIme runs after the zygote restart of the Vulkan fix: the default was the host IME at the end of the IME -// step; when it is not any more, set it back (AUTO-MAS _recheck_host_ime; the launcher only reports it). -func (m *Manager) recheckHostIme(i int, g guestIO) { - current, ok := defaultImeFrom(g.shell(hostImeReadDefault, 0).Out) - if ok && current == hostImeID { - return - } - if !ok { - m.Log.Warnf("实例 %d 宿主输入法: 重启 zygote 后读不出默认输入法", i) - return - } - restored, rok := defaultImeFrom(g.shell("ime set "+hostImeID+" >/dev/null; "+hostImeReadDefault, 0).Out) - if rok && restored == hostImeID { - m.Log.Infof("实例 %d 宿主输入法: 重启 zygote 后默认变成了 %s,已设回", i, orText(current, "没设")) - return - } - now := "读不出" - if rok { - now = orText(restored, "没设") - } - m.Log.Warnf("实例 %d 宿主输入法: 重启 zygote 后默认变成了 %s,没能设回(现在 %s)", i, orText(current, "没设"), now) -} diff --git a/internal/emu/ime_test.go b/internal/emu/ime_test.go index 772fad3..bcefb42 100644 --- a/internal/emu/ime_test.go +++ b/internal/emu/ime_test.go @@ -529,46 +529,6 @@ func TestRunHostImeProbeFails(t *testing.T) { } } -func TestRecheckHostIme(t *testing.T) { - cases := []struct { - name string - def string // before - fails bool // ime set does not take - after string - level logx.Level - msg string - }{ - {"still ours", hostImeID, false, hostImeID, -1, ""}, - {"lost: set back", gboard, false, hostImeID, logx.Info, - "实例 4 宿主输入法: 重启 zygote 后默认变成了 " + gboard + ",已设回"}, - {"lost: cannot set back", "", true, "", logx.Warn, - "实例 4 宿主输入法: 重启 zygote 后默认变成了 没设,没能设回(现在 没设)"}, - } - for _, c := range cases { - g := fakeImeGuest{imeState: imeState{installed: 3, build: releaseSigner, enabled: true, def: c.def, marker: true}, - imeFaults: imeFaults{setFails: c.fails}} - m, lines := recordingManager() - m.recheckHostIme(4, g.io()) - if g.def != c.after || len(g.unknown) > 0 { - t.Errorf("%s: default %q (unknown %q), want %q", c.name, g.def, g.unknown, c.after) - } - switch { - case c.msg == "" && len(*lines) != 0: - t.Errorf("%s: logged %v", c.name, *lines) - case c.msg != "" && (len(*lines) != 1 || (*lines)[0].level != c.level || (*lines)[0].msg != c.msg): - t.Errorf("%s: log %v, want %v %q", c.name, *lines, c.level, c.msg) - } - } - // settings unreadable: no ime set, a warning - m, lines := recordingManager() - m.recheckHostIme(4, guestIO{shell: func(string, time.Duration) adb.Result { - return adb.Result{Code: -1, Out: "adb shell 超时(30 秒)"} - }}) - if len(*lines) != 1 || (*lines)[0].msg != "实例 4 宿主输入法: 重启 zygote 后读不出默认输入法" { - t.Errorf("unreadable: %v", *lines) - } -} - // TestSetupHostImeSkips: the quiet and the warned skips before the guest is touched (no adb here at all). func TestSetupHostImeSkips(t *testing.T) { dir := testutil.TempDir(t) diff --git a/internal/emu/pixellauncher.go b/internal/emu/pixellauncher.go new file mode 100644 index 0000000..b89bd88 --- /dev/null +++ b/internal/emu/pixellauncher.go @@ -0,0 +1,85 @@ +package emu + +import ( + "context" + "fmt" + "strings" + "time" + + "modavd/internal/adb" +) + +// pixelLauncherRemoveScript removes the stock launcher for user 0 (run as root) and waits until the package manager +// has written that to disk: it only schedules the write of package-restrictions.xml (10 s later), so a framework +// restart or a power cut before then brings the launcher back at the next boot. 11-15: the first-boot setup removed +// it 2.6 s before the zygote restart of the guest library fixes, and every instance created since kept the stock +// launcher, which draws its taskbar over every home app once the display counts as a tablet (1080p, 280 dpi). +// Prints SAVED once the file's modification time changed, UNSAVED after 20 s without; exits 1 when pm fails. The time +// is taken after pm returns: a write that starts later has the removal in it, one that ran during pm may not have. +const pixelLauncherRemoveScript = `f=/data/system/users/0/package-restrictions.xml; ` + + `pm uninstall --user 0 ` + pixelLauncherPackage + ` || exit 1; b=$(stat -c %y $f 2>/dev/null); n=0; ` + + `while [ $n -lt 40 ]; do sleep 0.5; [ "$(stat -c %y $f 2>/dev/null)" != "$b" ] && { echo SAVED; exit 0; }; ` + + `n=$((n+1)); done; echo UNSAVED` + +// pixelLauncherRemoveTimeout covers the script's 20 s wait and pm itself. +const pixelLauncherRemoveTimeout = 45 * time.Second + +// pixelLauncherListed: `pm list packages --user 0 ` lists the stock launcher itself (the filter also matches +// longer names). +func pixelLauncherListed(out string) bool { + for _, line := range outputLines(out) { + if strings.TrimSpace(line) == "package:"+pixelLauncherPackage { + return true + } + } + return false +} + +// removePixelLauncher runs pixelLauncherRemoveScript. ok: pm removed it; saved: the removal is on disk. out is the +// script's output for the messages. +func (m *Manager) removePixelLauncher(ctx context.Context, i int) (ok, saved bool, out string) { + res := m.su(ctx, i, pixelLauncherRemoveScript, pixelLauncherRemoveTimeout) + ok, saved = pixelLauncherRemoveResult(res) + return ok, saved, res.Out +} + +// pixelLauncherRemoveResult reads pixelLauncherRemoveScript's result: ok when it exited 0 with SAVED or UNSAVED last. +func pixelLauncherRemoveResult(res adb.Result) (ok, saved bool) { + last := lastNonEmptyLine(res.Out) + ok = res.Code == 0 && (last == "SAVED" || last == "UNSAVED") + return ok, ok && last == "SAVED" +} + +// pixelLauncherRemoveReport is the log line of removePixelLauncher's result (what: what was done, for the message). +func pixelLauncherRemoveReport(i int, what string, ok, saved bool, out string) report { + switch { + case !ok: + return report{true, fmt.Sprintf("实例 %d %s失败,下次开机再试: %s", i, what, orText(tail(strings.TrimSpace(out), 200), "无输出"))} + case !saved: + return report{true, fmt.Sprintf("实例 %d %s:已卸载,但 20 秒内没见系统写盘,下次开机会再检查", i, what)} + } + return report{false, fmt.Sprintf("实例 %d %s:已卸载并写盘", i, what)} +} + +// ensurePixelLauncherRemoved removes the stock launcher again when an instance set up with the light launcher still +// has it for user 0 (see pixelLauncherRemoveScript for how that happened). Instances whose first-boot setup kept the +// stock launcher (meta launcher "pixel") are left alone. Every boot after the first; failures are only logged. +func (m *Manager) ensurePixelLauncherRemoved(ctx context.Context, i int) { + meta := m.Root.InstanceMeta(i) + if meta == nil { + return + } + if launcher, _ := meta.Get("launcher").Str(); launcher != lightLauncherPackage { + return + } + res := m.shell(ctx, i, "pm list packages --user 0 "+pixelLauncherPackage, 0) + if res.Code != 0 { + m.Log.Debugf("实例 %d 读不出原生桌面在不在: %s", i, tail(res.Out, 200)) + return + } + if !pixelLauncherListed(res.Out) { + return + } + ok, saved, out := m.removePixelLauncher(ctx, i) + m.logReport(pixelLauncherRemoveReport(i, "原生 Pixel 桌面又出现了(之前的卸载没写进磁盘),重新卸载", ok, saved, out)) +} diff --git a/internal/emu/qemu_errors_test.go b/internal/emu/qemu_errors_test.go index 63714a7..6ab2ff6 100644 --- a/internal/emu/qemu_errors_test.go +++ b/internal/emu/qemu_errors_test.go @@ -75,7 +75,7 @@ func TestQemuWaitsPropagateUnsupported(t *testing.T) { "waitQemuGone": func() error { return m.waitQemuGone(ctx, 1) }, "stopAndWait": func() error { return m.stopAndWait(ctx, 1) }, "waitBoot": func() error { - _, err := m.waitBoot(ctx, 1, 0, nil, nil, "", nil, nil, nil) + _, err := m.waitBoot(ctx, 1, 0, nil, nil, "", nil, nil, nil, nil) return err }, } { diff --git a/internal/emu/start.go b/internal/emu/start.go index 78fdbb2..5aaad95 100644 --- a/internal/emu/start.go +++ b/internal/emu/start.go @@ -28,10 +28,10 @@ const ( bootProgressFrom = 100 bootProgressTo = 600 progressBooted = 610 + progressVulkanFix = 615 progressTune = 620 progressIme = 700 progressFirstBoot = 800 - progressVulkanFix = 900 bootReportInterval = 5 * time.Second // how long a start waits for a boot lock nobody should hold (the start lock is ours) bootLockWait = 10 * time.Second @@ -216,6 +216,7 @@ func (m *Manager) start(ctx context.Context, inst *avd.Instance, opt StartOption var marker *setupMarker var mode bootSetup var qemu host.Process + earlyRestarted := false // earlyGuestFixes restarted zygote during the boot // owed: errors from here on leave a boot whose setup the next start finishes owed := func(err error) error { if marker == nil { @@ -355,7 +356,17 @@ func (m *Manager) start(ctx context.Context, inst *avd.Instance, opt StartOption prog.emit("launch", fmt.Sprintf("实例 %d 的模拟器已拉起(pid %d),等安卓开机", i, p.PID), progressLaunch) } - qemu, err = m.waitBoot(ctx, i, timeout, emulator, adopted, res.LogFile, prog, onQemu, &stopSeen) + // A launched boot of an instance set up before mounts the guest library fixes under the boot animation; the + // first boot of a new one leaves them to afterBoot (system_server's very first start is not cut short). + var early func() bool + if mode == setupLaunched && m.initialized(i) { + early = func() bool { + done, restarted := m.earlyGuestFixes(ctx, inst, prog) + earlyRestarted = earlyRestarted || restarted + return done + } + } + qemu, err = m.waitBoot(ctx, i, timeout, emulator, adopted, res.LogFile, prog, onQemu, &stopSeen, early) bootLock.Unlock() if err == nil { break @@ -400,7 +411,7 @@ func (m *Manager) start(ctx context.Context, inst *avd.Instance, opt StartOption return nil, m.startFailure(ctx, i, owed(err), unknownState) } res.FirstBoot = ab.FirstBoot - res.ZygoteRestarted = ab.ZygoteRestarted + res.ZygoteRestarted = ab.ZygoteRestarted || earlyRestarted res.LogcatFile = ab.LogcatFile res.HostIME = ab.HostIME @@ -489,9 +500,10 @@ func pollUntil(ctx context.Context, limit, every time.Duration, ok func() bool) // one may have missed a server restart and is. Fails when the deadline passes, when emulator.exe has exited while // no qemu is there, or when the adopted qemu is gone. onQemu, when set, is told the qemu process each time the scan // finds it (the setup marker records it the first time). stopSeen, when set and a qemu was adopted, is set to true -// once a stop of this instance is seen holding its lock (relaunchAfterAdoptedGone). +// once a stop of this instance is seen holding its lock (relaunchAfterAdoptedGone). early, when set, runs each time +// adb answers before the boot has completed, until it returns true (earlyGuestFixes). func (m *Manager) waitBoot(ctx context.Context, i int, timeout time.Duration, emulator, adopted *host.Process, - logPath string, prog *progressor, onQemu func(host.Process), stopSeen *bool) (host.Process, error) { + logPath string, prog *progressor, onQemu func(host.Process), stopSeen *bool, early func() bool) (host.Process, error) { launched := emulator != nil started := time.Now() deadline := started.Add(timeout) @@ -510,8 +522,15 @@ func (m *Manager) waitBoot(ctx context.Context, i int, timeout time.Duration, em // before the getprop, which may take seconds: a cancel then must find qemu already recorded onQemu(p) } - if found && m.bootCompleted(ctx, i, statusAdbTimeout, !launched) { - return p, nil + if found { + done, answered := waitBootProbe(m, ctx, i, statusAdbTimeout, !launched) + if done { + return p, nil + } + if answered && early != nil && early() { + early = nil + continue // ask getprop again right away + } } if err := ctxErr(ctx); err != nil { return host.Process{}, err @@ -592,6 +611,9 @@ var emulatorReportedBoot = func(r *root.Root, i int, qemu host.Process) bool { // waitBootFindQemu is the boot wait's process scan (a variable for the tests). var waitBootFindQemu = func(m *Manager, i int) (host.Process, bool, error) { return m.findQemu(i) } +// waitBootProbe is the boot wait's getprop (a variable for the tests). +var waitBootProbe = (*Manager).bootProbe + // registerBooted is adb's RegisterBooted for instance i (a variable for the tests). var registerBooted = func(ctx context.Context, m *Manager, i int) bool { return m.ADB.RegisterBooted(ctx, m.Root.Ports().Serial(i)) diff --git a/internal/emu/start_test.go b/internal/emu/start_test.go index c6b8514..9d97743 100644 --- a/internal/emu/start_test.go +++ b/internal/emu/start_test.go @@ -408,8 +408,8 @@ func TestProgressor(t *testing.T) { } func TestProgressOrder(t *testing.T) { - order := []int{progressLaunch, bootProgressFrom, bootProgressTo, progressBooted, progressTune, progressIme, - progressFirstBoot, progressVulkanFix, progressTotal} + order := []int{progressLaunch, bootProgressFrom, bootProgressTo, progressBooted, progressVulkanFix, progressTune, + progressIme, progressFirstBoot, progressTotal} for k := 1; k < len(order); k++ { if order[k] <= order[k-1] { t.Errorf("progress constants out of order at %d: %v", k, order) @@ -554,11 +554,11 @@ func TestWaitBootAdoptedGone(t *testing.T) { q, kill := startSleeper(t) kill() stopSeen := false - _, err := m.waitBoot(context.Background(), 3, 10*time.Second, nil, &q, "", &progressor{}, nil, &stopSeen) + _, err := m.waitBoot(context.Background(), 3, 10*time.Second, nil, &q, "", &progressor{}, nil, &stopSeen, nil) if !errors.Is(err, errAdoptedGone) || errs.CodeOf(err) != errs.LaunchFailed || stopSeen { t.Fatalf("adopted qemu gone: %v, stopSeen %v", err, stopSeen) } - _, err = m.waitBoot(context.Background(), 3, 10*time.Second, &q, nil, "", &progressor{}, nil, &stopSeen) + _, err = m.waitBoot(context.Background(), 3, 10*time.Second, &q, nil, "", &progressor{}, nil, &stopSeen, nil) if errors.Is(err, errAdoptedGone) || errs.CodeOf(err) != errs.LaunchFailed { t.Fatalf("launched emulator gone: %v", err) } @@ -570,11 +570,11 @@ func TestWaitBootAdoptedGone(t *testing.T) { defer l.Unlock() // a launched emulator.exe that still runs: the wait polls (and times out) without looking at the stop lock live, _ := startSleeper(t) - _, err = m.waitBoot(context.Background(), 3, 1500*time.Millisecond, &live, nil, "", &progressor{}, nil, &stopSeen) + _, err = m.waitBoot(context.Background(), 3, 1500*time.Millisecond, &live, nil, "", &progressor{}, nil, &stopSeen, nil) if errs.CodeOf(err) != errs.Timeout || stopSeen { t.Fatalf("launched emulator during a stop: %v, stopSeen %v", err, stopSeen) } - _, err = m.waitBoot(context.Background(), 3, 10*time.Second, nil, &q, "", &progressor{}, nil, &stopSeen) + _, err = m.waitBoot(context.Background(), 3, 10*time.Second, nil, &q, "", &progressor{}, nil, &stopSeen, nil) if !errors.Is(err, errAdoptedGone) || !stopSeen { t.Fatalf("adopted qemu gone during a stop: %v, stopSeen %v", err, stopSeen) }