diff --git a/contracts/process/process.go b/contracts/process/process.go new file mode 100644 index 000000000..e1092c95f --- /dev/null +++ b/contracts/process/process.go @@ -0,0 +1,72 @@ +package process + +import ( + "context" + "io" + "time" +) + +// OutputType represents the type of output stream produced by a running process. +type OutputType int + +const ( + // OutputTypeStdout indicates output written to the standard output stream. + OutputTypeStdout OutputType = iota + + // OutputTypeStderr indicates output written to the standard error stream. + OutputTypeStderr +) + +// OnOutputFunc is a callback function invoked when the process produces output. +// The typ parameter indicates whether the data came from stdout or stderr, +// and line contains the raw output bytes (typically a line of text). +type OnOutputFunc func(typ OutputType, line []byte) + +// Process defines an interface for configuring and running external processes. +// +// Implementations are mutable and should not be reused concurrently. +// Each method modifies the same underlying process configuration. +type Process interface { + // Env adds or overrides environment variables for the process. + // Modifies the current process configuration. + Env(vars map[string]string) Process + + // Input sets the stdin source for the process. + // By default, processes run without stdin input. + Input(in io.Reader) Process + + // Path sets the working directory where the process will be executed. + Path(path string) Process + + // Quietly suppresses all process output, discarding both stdout and stderr. + Quietly() Process + + // OnOutput registers a handler to receive stdout and stderr output + // while the process runs. Multiple handlers may be chained depending + // on the implementation. + OnOutput(handler OnOutputFunc) Process + + // Run starts the process, waits for it to complete, and returns the result. + // It returns an error if the process cannot be started or if execution fails. + Run(name string, arg ...string) (Result, error) + + // Start begins running the process asynchronously and returns a Running + // handle to monitor and control its execution. The caller must later + // wait or terminate the process explicitly. + Start(name string, arg ...string) (Running, error) + + // Timeout sets a maximum execution duration for the process. + // If the timeout is exceeded, the process will be terminated. + // A zero duration disables the timeout. + Timeout(timeout time.Duration) Process + + // TTY attaches the process to a pseudo-terminal, enabling interactive + // behavior (such as programs that require a TTY for input/output). + TTY() Process + + // WithContext binds the process lifecycle to the provided context. + // If the context is canceled or reaches its deadline, the process + // will be terminated. When combined with Timeout, the earlier of + // the two deadlines takes effect. + WithContext(ctx context.Context) Process +} diff --git a/contracts/process/result.go b/contracts/process/result.go new file mode 100644 index 000000000..3439fe23f --- /dev/null +++ b/contracts/process/result.go @@ -0,0 +1,34 @@ +package process + +// Result represents the outcome of a finished process execution. +// It provides access to exit status, captured output, and helper +// methods for inspecting process behavior. +type Result interface { + // Successful reports whether the process exited with a zero exit code. + Successful() bool + + // Failed reports whether the process exited with a non-zero exit code. + Failed() bool + + // ExitCode returns the process exit code. A zero value typically + // indicates success, while non-zero indicates failure. + ExitCode() int + + // Output returns the full contents written to stdout by the process. + Output() string + + // ErrorOutput returns the full contents written to stderr by the process. + ErrorOutput() string + + // Command returns the full command string used to start the process, + // including program name and arguments. + Command() string + + // SeeInOutput reports whether the given substring is present + // in the process stdout output. + SeeInOutput(needle string) bool + + // SeeInErrorOutput reports whether the given substring is present + // in the process stderr output. + SeeInErrorOutput(needle string) bool +} diff --git a/contracts/process/running.go b/contracts/process/running.go new file mode 100644 index 000000000..25227e8c7 --- /dev/null +++ b/contracts/process/running.go @@ -0,0 +1,49 @@ +package process + +import ( + "os" + "time" +) + +// Running represents a handle to a process that has been started and is still active. +// It provides methods for inspecting process state, retrieving output, and controlling +// its lifecycle. +type Running interface { + // PID returns the operating system process ID. + PID() int + + // Running reports whether the process still exists according to the OS. + // + // NOTE: This may return true for a "zombie" process (terminated but not + // reaped). You must eventually call Wait() to reap the process and release + // resources. A common pattern is to poll Running() in a goroutine and then + // call Wait() in the main flow to ensure cleanup. + Running() bool + + // Output returns the complete stdout captured from the process so far. + Output() string + + // ErrorOutput returns the complete stderr captured from the process so far. + ErrorOutput() string + + // LatestOutput returns the most recent chunk of stdout produced by the process. + // The definition of "latest" is implementation dependent. + LatestOutput() string + + // LatestErrorOutput returns the most recent chunk of stderr produced by the process. + // The definition of "latest" is implementation dependent. + LatestErrorOutput() string + + // Wait blocks until the process exits and returns its final Result. + // This call is required to reap the process and release system resources. + Wait() Result + + // Stop attempts to gracefully stop the process by sending the provided signal + // (defaulting to SIGTERM). If the process does not exit within the given timeout, + // it is forcibly killed (SIGKILL). + Stop(timeout time.Duration, sig ...os.Signal) error + + // Signal sends the given signal to the process. Returns an error if the process + // has not been started or no longer exists. + Signal(sig os.Signal) error +} diff --git a/mocks/process/OnOutputFunc.go b/mocks/process/OnOutputFunc.go new file mode 100644 index 000000000..eca7e8157 --- /dev/null +++ b/mocks/process/OnOutputFunc.go @@ -0,0 +1,69 @@ +// Code generated by mockery. DO NOT EDIT. + +package process + +import ( + process "github.com/goravel/framework/contracts/process" + mock "github.com/stretchr/testify/mock" +) + +// OnOutputFunc is an autogenerated mock type for the OnOutputFunc type +type OnOutputFunc struct { + mock.Mock +} + +type OnOutputFunc_Expecter struct { + mock *mock.Mock +} + +func (_m *OnOutputFunc) EXPECT() *OnOutputFunc_Expecter { + return &OnOutputFunc_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: typ, line +func (_m *OnOutputFunc) Execute(typ process.OutputType, line []byte) { + _m.Called(typ, line) +} + +// OnOutputFunc_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type OnOutputFunc_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - typ process.OutputType +// - line []byte +func (_e *OnOutputFunc_Expecter) Execute(typ interface{}, line interface{}) *OnOutputFunc_Execute_Call { + return &OnOutputFunc_Execute_Call{Call: _e.mock.On("Execute", typ, line)} +} + +func (_c *OnOutputFunc_Execute_Call) Run(run func(typ process.OutputType, line []byte)) *OnOutputFunc_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(process.OutputType), args[1].([]byte)) + }) + return _c +} + +func (_c *OnOutputFunc_Execute_Call) Return() *OnOutputFunc_Execute_Call { + _c.Call.Return() + return _c +} + +func (_c *OnOutputFunc_Execute_Call) RunAndReturn(run func(process.OutputType, []byte)) *OnOutputFunc_Execute_Call { + _c.Run(run) + return _c +} + +// NewOnOutputFunc creates a new instance of OnOutputFunc. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewOnOutputFunc(t interface { + mock.TestingT + Cleanup(func()) +}) *OnOutputFunc { + mock := &OnOutputFunc{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/process/Process.go b/mocks/process/Process.go new file mode 100644 index 000000000..8ede724f5 --- /dev/null +++ b/mocks/process/Process.go @@ -0,0 +1,569 @@ +// Code generated by mockery. DO NOT EDIT. + +package process + +import ( + context "context" + io "io" + + mock "github.com/stretchr/testify/mock" + + process "github.com/goravel/framework/contracts/process" + + time "time" +) + +// Process is an autogenerated mock type for the Process type +type Process struct { + mock.Mock +} + +type Process_Expecter struct { + mock *mock.Mock +} + +func (_m *Process) EXPECT() *Process_Expecter { + return &Process_Expecter{mock: &_m.Mock} +} + +// Env provides a mock function with given fields: vars +func (_m *Process) Env(vars map[string]string) process.Process { + ret := _m.Called(vars) + + if len(ret) == 0 { + panic("no return value specified for Env") + } + + var r0 process.Process + if rf, ok := ret.Get(0).(func(map[string]string) process.Process); ok { + r0 = rf(vars) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(process.Process) + } + } + + return r0 +} + +// Process_Env_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Env' +type Process_Env_Call struct { + *mock.Call +} + +// Env is a helper method to define mock.On call +// - vars map[string]string +func (_e *Process_Expecter) Env(vars interface{}) *Process_Env_Call { + return &Process_Env_Call{Call: _e.mock.On("Env", vars)} +} + +func (_c *Process_Env_Call) Run(run func(vars map[string]string)) *Process_Env_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(map[string]string)) + }) + return _c +} + +func (_c *Process_Env_Call) Return(_a0 process.Process) *Process_Env_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Process_Env_Call) RunAndReturn(run func(map[string]string) process.Process) *Process_Env_Call { + _c.Call.Return(run) + return _c +} + +// Input provides a mock function with given fields: in +func (_m *Process) Input(in io.Reader) process.Process { + ret := _m.Called(in) + + if len(ret) == 0 { + panic("no return value specified for Input") + } + + var r0 process.Process + if rf, ok := ret.Get(0).(func(io.Reader) process.Process); ok { + r0 = rf(in) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(process.Process) + } + } + + return r0 +} + +// Process_Input_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Input' +type Process_Input_Call struct { + *mock.Call +} + +// Input is a helper method to define mock.On call +// - in io.Reader +func (_e *Process_Expecter) Input(in interface{}) *Process_Input_Call { + return &Process_Input_Call{Call: _e.mock.On("Input", in)} +} + +func (_c *Process_Input_Call) Run(run func(in io.Reader)) *Process_Input_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(io.Reader)) + }) + return _c +} + +func (_c *Process_Input_Call) Return(_a0 process.Process) *Process_Input_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Process_Input_Call) RunAndReturn(run func(io.Reader) process.Process) *Process_Input_Call { + _c.Call.Return(run) + return _c +} + +// OnOutput provides a mock function with given fields: handler +func (_m *Process) OnOutput(handler process.OnOutputFunc) process.Process { + ret := _m.Called(handler) + + if len(ret) == 0 { + panic("no return value specified for OnOutput") + } + + var r0 process.Process + if rf, ok := ret.Get(0).(func(process.OnOutputFunc) process.Process); ok { + r0 = rf(handler) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(process.Process) + } + } + + return r0 +} + +// Process_OnOutput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnOutput' +type Process_OnOutput_Call struct { + *mock.Call +} + +// OnOutput is a helper method to define mock.On call +// - handler process.OnOutputFunc +func (_e *Process_Expecter) OnOutput(handler interface{}) *Process_OnOutput_Call { + return &Process_OnOutput_Call{Call: _e.mock.On("OnOutput", handler)} +} + +func (_c *Process_OnOutput_Call) Run(run func(handler process.OnOutputFunc)) *Process_OnOutput_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(process.OnOutputFunc)) + }) + return _c +} + +func (_c *Process_OnOutput_Call) Return(_a0 process.Process) *Process_OnOutput_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Process_OnOutput_Call) RunAndReturn(run func(process.OnOutputFunc) process.Process) *Process_OnOutput_Call { + _c.Call.Return(run) + return _c +} + +// Path provides a mock function with given fields: path +func (_m *Process) Path(path string) process.Process { + ret := _m.Called(path) + + if len(ret) == 0 { + panic("no return value specified for Path") + } + + var r0 process.Process + if rf, ok := ret.Get(0).(func(string) process.Process); ok { + r0 = rf(path) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(process.Process) + } + } + + return r0 +} + +// Process_Path_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Path' +type Process_Path_Call struct { + *mock.Call +} + +// Path is a helper method to define mock.On call +// - path string +func (_e *Process_Expecter) Path(path interface{}) *Process_Path_Call { + return &Process_Path_Call{Call: _e.mock.On("Path", path)} +} + +func (_c *Process_Path_Call) Run(run func(path string)) *Process_Path_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Process_Path_Call) Return(_a0 process.Process) *Process_Path_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Process_Path_Call) RunAndReturn(run func(string) process.Process) *Process_Path_Call { + _c.Call.Return(run) + return _c +} + +// Quietly provides a mock function with no fields +func (_m *Process) Quietly() process.Process { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Quietly") + } + + var r0 process.Process + if rf, ok := ret.Get(0).(func() process.Process); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(process.Process) + } + } + + return r0 +} + +// Process_Quietly_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Quietly' +type Process_Quietly_Call struct { + *mock.Call +} + +// Quietly is a helper method to define mock.On call +func (_e *Process_Expecter) Quietly() *Process_Quietly_Call { + return &Process_Quietly_Call{Call: _e.mock.On("Quietly")} +} + +func (_c *Process_Quietly_Call) Run(run func()) *Process_Quietly_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Process_Quietly_Call) Return(_a0 process.Process) *Process_Quietly_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Process_Quietly_Call) RunAndReturn(run func() process.Process) *Process_Quietly_Call { + _c.Call.Return(run) + return _c +} + +// Run provides a mock function with given fields: name, arg +func (_m *Process) Run(name string, arg ...string) (process.Result, error) { + _va := make([]interface{}, len(arg)) + for _i := range arg { + _va[_i] = arg[_i] + } + var _ca []interface{} + _ca = append(_ca, name) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Run") + } + + var r0 process.Result + var r1 error + if rf, ok := ret.Get(0).(func(string, ...string) (process.Result, error)); ok { + return rf(name, arg...) + } + if rf, ok := ret.Get(0).(func(string, ...string) process.Result); ok { + r0 = rf(name, arg...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(process.Result) + } + } + + if rf, ok := ret.Get(1).(func(string, ...string) error); ok { + r1 = rf(name, arg...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Process_Run_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Run' +type Process_Run_Call struct { + *mock.Call +} + +// Run is a helper method to define mock.On call +// - name string +// - arg ...string +func (_e *Process_Expecter) Run(name interface{}, arg ...interface{}) *Process_Run_Call { + return &Process_Run_Call{Call: _e.mock.On("Run", + append([]interface{}{name}, arg...)...)} +} + +func (_c *Process_Run_Call) Run(run func(name string, arg ...string)) *Process_Run_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]string, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Process_Run_Call) Return(_a0 process.Result, _a1 error) *Process_Run_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Process_Run_Call) RunAndReturn(run func(string, ...string) (process.Result, error)) *Process_Run_Call { + _c.Call.Return(run) + return _c +} + +// Start provides a mock function with given fields: name, arg +func (_m *Process) Start(name string, arg ...string) (process.Running, error) { + _va := make([]interface{}, len(arg)) + for _i := range arg { + _va[_i] = arg[_i] + } + var _ca []interface{} + _ca = append(_ca, name) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 process.Running + var r1 error + if rf, ok := ret.Get(0).(func(string, ...string) (process.Running, error)); ok { + return rf(name, arg...) + } + if rf, ok := ret.Get(0).(func(string, ...string) process.Running); ok { + r0 = rf(name, arg...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(process.Running) + } + } + + if rf, ok := ret.Get(1).(func(string, ...string) error); ok { + r1 = rf(name, arg...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Process_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' +type Process_Start_Call struct { + *mock.Call +} + +// Start is a helper method to define mock.On call +// - name string +// - arg ...string +func (_e *Process_Expecter) Start(name interface{}, arg ...interface{}) *Process_Start_Call { + return &Process_Start_Call{Call: _e.mock.On("Start", + append([]interface{}{name}, arg...)...)} +} + +func (_c *Process_Start_Call) Run(run func(name string, arg ...string)) *Process_Start_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]string, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(string) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Process_Start_Call) Return(_a0 process.Running, _a1 error) *Process_Start_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Process_Start_Call) RunAndReturn(run func(string, ...string) (process.Running, error)) *Process_Start_Call { + _c.Call.Return(run) + return _c +} + +// TTY provides a mock function with no fields +func (_m *Process) TTY() process.Process { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for TTY") + } + + var r0 process.Process + if rf, ok := ret.Get(0).(func() process.Process); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(process.Process) + } + } + + return r0 +} + +// Process_TTY_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TTY' +type Process_TTY_Call struct { + *mock.Call +} + +// TTY is a helper method to define mock.On call +func (_e *Process_Expecter) TTY() *Process_TTY_Call { + return &Process_TTY_Call{Call: _e.mock.On("TTY")} +} + +func (_c *Process_TTY_Call) Run(run func()) *Process_TTY_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Process_TTY_Call) Return(_a0 process.Process) *Process_TTY_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Process_TTY_Call) RunAndReturn(run func() process.Process) *Process_TTY_Call { + _c.Call.Return(run) + return _c +} + +// Timeout provides a mock function with given fields: timeout +func (_m *Process) Timeout(timeout time.Duration) process.Process { + ret := _m.Called(timeout) + + if len(ret) == 0 { + panic("no return value specified for Timeout") + } + + var r0 process.Process + if rf, ok := ret.Get(0).(func(time.Duration) process.Process); ok { + r0 = rf(timeout) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(process.Process) + } + } + + return r0 +} + +// Process_Timeout_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Timeout' +type Process_Timeout_Call struct { + *mock.Call +} + +// Timeout is a helper method to define mock.On call +// - timeout time.Duration +func (_e *Process_Expecter) Timeout(timeout interface{}) *Process_Timeout_Call { + return &Process_Timeout_Call{Call: _e.mock.On("Timeout", timeout)} +} + +func (_c *Process_Timeout_Call) Run(run func(timeout time.Duration)) *Process_Timeout_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(time.Duration)) + }) + return _c +} + +func (_c *Process_Timeout_Call) Return(_a0 process.Process) *Process_Timeout_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Process_Timeout_Call) RunAndReturn(run func(time.Duration) process.Process) *Process_Timeout_Call { + _c.Call.Return(run) + return _c +} + +// WithContext provides a mock function with given fields: ctx +func (_m *Process) WithContext(ctx context.Context) process.Process { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for WithContext") + } + + var r0 process.Process + if rf, ok := ret.Get(0).(func(context.Context) process.Process); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(process.Process) + } + } + + return r0 +} + +// Process_WithContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WithContext' +type Process_WithContext_Call struct { + *mock.Call +} + +// WithContext is a helper method to define mock.On call +// - ctx context.Context +func (_e *Process_Expecter) WithContext(ctx interface{}) *Process_WithContext_Call { + return &Process_WithContext_Call{Call: _e.mock.On("WithContext", ctx)} +} + +func (_c *Process_WithContext_Call) Run(run func(ctx context.Context)) *Process_WithContext_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *Process_WithContext_Call) Return(_a0 process.Process) *Process_WithContext_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Process_WithContext_Call) RunAndReturn(run func(context.Context) process.Process) *Process_WithContext_Call { + _c.Call.Return(run) + return _c +} + +// NewProcess creates a new instance of Process. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewProcess(t interface { + mock.TestingT + Cleanup(func()) +}) *Process { + mock := &Process{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/process/Result.go b/mocks/process/Result.go new file mode 100644 index 000000000..d26aaff21 --- /dev/null +++ b/mocks/process/Result.go @@ -0,0 +1,394 @@ +// Code generated by mockery. DO NOT EDIT. + +package process + +import mock "github.com/stretchr/testify/mock" + +// Result is an autogenerated mock type for the Result type +type Result struct { + mock.Mock +} + +type Result_Expecter struct { + mock *mock.Mock +} + +func (_m *Result) EXPECT() *Result_Expecter { + return &Result_Expecter{mock: &_m.Mock} +} + +// Command provides a mock function with no fields +func (_m *Result) Command() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Command") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Result_Command_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Command' +type Result_Command_Call struct { + *mock.Call +} + +// Command is a helper method to define mock.On call +func (_e *Result_Expecter) Command() *Result_Command_Call { + return &Result_Command_Call{Call: _e.mock.On("Command")} +} + +func (_c *Result_Command_Call) Run(run func()) *Result_Command_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Result_Command_Call) Return(_a0 string) *Result_Command_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Result_Command_Call) RunAndReturn(run func() string) *Result_Command_Call { + _c.Call.Return(run) + return _c +} + +// ErrorOutput provides a mock function with no fields +func (_m *Result) ErrorOutput() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ErrorOutput") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Result_ErrorOutput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ErrorOutput' +type Result_ErrorOutput_Call struct { + *mock.Call +} + +// ErrorOutput is a helper method to define mock.On call +func (_e *Result_Expecter) ErrorOutput() *Result_ErrorOutput_Call { + return &Result_ErrorOutput_Call{Call: _e.mock.On("ErrorOutput")} +} + +func (_c *Result_ErrorOutput_Call) Run(run func()) *Result_ErrorOutput_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Result_ErrorOutput_Call) Return(_a0 string) *Result_ErrorOutput_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Result_ErrorOutput_Call) RunAndReturn(run func() string) *Result_ErrorOutput_Call { + _c.Call.Return(run) + return _c +} + +// ExitCode provides a mock function with no fields +func (_m *Result) ExitCode() int { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ExitCode") + } + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// Result_ExitCode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExitCode' +type Result_ExitCode_Call struct { + *mock.Call +} + +// ExitCode is a helper method to define mock.On call +func (_e *Result_Expecter) ExitCode() *Result_ExitCode_Call { + return &Result_ExitCode_Call{Call: _e.mock.On("ExitCode")} +} + +func (_c *Result_ExitCode_Call) Run(run func()) *Result_ExitCode_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Result_ExitCode_Call) Return(_a0 int) *Result_ExitCode_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Result_ExitCode_Call) RunAndReturn(run func() int) *Result_ExitCode_Call { + _c.Call.Return(run) + return _c +} + +// Failed provides a mock function with no fields +func (_m *Result) Failed() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Failed") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Result_Failed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Failed' +type Result_Failed_Call struct { + *mock.Call +} + +// Failed is a helper method to define mock.On call +func (_e *Result_Expecter) Failed() *Result_Failed_Call { + return &Result_Failed_Call{Call: _e.mock.On("Failed")} +} + +func (_c *Result_Failed_Call) Run(run func()) *Result_Failed_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Result_Failed_Call) Return(_a0 bool) *Result_Failed_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Result_Failed_Call) RunAndReturn(run func() bool) *Result_Failed_Call { + _c.Call.Return(run) + return _c +} + +// Output provides a mock function with no fields +func (_m *Result) Output() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Output") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Result_Output_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Output' +type Result_Output_Call struct { + *mock.Call +} + +// Output is a helper method to define mock.On call +func (_e *Result_Expecter) Output() *Result_Output_Call { + return &Result_Output_Call{Call: _e.mock.On("Output")} +} + +func (_c *Result_Output_Call) Run(run func()) *Result_Output_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Result_Output_Call) Return(_a0 string) *Result_Output_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Result_Output_Call) RunAndReturn(run func() string) *Result_Output_Call { + _c.Call.Return(run) + return _c +} + +// SeeInErrorOutput provides a mock function with given fields: needle +func (_m *Result) SeeInErrorOutput(needle string) bool { + ret := _m.Called(needle) + + if len(ret) == 0 { + panic("no return value specified for SeeInErrorOutput") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(needle) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Result_SeeInErrorOutput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SeeInErrorOutput' +type Result_SeeInErrorOutput_Call struct { + *mock.Call +} + +// SeeInErrorOutput is a helper method to define mock.On call +// - needle string +func (_e *Result_Expecter) SeeInErrorOutput(needle interface{}) *Result_SeeInErrorOutput_Call { + return &Result_SeeInErrorOutput_Call{Call: _e.mock.On("SeeInErrorOutput", needle)} +} + +func (_c *Result_SeeInErrorOutput_Call) Run(run func(needle string)) *Result_SeeInErrorOutput_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Result_SeeInErrorOutput_Call) Return(_a0 bool) *Result_SeeInErrorOutput_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Result_SeeInErrorOutput_Call) RunAndReturn(run func(string) bool) *Result_SeeInErrorOutput_Call { + _c.Call.Return(run) + return _c +} + +// SeeInOutput provides a mock function with given fields: needle +func (_m *Result) SeeInOutput(needle string) bool { + ret := _m.Called(needle) + + if len(ret) == 0 { + panic("no return value specified for SeeInOutput") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(needle) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Result_SeeInOutput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SeeInOutput' +type Result_SeeInOutput_Call struct { + *mock.Call +} + +// SeeInOutput is a helper method to define mock.On call +// - needle string +func (_e *Result_Expecter) SeeInOutput(needle interface{}) *Result_SeeInOutput_Call { + return &Result_SeeInOutput_Call{Call: _e.mock.On("SeeInOutput", needle)} +} + +func (_c *Result_SeeInOutput_Call) Run(run func(needle string)) *Result_SeeInOutput_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Result_SeeInOutput_Call) Return(_a0 bool) *Result_SeeInOutput_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Result_SeeInOutput_Call) RunAndReturn(run func(string) bool) *Result_SeeInOutput_Call { + _c.Call.Return(run) + return _c +} + +// Successful provides a mock function with no fields +func (_m *Result) Successful() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Successful") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Result_Successful_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Successful' +type Result_Successful_Call struct { + *mock.Call +} + +// Successful is a helper method to define mock.On call +func (_e *Result_Expecter) Successful() *Result_Successful_Call { + return &Result_Successful_Call{Call: _e.mock.On("Successful")} +} + +func (_c *Result_Successful_Call) Run(run func()) *Result_Successful_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Result_Successful_Call) Return(_a0 bool) *Result_Successful_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Result_Successful_Call) RunAndReturn(run func() bool) *Result_Successful_Call { + _c.Call.Return(run) + return _c +} + +// NewResult creates a new instance of Result. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewResult(t interface { + mock.TestingT + Cleanup(func()) +}) *Result { + mock := &Result{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/process/Running.go b/mocks/process/Running.go new file mode 100644 index 000000000..2c8419427 --- /dev/null +++ b/mocks/process/Running.go @@ -0,0 +1,464 @@ +// Code generated by mockery. DO NOT EDIT. + +package process + +import ( + os "os" + + mock "github.com/stretchr/testify/mock" + + process "github.com/goravel/framework/contracts/process" + + time "time" +) + +// Running is an autogenerated mock type for the Running type +type Running struct { + mock.Mock +} + +type Running_Expecter struct { + mock *mock.Mock +} + +func (_m *Running) EXPECT() *Running_Expecter { + return &Running_Expecter{mock: &_m.Mock} +} + +// ErrorOutput provides a mock function with no fields +func (_m *Running) ErrorOutput() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ErrorOutput") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Running_ErrorOutput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ErrorOutput' +type Running_ErrorOutput_Call struct { + *mock.Call +} + +// ErrorOutput is a helper method to define mock.On call +func (_e *Running_Expecter) ErrorOutput() *Running_ErrorOutput_Call { + return &Running_ErrorOutput_Call{Call: _e.mock.On("ErrorOutput")} +} + +func (_c *Running_ErrorOutput_Call) Run(run func()) *Running_ErrorOutput_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Running_ErrorOutput_Call) Return(_a0 string) *Running_ErrorOutput_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Running_ErrorOutput_Call) RunAndReturn(run func() string) *Running_ErrorOutput_Call { + _c.Call.Return(run) + return _c +} + +// LatestErrorOutput provides a mock function with no fields +func (_m *Running) LatestErrorOutput() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for LatestErrorOutput") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Running_LatestErrorOutput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestErrorOutput' +type Running_LatestErrorOutput_Call struct { + *mock.Call +} + +// LatestErrorOutput is a helper method to define mock.On call +func (_e *Running_Expecter) LatestErrorOutput() *Running_LatestErrorOutput_Call { + return &Running_LatestErrorOutput_Call{Call: _e.mock.On("LatestErrorOutput")} +} + +func (_c *Running_LatestErrorOutput_Call) Run(run func()) *Running_LatestErrorOutput_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Running_LatestErrorOutput_Call) Return(_a0 string) *Running_LatestErrorOutput_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Running_LatestErrorOutput_Call) RunAndReturn(run func() string) *Running_LatestErrorOutput_Call { + _c.Call.Return(run) + return _c +} + +// LatestOutput provides a mock function with no fields +func (_m *Running) LatestOutput() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for LatestOutput") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Running_LatestOutput_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestOutput' +type Running_LatestOutput_Call struct { + *mock.Call +} + +// LatestOutput is a helper method to define mock.On call +func (_e *Running_Expecter) LatestOutput() *Running_LatestOutput_Call { + return &Running_LatestOutput_Call{Call: _e.mock.On("LatestOutput")} +} + +func (_c *Running_LatestOutput_Call) Run(run func()) *Running_LatestOutput_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Running_LatestOutput_Call) Return(_a0 string) *Running_LatestOutput_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Running_LatestOutput_Call) RunAndReturn(run func() string) *Running_LatestOutput_Call { + _c.Call.Return(run) + return _c +} + +// Output provides a mock function with no fields +func (_m *Running) Output() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Output") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Running_Output_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Output' +type Running_Output_Call struct { + *mock.Call +} + +// Output is a helper method to define mock.On call +func (_e *Running_Expecter) Output() *Running_Output_Call { + return &Running_Output_Call{Call: _e.mock.On("Output")} +} + +func (_c *Running_Output_Call) Run(run func()) *Running_Output_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Running_Output_Call) Return(_a0 string) *Running_Output_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Running_Output_Call) RunAndReturn(run func() string) *Running_Output_Call { + _c.Call.Return(run) + return _c +} + +// PID provides a mock function with no fields +func (_m *Running) PID() int { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for PID") + } + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + +// Running_PID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PID' +type Running_PID_Call struct { + *mock.Call +} + +// PID is a helper method to define mock.On call +func (_e *Running_Expecter) PID() *Running_PID_Call { + return &Running_PID_Call{Call: _e.mock.On("PID")} +} + +func (_c *Running_PID_Call) Run(run func()) *Running_PID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Running_PID_Call) Return(_a0 int) *Running_PID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Running_PID_Call) RunAndReturn(run func() int) *Running_PID_Call { + _c.Call.Return(run) + return _c +} + +// Running provides a mock function with no fields +func (_m *Running) Running() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Running") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Running_Running_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Running' +type Running_Running_Call struct { + *mock.Call +} + +// Running is a helper method to define mock.On call +func (_e *Running_Expecter) Running() *Running_Running_Call { + return &Running_Running_Call{Call: _e.mock.On("Running")} +} + +func (_c *Running_Running_Call) Run(run func()) *Running_Running_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Running_Running_Call) Return(_a0 bool) *Running_Running_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Running_Running_Call) RunAndReturn(run func() bool) *Running_Running_Call { + _c.Call.Return(run) + return _c +} + +// Signal provides a mock function with given fields: sig +func (_m *Running) Signal(sig os.Signal) error { + ret := _m.Called(sig) + + if len(ret) == 0 { + panic("no return value specified for Signal") + } + + var r0 error + if rf, ok := ret.Get(0).(func(os.Signal) error); ok { + r0 = rf(sig) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Running_Signal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Signal' +type Running_Signal_Call struct { + *mock.Call +} + +// Signal is a helper method to define mock.On call +// - sig os.Signal +func (_e *Running_Expecter) Signal(sig interface{}) *Running_Signal_Call { + return &Running_Signal_Call{Call: _e.mock.On("Signal", sig)} +} + +func (_c *Running_Signal_Call) Run(run func(sig os.Signal)) *Running_Signal_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(os.Signal)) + }) + return _c +} + +func (_c *Running_Signal_Call) Return(_a0 error) *Running_Signal_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Running_Signal_Call) RunAndReturn(run func(os.Signal) error) *Running_Signal_Call { + _c.Call.Return(run) + return _c +} + +// Stop provides a mock function with given fields: timeout, sig +func (_m *Running) Stop(timeout time.Duration, sig ...os.Signal) error { + _va := make([]interface{}, len(sig)) + for _i := range sig { + _va[_i] = sig[_i] + } + var _ca []interface{} + _ca = append(_ca, timeout) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Stop") + } + + var r0 error + if rf, ok := ret.Get(0).(func(time.Duration, ...os.Signal) error); ok { + r0 = rf(timeout, sig...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Running_Stop_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stop' +type Running_Stop_Call struct { + *mock.Call +} + +// Stop is a helper method to define mock.On call +// - timeout time.Duration +// - sig ...os.Signal +func (_e *Running_Expecter) Stop(timeout interface{}, sig ...interface{}) *Running_Stop_Call { + return &Running_Stop_Call{Call: _e.mock.On("Stop", + append([]interface{}{timeout}, sig...)...)} +} + +func (_c *Running_Stop_Call) Run(run func(timeout time.Duration, sig ...os.Signal)) *Running_Stop_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]os.Signal, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(os.Signal) + } + } + run(args[0].(time.Duration), variadicArgs...) + }) + return _c +} + +func (_c *Running_Stop_Call) Return(_a0 error) *Running_Stop_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Running_Stop_Call) RunAndReturn(run func(time.Duration, ...os.Signal) error) *Running_Stop_Call { + _c.Call.Return(run) + return _c +} + +// Wait provides a mock function with no fields +func (_m *Running) Wait() process.Result { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Wait") + } + + var r0 process.Result + if rf, ok := ret.Get(0).(func() process.Result); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(process.Result) + } + } + + return r0 +} + +// Running_Wait_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Wait' +type Running_Wait_Call struct { + *mock.Call +} + +// Wait is a helper method to define mock.On call +func (_e *Running_Expecter) Wait() *Running_Wait_Call { + return &Running_Wait_Call{Call: _e.mock.On("Wait")} +} + +func (_c *Running_Wait_Call) Run(run func()) *Running_Wait_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Running_Wait_Call) Return(_a0 process.Result) *Running_Wait_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Running_Wait_Call) RunAndReturn(run func() process.Result) *Running_Wait_Call { + _c.Call.Return(run) + return _c +} + +// NewRunning creates a new instance of Running. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRunning(t interface { + mock.TestingT + Cleanup(func()) +}) *Running { + mock := &Running{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/process/output_writer.go b/process/output_writer.go new file mode 100644 index 000000000..66f07472d --- /dev/null +++ b/process/output_writer.go @@ -0,0 +1,54 @@ +package process + +import ( + "bytes" + "io" + + contractsprocess "github.com/goravel/framework/contracts/process" +) + +func NewOutputWriter(typ contractsprocess.OutputType, handler contractsprocess.OnOutputFunc) *OutputWriter { + return &OutputWriter{ + typ: typ, + handler: handler, + buffer: bytes.NewBuffer(nil), + } +} + +type OutputWriter struct { + typ contractsprocess.OutputType + handler contractsprocess.OnOutputFunc + buffer *bytes.Buffer +} + +func (w *OutputWriter) Write(p []byte) (n int, err error) { + n = len(p) + + if _, err := w.buffer.Write(p); err != nil { + return 0, err + } + + var line []byte + for { + line, err = w.buffer.ReadBytes('\n') + + if err == io.EOF { + // No complete line found, put data back and return + w.buffer.Write(line) + return n, nil + } + + if err != nil { + return n, err + } + + // We have a complete line (including the newline) + // Remove the trailing newline before sending to handler + line = line[:len(line)-1] + + lineCopy := make([]byte, len(line)) + copy(lineCopy, line) + + w.handler(w.typ, lineCopy) + } +} diff --git a/process/output_writer_test.go b/process/output_writer_test.go new file mode 100644 index 000000000..6eb2e43b2 --- /dev/null +++ b/process/output_writer_test.go @@ -0,0 +1,155 @@ +package process + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + + contractsprocess "github.com/goravel/framework/contracts/process" +) + +func TestOutputWriter_Write_SingleLine(t *testing.T) { + var receivedType contractsprocess.OutputType + var receivedLine []byte + + writer := NewOutputWriter(contractsprocess.OutputTypeStdout, func(typ contractsprocess.OutputType, line []byte) { + receivedType = typ + receivedLine = append([]byte{}, line...) + }) + + n, err := writer.Write([]byte("hello\n")) + assert.NoError(t, err) + assert.Equal(t, 6, n) + assert.Equal(t, contractsprocess.OutputTypeStdout, receivedType) + assert.Equal(t, "hello", string(receivedLine)) +} + +func TestOutputWriter_Write_MultipleLines(t *testing.T) { + var lines []string + var types []contractsprocess.OutputType + + writer := NewOutputWriter(contractsprocess.OutputTypeStderr, func(typ contractsprocess.OutputType, line []byte) { + types = append(types, typ) + lines = append(lines, string(line)) + }) + + n, err := writer.Write([]byte("line1\nline2\nline3\n")) + assert.NoError(t, err) + assert.Equal(t, 18, n) + assert.Equal(t, 3, len(lines)) + assert.Equal(t, []string{"line1", "line2", "line3"}, lines) + assert.Equal(t, []contractsprocess.OutputType{ + contractsprocess.OutputTypeStderr, + contractsprocess.OutputTypeStderr, + contractsprocess.OutputTypeStderr, + }, types) +} + +func TestOutputWriter_Write_PartialLines(t *testing.T) { + var lines []string + + writer := NewOutputWriter(contractsprocess.OutputTypeStdout, func(typ contractsprocess.OutputType, line []byte) { + lines = append(lines, string(line)) + }) + + // Write partial line + n, err := writer.Write([]byte("partial")) + assert.NoError(t, err) + assert.Equal(t, 7, n) + assert.Empty(t, lines, "No callback for partial line") + + // Complete the line + n, err = writer.Write([]byte(" line\n")) + assert.NoError(t, err) + assert.Equal(t, 6, n) + assert.Equal(t, []string{"partial line"}, lines) +} + +func TestOutputWriter_Write_BufferHandling(t *testing.T) { + var lines []string + + writer := NewOutputWriter(contractsprocess.OutputTypeStdout, func(typ contractsprocess.OutputType, line []byte) { + lines = append(lines, string(line)) + }) + + // Write multiple chunks with partial lines + n, err := writer.Write([]byte("first")) + assert.NoError(t, err) + assert.Equal(t, 5, n) + + n, err = writer.Write([]byte(" line\nsecond")) + assert.NoError(t, err) + assert.Equal(t, 12, n) + + n, err = writer.Write([]byte(" line\n")) + assert.NoError(t, err) + assert.Equal(t, 6, n) + + n, err = writer.Write([]byte("third line without newline")) + assert.NoError(t, err) + assert.Equal(t, 26, n) + + n, err = writer.Write([]byte("\nfourth line\n")) + assert.NoError(t, err) + assert.Equal(t, 13, n) + + assert.Equal(t, []string{ + "first line", + "second line", + "third line without newline", + "fourth line", + }, lines) +} + +func TestOutputWriter_Write_EmptyLines(t *testing.T) { + var lines []string + + writer := NewOutputWriter(contractsprocess.OutputTypeStdout, func(typ contractsprocess.OutputType, line []byte) { + lines = append(lines, string(line)) + }) + + n, err := writer.Write([]byte("\n\n\n")) + assert.NoError(t, err) + assert.Equal(t, 3, n) + assert.Equal(t, []string{"", "", ""}, lines) +} + +func TestOutputWriter_Write_LineModification(t *testing.T) { + // Test that modifying the line in the callback doesn't affect future callbacks + var allLines []string + + writer := NewOutputWriter(contractsprocess.OutputTypeStdout, func(typ contractsprocess.OutputType, line []byte) { + allLines = append(allLines, string(line)) + // Modify the line - should not affect original buffer + if len(line) > 0 { + line[0] = 'X' + } + }) + + n, err := writer.Write([]byte("line1\nline2\n")) + assert.NoError(t, err) + assert.Equal(t, 12, n) + assert.Equal(t, []string{"line1", "line2"}, allLines) +} + +func TestOutputWriter_Write_LargeInput(t *testing.T) { + // Test with a large input that spans multiple internal buffer sizes + lineCount := 0 + writer := NewOutputWriter(contractsprocess.OutputTypeStdout, func(typ contractsprocess.OutputType, line []byte) { + lineCount++ + }) + + // Create a large buffer with many lines + var buf bytes.Buffer + for i := 0; i < 1000; i++ { + buf.WriteString("This is line number ") + buf.WriteString(string(rune('0' + i%10))) + buf.WriteString("\n") + } + + n, err := writer.Write(buf.Bytes()) + assert.NoError(t, err) + assert.Equal(t, buf.Len(), n) + assert.Equal(t, 1000, lineCount) +} diff --git a/process/process.go b/process/process.go new file mode 100644 index 000000000..cad3d8847 --- /dev/null +++ b/process/process.go @@ -0,0 +1,144 @@ +package process + +import ( + "bytes" + "context" + "io" + "os" + "os/exec" + "time" + + contractsprocess "github.com/goravel/framework/contracts/process" +) + +var _ contractsprocess.Process = (*Process)(nil) + +type Process struct { + ctx context.Context + env []string + input io.Reader + path string + quietly bool + onOutput contractsprocess.OnOutputFunc + timeout time.Duration + tty bool +} + +func New() *Process { + return &Process{ + ctx: context.Background(), + } +} + +func (r *Process) Env(vars map[string]string) contractsprocess.Process { + if r.env == nil { + r.env = make([]string, 0, len(vars)) + } + for k, v := range vars { + r.env = append(r.env, k+"="+v) + } + return r +} + +func (r *Process) Input(in io.Reader) contractsprocess.Process { + r.input = in + return r +} + +func (r *Process) Path(path string) contractsprocess.Process { + r.path = path + return r +} + +func (r *Process) Quietly() contractsprocess.Process { + r.quietly = true + return r +} + +func (r *Process) OnOutput(handler contractsprocess.OnOutputFunc) contractsprocess.Process { + r.onOutput = handler + return r +} + +func (r *Process) Run(name string, args ...string) (contractsprocess.Result, error) { + return r.run(name, args...) +} + +func (r *Process) Start(name string, args ...string) (contractsprocess.Running, error) { + return r.start(name, args...) +} + +func (r *Process) Timeout(timeout time.Duration) contractsprocess.Process { + r.timeout = timeout + return r +} + +func (r *Process) TTY() contractsprocess.Process { + r.tty = true + return r +} + +func (r *Process) WithContext(ctx context.Context) contractsprocess.Process { + if ctx == nil { + ctx = context.Background() + } + + r.ctx = ctx + return r +} + +func (r *Process) run(name string, args ...string) (contractsprocess.Result, error) { + running, err := r.start(name, args...) + if err != nil { + return nil, err + } + return running.Wait(), nil +} + +func (r *Process) start(name string, args ...string) (contractsprocess.Running, error) { + ctx := r.ctx + if r.timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, r.timeout) + _ = cancel + } + + cmd := exec.CommandContext(ctx, name, args...) + if r.path != "" { + cmd.Dir = r.path + } + setSysProcAttr(cmd) + + if len(r.env) > 0 { + cmd.Env = append(os.Environ(), r.env...) + } + + stdoutBuffer := &bytes.Buffer{} + stderrBuffer := &bytes.Buffer{} + + if r.input != nil { + cmd.Stdin = r.input + } else if r.tty { + cmd.Stdin = os.Stdin + } + + stdoutWriters := []io.Writer{stdoutBuffer} + stderrWriters := []io.Writer{stderrBuffer} + + if !r.quietly { + stdoutWriters = append(stdoutWriters, os.Stdout) + stderrWriters = append(stderrWriters, os.Stderr) + } + if r.onOutput != nil { + stdoutWriters = append(stdoutWriters, NewOutputWriter(contractsprocess.OutputTypeStdout, r.onOutput)) + stderrWriters = append(stderrWriters, NewOutputWriter(contractsprocess.OutputTypeStderr, r.onOutput)) + } + cmd.Stdout = io.MultiWriter(stdoutWriters...) + cmd.Stderr = io.MultiWriter(stderrWriters...) + + if err := cmd.Start(); err != nil { + return nil, err + } + + return NewRunning(cmd, stdoutBuffer, stderrBuffer), nil +} diff --git a/process/process_unix.go b/process/process_unix.go new file mode 100644 index 000000000..29ce23640 --- /dev/null +++ b/process/process_unix.go @@ -0,0 +1,13 @@ +//go:build !windows + +package process + +import ( + "os/exec" + "syscall" +) + +// setSysProcAttr sets POSIX-specific process attributes. +func setSysProcAttr(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} +} diff --git a/process/process_unix_test.go b/process/process_unix_test.go new file mode 100644 index 000000000..0be047ebb --- /dev/null +++ b/process/process_unix_test.go @@ -0,0 +1,142 @@ +//go:build !windows + +package process + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + contractsprocess "github.com/goravel/framework/contracts/process" +) + +func TestProcess_Run_Unix(t *testing.T) { + tests := []struct { + name string + args []string + setup func(p *Process) + expectOK bool + check func(t *testing.T, res *Result) + }{ + { + name: "echo to stdout", + args: []string{"sh", "-c", "printf 'hello'"}, + setup: func(p *Process) { + p.Quietly() + }, + expectOK: true, + check: func(t *testing.T, res *Result) { + assert.Equal(t, "hello", res.Output()) + assert.Equal(t, "", res.ErrorOutput()) + assert.True(t, res.Successful()) + }, + }, + { + name: "stderr and non-zero exit", + args: []string{"sh", "-c", "printf 'bad' 1>&2; exit 2"}, + setup: func(p *Process) { + p.Quietly() + }, + expectOK: true, // Run doesn't error on non-zero exit + check: func(t *testing.T, res *Result) { + assert.Equal(t, "bad", res.ErrorOutput()) + assert.Equal(t, "", res.Output()) + assert.False(t, res.Successful()) + assert.Equal(t, 2, res.ExitCode()) + }, + }, + { + name: "env vars passed to process", + args: []string{"sh", "-c", "printf \"$FOO\""}, + setup: func(p *Process) { + p.Env(map[string]string{"FOO": "BAR"}).Quietly() + }, + expectOK: true, + check: func(t *testing.T, res *Result) { + assert.Equal(t, "BAR", res.Output()) + }, + }, + { + name: "working directory changes", + args: []string{"./script.sh"}, + setup: func(p *Process) { + dir := t.TempDir() + path := filepath.Join(dir, "script.sh") + _ = os.WriteFile(path, []byte("#!/bin/sh\nprintf 'ok'\n"), 0o755) + _ = os.Chmod(path, 0o755) + p.Path(dir).Quietly() + }, + expectOK: true, + check: func(t *testing.T, res *Result) { + assert.Equal(t, "ok", res.Output()) + }, + }, + { + name: "stdin is piped", + args: []string{"sh", "-c", "cat"}, + setup: func(p *Process) { + p.Input(bytes.NewBufferString("ping")).Quietly() + }, + expectOK: true, + check: func(t *testing.T, res *Result) { + assert.Equal(t, "ping", res.Output()) + }, + }, + { + name: "timeout cancels long-running process", + args: []string{"sh", "-c", "sleep 1"}, + setup: func(p *Process) { + p.Timeout(100 * time.Millisecond).Quietly() + }, + expectOK: true, // Run doesn't error on timeout + check: func(t *testing.T, res *Result) { + assert.False(t, res.Successful()) + assert.NotEqual(t, 0, res.ExitCode()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := New() + tt.setup(p) + res, err := p.Run(tt.args[0], tt.args[1:]...) + assert.Equal(t, tt.expectOK, err == nil) + assert.NotNil(t, res) + r, ok := res.(*Result) + assert.True(t, ok, "unexpected result type") + tt.check(t, r) + }) + } +} + +func TestProcess_OnOutput_Callbacks_Unix(t *testing.T) { + var outLines, errLines [][]byte + p := New() + p.OnOutput(func(typ contractsprocess.OutputType, line []byte) { + if typ == contractsprocess.OutputTypeStdout { + outLines = append(outLines, append([]byte(nil), line...)) + } else { + errLines = append(errLines, append([]byte(nil), line...)) + } + }).Quietly() + res, err := p.Run("sh", "-c", "printf 'a\n'; printf 'b\n' 1>&2") + assert.NoError(t, err) + assert.True(t, res.Successful()) + if assert.NotEmpty(t, outLines) { + assert.Equal(t, "a", strings.TrimSpace(string(outLines[0]))) + } + if assert.NotEmpty(t, errLines) { + assert.Equal(t, "b", strings.TrimSpace(string(errLines[0]))) + } +} + +func TestProcess_Start_ErrorOnMissingCommand_Unix(t *testing.T) { + _, err := New().Quietly().Start("") + assert.Error(t, err) +} diff --git a/process/process_windows.go b/process/process_windows.go new file mode 100644 index 000000000..a62452761 --- /dev/null +++ b/process/process_windows.go @@ -0,0 +1,8 @@ +//go:build windows + +package process + +import "os/exec" + +// setSysProcAttr is a no-op on Windows; Setpgid isn't available. +func setSysProcAttr(cmd *exec.Cmd) {} diff --git a/process/process_windows_test.go b/process/process_windows_test.go new file mode 100644 index 000000000..18a4d36e5 --- /dev/null +++ b/process/process_windows_test.go @@ -0,0 +1,100 @@ +//go:build windows + +package process + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestProcess_Run_Windows(t *testing.T) { + tests := []struct { + name string + args []string + setup func(p *Process) + expectOK bool + check func(t *testing.T, res *Result) + }{ + { + name: "echo via cmd", + args: []string{"cmd", "/C", "echo hello"}, + setup: func(p *Process) { + p.Quietly() + }, + expectOK: true, + check: func(t *testing.T, res *Result) { + assert.Equal(t, "hello\r\n", res.Output()) + assert.True(t, res.Successful()) + }, + }, + { + name: "stderr and non-zero", + args: []string{"powershell", "-NoLogo", "-NoProfile", "-Command", "Write-Error 'bad'; exit 2"}, + setup: func(p *Process) { + // powershell: write-error writes to stderr and returns non-zero + p.Quietly() + }, + expectOK: true, // Run doesn't error on non-zero exit + check: func(t *testing.T, res *Result) { + assert.Contains(t, res.ErrorOutput(), "bad") + assert.False(t, res.Successful()) + assert.NotEqual(t, 0, res.ExitCode()) + }, + }, + { + name: "working directory changes", + args: []string{"cmd", "/C", "script.bat"}, + setup: func(p *Process) { + dir := t.TempDir() + path := filepath.Join(dir, "script.bat") + _ = os.WriteFile(path, []byte("@echo off\r\necho ok\r\n"), 0644) + p.Path(dir).Quietly() + }, + expectOK: true, + check: func(t *testing.T, res *Result) { + assert.Equal(t, "ok\r\n", res.Output()) + }, + }, + { + name: "stdin is piped", + args: []string{"cmd", "/C", "more"}, + setup: func(p *Process) { + p.Input(bytes.NewBufferString("ping\r\n")).Quietly() + }, + expectOK: true, + check: func(t *testing.T, res *Result) { + assert.Equal(t, "ping", strings.TrimSpace(res.Output())) + }, + }, + { + name: "timeout cancels long-running process", + args: []string{"powershell", "-NoLogo", "-NoProfile", "-Command", "Start-Sleep -Seconds 2"}, + setup: func(p *Process) { + p.Timeout(200 * time.Millisecond).Quietly() + }, + expectOK: true, // Run doesn't error on timeout + check: func(t *testing.T, res *Result) { + assert.False(t, res.Successful()) + assert.NotEqual(t, 0, res.ExitCode()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := New() + tt.setup(p) + res, err := p.Run(tt.args[0], tt.args[1:]...) + assert.Equal(t, tt.expectOK, err == nil) + r, ok := res.(*Result) + assert.True(t, ok, "unexpected result type") + tt.check(t, r) + }) + } +} diff --git a/process/result.go b/process/result.go new file mode 100644 index 000000000..ffa69c5c9 --- /dev/null +++ b/process/result.go @@ -0,0 +1,81 @@ +package process + +import ( + "strings" + + contractsprocess "github.com/goravel/framework/contracts/process" +) + +var _ contractsprocess.Result = (*Result)(nil) + +type Result struct { + exitCode int + command string + stdout string + stderr string +} + +func NewResult(exitCode int, command, stdout, stderr string) *Result { + return &Result{ + exitCode: exitCode, + command: command, + stdout: stdout, + stderr: stderr, + } +} + +func (r *Result) Successful() bool { + if r == nil { + return false + } + return r.exitCode == 0 +} + +func (r *Result) Failed() bool { + if r == nil { + return true + } + return r.exitCode != 0 +} + +func (r *Result) ExitCode() int { + if r == nil { + return -1 + } + return r.exitCode +} + +func (r *Result) Output() string { + if r == nil { + return "" + } + return r.stdout +} + +func (r *Result) ErrorOutput() string { + if r == nil { + return "" + } + return r.stderr +} + +func (r *Result) Command() string { + if r == nil { + return "" + } + return r.command +} + +func (r *Result) SeeInOutput(needle string) bool { + if r == nil || needle == "" { + return false + } + return strings.Contains(r.stdout, needle) +} + +func (r *Result) SeeInErrorOutput(needle string) bool { + if r == nil || needle == "" { + return false + } + return strings.Contains(r.stderr, needle) +} diff --git a/process/result_test.go b/process/result_test.go new file mode 100644 index 000000000..1814c9046 --- /dev/null +++ b/process/result_test.go @@ -0,0 +1,94 @@ +package process + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResultMethods_TableDriven(t *testing.T) { + tests := []struct { + name string + res *Result + expectSuccess bool + expectFailed bool + expectExitCode int + expectOutput string + expectErrOut string + expectCommand string + seeOut string + seeErr string + seeOutWant bool + seeErrWant bool + }{ + { + name: "nil receiver", + res: nil, + expectSuccess: false, + expectFailed: true, + expectExitCode: -1, + expectOutput: "", + expectErrOut: "", + expectCommand: "", + seeOut: "anything", + seeErr: "anything", + seeOutWant: false, + seeErrWant: false, + }, + { + name: "successful result", + res: NewResult(0, "echo hi", "hi", ""), + expectSuccess: true, + expectFailed: false, + expectExitCode: 0, + expectOutput: "hi", + expectErrOut: "", + expectCommand: "echo hi", + seeOut: "hi", + seeErr: "oops", + seeOutWant: true, + seeErrWant: false, + }, + { + name: "failed result with stderr", + res: NewResult(2, "cmd", "out", "err msg"), + expectSuccess: false, + expectFailed: true, + expectExitCode: 2, + expectOutput: "out", + expectErrOut: "err msg", + expectCommand: "cmd", + seeOut: "nope", + seeErr: "err", + seeOutWant: false, + seeErrWant: true, + }, + { + name: "empty needle returns false", + res: NewResult(0, "cmd", "abc", "xyz"), + expectSuccess: true, + expectFailed: false, + expectExitCode: 0, + expectOutput: "abc", + expectErrOut: "xyz", + expectCommand: "cmd", + seeOut: "", + seeErr: "", + seeOutWant: false, + seeErrWant: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expectSuccess, test.res.Successful()) + assert.Equal(t, test.expectFailed, test.res.Failed()) + assert.Equal(t, test.expectExitCode, test.res.ExitCode()) + assert.Equal(t, test.expectOutput, test.res.Output()) + assert.Equal(t, test.expectErrOut, test.res.ErrorOutput()) + assert.Equal(t, test.expectCommand, test.res.Command()) + assert.Equal(t, test.seeOutWant, test.res.SeeInOutput(test.seeOut)) + assert.Equal(t, test.seeErrWant, test.res.SeeInErrorOutput(test.seeErr)) + }) + } +} diff --git a/process/running.go b/process/running.go new file mode 100644 index 000000000..737c824b5 --- /dev/null +++ b/process/running.go @@ -0,0 +1,114 @@ +package process + +import ( + "bytes" + "os/exec" + "sync" + + contractsprocess "github.com/goravel/framework/contracts/process" +) + +var _ contractsprocess.Running = (*Running)(nil) + +type Running struct { + cmd *exec.Cmd + stdoutBuffer *bytes.Buffer + stderrBuffer *bytes.Buffer + + once sync.Once + result contractsprocess.Result + resultMu sync.RWMutex +} + +func NewRunning(cmd *exec.Cmd, stdout, stderr *bytes.Buffer) *Running { + return &Running{ + cmd: cmd, + stdoutBuffer: stdout, + stderrBuffer: stderr, + } +} + +func (r *Running) Wait() contractsprocess.Result { + r.once.Do(func() { + err := r.cmd.Wait() + res := buildResult(r, err) + r.resultMu.Lock() + r.result = res + r.resultMu.Unlock() + }) + r.resultMu.RLock() + defer r.resultMu.RUnlock() + return r.result +} + +func (r *Running) PID() int { + if r.cmd == nil || r.cmd.Process == nil { + return 0 + } + return r.cmd.Process.Pid +} + +func (r *Running) Command() string { + if r.cmd == nil { + return "" + } + return r.cmd.String() +} + +func (r *Running) Output() string { + if r.stdoutBuffer == nil { + return "" + } + return r.stdoutBuffer.String() +} + +func (r *Running) ErrorOutput() string { + if r.stderrBuffer == nil { + return "" + } + return r.stderrBuffer.String() +} + +func (r *Running) LatestOutput() string { + return lastN(r.stdoutBuffer, 4096) +} + +func (r *Running) LatestErrorOutput() string { + return lastN(r.stderrBuffer, 4096) +} + +func buildResult(r *Running, waitErr error) *Result { + exitCode := 0 + if r.cmd != nil && r.cmd.ProcessState != nil { + exitCode = r.cmd.ProcessState.ExitCode() + } else if waitErr != nil { + exitCode = -1 + } + + command := "" + if r.cmd != nil { + command = r.Command() + } + + stdout := "" + if r.stdoutBuffer != nil { + stdout = r.stdoutBuffer.String() + } + stderr := "" + if r.stderrBuffer != nil { + stderr = r.stderrBuffer.String() + } + + return NewResult(exitCode, command, stdout, stderr) +} + +func lastN(buf *bytes.Buffer, n int) string { + if buf == nil { + return "" + } + s := buf.String() + if len(s) <= n { + return s + } + return s[len(s)-n:] +} diff --git a/process/running_unix.go b/process/running_unix.go new file mode 100644 index 000000000..b4f47fa4f --- /dev/null +++ b/process/running_unix.go @@ -0,0 +1,88 @@ +//go:build !windows + +package process + +import ( + "errors" + "os" + "time" + + "golang.org/x/sys/unix" +) + +// Running actively queries the OS to see if the process still exists. +// +// NOTE: This method returns `true` for a "zombie" process (one that has terminated +// but has not been reaped by a call to `Wait()`). Due to this, a monitoring loop +// requires a concurrent call to `result.Wait()` to ensure termination. +// +// Correct Usage Example: +// +// done := make(chan struct{}) +// result, _ := process.New().Start("sleep", "2") +// +// go func() { +// defer close(done) +// // This loop will continue as long as the process is running OR a zombie. +// for result.Running() { +// fmt.Println("Process is still running...") +// time.Sleep(500 * time.Millisecond) +// } +// }() +// +// // This call is mandatory. It blocks, reaps the zombie, and allows the goroutine to exit. +// result.Wait() +// <-done // Wait for monitoring goroutine to finish. +func (r *Running) Running() bool { + if r.cmd == nil || r.cmd.Process == nil { + return false + } + + // Actively probe the OS by sending a null signal. + // A nil error means the OS found the process (running or zombie). + err := r.cmd.Process.Signal(unix.Signal(0)) + return err == nil +} + +func (r *Running) Kill() error { + return r.Signal(unix.SIGKILL) +} + +func (r *Running) Signal(sig os.Signal) error { + if r.cmd == nil || r.cmd.Process == nil { + return errors.New("process not started") + } + return r.cmd.Process.Signal(sig) +} + +// Stop gracefully stops the process by sending SIGTERM and waiting for a timeout. +func (r *Running) Stop(timeout time.Duration, sig ...os.Signal) error { + if !r.Running() { + return nil + } + + var signalToSend os.Signal = unix.SIGTERM + if len(sig) > 0 { + signalToSend = sig[0] + } + + if err := r.Signal(signalToSend); err != nil { + if errors.Is(err, os.ErrProcessDone) { + return nil + } + return err + } + + done := make(chan struct{}) + go func() { + r.Wait() + close(done) + }() + + select { + case <-time.After(timeout): + return r.Kill() + case <-done: + return nil + } +} diff --git a/process/running_unix_test.go b/process/running_unix_test.go new file mode 100644 index 000000000..c2a6f5782 --- /dev/null +++ b/process/running_unix_test.go @@ -0,0 +1,67 @@ +//go:build !windows + +package process + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/sys/unix" +) + +func TestRunning_Basics_Unix(t *testing.T) { + tests := []struct { + name string + cmd []string + check func(t *testing.T, run *Running) + }{ + { + name: "PID Command Running and Wait", + cmd: []string{"sh", "-c", "sleep 0.2"}, + check: func(t *testing.T, run *Running) { + assert.NotEqual(t, 0, run.PID()) + assert.Contains(t, run.Command(), "sleep 0.2") + assert.True(t, run.Running()) + res := run.Wait() + assert.NotNil(t, res) + assert.Equal(t, 0, res.ExitCode()) + }, + }, + { + name: "LatestOutput sizes", + cmd: []string{"sh", "-c", "yes x | head -c 5000 1>/dev/stdout; yes y | head -c 5000 1>/dev/stderr"}, + check: func(t *testing.T, run *Running) { + res := run.Wait() + assert.True(t, res.Successful()) + assert.GreaterOrEqual(t, len(run.Output()), 4096) + assert.GreaterOrEqual(t, len(run.ErrorOutput()), 4096) + assert.Equal(t, 4096, len(run.LatestOutput())) + assert.Equal(t, 4096, len(run.LatestErrorOutput())) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + r, err := New().Quietly().Start(test.cmd[0], test.cmd[1:]...) + assert.NoError(t, err) + run, ok := r.(*Running) + assert.True(t, ok, "unexpected running type") + if ok { + test.check(t, run) + } + }) + } +} + +func TestRunning_SignalAndStop_Unix(t *testing.T) { + r, err := New().Quietly().Start("sh", "-c", "sleep 10") + assert.NoError(t, err) + run := r.(*Running) + assert.True(t, run.Running()) + assert.NoError(t, run.Signal(unix.SIGTERM)) + _ = run.Stop(50 * time.Millisecond) + res := run.Wait() + assert.False(t, res.Successful()) +} diff --git a/process/running_windows.go b/process/running_windows.go new file mode 100644 index 000000000..c695ae77b --- /dev/null +++ b/process/running_windows.go @@ -0,0 +1,66 @@ +//go:build windows + +package process + +import ( + "errors" + "os" + "time" +) + +// Running actively queries the OS to see if the process still exists. +// +// NOTE: This method returns `true` for a terminated process that has not yet +// been cleaned up by a call to `Wait()`. Due to this, a monitoring loop +// requires a concurrent call to `result.Wait()` to ensure termination. +// +// Correct Usage Example: +// +// done := make(chan struct{}) +// result, _ := process.New().Start("timeout", "2") +// +// go func() { +// defer close(done) +// // This loop will continue as long as the process object exists. +// for result.Running() { +// fmt.Println("Process is still running...") +// time.Sleep(500 * time.Millisecond) +// } +// }() +// +// // This call is mandatory. It blocks, cleans up the process, and allows the goroutine to exit. +// result.Wait() +// <-done // Wait for monitoring goroutine to finish. +func (r *Running) Running() bool { + if r.cmd == nil || r.cmd.Process == nil { + return false + } + + // Actively probe the OS by trying to find the process by its PID. + // A nil error means the OS found the process. + _, err := os.FindProcess(r.cmd.Process.Pid) + return err == nil +} + +func (r *Running) Kill() error { + if r.cmd == nil || r.cmd.Process == nil { + return errors.New("process not started") + } + return r.cmd.Process.Kill() +} + +// Signal sends a signal; on Windows, only os.Interrupt and os.Kill are supported. +func (r *Running) Signal(sig os.Signal) error { + if r.cmd == nil || r.cmd.Process == nil { + return errors.New("process not started") + } + return r.cmd.Process.Signal(sig) +} + +// Stop terminates the process; on Windows, this is an alias for Kill(). +func (r *Running) Stop(timeout time.Duration, sig ...os.Signal) error { + if r.cmd == nil || r.cmd.Process == nil || !r.Running() { + return nil + } + return r.Kill() +} diff --git a/process/running_windows_test.go b/process/running_windows_test.go new file mode 100644 index 000000000..14b0075ef --- /dev/null +++ b/process/running_windows_test.go @@ -0,0 +1,73 @@ +//go:build windows + +package process + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestRunning_Basics_Windows(t *testing.T) { + tests := []struct { + name string + args []string + setup func(p *Process) + check func(t *testing.T, run *Running) + }{ + { + name: "PID Command Running and Wait", + args: []string{"powershell", "-NoLogo", "-NoProfile", "-Command", "Start-Sleep -Milliseconds 200"}, + setup: func(p *Process) {}, + check: func(t *testing.T, run *Running) { + assert.NotEqual(t, 0, run.PID()) + assert.Contains(t, run.Command(), "powershell") + assert.True(t, run.Running()) + res := run.Wait() + assert.NotNil(t, res) + assert.Equal(t, 0, res.ExitCode()) + }, + }, + { + name: "LatestOutput sizes", + args: []string{ + "powershell", "-NoLogo", "-NoProfile", "-Command", + "$s='x'*5000; [Console]::Out.Write($s); $e='y'*5000; [Console]::Error.Write($e)", + }, + setup: func(p *Process) { + // Generate large stdout/stderr using PowerShell + }, + check: func(t *testing.T, run *Running) { + _ = run.Wait() + // Windows console writes may be buffered differently; assert non-empty + assert.Greater(t, len(run.Output()), 0) + assert.Greater(t, len(run.ErrorOutput()), 0) + assert.LessOrEqual(t, len(run.LatestOutput()), 4096) + assert.LessOrEqual(t, len(run.LatestErrorOutput()), 4096) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := New() + tt.setup(p) + r, err := p.Start(tt.args[0], tt.args[1:]...) + assert.NoError(t, err) + run, ok := r.(*Running) + assert.True(t, ok, "unexpected running type") + tt.check(t, run) + }) + } +} + +func TestRunning_Stop_Windows(t *testing.T) { + r, err := New().Start("powershell", "-NoLogo", "-NoProfile", "-Command", "Start-Sleep -Seconds 10") + assert.NoError(t, err) + run := r.(*Running) + // On Windows, Stop is alias for Kill + assert.NoError(t, run.Stop(100*time.Millisecond)) + res := run.Wait() + assert.False(t, res.Successful()) +}