feat: Add Go examples for process management
- Add Linux-specific cgo functions to set and get process titles. - Add a demonstration of getting a child process's PID. - Add an example of using 'exec -a' to set a child process title.
This commit is contained in:
parent
a9692b87f1
commit
f080114325
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
// 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.")
|
||||||
|
}
|
Loading…
Reference in New Issue