diff --git a/internal/adb/adb.go b/internal/adb/adb.go index 925743e..c08c31c 100644 --- a/internal/adb/adb.go +++ b/internal/adb/adb.go @@ -148,7 +148,7 @@ 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 case-insensitively (Windows). +// replaced using the host's environment-key rules. func (c *Client) Env() []string { sdk := c.Root.RuntimeSDK() set := map[string]string{ @@ -160,20 +160,20 @@ func (c *Client) Env() []string { for k, v := range root.NoLocalScanEnv { set[k] = v } - return mergeEnv(os.Environ(), set) + return mergeEnv(serverBaseEnv(os.Environ()), set) } -// mergeEnv returns base with the keys of set replaced in place (compared case-insensitively, as Windows does; later +// mergeEnv returns base with the keys of set replaced in place (case-insensitively on Windows; later // duplicates dropped) and the keys base lacks appended in sorted order. func mergeEnv(base []string, set map[string]string) []string { byUpper := make(map[string]string, len(set)) // upper-case key -> key as given in set for k := range set { - byUpper[strings.ToUpper(k)] = k + byUpper[normalizeEnvKey(k)] = k } out := make([]string, 0, len(base)+len(set)) done := map[string]bool{} for _, kv := range base { - up := strings.ToUpper(envKey(kv)) + up := normalizeEnvKey(envKey(kv)) k, ok := byUpper[up] if !ok { out = append(out, kv) @@ -186,7 +186,7 @@ func mergeEnv(base []string, set map[string]string) []string { } keys := make([]string, 0, len(set)) for k := range set { - if !done[strings.ToUpper(k)] { + if !done[normalizeEnvKey(k)] { keys = append(keys, k) } } @@ -224,7 +224,7 @@ var ( func (c *Client) portConflict(k serverKind, port int, pid uint32, name string) *errs.Error { return errs.Newf(errs.PortConflict, - "魔改 AVD 的%s adb 端口 %d 被 %s(PID %d)占用,它不是 adb。请在 %s 里把 %s 改成一个没被占用的端口"+ + "魔改 AVD 的%s adb 端口 %d 被 %s(PID %d)占用,"+serverConflictReason+"。请在 %s 里把 %s 改成一个没被占用的端口"+ "(不能用 5037 和各实例的控制台 / adb / gRPC 端口,也不能和 %s 相同),或者先关掉占用它的程序", k.label, port, name, pid, c.Root.MetadataPath(), k.key, k.otherKey) } @@ -250,6 +250,19 @@ func listening(port int) (bool, error) { return up, err } +// serverListening also verifies the native server's ownership. A busy port alone +// must never authorize an adb client or an emulator registration. +func (c *Client) serverListening(k serverKind, port int) (bool, error) { + up, pid, name, err := listener(port) + if err != nil || !up { + return up, err + } + if !serverBelongsTo(c.Root, port, pid, name) { + return false, c.portConflict(k, port, pid, name) + } + return true, nil +} + func isAdbName(name string) bool { n := strings.ToLower(name) want := strings.ToLower(root.CurrentPlatform().ADB) @@ -276,7 +289,8 @@ func sleepCtx(ctx context.Context, d time.Duration) bool { // the port, and waits up to timeout until it listens (polling the listener table every 100 ms, not waiting for the // start-server client, which takes ~2.1 s; a client that exits non-zero while the port is still closed = failed). // Returns whether it listens. A listener whose process name is not adb.exe is errs.PortConflict with the AUTO-MAS -// message (change adbServerPort in mas-avd.json); an unreadable owner name counts as adb. A root whose mas-avd.json +// message (change adbServerPort in mas-avd.json). Darwin also requires the native executable and root launch +// environment; Windows retains its original name check, including unreadable names. A root whose mas-avd.json // server ports root.CheckAdbPorts refuses gets that errs.InvalidArgument error, before anything is looked at or started. func (c *Client) EnsureServer(ctx context.Context, timeout time.Duration) (bool, error) { return c.ensure(ctx, privateServer, c.Root.AdbServerPort(), timeout) @@ -294,19 +308,19 @@ func (c *Client) ensure(ctx context.Context, k serverKind, port int, timeout tim if err := c.Root.CheckAdbPorts(); err != nil { return false, err } - up, pid, name, err := listener(port) + up, err := c.serverListening(k, port) if err != nil { return false, err } if up { - if name != "" && !isAdbName(name) { - return false, c.portConflict(k, port, pid, name) - } return true, nil } if err := ctx.Err(); err != nil { return false, canceled(ctx) } + if err := prepareServerRoot(c.Root); err != nil { + return false, err + } 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 @@ -318,20 +332,20 @@ 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 waitListening(ctx, port, timeout, client) + return c.waitListening(ctx, k, port, timeout, client) } // waitListening polls until port listens, at most timeout (AUTO-MAS _wait_listening). It does not wait for the // start-server client to exit, but gives up early when that client has failed. -func waitListening(ctx context.Context, port int, timeout time.Duration, client host.Process) (bool, error) { +func (c *Client) waitListening(ctx context.Context, k serverKind, port int, timeout time.Duration, client host.Process) (bool, error) { deadline := time.Now().Add(timeout) for { - up, err := listening(port) + up, err := c.startedServerListening(k, port) if err != nil || up { return up, err } if clientFailed(client) { - return listening(port) + return c.startedServerListening(k, port) } if !time.Now().Before(deadline) { return false, nil @@ -604,6 +618,9 @@ func (c *Client) Reregister(ctx context.Context, serial string) bool { if c.GetState(ctx, serial, reregisterStateTimeout) == "device" { return true // another caller just registered it } + if !registrationStillOwned(c, console, qemu) { + return false + } c.Log.Warnf("私有 adb server(端口 %d)上没有 %s,按 adb 端口 %d 重新登记", serverPort, serial, console+1) started := time.Now() if err := Register(ctx, serverPort, console+1, registerTimeout); err != nil { @@ -664,14 +681,11 @@ func (c *Client) PrepareForClose(ctx context.Context, console int, timeout time. started := time.Now() port := c.Root.AdbServerPort() serial := "emulator-" + strconv.Itoa(console) - up, pid, name, err := listener(port) + up, err := c.serverListening(privateServer, port) if err != nil { return err.Error() } if up { - if name != "" && !isAdbName(name) { - return c.portConflict(privateServer, port, pid, name).Error() - } res := c.runOnce(ctx, RunOptions{Serial: serial, Timeout: timeout, TotalTimeout: true}, []string{"get-state"}) if res.Code == 0 || !DeviceMissing(serial, res.Out) { return "" @@ -696,7 +710,8 @@ func (c *Client) registerForClose(ctx context.Context, console int, started time server string) string { port := c.Root.AdbServerPort() serial := "emulator-" + strconv.Itoa(console) - if _, ok := qemuOnAdbPort(c.Root, console); !ok { + qemu, ok := qemuOnAdbPort(c.Root, console) + if !ok || !registrationStillOwned(c, console, qemu) { return fmt.Sprintf("adb 端口 %d 上不是这台实例的模拟器,不替它登记", console+1) } remaining := timeout - time.Since(started) @@ -755,6 +770,9 @@ func QemuOnAdbPort(r *root.Root, console int) (host.Process, bool) { if err != nil || !qemuArgsMatch(cmdline, console, root.AvdName(i)) || !QemuBelongsTo(r, p, i) { return host.Process{}, false } + if !qemuListenerUnchanged(p, console+1) { + return host.Process{}, false + } return p, true } diff --git a/internal/adb/console.go b/internal/adb/console.go index 3a03520..6a426eb 100644 --- a/internal/adb/console.go +++ b/internal/adb/console.go @@ -20,13 +20,10 @@ const defaultConsoleTimeout = 10 * time.Second const consoleTokenFile = ".emulator_console_auth_token" -// ConsoleToken is the console auth token the emulator writes at first start: %USERPROFILE%\.emulator_console_auth_token, +// ConsoleToken is the console auth token the emulator writes in the native user's home directory at first start, // trimmed; "" when missing (then no auth line is sent). func ConsoleToken() string { - home := os.Getenv("USERPROFILE") - if home == "" { - home, _ = os.UserHomeDir() - } + home := consoleHome() if home == "" { return "" } diff --git a/internal/adb/env_other.go b/internal/adb/env_other.go new file mode 100644 index 0000000..61fffa9 --- /dev/null +++ b/internal/adb/env_other.go @@ -0,0 +1,12 @@ +//go:build !windows + +package adb + +import "os" + +func normalizeEnvKey(key string) string { return key } + +func consoleHome() string { + home, _ := os.UserHomeDir() + return home +} diff --git a/internal/adb/env_windows.go b/internal/adb/env_windows.go new file mode 100644 index 0000000..ee6bcf9 --- /dev/null +++ b/internal/adb/env_windows.go @@ -0,0 +1,18 @@ +//go:build windows + +package adb + +import ( + "os" + "strings" +) + +func normalizeEnvKey(key string) string { return strings.ToUpper(key) } + +func consoleHome() string { + home := os.Getenv("USERPROFILE") + if home == "" { + home, _ = os.UserHomeDir() + } + return home +} diff --git a/internal/adb/identity.go b/internal/adb/identity.go index 38a17d4..c628285 100644 --- a/internal/adb/identity.go +++ b/internal/adb/identity.go @@ -17,6 +17,9 @@ import ( "modavd/internal/root" ) +// OwnershipSupported reports whether QemuBelongsTo can verify native process ownership. +const OwnershipSupported = true + // QemuBelongsTo reports whether qemu process p, already matched to instance i by its "-avd mas_" and "-ports // ,..." arguments, is instance i of root r. Every root names its instances mas_0, mas_1 ... and uses the same // port plan, so those arguments also match the same-numbered instance of any other root on the machine (a test root diff --git a/internal/adb/identity_darwin.go b/internal/adb/identity_darwin.go new file mode 100644 index 0000000..9baf0f8 --- /dev/null +++ b/internal/adb/identity_darwin.go @@ -0,0 +1,200 @@ +//go:build darwin && cgo + +package adb + +import ( + "errors" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "syscall" + + "modavd/internal/host" + "modavd/internal/root" +) + +const OwnershipSupported = true + +// EmulatorDiscoveryDir follows the pinned core's ConfigDirs.cpp: HOME's cache +// directory, or the emulator user directory when HOME is empty. TMPDIR and +// ANDROID_USER_HOME do not select the discovery directory on Darwin. +func EmulatorDiscoveryDir() string { return emulatorDiscoveryDir(os.Getenv) } + +func emulatorDiscoveryDir(getenv func(string) string) string { + if home := getenv("HOME"); home != "" { + return filepath.Join(home, "Library", "Caches", "TemporaryItems", "avd", "running") + } + return filepath.Join(emulatorUserDir(getenv), "avd", "running") +} + +func emulatorUserDir(getenv func(string) string) string { + if home := getenv("ANDROID_EMULATOR_HOME"); home != "" { + return home + } + for _, key := range []string{"ANDROID_PREFS_ROOT", "ANDROID_SDK_HOME"} { + if home := getenv(key); home != "" { + if sub := filepath.Join(home, ".android"); isDir(sub) { + return sub + } + return home + } + } + home, _ := os.UserHomeDir() + if home == "" { + home = os.TempDir() + if home == "" { + home = "/tmp" + } + } + return filepath.Join(home, ".android") +} + +func isDir(path string) bool { + st, err := os.Stat(path) + return err == nil && st.IsDir() +} + +// samePath uses file identity, honoring both symlinks and the volume's actual +// case rules. A missing or unreadable path cannot establish ownership. +func samePath(a, b string) bool { + if a == "" || b == "" { + return false + } + as, err := os.Stat(a) + if err != nil { + return false + } + bs, err := os.Stat(b) + return err == nil && os.SameFile(as, bs) +} + +func instanceDir(r *root.Root, i int) string { + return filepath.Join(r.AvdHome(), root.AvdName(i)+".avd") +} + +func processUnchanged(p host.Process) bool { + if p.PID == 0 || p.Created == 0 { + return false + } + current, ok := host.FindProcess(p.PID) + return ok && current.Created == p.Created +} + +// readIdentityFile never creates, locks or modifies the emulator's files. Read +// through one handle and reject symlinks, special files, replacement and writes +// during the read, rather than accepting mixed evidence from different files. +func readIdentityFile(path string, limit int64) ([]byte, os.FileInfo, error) { + fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_CLOEXEC|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0) + if err != nil { + return nil, nil, err + } + f := os.NewFile(uintptr(fd), path) + defer f.Close() + before, err := f.Stat() + if err != nil { + return nil, nil, err + } + if !before.Mode().IsRegular() || before.Size() > limit { + return nil, nil, errors.New("无效的模拟器归属文件") + } + b, err := io.ReadAll(io.LimitReader(f, limit+1)) + if err != nil { + return nil, nil, err + } + after, err := f.Stat() + if err != nil { + return nil, nil, err + } + current, err := os.Lstat(path) + if err != nil { + return nil, nil, err + } + if int64(len(b)) != before.Size() || !os.SameFile(before, after) || !os.SameFile(before, current) || + before.Size() != after.Size() || before.Size() != current.Size() || + !before.ModTime().Equal(after.ModTime()) || !before.ModTime().Equal(current.ModTime()) { + return nil, nil, errors.New("模拟器归属文件在读取时变化") + } + return b, before, nil +} + +// runningAvdDir permits the startup lock fallback only when the runtime file is +// absent or belongs to an older incarnation of this PID. Current but malformed, +// conflicting or unreadable metadata is never a reason to adopt the process. +func runningAvdDir(dir string, p host.Process) (avdDir string, fallback bool) { + path := filepath.Join(dir, "pid_"+strconv.FormatUint(uint64(p.PID), 10)+".ini") + b, st, err := readIdentityFile(path, 1<<20) + if err != nil { + return "", os.IsNotExist(err) + } + if st.ModTime().Before(p.StartedAt()) { + return "", true + } + for _, line := range strings.Split(string(b), "\n") { + key, value, ok := strings.Cut(line, "=") + if !ok || strings.TrimSpace(key) != "avd.dir" { + continue + } + value = strings.TrimSpace(value) + if value == "" || (avdDir != "" && !samePath(avdDir, value)) { + return "", false + } + avdDir = value + } + return avdDir, false +} + +// The pinned POSIX filelock.cpp writes PID + NUL to a mkstemp file, closes it, +// then link()s it to hardware-qemu.ini.lock and unlinks the temporary name. It +// retains no FD or flock. A fresh lock naming this live process identifies its +// instance even when two roots use the same SDK and instance/port arguments. +func runLockHeldBy(avdDir string, p host.Process) bool { + b, st, err := readIdentityFile(filepath.Join(avdDir, "hardware-qemu.ini.lock"), 32) + if err != nil || p.StartedAt().IsZero() || st.ModTime().Before(p.StartedAt()) { + return false + } + pid, err := strconv.ParseUint(strings.TrimRight(string(b), "\x00\r\n \t"), 10, 32) + return err == nil && uint32(pid) == p.PID +} + +// QemuBelongsTo verifies both the native executable and root-specific AVD +// evidence, then checks the process creation identity again. A current runtime +// record naming another root overrides any startup lock or argument match. +func QemuBelongsTo(r *root.Root, p host.Process, i int) bool { + if r == nil || !root.ValidIndex(i) || !processUnchanged(p) { + return false + } + exe, err := p.ImagePath() + if err != nil { + return false + } + platform := root.CurrentPlatform() + qemuDir := filepath.Join(r.RuntimeSDK(), "emulator", "qemu", platform.QemuDir) + if !platform.Supported() || (!samePath(exe, filepath.Join(qemuDir, platform.QEMU)) && + !samePath(exe, filepath.Join(qemuDir, platform.QEMUHeadless))) { + return false + } + env, ok := processEnvironment(p) + if !ok { + return false + } + if home := env["ANDROID_AVD_HOME"]; home != "" && !samePath(home, r.AvdHome()) { + return false + } + dir := emulatorDiscoveryDir(func(key string) string { return env[key] }) + avdDir, fallback := runningAvdDir(dir, p) + target := instanceDir(r, i) + owned := avdDir != "" && samePath(avdDir, target) + if avdDir == "" && fallback { + owned = runLockHeldBy(target, p) + } + return owned && processUnchanged(p) +} + +// processEnvironment uses the host's checked native query. An unreadable or +// changing process cannot establish root ownership. +func processEnvironment(p host.Process) (map[string]string, bool) { + env, err := p.Environment() + return env, err == nil && processUnchanged(p) +} diff --git a/internal/adb/identity_other.go b/internal/adb/identity_other.go index 81f37b7..aa034b6 100644 --- a/internal/adb/identity_other.go +++ b/internal/adb/identity_other.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build !windows && (!darwin || !cgo) package adb @@ -10,6 +10,8 @@ import ( "modavd/internal/root" ) +const OwnershipSupported = false + // QemuBelongsTo is unavailable until the non-Windows process and file-lock // implementation is provided; callers conservatively reject the process. func QemuBelongsTo(*root.Root, host.Process, int) bool { return false } diff --git a/internal/adb/server_identity_darwin.go b/internal/adb/server_identity_darwin.go new file mode 100644 index 0000000..bf393bc --- /dev/null +++ b/internal/adb/server_identity_darwin.go @@ -0,0 +1,82 @@ +//go:build darwin && cgo + +package adb + +import ( + "os" + + "modavd/internal/errs" + "modavd/internal/host" + "modavd/internal/root" +) + +const serverConflictReason = "无法确认它是这个根目录的 adb" + +// Ownership compares the AVD home by filesystem identity. Prepare that empty +// directory before launching a server for a root without any instances yet. +// Existing listeners are checked before this function, so a foreign server +// cannot cause a directory to be created or another server to be started. +func prepareServerRoot(r *root.Root) error { + if err := os.MkdirAll(r.AvdHome(), 0o755); err != nil { + return errs.Wrap(errs.Internal, err, "创建私有 adb 的 AVD 目录失败: "+r.AvdHome()) + } + return nil +} + +// An inherited socket/address can redirect an adb client away from its private +// port despite the emulator's ANDROID_ADB_SERVER_PORT. Keep the native command +// line's -P authoritative for both the private and script servers. +func serverBaseEnv(env []string) []string { + out := make([]string, 0, len(env)) + for _, entry := range env { + switch envKey(entry) { + case "ADB_SERVER_SOCKET", "ANDROID_ADB_SERVER_ADDRESS": + continue + } + out = append(out, entry) + } + return out +} + +func serverBelongsTo(r *root.Root, port int, pid uint32, name string) bool { + if r == nil || !isAdbName(name) { + return false + } + p, ok := host.FindProcess(pid) + if !ok || !isAdbName(p.Name) { + return false + } + exe, err := p.ImagePath() + if err != nil || !samePath(exe, r.AdbExe()) { + return false + } + env, ok := processEnvironment(p) + if !ok || !samePath(env["ANDROID_AVD_HOME"], r.AvdHome()) || + !samePath(env["ANDROID_SDK_ROOT"], r.RuntimeSDK()) { + return false + } + for key, value := range root.NoLocalScanEnv { + if env[key] != value { + return false + } + } + return qemuListenerUnchanged(p, port) +} + +func (c *Client) startedServerListening(k serverKind, port int) (bool, error) { + return c.serverListening(k, port) +} + +func qemuListenerUnchanged(p host.Process, port int) bool { + pid, up, err := host.ListenerPID(port) + return err == nil && up && pid == p.PID && processUnchanged(p) +} + +func registrationStillOwned(c *Client, console int, previous host.Process) bool { + p, ok := QemuOnAdbPort(c.Root, console) + if !ok || p.PID != previous.PID || p.Created != previous.Created { + return false + } + up, err := c.serverListening(privateServer, c.Root.AdbServerPort()) + return err == nil && up +} diff --git a/internal/adb/server_identity_other.go b/internal/adb/server_identity_other.go new file mode 100644 index 0000000..0143024 --- /dev/null +++ b/internal/adb/server_identity_other.go @@ -0,0 +1,28 @@ +//go:build !darwin || !cgo + +package adb + +import ( + "modavd/internal/host" + "modavd/internal/root" +) + +const serverConflictReason = "它不是 adb" + +func prepareServerRoot(*root.Root) error { return nil } + +func serverBaseEnv(env []string) []string { return env } + +// Preserve the native Windows server check, including an unreadable name. +func serverBelongsTo(_ *root.Root, _ int, _ uint32, name string) bool { + return name == "" || isAdbName(name) +} + +// Keep Windows' original startup wait: it observes the listening state after +// spawning adb. Darwin additionally verifies who acquired the port. +func (c *Client) startedServerListening(_ serverKind, port int) (bool, error) { + return listening(port) +} + +func registrationStillOwned(*Client, int, host.Process) bool { return true } +func qemuListenerUnchanged(host.Process, int) bool { return true } diff --git a/internal/emu/gates_other_test.go b/internal/emu/gates_other_test.go index 1b34b03..caa1ca8 100644 --- a/internal/emu/gates_other_test.go +++ b/internal/emu/gates_other_test.go @@ -10,6 +10,7 @@ import ( "sort" "testing" + "modavd/internal/adb" "modavd/internal/avd" "modavd/internal/errs" "modavd/internal/host" @@ -65,6 +66,9 @@ func TestUnsupportedOperationsLeaveRootUntouched(t *testing.T) { } func TestQemuOfRejectsUnsupportedHost(t *testing.T) { + if adb.OwnershipSupported && root.CurrentPlatform().Supported() { + t.Skip("native process ownership is implemented on this host") + } r, err := root.New(testutil.TempDir(t)) if err != nil { t.Fatal(err) @@ -76,6 +80,9 @@ func TestQemuOfRejectsUnsupportedHost(t *testing.T) { } func TestQemuScanRejectsUnsupportedCommandLine(t *testing.T) { + if adb.OwnershipSupported && root.CurrentPlatform().Supported() { + t.Skip("native command-line queries are implemented on this host") + } r, err := root.New(testutil.TempDir(t)) if err != nil { t.Fatal(err) diff --git a/internal/emu/manager.go b/internal/emu/manager.go index 9d7a90b..e8b9d54 100644 --- a/internal/emu/manager.go +++ b/internal/emu/manager.go @@ -183,10 +183,8 @@ func scanQemuOf(roots []*root.Root, indexes []int) (map[int]host.Process, error) } return found, nil } - // Process queries alone do not implement QemuBelongsTo's host ownership - // checks. Keep discovery unsupported until that identity chain is available. - if err := requireWindows(); err != nil { - return nil, err + if !adb.OwnershipSupported || !root.CurrentPlatform().Supported() { + return nil, errs.New(errs.Unsupported, "当前平台尚不支持模拟器进程归属核验") } plans := make([]root.PortPlan, len(roots)) for k, r := range roots { diff --git a/internal/macos/environment_darwin.go b/internal/macos/environment_darwin.go new file mode 100644 index 0000000..7c45d02 --- /dev/null +++ b/internal/macos/environment_darwin.go @@ -0,0 +1,37 @@ +//go:build darwin && cgo + +package macos + +import ( + "bytes" + "errors" + "strings" +) + +// Environment returns the original launch environment from KERN_PROCARGS2. +// Like CommandLine, the native query verifies this process's creation identity +// before and after reading it. Malformed or duplicate entries are ambiguous and +// must not authorize another component to adopt the process. +func (p Process) Environment() (map[string]string, error) { + _, buf, err := p.processArguments() + if err != nil { + return nil, err + } + env := make(map[string]string) + for len(buf) > 0 && buf[0] != 0 { + end := bytes.IndexByte(buf, 0) + if end < 0 { + return nil, errors.New("进程环境不完整") + } + key, value, ok := strings.Cut(string(buf[:end]), "=") + if !ok || key == "" { + return nil, errors.New("进程环境条目无效") + } + if _, duplicate := env[key]; duplicate { + return nil, errors.New("进程环境含重复变量") + } + env[key] = value + buf = buf[end+1:] + } + return env, nil +} diff --git a/internal/macos/proc_darwin.go b/internal/macos/proc_darwin.go index 26b74dd..b5eb889 100644 --- a/internal/macos/proc_darwin.go +++ b/internal/macos/proc_darwin.go @@ -170,50 +170,57 @@ func (p Process) Alive() bool { } func (p Process) CommandLine() ([]string, error) { + args, _, err := p.processArguments() + return args, err +} + +// processArguments shares the native process record between argument and +// environment queries, including creation checks around the PID-based read. +func (p Process) processArguments() ([]string, []byte, error) { if _, err := p.checkedInfo(); err != nil { - return nil, err + return nil, nil, err } var limit C.int if code := C.argument_limit(&limit); code != 0 { - return nil, fmt.Errorf("读不到 kern.argmax: %w", syscall.Errno(code)) + return nil, nil, fmt.Errorf("读不到 kern.argmax: %w", syscall.Errno(code)) } if limit < 4 || limit > 64<<20 { - return nil, fmt.Errorf("无效的 kern.argmax: %d", limit) + return nil, nil, fmt.Errorf("无效的 kern.argmax: %d", limit) } buf := make([]byte, int(limit)) size := C.size_t(len(buf)) code := C.process_args(C.int(p.PID), unsafe.Pointer(&buf[0]), &size) if _, err := p.checkedInfo(); err != nil { - return nil, err + return nil, nil, err } if code != 0 { - return nil, fmt.Errorf("读不到进程 %d 的命令行: %w", p.PID, syscall.Errno(code)) + return nil, nil, fmt.Errorf("读不到进程 %d 的命令行: %w", p.PID, syscall.Errno(code)) } if size < 4 || uint64(size) > uint64(len(buf)) { - return nil, errors.New("进程命令行长度无效") + return nil, nil, errors.New("进程命令行长度无效") } buf = buf[:int(size)] argc := int(binary.NativeEndian.Uint32(buf[:4])) buf = buf[4:] if argc < 1 || argc > len(buf) { - return nil, errors.New("进程参数数量无效") + return nil, nil, errors.New("进程参数数量无效") } // KERN_PROCARGS2: argc, executable path, NUL padding, then argc C strings. end := bytes.IndexByte(buf, 0) if end < 0 { - return nil, errors.New("进程命令行缺少程序路径") + return nil, nil, errors.New("进程命令行缺少程序路径") } buf = bytes.TrimLeft(buf[end:], "\x00") args := make([]string, 0, argc) for range argc { end = bytes.IndexByte(buf, 0) if end < 0 { - return nil, errors.New("进程命令行不完整") + return nil, nil, errors.New("进程命令行不完整") } args = append(args, string(buf[:end])) buf = buf[end+1:] } - return args, nil + return args, buf, nil } func (p Process) ImagePath() (string, error) {