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
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ modavd [--root <dir>] [--json] [--quiet] [--verbose] <command> [args]
| `disk <i> 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). 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 |
| `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 logs of the manager, the boot watch and the private adb servers (`modavd-ui.log`, `modavd-bootwatch.log`, `adb-server.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 <file or folder>...` | 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 <i> show\|hide`, `install <i> <apk>`, `shell <i> <command>`, `adb <i> -- <args>` | 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") |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ modavd [--root <目录>] [--json] [--quiet] [--verbose] <命令> [参数]
| `disk <i> 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]` | 收集问题信息并打包(只读)。除模拟器日志外,还收管理器和开机守护的日志(`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` 则保留暂存目录并在错误中给出路径 |
| `collect-info [--out-dir D] [--stage-dir D] [--keep-stage] [--no-explorer]` | 收集问题信息并打包(只读)。除模拟器日志外,还收管理器、开机守护和私有 adb server 的日志(`modavd-ui.log`、`modavd-bootwatch.log`、`adb-server.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 <i> show\|hide`、`install <i> <apk>`、`shell <i> <命令>`、`adb <i> -- <参数>` | 窗口、装 APK、客体命令、adb 透传 |
| `update [status\|check\|prepare\|install\|recover] [--automas-version V]` | 更新根目录里的整个 ModAVD 包,按「设置」里的更新源(见「更新 ModAVD」) |
Expand Down
41 changes: 36 additions & 5 deletions internal/adb/adb.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,17 @@ func (g *hookGroup) wait(timeout time.Duration) bool {
}

// Env is the environment for emulator.exe, adb servers and adb clients: ours plus ANDROID_SDK_ROOT and ANDROID_HOME =
// RuntimeSDK, ANDROID_AVD_HOME = AvdHome, ANDROID_ADB_SERVER_PORT = AdbServerPort and root.NoLocalScanEnv. Keys are
// replaced using the host's environment-key rules.
// RuntimeSDK, ANDROID_AVD_HOME = AvdHome, ANDROID_ADB_SERVER_PORT = AdbServerPort, ANDROID_ADB_LOG_PATH =
// ServerLogPath (also for a server an adb client starts by itself, such as the emulator's own adb commands at the end
// of boot) and root.NoLocalScanEnv. Keys are replaced using the host's environment-key rules.
func (c *Client) Env() []string {
sdk := c.Root.RuntimeSDK()
set := map[string]string{
"ANDROID_SDK_ROOT": sdk,
"ANDROID_HOME": sdk,
"ANDROID_AVD_HOME": c.Root.AvdHome(),
"ANDROID_ADB_SERVER_PORT": strconv.Itoa(c.Root.AdbServerPort()),
"ANDROID_ADB_LOG_PATH": ServerLogPath(c.Root),
}
for k, v := range root.NoLocalScanEnv {
set[k] = v
Expand Down Expand Up @@ -315,12 +317,14 @@ func (c *Client) ensure(ctx context.Context, k serverKind, port int, timeout tim
if up {
return true, nil
}
c.reportGoneServer(k, port)
if err := ctx.Err(); err != nil {
return false, canceled(ctx)
}
if err := prepareServerRoot(c.Root); err != nil {
return false, err
}
prepareServerLog(c.Root)
c.Log.Infof("启动魔改 AVD %s adb server(端口 %d)", k.label, port)
// StartOrphan rather than StartDetached: an MCP client ends modavd's whole process tree when it disconnects, and
// while the start-server client (our child for ~2 s) is alive the server it forks is part of that tree. Losing
Expand All @@ -332,7 +336,15 @@ func (c *Client) ensure(ctx context.Context, k serverKind, port int, timeout tim
return false, errs.Wrap(errs.Internal, err, fmt.Sprintf("启动魔改 AVD %s adb server(端口 %d)失败: %s",
k.label, port, errs.As(err).Message))
}
return c.waitListening(ctx, k, port, timeout, client)
up, err = c.waitListening(ctx, k, port, timeout, client)
switch {
case err != nil:
case up:
c.rememberServer(port)
case ctx.Err() == nil:
c.reportServerLog(k, port, timeout)
}
return up, err
}

// waitListening polls until port listens, at most timeout (AUTO-MAS _wait_listening). It does not wait for the
Expand Down Expand Up @@ -591,19 +603,38 @@ func consolePortOf(plan root.PortPlan, serial string) (int, bool) {
// this root) and it is at least 60 s old; then get-state, Register, and up to 5 s of get-state == "device".
// Serialized per serial within this process. Starts OnReregistered on success.
func (c *Client) Reregister(ctx context.Context, serial string) bool {
return c.reregister(ctx, serial, false)
}

// RegisterBooted is Reregister for the boot wait once the emulator has reported Android booted (its
// bootcompleted.ini is newer than its qemu): its one registration, at the end of boot, has happened, so a private
// server that does not know serial now came up only after it or was restarted since, and nobody else will register
// it. The caller holds the boot lock and knows the boot is over, so neither Booting(serial) nor the qemu age apply;
// the private server must be listening already (nothing is started here).
func (c *Client) RegisterBooted(ctx context.Context, serial string) bool {
if c.Root == nil {
return false
}
if up, err := c.serverListening(privateServer, c.Root.AdbServerPort()); err != nil || !up {
return false
}
return c.reregister(ctx, serial, true)
}

func (c *Client) reregister(ctx context.Context, serial string, booted bool) bool {
if c.Root == nil {
return false // no root, no instance of its own (QemuOnAdbPort says so too)
}
console, ok := consolePortOf(c.Root.Ports(), serial)
if !ok || (c.Booting != nil && c.Booting(serial)) {
if !ok || (!booted && c.Booting != nil && c.Booting(serial)) {
return false
}
qemu, ok := qemuOnAdbPort(c.Root, console)
if !ok {
return false
}
startedAt := qemu.StartedAt()
if startedAt.IsZero() || time.Since(startedAt) < reregisterMinQemuAge {
if !booted && (startedAt.IsZero() || time.Since(startedAt) < reregisterMinQemuAge) {
return false
}
lock := serialLock(serial)
Expand Down
173 changes: 173 additions & 0 deletions internal/adb/serverlog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package adb

import (
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"

"modavd/internal/host"
"modavd/internal/root"
)

// The adb servers a root starts write their own log to <root>\logs\adb-server.log (ANDROID_ADB_LOG_PATH in Env), not
// to %TEMP%\adb.log, which every adb server of the Windows user shares. When a server does not come up, or one that
// came up is gone again, its last lines say why: cannot bind, killed by remote request, a fatal check. A log that
// just stops says that something ended the process from outside. Without it, a server that never listened was
// a 20 s wait with no reason, again and again (a user's create waited 300 s for an Android that had booted after 72 s).

// ServerLogName is the adb server log in the root's logs\.
const ServerLogName = "adb-server.log"

const (
// serverLogMax: past this the log moves to .1 before the next server start (a running server keeps it open,
// and then it stays). Every start writes a few lines; this is thousands of starts.
serverLogMax = 2 << 20
// serverLogTailRead and serverLogTailLines bound what ServerLogTail reads and returns.
serverLogTailRead = 64 << 10
serverLogTailLines = 8
serverStartMarker = "--- adb starting"
)

// ServerLogPath is <root>\logs\adb-server.log.
func ServerLogPath(r *root.Root) string { return filepath.Join(r.LogsDir(), ServerLogName) }

// prepareServerLog makes the logs directory (adb cannot open its log without it, and a server that cannot open its
// log exits) and moves a log past serverLogMax to .1.
func prepareServerLog(r *root.Root) {
path := ServerLogPath(r)
_ = os.MkdirAll(filepath.Dir(path), 0o755)
if fi, err := os.Stat(path); err == nil && fi.Size() > serverLogMax {
_ = os.Remove(path + ".1")
_ = os.Rename(path, path+".1")
}
}

// ServerLogTail is the newest server start in the log: its "--- adb starting (pid N) ---" line and the last lines
// after it, at most serverLogTailLines; nil when the log is missing or empty.
func ServerLogTail(r *root.Root) []string {
f, err := root.OpenShared(ServerLogPath(r))
if err != nil {
return nil
}
defer f.Close()
if fi, err := f.Stat(); err == nil && fi.Size() > serverLogTailRead {
if _, err := f.Seek(-serverLogTailRead, io.SeekEnd); err != nil {
return nil
}
}
b, err := io.ReadAll(f)
if err != nil {
return nil
}
text := DecodeOutput(b)
if i := strings.LastIndex(text, serverStartMarker); i >= 0 {
text = text[i:]
}
var lines []string
for _, l := range strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n") {
if l = strings.TrimRight(l, " \t\r"); strings.TrimSpace(l) != "" {
lines = append(lines, l)
}
}
if len(lines) > serverLogTailLines && strings.HasPrefix(lines[0], serverStartMarker) {
lines = append(lines[:1:1], lines[len(lines)-(serverLogTailLines-1):]...)
} else if len(lines) > serverLogTailLines {
lines = lines[len(lines)-serverLogTailLines:]
}
return lines
}

// ServerLogLast is the last line of ServerLogTail, "" when there is none: the one-line reason for a message.
func ServerLogLast(r *root.Root) string {
lines := ServerLogTail(r)
if len(lines) == 0 {
return ""
}
return strings.TrimSpace(lines[len(lines)-1])
}

// startedServer is a server this process started and saw listening.
type startedServer struct {
pid uint32
created uint64
at time.Time
}

var serverState = struct {
sync.Mutex
started map[string]startedServer // by serverKey
reported map[string]string // the log lines last reported, by serverKey
}{started: map[string]startedServer{}, reported: map[string]string{}}

func serverKey(r *root.Root, port int) string {
return strings.ToLower(filepath.Clean(r.Dir)) + "|" + strconv.Itoa(port)
}

// rememberServer records who listens on port now, after this process started a server there.
func (c *Client) rememberServer(port int) {
pid, ok, err := host.ListenerPID(port)
if err != nil || !ok || pid == 0 {
return
}
s := startedServer{pid: pid, at: time.Now()}
if p, found := host.FindProcess(pid); found {
s.created = p.Created
}
serverState.Lock()
serverState.started[serverKey(c.Root, port)] = s
serverState.Unlock()
}

// reportGoneServer: nothing listens on port, but this process saw a server it started listen there. Says whether
// that server has exited or is still there without listening, with the end of the log; forgets it.
func (c *Client) reportGoneServer(k serverKind, port int) {
key := serverKey(c.Root, port)
serverState.Lock()
s, ok := serverState.started[key]
delete(serverState.started, key)
serverState.Unlock()
if !ok {
return
}
p, found := host.FindProcess(s.pid)
what := "已经退出了"
if found && (s.created == 0 || p.Created == s.created) {
what = "还在,但不再监听"
}
c.Log.Warnf("魔改 AVD %s adb server(端口 %d,pid %d,%s 起来的)%s%s", k.label, port, s.pid,
s.at.Format("15:04:05"), what, logTailText(ServerLogTail(c.Root)))
}

// reportServerLog says why a server started for port did not listen within timeout: the end of its log, once per
// distinct content (a server that fails the same way every 20 s is reported once).
func (c *Client) reportServerLog(k serverKind, port int, timeout time.Duration) {
lines := ServerLogTail(c.Root)
sig := ""
if len(lines) > 1 {
sig = strings.Join(lines[1:], "\n") // without the line that names the pid
}
key := serverKey(c.Root, port)
serverState.Lock()
same := len(lines) > 0 && serverState.reported[key] == sig
serverState.reported[key] = sig
serverState.Unlock()
if same {
return
}
c.Log.Warnf("魔改 AVD %s adb server(端口 %d)%.0f 秒内没监听上%s", k.label, port, timeout.Seconds(),
logTailText(lines))
}

// logTailText is "; adb 自己的日志(logs\adb-server.log)最后几行:" and the lines, indented, or a note that the log
// has nothing.
func logTailText(lines []string) string {
if len(lines) == 0 {
return `;adb 自己的日志(logs\` + ServerLogName + `)里没有记录`
}
return fmt.Sprintf(";adb 自己的日志(logs\\%s)最后几行:\n %s", ServerLogName, strings.Join(lines, "\n "))
}
Loading
Loading