package staros import ( "bytes" "context" "encoding/base64" "encoding/binary" "errors" "io/ioutil" "os" "path/filepath" "runtime" "strings" "testing" "time" "unicode/utf16" ) func testCommandArgs(script string) (string, []string) { if runtime.GOOS == "windows" { return "cmd.exe", []string{"/c", script} } return "sh", []string{"-c", script} } func testWindowsPowerShellArgs(script string) (string, []string) { utf16Script := utf16.Encode([]rune(script)) encoded := make([]byte, len(utf16Script)*2) for i, r := range utf16Script { binary.LittleEndian.PutUint16(encoded[i*2:], uint16(r)) } return "powershell.exe", []string{"-NoProfile", "-EncodedCommand", base64.StdEncoding.EncodeToString(encoded)} } type closeTrackingWriteCloser struct { closed bool } func (w *closeTrackingWriteCloser) Write(data []byte) (int, error) { return len(data), nil } func (w *closeTrackingWriteCloser) Close() error { w.closed = true return nil } func TestStarCmdCapturesOutputAndExitCode(t *testing.T) { script := "printf 'hello'; printf 'err' 1>&2" command, args := testCommandArgs(script) if runtime.GOOS == "windows" { command, args = testWindowsPowerShellArgs("[Console]::Out.Write('hello'); [Console]::Error.Write('err')") } cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } <-cmd.Stoped() out, outErr := cmd.AllOutPut() if out != "hello" { t.Fatalf("expected stdout %q, got %q", "hello", out) } if outErr == nil || outErr.Error() != "err" { t.Fatalf("expected stderr error %q, got %v", "err", outErr) } if got := cmd.ExitCode(); got != 0 { t.Fatalf("expected exit code 0, got %d", got) } } func TestStarCmdWaitReturnsProcessError(t *testing.T) { command, args := testCommandArgs("exit 7") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Wait(); !errors.Is(err, errCommandProcessNotStarted) { t.Fatalf("expected errCommandProcessNotStarted before start, got %v", err) } if err := cmd.Start(); err != nil { t.Fatal(err) } if err := cmd.Wait(); err == nil { t.Fatal("expected wait error for non-zero exit") } if got := cmd.ExitCode(); got != 7 { t.Fatalf("expected exit code 7, got %d", got) } if err := cmd.Wait(); err == nil { t.Fatal("expected repeated Wait to keep final process error") } } func TestStarCmdWaitTimeoutAndContext(t *testing.T) { command, args := testCommandArgs("sleep 1") if runtime.GOOS == "windows" { command, args = testCommandArgs("ping -n 2 127.0.0.1 >nul") } cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } if err := cmd.WaitTimeout(10 * time.Millisecond); !errors.Is(err, ERR_TIMEOUT) { t.Fatalf("expected ERR_TIMEOUT, got %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() if err := cmd.WaitContext(ctx); !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("expected context deadline, got %v", err) } if err := cmd.WaitTimeout(3 * time.Second); err != nil { t.Fatalf("expected command to finish, got %v", err) } } func TestStarCmdWaitReturnsResultAfterProcessDone(t *testing.T) { command, args := testCommandArgs("exit 0") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } if err := cmd.Wait(); err != nil { t.Fatal(err) } if err := cmd.WaitTimeout(0); err != nil { t.Fatalf("expected finished command to beat zero timeout, got %v", err) } ctx, cancel := context.WithCancel(context.Background()) cancel() if err := cmd.WaitContext(ctx); err != nil { t.Fatalf("expected finished command to beat canceled context, got %v", err) } } func TestStarCmdWaitContextFinishedCommandWinsOverCanceledContext(t *testing.T) { t.Run("success", func(t *testing.T) { command, args := testCommandArgs("exit 0") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } if err := cmd.Wait(); err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) cancel() if err := cmd.WaitContext(ctx); err != nil { t.Fatalf("finished successful command should win over canceled context, got %v", err) } }) t.Run("failed", func(t *testing.T) { command, args := testCommandArgs("exit 7") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } waitErr := cmd.Wait() if waitErr == nil { t.Fatal("expected command wait error") } ctx, cancel := context.WithCancel(context.Background()) cancel() if err := cmd.WaitContext(ctx); err == nil || err.Error() != waitErr.Error() { t.Fatalf("finished failed command should win over canceled context, got %v, want %v", err, waitErr) } }) } func TestStarCmdStoppedAlias(t *testing.T) { command, args := testCommandArgs("exit 0") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } select { case <-cmd.Stopped(): case <-time.After(time.Second): t.Fatal("Stopped should close after command exits") } select { case <-cmd.Stoped(): case <-time.After(time.Second): t.Fatal("Stoped compatibility alias should close after command exits") } } func TestStarCmdStopedPublishesFinalExitCode(t *testing.T) { command, args := testCommandArgs("exit 7") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } <-cmd.Stoped() if cmd.IsRunning() { t.Fatal("command should not be running after Stoped closes") } if got := cmd.ExitCode(); got != 7 { t.Fatalf("expected exit code 7, got %d", got) } } func TestStarCmdRejectsRepeatedStart(t *testing.T) { command, args := testCommandArgs("exit 0") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } if err := cmd.Start(); !errors.Is(err, errCommandAlreadyStarted) { t.Fatalf("expected errCommandAlreadyStarted, got %v", err) } <-cmd.Stoped() if err := cmd.Start(); !errors.Is(err, errCommandAlreadyStarted) { t.Fatalf("expected errCommandAlreadyStarted after exit, got %v", err) } } func TestStarCmdStartFailureClosesStoped(t *testing.T) { cmd, err := Command("__staros_missing_command__") if err != nil { t.Fatal(err) } if err := cmd.Start(); err == nil { t.Fatal("expected start failure") } select { case <-cmd.Stoped(): case <-time.After(time.Second): t.Fatal("Stoped should close after start failure") } if cmd.IsRunning() { t.Fatal("command should not be running after start failure") } if got := cmd.ExitCode(); got != -1 { t.Fatalf("expected exit code -1 after start failure, got %d", got) } } func TestStarCmdCapturesLargeOutput(t *testing.T) { expected := strings.Repeat("x", 256*1024) script := "awk 'BEGIN{for(i=0;i<262144;i++) printf \"x\"}'" command, args := testCommandArgs(script) if runtime.GOOS == "windows" { command, args = testWindowsPowerShellArgs("[Console]::Out.Write(('x' * 262144))") } cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } <-cmd.Stoped() out, err := cmd.AllOutPut() if err != nil { t.Fatal(err) } if !bytes.Equal([]byte(out), []byte(expected)) { t.Fatalf("expected %d stdout bytes, got %d", len(expected), len(out)) } } func TestStarCmdStreamsOutput(t *testing.T) { script := "printf 'out'; printf 'err' 1>&2" command, args := testCommandArgs(script) if runtime.GOOS == "windows" { command, args = testWindowsPowerShellArgs("[Console]::Out.Write('out'); [Console]::Error.Write('err')") } cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } stdout := cmd.StdoutChan() stderr := cmd.StderrChan() output := cmd.OutputChan() if err := cmd.Start(); err != nil { t.Fatal(err) } var stdoutData, stderrData string var outputData []StarCmdOutput for stdout != nil || stderr != nil || output != nil { select { case data, ok := <-stdout: if !ok { stdout = nil continue } stdoutData += string(data) case data, ok := <-stderr: if !ok { stderr = nil continue } stderrData += string(data) case data, ok := <-output: if !ok { output = nil continue } outputData = append(outputData, data) case <-time.After(3 * time.Second): t.Fatal("stream output timed out") } } if stdoutData != "out" { t.Fatalf("expected streamed stdout %q, got %q", "out", stdoutData) } if stderrData != "err" { t.Fatalf("expected streamed stderr %q, got %q", "err", stderrData) } var seenStdout, seenStderr bool for _, item := range outputData { switch item.Stream { case StarCmdOutputStdout: seenStdout = seenStdout || string(item.Data) == "out" case StarCmdOutputStderr: seenStderr = seenStderr || string(item.Data) == "err" default: t.Fatalf("unknown output stream %v", item.Stream) } } if !seenStdout || !seenStderr { t.Fatalf("expected combined output stream to include stdout and stderr, got %#v", outputData) } } func TestStarCmdStreamNilReturnsClosedChannels(t *testing.T) { var cmd *StarCmd select { case _, ok := <-cmd.StdoutChan(): if ok { t.Fatal("nil stdout stream should be closed") } case <-time.After(time.Second): t.Fatal("nil stdout stream should close immediately") } select { case _, ok := <-cmd.StderrChan(): if ok { t.Fatal("nil stderr stream should be closed") } case <-time.After(time.Second): t.Fatal("nil stderr stream should close immediately") } select { case _, ok := <-cmd.OutputChan(): if ok { t.Fatal("nil output stream should be closed") } case <-time.After(time.Second): t.Fatal("nil output stream should close immediately") } } func TestStarCmdRedirectOutputWriterKeepsCapture(t *testing.T) { script := "printf 'out'; printf 'err' 1>&2" command, args := testCommandArgs(script) if runtime.GOOS == "windows" { command, args = testWindowsPowerShellArgs("[Console]::Out.Write('out'); [Console]::Error.Write('err')") } cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } var redirected bytes.Buffer if err := cmd.RedirectOutput(&redirected); err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } <-cmd.Stopped() if got := redirected.String(); got != "outerr" && got != "errout" { t.Fatalf("expected redirected stdout/stderr bytes, got %q", got) } if out := cmd.AllStdOut(); out != "out" { t.Fatalf("expected captured stdout %q, got %q", "out", out) } if err := cmd.AllStdErr(); err == nil || err.Error() != "err" { t.Fatalf("expected captured stderr %q, got %v", "err", err) } } func TestStarCmdRedirectFiles(t *testing.T) { dir := t.TempDir() stdoutFile := filepath.Join(dir, "stdout.txt") stderrFile := filepath.Join(dir, "stderr.txt") script := "printf 'out'; printf 'err' 1>&2" command, args := testCommandArgs(script) if runtime.GOOS == "windows" { command, args = testWindowsPowerShellArgs("[Console]::Out.Write('out'); [Console]::Error.Write('err')") } cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.RedirectStdoutFile(stdoutFile); err != nil { t.Fatal(err) } if err := cmd.RedirectStderrFile(stderrFile); err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } <-cmd.Stopped() stdoutData, err := ioutil.ReadFile(stdoutFile) if err != nil { t.Fatal(err) } stderrData, err := ioutil.ReadFile(stderrFile) if err != nil { t.Fatal(err) } if string(stdoutData) != "out" { t.Fatalf("expected stdout file %q, got %q", "out", string(stdoutData)) } if string(stderrData) != "err" { t.Fatalf("expected stderr file %q, got %q", "err", string(stderrData)) } } func TestStarCmdRedirectStdin(t *testing.T) { command, args := testCommandArgs("cat") if runtime.GOOS == "windows" { command, args = testCommandArgs("more") } cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.RedirectStdin(strings.NewReader("hello\n")); err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } <-cmd.Stopped() if out := cmd.AllStdOut(); !strings.Contains(out, "hello") { t.Fatalf("expected redirected stdin in stdout, got %q", out) } if err := cmd.AllStdErr(); err != nil { t.Fatalf("redirected stdin should not create command error, got %v", err) } if err := cmd.WriteCmdE("again"); !errors.Is(err, errCommandStdinUnavailable) { t.Fatalf("expected errCommandStdinUnavailable after stdin redirect, got %v", err) } } func TestStarCmdRedirectStdinClosesManagedPipe(t *testing.T) { command, args := testCommandArgs("cat") if runtime.GOOS == "windows" { command, args = testCommandArgs("more") } cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } tracker := &closeTrackingWriteCloser{} cmd.lock.Lock() cmd.infile = tracker cmd.inclosed = false cmd.lock.Unlock() if err := cmd.RedirectStdin(strings.NewReader("hello\n")); err != nil { t.Fatal(err) } if !tracker.closed { t.Fatal("RedirectStdin should close the previously managed stdin pipe") } if err := cmd.WriteCmdE("again"); !errors.Is(err, errCommandStdinUnavailable) { t.Fatalf("expected managed stdin to be unavailable after redirect, got %v", err) } } func TestStarCmdDetachClosesManagedPipe(t *testing.T) { command, args := testCommandArgs("exit 0") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } tracker := &closeTrackingWriteCloser{} cmd.lock.Lock() original := cmd.infile cmd.infile = tracker cmd.inclosed = false cmd.lock.Unlock() if original != nil { _ = original.Close() } if err := cmd.DetachE(); errors.Is(err, ERR_UNSUPPORTED) { t.Skip(err) } else if err != nil { t.Fatal(err) } if !tracker.closed { t.Fatal("DetachE should close the managed stdin pipe") } if err := cmd.WriteCmdE("again"); !errors.Is(err, errCommandStdinClosed) { t.Fatalf("expected detached stdin to be closed, got %v", err) } } func TestStarCmdRedirectRejectsInvalidState(t *testing.T) { command, args := testCommandArgs("exit 0") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.RedirectStdout(nil); !errors.Is(err, errCommandRedirectNil) { t.Fatalf("expected errCommandRedirectNil, got %v", err) } if err := cmd.Start(); err != nil { t.Fatal(err) } if err := cmd.RedirectStdout(&bytes.Buffer{}); !errors.Is(err, errCommandAlreadyStarted) { t.Fatalf("expected errCommandAlreadyStarted, got %v", err) } <-cmd.Stopped() } func TestStarCmdCloseStdinLetsCommandExit(t *testing.T) { script := "cat" if runtime.GOOS == "windows" { script = "more" } command, args := testCommandArgs(script) cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } if err := cmd.WriteCmdE("hello"); err != nil { t.Fatal(err) } if err := cmd.CloseStdinE(); err != nil { t.Fatal(err) } select { case <-cmd.Stoped(): case <-time.After(3 * time.Second): t.Fatal("command should exit after stdin closes") } if out := cmd.AllStdOut(); !strings.Contains(out, "hello") { t.Fatalf("expected echoed stdin, got %q", out) } if err := cmd.CloseStdinE(); !errors.Is(err, errCommandStdinClosed) { t.Fatalf("expected errCommandStdinClosed, got %v", err) } if err := cmd.WriteCmdE("again"); !errors.Is(err, errCommandStdinClosed) { t.Fatalf("expected errCommandStdinClosed after close, got %v", err) } } func TestStarCmdWriteStdinRawDoesNotAppendNewline(t *testing.T) { script := "cat" if runtime.GOOS == "windows" { script = "more" } command, args := testCommandArgs(script) cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } if err := cmd.WriteStdinStringE("raw"); err != nil { t.Fatal(err) } if err := cmd.WriteStdinE([]byte("-bytes")); err != nil { t.Fatal(err) } if err := cmd.CloseStdinE(); err != nil { t.Fatal(err) } if err := cmd.WaitTimeout(3 * time.Second); err != nil { t.Fatal(err) } if out := cmd.AllStdOut(); !strings.Contains(out, "raw-bytes") { t.Fatalf("expected raw stdin without inserted newline, got %q", out) } } func TestStarCmdNilGuards(t *testing.T) { var cmd *StarCmd if cmd.IsRunning() { t.Fatal("nil StarCmd should not be running") } if got := cmd.GetPid(); got != -1 { t.Fatalf("expected nil pid -1, got %d", got) } if err := cmd.Release(); !errors.Is(err, errNilCommand) { t.Fatalf("expected errNilCommand, got %v", err) } if err := cmd.SetKeepCaps(); !errors.Is(err, errNilCommand) { t.Fatalf("expected errNilCommand, got %v", err) } if err := cmd.ReleaseE(); !errors.Is(err, errNilCommand) { t.Fatalf("expected errNilCommand, got %v", err) } if err := cmd.DetachE(); !errors.Is(err, errNilCommand) { t.Fatalf("expected errNilCommand, got %v", err) } if err := cmd.SetRunUserE(0, 0, nil); !errors.Is(err, errNilCommand) { t.Fatalf("expected errNilCommand, got %v", err) } if err := cmd.WriteCmdE("noop"); !errors.Is(err, errNilCommand) { t.Fatalf("expected errNilCommand, got %v", err) } if err := cmd.WriteStdinE([]byte("noop")); !errors.Is(err, errNilCommand) { t.Fatalf("expected errNilCommand, got %v", err) } if err := cmd.WriteStdinStringE("noop"); !errors.Is(err, errNilCommand) { t.Fatalf("expected errNilCommand, got %v", err) } if err := cmd.WriteStdinLineE("noop"); !errors.Is(err, errNilCommand) { t.Fatalf("expected errNilCommand, got %v", err) } if err := cmd.Wait(); !errors.Is(err, errNilCommand) { t.Fatalf("expected errNilCommand, got %v", err) } if err := cmd.CloseStdinE(); !errors.Is(err, errNilCommand) { t.Fatalf("expected errNilCommand, got %v", err) } } func TestStarCmdReleaseUsesStartLifecycle(t *testing.T) { command, args := testCommandArgs("exit 0") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.ReleaseE(); errors.Is(err, ERR_UNSUPPORTED) { t.Skip(err) } else if err != nil { t.Fatal(err) } <-cmd.Stoped() if got := cmd.ExitCode(); got != 0 { t.Fatalf("expected exit code 0, got %d", got) } if err := cmd.ReleaseE(); !errors.Is(err, errCommandAlreadyReleased) { t.Fatalf("expected errCommandAlreadyReleased, got %v", err) } } func TestStarCmdReleaseAfterStartKeepsLifecycle(t *testing.T) { command, args := testCommandArgs("exit 0") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } if err := cmd.ReleaseE(); errors.Is(err, ERR_UNSUPPORTED) { t.Skip(err) } else if err != nil { t.Fatal(err) } <-cmd.Stoped() if got := cmd.ExitCode(); got != 0 { t.Fatalf("expected exit code 0, got %d", got) } } func TestStarCmdDetachRejectsRepeatedDetach(t *testing.T) { command, args := testCommandArgs("exit 0") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.DetachE(); errors.Is(err, ERR_UNSUPPORTED) { t.Skip(err) } else if err != nil { t.Fatal(err) } if err := cmd.DetachE(); !errors.Is(err, errCommandAlreadyDetached) { t.Fatalf("expected errCommandAlreadyDetached, got %v", err) } if err := cmd.Start(); !errors.Is(err, errCommandDetached) { t.Fatalf("expected errCommandDetached, got %v", err) } } func TestStarCmdDetachPublishesWaitResult(t *testing.T) { command, args := testCommandArgs("exit 0") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.DetachE(); errors.Is(err, ERR_UNSUPPORTED) { t.Skip(err) } else if err != nil { t.Fatal(err) } if err := cmd.WaitTimeout(0); err != nil { t.Fatalf("detached command should publish final wait result, got %v", err) } ctx, cancel := context.WithCancel(context.Background()) cancel() if err := cmd.WaitContext(ctx); err != nil { t.Fatalf("detached command should beat canceled context, got %v", err) } waitErr := make(chan error, 1) go func() { waitErr <- cmd.Wait() }() select { case err := <-waitErr: if err != nil { t.Fatalf("detached command wait got %v", err) } case <-time.After(time.Second): t.Fatal("detached command wait did not observe final result") } } func TestStarCmdDetachDoesNotCaptureOutput(t *testing.T) { script := "printf 'detached'; printf 'err' 1>&2" if runtime.GOOS == "windows" { script = "&2" } command, args := testCommandArgs(script) cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.DetachE(); errors.Is(err, ERR_UNSUPPORTED) { t.Skip(err) } else if err != nil { t.Fatal(err) } <-cmd.Stoped() if out := cmd.AllStdOut(); out != "" { t.Fatalf("detached command should not be captured, got stdout %q", out) } if err := cmd.AllStdErr(); err != nil { t.Fatalf("detached command should not capture stderr, got %v", err) } } func TestStarCmdDetachRejectsStartedCommand(t *testing.T) { command, args := testCommandArgs("exit 0") cmd, err := Command(command, args...) if err != nil { t.Fatal(err) } if err := cmd.Start(); err != nil { t.Fatal(err) } if err := cmd.DetachE(); errors.Is(err, ERR_UNSUPPORTED) { t.Skip(err) } else if !errors.Is(err, errCommandAlreadyStarted) { t.Fatalf("expected errCommandAlreadyStarted, got %v", err) } <-cmd.Stoped() } func TestFindProcessByPidCurrentProcess(t *testing.T) { pid := os.Getpid() process, err := FindProcessByPid(int64(pid)) if errors.Is(err, ERR_UNSUPPORTED) { t.Skip(err) } if err != nil { t.Fatal(err) } if process.Pid != int64(pid) { t.Fatalf("expected pid %d, got %d", pid, process.Pid) } } func TestStopedNilReturnsClosedChannel(t *testing.T) { var cmd *StarCmd select { case <-cmd.Stoped(): case <-time.After(time.Second): t.Fatal("nil Stoped channel should already be closed") } select { case <-cmd.Stopped(): case <-time.After(time.Second): t.Fatal("nil Stopped channel should already be closed") } }