Merge pull request #1395 from safing/feature/tauri-migration

Add utility apis for processes required for tauri migration
This commit is contained in:
Daniel Hovie 2023-12-22 14:19:26 +01:00 committed by GitHub
commit 355a483d5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 874 additions and 18 deletions

1
.gitignore vendored
View file

@ -21,7 +21,6 @@ vendor
testing
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

View file

@ -0,0 +1,119 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
package ebpf
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
// loadBpf returns the embedded CollectionSpec for bpf.
func loadBpf() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_BpfBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load bpf: %w", err)
}
return spec, err
}
// loadBpfObjects loads bpf and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *bpfObjects
// *bpfPrograms
// *bpfMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadBpf()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// bpfSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfSpecs struct {
bpfProgramSpecs
bpfMapSpecs
}
// bpfSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfProgramSpecs struct {
EnterExecve *ebpf.ProgramSpec `ebpf:"enter_execve"`
}
// bpfMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfMapSpecs struct {
PmExecMap *ebpf.MapSpec `ebpf:"pm_exec_map"`
}
// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
bpfPrograms
bpfMaps
}
func (o *bpfObjects) Close() error {
return _BpfClose(
&o.bpfPrograms,
&o.bpfMaps,
)
}
// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
PmExecMap *ebpf.Map `ebpf:"pm_exec_map"`
}
func (m *bpfMaps) Close() error {
return _BpfClose(
m.PmExecMap,
)
}
// bpfPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfPrograms struct {
EnterExecve *ebpf.Program `ebpf:"enter_execve"`
}
func (p *bpfPrograms) Close() error {
return _BpfClose(
p.EnterExecve,
)
}
func _BpfClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed bpf_bpfeb.o
var _BpfBytes []byte

Binary file not shown.

View file

@ -0,0 +1,119 @@
// Code generated by bpf2go; DO NOT EDIT.
//go:build 386 || amd64 || amd64p32 || arm || arm64 || loong64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
package ebpf
import (
"bytes"
_ "embed"
"fmt"
"io"
"github.com/cilium/ebpf"
)
// loadBpf returns the embedded CollectionSpec for bpf.
func loadBpf() (*ebpf.CollectionSpec, error) {
reader := bytes.NewReader(_BpfBytes)
spec, err := ebpf.LoadCollectionSpecFromReader(reader)
if err != nil {
return nil, fmt.Errorf("can't load bpf: %w", err)
}
return spec, err
}
// loadBpfObjects loads bpf and converts it into a struct.
//
// The following types are suitable as obj argument:
//
// *bpfObjects
// *bpfPrograms
// *bpfMaps
//
// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
spec, err := loadBpf()
if err != nil {
return err
}
return spec.LoadAndAssign(obj, opts)
}
// bpfSpecs contains maps and programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfSpecs struct {
bpfProgramSpecs
bpfMapSpecs
}
// bpfSpecs contains programs before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfProgramSpecs struct {
EnterExecve *ebpf.ProgramSpec `ebpf:"enter_execve"`
}
// bpfMapSpecs contains maps before they are loaded into the kernel.
//
// It can be passed ebpf.CollectionSpec.Assign.
type bpfMapSpecs struct {
PmExecMap *ebpf.MapSpec `ebpf:"pm_exec_map"`
}
// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
bpfPrograms
bpfMaps
}
func (o *bpfObjects) Close() error {
return _BpfClose(
&o.bpfPrograms,
&o.bpfMaps,
)
}
// bpfMaps contains all maps after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfMaps struct {
PmExecMap *ebpf.Map `ebpf:"pm_exec_map"`
}
func (m *bpfMaps) Close() error {
return _BpfClose(
m.PmExecMap,
)
}
// bpfPrograms contains all programs after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfPrograms struct {
EnterExecve *ebpf.Program `ebpf:"enter_execve"`
}
func (p *bpfPrograms) Close() error {
return _BpfClose(
p.EnterExecve,
)
}
func _BpfClose(closers ...io.Closer) error {
for _, closer := range closers {
if err := closer.Close(); err != nil {
return err
}
}
return nil
}
// Do not access this directly.
//
//go:embed bpf_bpfel.o
var _BpfBytes []byte

Binary file not shown.

View file

@ -0,0 +1,249 @@
package ebpf
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"runtime"
"runtime/debug"
"strings"
"sync"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit"
"github.com/hashicorp/go-multierror"
"golang.org/x/sys/unix"
"github.com/safing/portbase/log"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang -cflags "-O2 -g -Wall -Werror" bpf ../programs/exec.c
// These constants are defined in `bpf/handler.c` and must be kept in sync.
const (
arglen = 32
argsize = 1024
)
var errTracerClosed = errors.New("tracer is closed")
// event contains details about each exec call, sent from the eBPF program to
// userspace through a perf ring buffer. This type must be kept in sync with
// `event_t` in `bpf/handler.c`.
type event struct {
// Details about the process being launched.
Filename [argsize]byte
Argv [arglen][argsize]byte
Argc uint32
UID uint32
GID uint32
PID uint32
// Name of the calling process.
Comm [argsize]byte
}
// Event contains data about each exec event with many fields for easy
// filtering and logging.
type Event struct {
Filename string `json:"filename"`
// Argv contains the raw argv supplied to the process, including argv[0]
// (which is equal to `filepath.Base(e.Filename)` in most circumstances).
Argv []string `json:"argv"`
// Truncated is true if we were unable to read all process arguments into
// Argv because there were more than ARGLEN arguments.
Truncated bool `json:"truncated"`
// These values are of the new process. Keep in mind that the exec call may
// fail and the PID will be released in such a case.
PID uint32 `json:"pid"`
UID uint32 `json:"uid"`
GID uint32 `json:"gid"`
// Comm is the "name" of the parent process, usually the filename of the
// executable (but not always).
Comm string `json:"comm"`
}
// Tracer is the exec tracer itself.
// It must be closed after use.
type Tracer struct {
objs bpfObjects
tp link.Link
rb *ringbuf.Reader
closeLock sync.Mutex
closed chan struct{}
}
// New instantiates all of the BPF objects into the running kernel, starts
// tracing, and returns the created Tracer. After calling this successfully, the
// caller should immediately attach a for loop running `h.Read()`.
//
// The returned Tracer MUST be closed when not needed anymore otherwise kernel
// resources may be leaked.
func New() (*Tracer, error) {
t := &Tracer{
tp: nil,
rb: nil,
closeLock: sync.Mutex{},
closed: make(chan struct{}),
}
if err := loadBpfObjects(&t.objs, nil); err != nil {
return nil, fmt.Errorf("ebpf: failed to load ebpf object: %w", err)
}
if err := t.start(); err != nil {
// Best effort.
_ = t.Close()
return nil, fmt.Errorf("start tracer: %w", err)
}
// It could be very bad if someone forgot to close this, so we'll try to
// detect when it doesn't get closed and log a warning.
stack := debug.Stack()
runtime.SetFinalizer(t, func(t *Tracer) {
err := t.Close()
if errors.Is(err, errTracerClosed) {
return
}
log.Infof("tracer was finalized but was not closed, created at: %s", stack)
log.Infof("tracers must be closed when finished with to avoid leaked kernel resources")
if err != nil {
log.Errorf("closing tracer failed: %+v", err)
}
})
return t, nil
}
// start loads the eBPF programs and maps into the kernel and starts them.
// You should immediately attach a for loop running `h.Read()` after calling
// this successfully.
func (t *Tracer) start() error {
// If we don't startup successfully, we need to make sure all of the
// stuff is cleaned up properly or we'll be leaking kernel resources.
ok := false
defer func() {
if !ok {
// Best effort.
_ = t.Close()
}
}()
// Allow the current process to lock memory for eBPF resources. This
// does nothing on 5.11+ kernels which don't need this.
err := rlimit.RemoveMemlock()
if err != nil {
return fmt.Errorf("remove memlock: %w", err)
}
// Attach the eBPF program to the `sys_enter_execve` tracepoint, which
// is triggered at the beginning of each `execve()` syscall.
t.tp, err = link.Tracepoint("syscalls", "sys_enter_execve", t.objs.EnterExecve, nil)
if err != nil {
return fmt.Errorf("open tracepoint: %w", err)
}
// Create the reader for the event ringbuf.
t.rb, err = ringbuf.NewReader(t.objs.PmExecMap)
if err != nil {
return fmt.Errorf("open ringbuf reader: %w", err)
}
ok = true
return nil
}
// Read reads an event from the eBPF program via the ringbuf, parses it and
// returns it. If the *tracer is closed during the blocked call, and error that
// wraps io.EOF will be returned.
func (t *Tracer) Read() (*Event, error) {
rb := t.rb
if rb == nil {
return nil, errors.New("ringbuf reader is not initialized, tracer may not be open or may have been closed")
}
record, err := rb.Read()
if err != nil {
if errors.Is(err, ringbuf.ErrClosed) {
return nil, fmt.Errorf("tracer closed: %w", io.EOF)
}
return nil, fmt.Errorf("read from ringbuf: %w", err)
}
// Parse the ringbuf event entry into an event structure.
var rawEvent event
err = binary.Read(bytes.NewBuffer(record.RawSample), binary.NativeEndian, &rawEvent)
if err != nil {
return nil, fmt.Errorf("parse raw ringbuf entry into event struct: %w", err)
}
ev := &Event{
Filename: unix.ByteSliceToString(rawEvent.Filename[:]),
Argv: []string{}, // populated below
Truncated: rawEvent.Argc == arglen+1,
PID: rawEvent.PID,
UID: rawEvent.UID,
GID: rawEvent.GID,
Comm: unix.ByteSliceToString(rawEvent.Comm[:]),
}
// Copy only the args we're allowed to read from the array. If we read more
// than rawEvent.Argc, we could be copying non-zeroed memory.
argc := int(rawEvent.Argc)
if argc > arglen {
argc = arglen
}
for i := 0; i < argc; i++ {
str := unix.ByteSliceToString(rawEvent.Argv[i][:])
if strings.TrimSpace(str) != "" {
ev.Argv = append(ev.Argv, str)
}
}
return ev, nil
}
// Close gracefully closes and frees all resources associated with the eBPF
// tracepoints, maps and other resources. Any blocked `Read()` operations will
// return an error that wraps `io.EOF`.
func (t *Tracer) Close() error {
t.closeLock.Lock()
defer t.closeLock.Unlock()
select {
case <-t.closed:
return errTracerClosed
default:
}
close(t.closed)
runtime.SetFinalizer(t, nil)
// Close everything started in h.Start() in reverse order.
var merr error
if t.rb != nil {
err := t.rb.Close()
if err != nil {
merr = multierror.Append(merr, fmt.Errorf("close ringbuf reader: %w", err))
}
}
if t.tp != nil {
err := t.tp.Close()
if err != nil {
merr = multierror.Append(merr, fmt.Errorf("close tracepoint: %w", err))
}
}
err := t.objs.Close()
if err != nil {
merr = multierror.Append(merr, fmt.Errorf("close eBPF objects: %w", err))
}
return merr
}

View file

@ -0,0 +1,116 @@
#include "vmlinux-x86.h"
#include "bpf/bpf_helpers.h"
#include "bpf/bpf_tracing.h"
#define ARGLEN 32 // maximum amount of args in argv we'll copy
#define ARGSIZE 1024 // maximum byte length of each arg in argv we'll copy
char __license[] SEC("license") = "GPL";
// Ring buffer for all connection events
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24);
} pm_exec_map SEC(".maps");
// This struct is defined according to
// /sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/format
struct exec_info {
u16 common_type; // offset=0, size=2
u8 common_flags; // offset=2, size=1
u8 common_preempt_count; // offset=3, size=1
s32 common_pid; // offset=4, size=4
s32 syscall_nr; // offset=8, size=4
u32 pad; // offset=12, size=4 (pad)
const u8 *filename; // offset=16, size=8 (ptr)
const u8 *const *argv; // offset=24, size=8 (ptr)
const u8 *const *envp; // offset=32, size=8 (ptr)
};
// The event struct. This struct must be kept in sync with the Golang
// counterpart.
struct event_t {
// Details about the process being launched.
u8 filename[ARGSIZE];
u8 argv[ARGLEN][ARGSIZE];
u32 argc; // set to ARGLEN + 1 if there were more than ARGLEN arguments
u32 uid;
u32 gid;
u32 pid;
// Name of the calling process.
u8 comm[ARGSIZE];
};
// Tracepoint at the top of execve() syscall.
SEC("tracepoint/syscalls/sys_enter_execve")
s32 enter_execve(struct exec_info *ctx) {
// Reserve memory for our event on the `events` ring buffer defined above.
struct event_t *event;
event = bpf_ringbuf_reserve(&pm_exec_map, sizeof(struct event_t), 0);
if (!event) {
bpf_printk("could not reserve ringbuf memory");
return 1;
}
// Store process/calling process details.
u64 uidgid = bpf_get_current_uid_gid();
u64 pidtgid = bpf_get_current_pid_tgid();
event->uid = uidgid; // uid is the first 32 bits
event->gid = uidgid >> 32; // gid is the last 32 bits NOLINT(readability-magic-numbers)
event->pid = pidtgid; // pid is the first 32 bits
s32 ret = bpf_get_current_comm(&event->comm, sizeof(event->comm));
if (ret) {
bpf_printk("could not get current comm: %d", ret);
bpf_ringbuf_discard(event, 0);
return 1;
}
// Write the filename in addition to argv[0] because the filename contains
// the full path to the file which could be more useful in some situations.
ret = bpf_probe_read_user_str(event->filename, sizeof(event->filename), ctx->filename);
if (ret < 0) {
bpf_printk("could not read filename into event struct: %d", ret);
bpf_ringbuf_discard(event, 0);
return 1;
}
// Copy everything from ctx->argv to event->argv, incrementing event->argc
// as we go.
for (s32 i = 0; i < ARGLEN; i++) {
if (!(&ctx->argv[i])) {
goto out;
}
// Copying the arg into it's own variable before copying it into
// event->argv[i] prevents memory corruption.
const u8 *argp = NULL;
ret = bpf_probe_read_user(&argp, sizeof(argp), &ctx->argv[i]);
if (ret || !argp) {
goto out;
}
// Copy argp to event->argv[i].
ret = bpf_probe_read_user_str(event->argv[i], sizeof(event->argv[i]), argp);
if (ret < 0) {
bpf_printk("read argv %d: %d", i, ret);
goto out;
}
event->argc++;
}
// This won't get hit if we `goto out` in the loop above. This is to signify
// to userspace that we couldn't copy all of the arguments because it
// exceeded ARGLEN.
event->argc++;
out:
// Write the event to the ring buffer and notify userspace. This will cause
// the `Read()` call in userspace to return if it was blocked.
bpf_ringbuf_submit(event, 0);
return 0;
}

View file

@ -1,17 +1,45 @@
package process
import (
"errors"
"fmt"
"net/http"
"strconv"
"github.com/safing/portbase/api"
"github.com/safing/portmaster/profile"
)
func registerAPIEndpoints() error {
if err := api.RegisterEndpoint(api.Endpoint{
Name: "Get Process Tag Metadata",
Description: "Get information about process tags.",
Path: "process/tags",
Read: api.PermitUser,
BelongsTo: module,
StructFunc: handleProcessTagMetadata,
Name: "Get Process Tag Metadata",
Description: "Get information about process tags.",
}); err != nil {
return err
}
if err := api.RegisterEndpoint(api.Endpoint{
Name: "Get Processes by Profile",
Description: "Get all recently active processes using the given profile",
Path: "process/list/by-profile/{source:[a-z]+}/{id:[A-z0-9-]+}",
Read: api.PermitUser,
BelongsTo: module,
StructFunc: handleGetProcessesByProfile,
}); err != nil {
return err
}
if err := api.RegisterEndpoint(api.Endpoint{
Name: "Get Process Group Leader By PID",
Description: "Load a process group leader by a child PID",
Path: "process/group-leader/{pid:[0-9]+}",
Read: api.PermitUser,
BelongsTo: module,
StructFunc: handleGetProcessGroupLeader,
}); err != nil {
return err
}
@ -37,3 +65,35 @@ func handleProcessTagMetadata(ar *api.Request) (i interface{}, err error) {
return resp, nil
}
func handleGetProcessesByProfile(ar *api.Request) (any, error) {
source := ar.URLVars["source"]
id := ar.URLVars["id"]
if id == "" || source == "" {
return nil, api.ErrorWithStatus(fmt.Errorf("missing profile source/id"), http.StatusBadRequest)
}
result := GetProcessesWithProfile(ar.Context(), profile.ProfileSource(source), id, true)
return result, nil
}
func handleGetProcessGroupLeader(ar *api.Request) (any, error) {
pid, err := strconv.ParseInt(ar.URLVars["pid"], 10, 0)
if err != nil {
return nil, api.ErrorWithStatus(err, http.StatusBadRequest)
}
process, err := GetOrFindProcess(ar.Context(), int(pid))
if err != nil {
return nil, api.ErrorWithStatus(err, http.StatusInternalServerError)
}
err = process.FindProcessGroupLeader(ar.Context())
switch {
case process.Leader() != nil:
return process.Leader(), nil
case err != nil:
return nil, api.ErrorWithStatus(err, http.StatusInternalServerError)
default:
return nil, api.ErrorWithStatus(errors.New("leader not found"), http.StatusNotFound)
}
}

View file

@ -1,7 +1,10 @@
package process
import (
"context"
"fmt"
"slices"
"strings"
"sync"
"time"
@ -10,6 +13,7 @@ import (
"github.com/safing/portbase/database"
"github.com/safing/portbase/log"
"github.com/safing/portmaster/profile"
)
const processDatabaseNamespace = "network:tree"
@ -46,6 +50,35 @@ func All() map[int]*Process {
return all
}
// GetProcessesWithProfile returns all processes that use the given profile.
// If preferProcessGroupLeader is set, it returns the process group leader instead, if available.
func GetProcessesWithProfile(ctx context.Context, profileSource profile.ProfileSource, profileID string, preferProcessGroupLeader bool) []*Process {
log.Tracer(ctx).Debugf("process: searching for processes belonging to %s", profile.MakeScopedID(profileSource, profileID))
// Get all processes that match the given profile.
procs := make([]*Process, 0, 8)
for _, p := range All() {
lp := p.profile.LocalProfile()
if lp != nil && lp.Source == profileSource && lp.ID == profileID {
if preferProcessGroupLeader && p.Leader() != nil {
procs = append(procs, p.Leader())
} else {
procs = append(procs, p)
}
}
}
// Sort and compact.
slices.SortFunc[[]*Process, *Process](procs, func(a, b *Process) int {
return strings.Compare(a.processKey, b.processKey)
})
slices.CompactFunc[[]*Process, *Process](procs, func(a, b *Process) bool {
return a.processKey == b.processKey
})
return procs
}
// Save saves the process to the internal state and pushes an update.
func (p *Process) Save() {
p.Lock()

View file

@ -29,6 +29,13 @@ func GetProcessWithProfile(ctx context.Context, pid int) (process *Process, err
return GetUnidentifiedProcess(ctx), err
}
// Get process group leader, which is the process "nearest" to the user and
// will have more/better information for finding names ans icons, for example.
err = process.FindProcessGroupLeader(ctx)
if err != nil {
log.Warningf("process: failed to get process group leader for %s: %s", process, err)
}
changed, err := process.GetProfile(ctx)
if err != nil {
log.Tracer(ctx).Errorf("process: failed to get profile for process %s: %s", process, err)

View file

@ -30,20 +30,26 @@ type Process struct {
// Process attributes.
// Don't change; safe for concurrent access.
Name string
UserID int
UserName string
UserHome string
Pid int
CreatedAt int64
Name string
UserID int
UserName string
UserHome string
Pid int
CreatedAt int64
ParentPid int
ParentCreatedAt int64
Path string
ExecName string
Cwd string
CmdLine string
FirstArg string
Env map[string]string
LeaderPid int
leader *Process
Path string
ExecName string
Cwd string
CmdLine string
FirstArg string
Env map[string]string
// unique process identifier ("Pid-CreatedAt")
processKey string
@ -91,6 +97,16 @@ func (p *Process) Profile() *profile.LayeredProfile {
return p.profile
}
// Leader returns the process group leader that is attached to the process.
// This will not trigger a new search for the process group leader, it only
// returns existing data.
func (p *Process) Leader() *Process {
p.Lock()
defer p.Unlock()
return p.leader
}
// IsIdentified returns whether the process has been identified or if it
// represents some kind of unidentified process.
func (p *Process) IsIdentified() bool {
@ -246,7 +262,7 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
// TODO: User Home
// new.UserHome, err =
// Parent process id
// Parent process ID
ppid, err := pInfo.PpidWithContext(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get PPID for p%d: %w", pInfo.Pid, err)
@ -263,6 +279,19 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
process.ParentCreatedAt = parentCreatedAt
}
// Leader process ID
// Get process group ID to find group leader, which is the process "nearest"
// to the user and will have more/better information for finding names and
// icons, for example.
leaderPid, err := GetProcessGroupID(ctx, process.Pid)
if err != nil {
// Fail gracefully.
log.Warningf("process: failed to get process group ID for p%d: %s", process.Pid, err)
process.LeaderPid = UndefinedProcessID
} else {
process.LeaderPid = leaderPid
}
// Path
process.Path, err = pInfo.ExeWithContext(ctx)
if err != nil {

View file

@ -3,5 +3,21 @@
package process
import (
"context"
)
// SystemProcessID is the PID of the System/Kernel itself.
const SystemProcessID = 0
// GetProcessGroupLeader returns the process that leads the process group.
// Returns nil on unsupported platforms.
func (p *Process) FindProcessGroupLeader(ctx context.Context) error {
return nil
}
// GetProcessGroupID returns the process group ID of the given PID.
// Returns undefined process ID on unsupported platforms.
func GetProcessGroupID(ctx context.Context, pid int) (int, error) {
return UndefinedProcessID, nil
}

View file

@ -1,4 +1,96 @@
package process
// SystemProcessID is the PID of the System/Kernel itself.
const SystemProcessID = 0
import (
"context"
"fmt"
"syscall"
"github.com/safing/portbase/log"
)
const (
// SystemProcessID is the PID of the System/Kernel itself.
SystemProcessID = 0
// SystemInitID is the PID of the system init process.
SystemInitID = 1
)
// FindProcessGroupLeader returns the process that leads the process group.
// Returns nil when process ID is not valid (or virtual).
// If the process group leader is found, it is set on the process.
// If that process does not exist anymore, then the highest existing parent process is returned.
// If an error occurs, the best match is set.
func (p *Process) FindProcessGroupLeader(ctx context.Context) error {
p.Lock()
defer p.Unlock()
// Return the leader if we already have it.
if p.leader != nil {
return nil
}
// Check if we have the process group leader PID.
if p.LeaderPid == UndefinedProcessID {
return nil
}
// Return nil if we already are the leader.
if p.LeaderPid == p.Pid {
return nil
}
// Get process leader process.
leader, err := GetOrFindProcess(ctx, p.LeaderPid)
if err == nil {
p.leader = leader
log.Tracer(ctx).Debugf("process: found process leader of %d: pid=%d pgid=%d", p.Pid, leader.Pid, leader.LeaderPid)
return nil
}
// If we can't get the process leader process, it has likely already exited.
// In that case, find the highest existing parent process within the process group.
var (
nextParentPid = p.ParentPid
lastParent *Process
)
for {
// Get next parent.
parent, err := GetOrFindProcess(ctx, nextParentPid)
if err != nil {
p.leader = lastParent
return fmt.Errorf("failed to find parent %d: %w", nextParentPid, err)
}
// Check if we are ready to return.
switch {
case parent.Pid == p.LeaderPid:
// Found the process group leader!
p.leader = parent
return nil
case parent.LeaderPid != p.LeaderPid:
// We are leaving the process group. Return the previous parent.
p.leader = lastParent
log.Tracer(ctx).Debugf("process: found process leader (highest parent) of %d: pid=%d pgid=%d", p.Pid, parent.Pid, parent.LeaderPid)
return nil
case parent.ParentPid == SystemProcessID,
parent.ParentPid == SystemInitID:
// Next parent is system or init.
// Use current parent.
p.leader = parent
log.Tracer(ctx).Debugf("process: found process leader (highest parent) of %d: pid=%d pgid=%d", p.Pid, parent.Pid, parent.LeaderPid)
return nil
}
// Check next parent.
lastParent = parent
nextParentPid = parent.ParentPid
}
}
// GetProcessGroupID returns the process group ID of the given PID.
func GetProcessGroupID(ctx context.Context, pid int) (int, error) {
return syscall.Getpgid(pid)
}

View file

@ -1,4 +1,21 @@
package process
import (
"context"
)
// SystemProcessID is the PID of the System/Kernel itself.
const SystemProcessID = 4
// GetProcessGroupLeader returns the process that leads the process group.
// Returns nil on Windows, as it does not have process groups.
func (p *Process) FindProcessGroupLeader(ctx context.Context) error {
// TODO: Get "main" process of process job object.
return nil
}
// GetProcessGroupID returns the process group ID of the given PID.
// Returns the undefined process ID on Windows, as it does not have process groups.
func GetProcessGroupID(ctx context.Context, pid int) (int, error) {
return UndefinedProcessID, nil
}