Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 22 additions & 28 deletions internal/emu/afterboot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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",
Expand All @@ -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) },
Expand All @@ -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
Expand All @@ -247,7 +242,6 @@ func (m *Manager) afterBoot(ctx context.Context, inst *avd.Instance, mode bootSe
}
step()
}
r.HostIME = imeDefault
return r, ctxErr(ctx)
}

Expand Down
6 changes: 3 additions & 3 deletions internal/emu/bootadb_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
}
Expand Down
17 changes: 13 additions & 4 deletions internal/emu/createinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
37 changes: 24 additions & 13 deletions internal/emu/createinit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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]
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
104 changes: 104 additions & 0 deletions internal/emu/early_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading
Loading