diff --git a/exec_with_title.go b/exec_with_title.go new file mode 100644 index 0000000..af667ef --- /dev/null +++ b/exec_with_title.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "os/exec" + "strings" +) + +// ExecWithTitle executes a command with a custom process title visible to `ps` on Linux. +// It uses `bash -c "exec -a ..."` to achieve this. +// title: The custom title for the process. +// commandAndArgs: The command and its arguments to execute. +func ExecWithTitle(title string, commandAndArgs ...string) error { + if len(commandAndArgs) == 0 { + return fmt.Errorf("no command provided to execute") + } + + // The command to run is the first element. + command := commandAndArgs[0] + // The rest are its arguments. + args := commandAndArgs[1:] + + // We need to construct the full command string for the shell. + // Using fmt.Sprintf with %q is a simple way to handle basic shell quoting. + // The final command will look like: exec -a 'My Title' 'sleep' '30' + fullCommand := fmt.Sprintf("exec -a %q %q %s", + title, + command, + strings.Join(quoteArgs(args), " "), + ) + + // The actual command we run is bash with the "-c" flag and our constructed string. + cmd := exec.Command("bash", "-c", fullCommand) + + fmt.Printf("Running command: %s\n", cmd.String()) + + // Start the process. + err := cmd.Start() + if err != nil { + return fmt.Errorf("failed to start command: %w", err) + } + + fmt.Printf("--> Process started with PID %d. Check 'ps aux | grep %d'.\n", cmd.Process.Pid, cmd.Process.Pid) + fmt.Println("--> The title should appear as:", title) + fmt.Println("Waiting for command to complete...") + + // Wait for the command to complete. + return cmd.Wait() +} + +// quoteArgs is a helper to wrap each argument in quotes for the shell. +func quoteArgs(args []string) []string { + quoted := make([]string, len(args)) + for i, arg := range args { + quoted[i] = fmt.Sprintf("%q", arg) + } + return quoted +} + +func main() { + fmt.Println("Starting a 'sleep 30' process with a custom title...") + fmt.Println("You will have 30 seconds to run 'ps' in another terminal to see it.") + + err := ExecWithTitle("My Custom Sleeper (Job #42)", "sleep", "30") + if err != nil { + fmt.Printf("\nCommand finished with error: %v\n", err) + } else { + fmt.Println("\nCommand finished successfully.") + } +} diff --git a/get_pid.go b/get_pid.go new file mode 100644 index 0000000..ce5e18d --- /dev/null +++ b/get_pid.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "os/exec" +) + +func main() { + fmt.Println("Starting a 'sleep 15' command...") + + // 1. Create the command. + cmd := exec.Command("sleep", "15") + + // 2. Start the command. This is non-blocking. + err := cmd.Start() + if err != nil { + fmt.Printf("Error starting command: %v\n", err) + return + } + + // 3. If Start() succeeded, the PID is now available. + // The check for err != nil above is critical to prevent a panic + // from a nil pointer dereference on cmd.Process. + pid := cmd.Process.Pid + fmt.Printf("--> Successfully started process with PID: %d\n", pid) + fmt.Println("--> You can verify this with 'ps aux | grep", pid, "'") + + fmt.Println("Waiting for the process to finish in the background...") + + // 4. Wait for the command to complete and release its resources. + err = cmd.Wait() + if err != nil { + fmt.Printf("Command finished with error: %v\n", err) + } else { + fmt.Println("Command finished successfully.") + } +} diff --git a/proctitle_demo.go b/proctitle_demo.go new file mode 100644 index 0000000..676f5b4 --- /dev/null +++ b/proctitle_demo.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/erikdubbelboer/gspt" +) + +func main() { + // The initial process title is os.Args[0] + fmt.Printf("PID: %d\n", os.Getpid()) + fmt.Println("Initial process title should be 'proctitle_demo'") + fmt.Println("Run 'ps -f -p ", os.Getpid(), "' in another terminal to check.") + time.Sleep(15 * time.Second) + + // Set a new process title + newTitle := "proctitle_demo (processing data)" + gspt.SetProcTitle(newTitle) + + fmt.Println("\nProcess title changed!") + fmt.Println("New title should be:", newTitle) + fmt.Println("Run 'ps -f -p ", os.Getpid(), "' again to see the change.") + + // Keep the process alive to give you time to check + for { + time.Sleep(10 * time.Second) + } +} + diff --git a/proctitle_linux.go b/proctitle_linux.go new file mode 100644 index 0000000..372ae7b --- /dev/null +++ b/proctitle_linux.go @@ -0,0 +1,138 @@ +//go:build linux + +package main + +/* +#include +#include + +// Pointers to the original argv memory block. +// These are populated by the init() constructor function before Go's main runs. +static char **argv_start = NULL; +static int argv_len = 0; + +// A C constructor function that runs before the Go runtime initializes. +// It saves the original argc and argv passed to the program. +__attribute__((constructor)) +static void init(int argc, char **argv) { + if (argc == 0 || argv == NULL || *argv == NULL) { + return; + } + + // Calculate the total length of the memory occupied by the arguments. + // This is the maximum length our new title can be. + for (int i = 0; i < argc; i++) { + // The space for the argument string plus the null terminator. + argv_len += strlen(argv[i]) + 1; + } + // The last argument is followed by a NULL pointer, so we back up one byte + // from that to get to the final null terminator of the last argument string. + argv_len--; + + argv_start = argv; +} + +// set_title performs the actual memory modification. +static void set_title(const char *title) { + // If argv_start is null, the constructor didn't run, so we can't do anything. + if (argv_start == NULL) { + return; + } + + // Check if the new title is too long. + int title_len = strlen(title); + if (title_len >= argv_len) { + // If it is, we can't safely set it, so we do nothing. + return; + } + + // Copy the new title into the argument memory. + strcpy(argv_start[0], title); + + // Zero out the rest of the original argument memory space. + // This is important to prevent old argument data from appearing in `ps`. + memset(argv_start[0] + title_len, 0, argv_len - title_len); + + // Some systems might need the pointer after the first argument to be NULL. + if (argv_start[1] != NULL) { + argv_start[1] = NULL; + } +} +*/ +import "C" + +import ( + "bytes" + "fmt" + "os" + "time" + "unsafe" +) + +// SetProcTitle sets the process title for `ps` to display. +// This is a Linux-only implementation using cgo. +// The new title cannot be longer than the original command line string. +func SetProcTitle(title string) { + // C.CString allocates memory in the C heap. We must free it. + cs := C.CString(title) + defer C.free(unsafe.Pointer(cs)) + C.set_title(cs) +} + +// GetProcTitle retrieves the current process title as seen by `ps`. +// This is a Linux-only implementation that reads the special /proc/self/cmdline file. +func GetProcTitle() (string, error) { + // On Linux, the kernel exposes the command line of a process in /proc/[pid]/cmdline. + // /proc/self is a symlink to the current process's directory. + // The arguments are separated by null bytes. + cmdlineBytes, err := os.ReadFile("/proc/self/cmdline") + if err != nil { + return "", fmt.Errorf("could not read /proc/self/cmdline: %w", err) + } + + // The process title we set is the first null-terminated string in this file. + // We find the first null byte to isolate the title. + firstNull := bytes.IndexByte(cmdlineBytes, 0) + if firstNull == -1 { + // This would be unusual, but if there are no nulls, return the whole content. + return string(cmdlineBytes), nil + } + + return string(cmdlineBytes[:firstNull]), nil +} + +func main() { + pid := os.Getpid() + fmt.Printf("PID: %d\n", pid) + + // Get and show the initial title + initialTitle, err := GetProcTitle() + if err != nil { + fmt.Fprintf(os.Stderr, "Could not get initial title: %v\n", err) + } else { + fmt.Printf("Initial title via GetProcTitle(): '%s'\n", initialTitle) + } + + fmt.Printf("--> Check it now with: ps -f -p %d\n", pid) + fmt.Println("Waiting 15 seconds before changing the title...") + time.Sleep(15 * time.Second) + + // Now, set a new title. + newTitle := "MyGoProcess (processing tasks)" + fmt.Printf("\nSetting title to: '%s'\n", newTitle) + SetProcTitle(newTitle) + + // Get and show the new title to confirm it was set + readTitle, err := GetProcTitle() + if err != nil { + fmt.Fprintf(os.Stderr, "Could not get new title: %v\n", err) + } else { + fmt.Printf("Retrieved title via GetProcTitle(): '%s'\n", readTitle) + } + + fmt.Printf("--> Check it again with: ps -f -p %d\n", pid) + fmt.Println("The process will exit in 15 seconds.") + time.Sleep(15 * time.Second) + + fmt.Println("\nDone.") +}