- 重构 sysconf 为文档模型 INI Parser 与 Config Framework - 强化 hosts 解析、插入校验、写回与异常输入处理 - 完善 StarCmd 生命周期、等待 API、流式输出与 IO 重定向 - 扩展跨平台文件时间、文件锁、内存、进程与网络能力 - 将 Windows 进程适配更新到 b612.me/wincmd v0.1.0 - 移除本地 wincmd/win32api replace,改用发布版依赖 - 将最低 Go 版本提升到 1.18 - 补充 hosts、sysconf、FileLock、StarCmd 与平台适配回归测试
814 lines
22 KiB
Go
814 lines
22 KiB
Go
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 = "<nul set /p =detached & <nul set /p =err 1>&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")
|
|
}
|
|
}
|