diff --git a/cmd/cmd.go b/cmd/cmd.go index 5d835409f..290b5da7a 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -75,5 +75,9 @@ func New(ctx context.Context) *cobra.Command { cli.AddCommand(sync.New()) cli.AddCommand(version.New()) + cli.AddCommand(&cobra.Command{ + Use: "stream-", + }) + return cli } diff --git a/cmd/selftest/print_stdin.go b/cmd/selftest/print_stdin.go new file mode 100644 index 000000000..a864f6c4a --- /dev/null +++ b/cmd/selftest/print_stdin.go @@ -0,0 +1,26 @@ +package selftest + +import ( + "os" + + "github.com/spf13/cobra" +) + +const ( + PrintStdinParentPid = "DATABRICKS_CLI_PRINT_STDIN_PARENT_PID" +) + +// TODO CONTINUE: Write command that wait for each other via the PID. +// Ensure to check the process name as the PID otherwise can be reused pretty +// quick. + +func newPrintStdin() *cobra.Command { + return &cobra.Command{ + Use: "print-stdin", + RunE: func(cmd *cobra.Command, args []string) error { + if os.Getenv(PrintStdinParentPid) != "" { + return nil + } + }, + } +} diff --git a/cmd/selftest/selftest.go b/cmd/selftest/selftest.go new file mode 100644 index 000000000..5c487e783 --- /dev/null +++ b/cmd/selftest/selftest.go @@ -0,0 +1,15 @@ +package selftest + +import ( + "github.com/spf13/cobra" +) + +func New() *cobra.Command { + cmd := &cobra.Command{ + Use: "selftest", + Short: "Non functional CLI commands that are useful for testing", + } + + cmd.AddCommand(newPrintStdin()) + return cmd +} diff --git a/libs/daemon/daemon.go b/libs/daemon/daemon.go new file mode 100644 index 000000000..aeafa0847 --- /dev/null +++ b/libs/daemon/daemon.go @@ -0,0 +1,75 @@ +package daemon + +import ( + "fmt" + "io" + "os" + "os/exec" + "strconv" +) + +type Daemon struct { + // If provided, the child process will create a pid file at this path. + PidFilePath string + + // Environment variables to set in the child process. + Env []string + + // Arguments to pass to the child process. + Args []string + + cmd *exec.Cmd + stdin io.WriteCloser +} + +func (d *Daemon) Start() error { + cli, err := os.Executable() + if err != nil { + return err + } + + d.cmd = exec.Command(cli, d.Args...) + d.cmd.Env = d.Env + d.cmd.SysProcAttr = sysProcAttr() + + d.stdin, err = d.cmd.StdinPipe() + if err != nil { + return fmt.Errorf("failed to get stdin pipe: %w", err) + } + + err = d.cmd.Start() + if err != nil { + return err + } + + if d.PidFilePath != "" { + err = os.WriteFile(d.PidFilePath, []byte(strconv.Itoa(d.cmd.Process.Pid)), 0o644) + if err != nil { + return fmt.Errorf("failed to write pid file: %w", err) + } + } + + return nil +} + +func (d *Daemon) Release() error { + if d.PidFilePath != "" { + err := os.Remove(d.PidFilePath) + if err != nil { + return fmt.Errorf("failed to remove pid file: %w", err) + } + } + + if d.stdin != nil { + err := d.stdin.Close() + if err != nil { + return fmt.Errorf("failed to close stdin: %w", err) + } + } + + if d.cmd == nil { + return nil + } + + return d.cmd.Process.Release() +} diff --git a/libs/daemon/daemon_unix.go b/libs/daemon/daemon_unix.go new file mode 100644 index 000000000..75bafdb84 --- /dev/null +++ b/libs/daemon/daemon_unix.go @@ -0,0 +1,24 @@ +//go:build linux || darwin + +package daemon + +import "syscall" + +// References: +// 1. linux: https://go.dev/src/syscall/exec_linux.go +// 2. macos (arm): https://go.dev/src/syscall/exec_libc2.go +func sysProcAttr() *syscall.SysProcAttr { + return &syscall.SysProcAttr{ + // Create a new session for the child process. This ensures that the daemon + // is not terminated when the parent session is closed. This can happen + // for example when a ssh session is terminated. + // TODO: Test this. + Setsid: true, + Noctty: true, + + // Start a new process group for the child process. This ensures that + // termination signals to the parent's process group are not propagated to + // the child process. + Setpgid: true, + } +} diff --git a/libs/daemon/daemon_windows.go b/libs/daemon/daemon_windows.go new file mode 100644 index 000000000..ab88391fc --- /dev/null +++ b/libs/daemon/daemon_windows.go @@ -0,0 +1,15 @@ +//go:build windows + +package daemon + +import ( + "syscall" + + "golang.org/x/sys/windows" +) + +func sysProcAttr() *syscall.SysProcAttr { + return &syscall.SysProcAttr{ + CreationFlags: windows.CREATE_NEW_PROCESS_GROUP | windows.DETACHED_PROCESS | windows.CREATE_NO_WINDOW, + } +}