2 Commits

Author SHA1 Message Date
brent saner
241a46c9b4 commit this WIP stuff to a feature branch 2025-12-06 19:19:53 -05:00
brent saner
803be548cf v1.15.0
* IDState cleaned up. Should work on all *NIXes now.
* Can now get IDState of arbitrary PID.
* Shuffled some env stuff around.
2025-11-07 23:11:47 -05:00
22 changed files with 1408 additions and 606 deletions

11
consts_nix.go Normal file
View File

@@ -0,0 +1,11 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
package sysutils
const (
envSudoCmd string = "SUDO_COMMAND"
envSudoHome string = "SUDO_HOME"
envSudoGrp string = "SUDO_GID"
envSudoUid string = "SUDO_UID"
envSudoUname string = "SUDO_USER"
)

View File

@@ -1,15 +1,22 @@
package envs
import (
"regexp"
)
"os"
// Compiled regex patterns.
var (
reMaybeInt *regexp.Regexp = regexp.MustCompile(`^(?P<sign>\+|-)[0-9]+$`)
reMaybeFloat *regexp.Regexp = regexp.MustCompile(`(?P<sign>\+|-)?[0-9]+\.[0-9]+$`)
"github.com/shirou/gopsutil/v4/process"
"r00t2.io/sysutils/internal"
)
var (
StructTagInterpolate string = "envsub"
)
var (
defEnv *StaticEnv = &StaticEnv{
dynamic: true,
self: true,
// don't need a process.NewProcess since the only extra thing it does is check if the PID exists.
proc: &process.Process{Pid: int32(os.Getpid())},
envVars: internal.EnvListToMap(os.Environ()),
}
)

View File

@@ -1,22 +1,88 @@
package envs
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"math"
"os"
"reflect"
"strings"
"sync"
"r00t2.io/goutils/multierr"
"r00t2.io/goutils/structutils"
"github.com/shirou/gopsutil/v4/process"
"r00t2.io/sysutils/errs"
"r00t2.io/sysutils/internal"
"r00t2.io/sysutils/paths"
)
/*
Current returns a *copy* of the StaticEnv for this current process' environment.
It is set to dynamically refresh with strictRefresh mode set to false.
(see [NewEnvFromPid] docs for what these do/mean.)
Assuming permissions haven't wildly gone silly during runtime, it shouldn't ever
have issues with dynamic refreshing.
It will never panic regardless.
*/
func Current() (s *StaticEnv) {
s = &StaticEnv{
dynamic: defEnv.dynamic,
envVars: defEnv.GetEnvMap(),
}
for k, v := range defEnv.envVars {
s.envVars[k] = v
}
return
}
/*
NewEnvFromMap returns a [StaticEnv] from a fixed list of environment variables.
This is primarily useful for mocking and other tests.
*/
func NewEnvFromMap(envMap map[string]string) (s *StaticEnv, err error) {
if envMap == nil {
err = errs.ErrNilPtr
}
s = &StaticEnv{
envVars: envMap,
}
return
}
/*
NewEnvFromPid returns a [StaticEnv] from a given PID.
If dynamicRefresh is true, the env vars will be refreshed from the process
on every method call.
Note that this will obviously cause errors/panics if the process it binds to disappears
during runtime,
or
*/
func NewEnvFromPid(pid uint, dynamicRefresh, strictRefresh bool) (s *StaticEnv, err error) {
if pid > math.MaxInt32 {
err = errs.ErrHighPid
return
}
s = &StaticEnv{
dynamic: dynamicRefresh,
}
if s.proc, err = process.NewProcess(int32(pid)); err != nil {
return
}
if err = s.Refresh(); err != nil {
return
}
// Test the ability to attach to procs.
err = s.platChecks()
s.strict = strictRefresh
return
}
/*
DefEnv operates like Python's .get() method on dicts (maps);
if the environment variable specified by key does not exist/is not specified,
@@ -34,7 +100,7 @@ func DefEnv(key, fallback string) (value string) {
return
}
// DefEnvBlank is like DefEnv but will ADDITIONALLY/ALSO apply fallback if key is *defined/exists but is an empty string*.
// DefEnvBlank is like [DefEnv] but will ADDITIONALLY/ALSO apply fallback if key is *defined/exists but is an empty string*.
func DefEnvBlank(key, fallback string) (value string) {
value = DefEnv(key, fallback)
@@ -45,7 +111,7 @@ func DefEnvBlank(key, fallback string) (value string) {
return
}
// GetEnvErr returns the value of key if it exists. If it does not exist, err will be an EnvErrNoVal.
// GetEnvErr returns the value of key if it exists. If it does not exist, err will be an [EnvErrNoVal].
func GetEnvErr(key string) (value string, err error) {
var exists bool
@@ -61,13 +127,13 @@ func GetEnvErr(key string) (value string, err error) {
}
/*
GetEnvErrNoBlank behaves exactly like GetEnvErr with the
GetEnvErrNoBlank behaves exactly like [GetEnvErr] with the
additional stipulation that the value must not be empty.
An error for a value that is non-empty but whitespace only (e.g. VARNM="\t")
can be returned if ignoreWhitespace == true.
(If it is, an EnvErrNoVal will also be returned.)
(If it is, an [EnvErrNoVal] will also be returned.)
*/
func GetEnvErrNoBlank(key string, ignoreWhitespace bool) (value string, err error) {
@@ -98,7 +164,19 @@ func GetEnvMap() (envVars map[string]string) {
var envList []string = os.Environ()
envVars = envListToMap(envList)
envVars = internal.EnvListToMap(envList)
return
}
// GetEnvMust wraps GetEnvErr but will panic on error.
func GetEnvMust(key string) (value string) {
var err error
if value, err = GetEnvErr(key); err != nil {
panic(err)
}
return
}
@@ -108,20 +186,20 @@ GetEnvMapNative returns a map of all environment variables, but attempts to "nat
All values are interfaces. It is up to the caller to typeswitch them to proper types.
Note that the PATH/Path environment variable (for *Nix and Windows, respectively) will be
a []string (as per GetPathEnv). No other env vars, even if they contain os.PathListSeparator,
a []string (as per [GetPathEnv]). No other env vars, even if they contain [os.PathListSeparator],
will be transformed to a slice or the like.
If an error occurs during parsing the path env var, it will be rendered as a string.
All number types will attempt to be their 64-bit version (i.e. int64, uint64, float64, etc.).
If a type cannot be determined for a value, its string form will be used
(as it would be found in GetEnvMap).
(as it would be found in [GetEnvMap]).
*/
func GetEnvMapNative() (envMap map[string]interface{}) {
var stringMap map[string]string = GetEnvMap()
envMap = nativizeEnvMap(stringMap)
envMap = internal.NativizeEnvMap(stringMap)
return
}
@@ -144,7 +222,7 @@ If val is "" and ok is true, this means that one of the specified variable names
set but is set to an empty value. If ok is false, none of the specified variables
are set.
It is a thin wrapper around GetFirstWithRef.
It is a thin wrapper around [GetFirstWithRef].
*/
func GetFirst(varNames []string) (val string, ok bool) {
@@ -154,7 +232,7 @@ func GetFirst(varNames []string) (val string, ok bool) {
}
/*
GetFirstWithRef behaves exactly like GetFirst, but with an additional returned value, idx,
GetFirstWithRef behaves exactly like [GetFirst], but with an additional returned value, idx,
which specifies the index in varNames in which a set variable was found. e.g. if:
GetFirstWithRef([]string{"FOO", "FOOBAR", "FOOBAZ"})
@@ -182,16 +260,10 @@ func GetFirstWithRef(varNames []string) (val string, ok bool, idx int) {
// GetPathEnv returns a slice of the PATH variable's items.
func GetPathEnv() (pathList []string, err error) {
var pathVar string = internal.GetPathEnvName()
pathList = make([]string, 0)
for _, p := range strings.Split(os.Getenv(pathVar), string(os.PathListSeparator)) {
if err = paths.RealPath(&p); err != nil {
return
}
pathList = append(pathList, p)
if pathList, err = internal.GetPathEnv(); err != nil {
return
}
return
}
@@ -201,60 +273,34 @@ It gets the environment variables of a given process' PID.
*/
func GetPidEnvMap(pid uint32) (envMap map[string]string, err error) {
var envBytes []byte
var envList []string
var envArr [][]byte
var procPath string
var exists bool
envMap = make(map[string]string)
procPath = fmt.Sprintf("/proc/%v/environ", pid)
if exists, err = paths.RealPathExists(&procPath); err != nil {
if envMap, err = internal.GetPidEnvMap(pid); err != nil {
return
}
if !exists {
err = errors.New(fmt.Sprintf("information for pid %v does not exist", pid))
return
}
if envBytes, err = ioutil.ReadFile(procPath); err != nil {
return
}
envArr = bytes.Split(envBytes, []byte{0x0})
envList = make([]string, len(envArr))
for idx, b := range envArr {
envList[idx] = string(b)
}
envMap = envListToMap(envList)
return
}
/*
GetPidEnvMapNative, like GetEnvMapNative, returns a map of all environment variables, but attempts to "nativize" them.
GetPidEnvMapNative returns a map of all environment variables (like [GetEnvMapNative]), but attempts to "nativize" them.
All values are interfaces. It is up to the caller to typeswitch them to proper types.
See the documentation for GetEnvMapNative for details.
See the documentation for [GetEnvMapNative] for details.
*/
func GetPidEnvMapNative(pid uint32) (envMap map[string]interface{}, err error) {
var stringMap map[string]string
if stringMap, err = GetPidEnvMap(pid); err != nil {
if stringMap, err = internal.GetPidEnvMap(pid); err != nil {
return
}
envMap = nativizeEnvMap(stringMap)
envMap = internal.NativizeEnvMap(stringMap)
return
}
/*
HasEnv is much like os.LookupEnv, but only returns a boolean for
HasEnv is much like [os.LookupEnv], but only returns a boolean indicating
if the environment variable key exists or not.
This is useful anywhere you may need to set a boolean in a func call
@@ -280,7 +326,7 @@ and performs variable substitution on strings from environment variables.
It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and,
if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%).
For structs, the tag name used can be changed by setting the StructTagInterpolate
For structs, the tag name used can be changed by setting the [StructTagInterpolate]
variable in this submodule; the default is `envsub`.
If the tag value is "-", the field will be skipped.
For map fields within structs etc., the default is to apply interpolation to both keys and values.
@@ -347,9 +393,9 @@ from environment variables.
It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and,
if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%).
If s is nil, nothing will be done and err will be ErrNilPtr.
If s is nil, nothing will be done and err will be [errs.ErrNilPtr].
This is a standalone function that is much more performant than Interpolate
This is a standalone function that is much more performant than [Interpolate]
at the cost of rigidity.
*/
func InterpolateString(s *string) (err error) {
@@ -368,312 +414,3 @@ func InterpolateString(s *string) (err error) {
return
}
// interpolateMap is used by Interpolate for maps. v should be a reflect.Value of a map.
func interpolateMap(v reflect.Value) (err error) {
var kVal reflect.Value
var vVal reflect.Value
var newMap reflect.Value
var wg sync.WaitGroup
var numJobs int
var errChan chan error
var doneChan chan bool = make(chan bool, 1)
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
var t reflect.Type = v.Type()
var kind reflect.Kind = t.Kind()
if kind != reflect.Map {
err = errs.ErrBadType
return
}
if v.IsNil() || v.IsZero() || !v.IsValid() {
return
}
numJobs = v.Len()
errChan = make(chan error, numJobs)
wg.Add(numJobs)
newMap = reflect.MakeMap(v.Type())
for _, kVal = range v.MapKeys() {
vVal = v.MapIndex(kVal)
go func(key, val reflect.Value) {
var mapErr error
var newKey reflect.Value
var newVal reflect.Value
newKey = reflect.New(key.Type()).Elem()
newVal = reflect.New(val.Type()).Elem()
newKey.Set(key.Convert(newKey.Type()))
newVal.Set(val.Convert(newVal.Type()))
defer wg.Done()
// key
if key.Kind() == reflect.String {
if mapErr = interpolateStringReflect(newKey); mapErr != nil {
errChan <- mapErr
return
}
} else {
if mapErr = interpolateValue(newKey); mapErr != nil {
errChan <- mapErr
return
}
}
// value
if val.Kind() == reflect.String {
if mapErr = interpolateStringReflect(newVal); mapErr != nil {
errChan <- mapErr
return
}
} else {
if mapErr = interpolateValue(newVal); mapErr != nil {
errChan <- mapErr
return
}
}
newMap.SetMapIndex(newKey.Convert(key.Type()), newVal.Convert(key.Type()))
}(kVal, vVal)
}
go func() {
wg.Wait()
close(errChan)
doneChan <- true
}()
<-doneChan
for i := 0; i < numJobs; i++ {
if err = <-errChan; err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
v.Set(newMap.Convert(v.Type()))
return
}
// interpolateSlice is used by Interpolate for slices and arrays. v should be a reflect.Value of a slice/array.
func interpolateSlice(v reflect.Value) (err error) {
var wg sync.WaitGroup
var errChan chan error
var numJobs int
var doneChan chan bool = make(chan bool, 1)
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
var t reflect.Type = v.Type()
var kind reflect.Kind = t.Kind()
switch kind {
case reflect.Slice:
if v.IsNil() || v.IsZero() || !v.IsValid() {
return
}
case reflect.Array:
if v.IsZero() || !v.IsValid() {
return
}
default:
err = errs.ErrBadType
return
}
numJobs = v.Len()
errChan = make(chan error, numJobs)
wg.Add(numJobs)
for i := 0; i < v.Len(); i++ {
go func(idx int) {
var sErr error
defer wg.Done()
if v.Index(idx).Kind() == reflect.String {
if sErr = interpolateStringReflect(v.Index(idx)); sErr != nil {
errChan <- sErr
return
}
} else {
if sErr = interpolateValue(v.Index(idx)); sErr != nil {
errChan <- sErr
return
}
}
}(i)
}
go func() {
wg.Wait()
close(errChan)
doneChan <- true
}()
<-doneChan
for i := 0; i < numJobs; i++ {
if err = <-errChan; err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
return
}
// interpolateStringReflect is used for structs/nested strings using reflection.
func interpolateStringReflect(v reflect.Value) (err error) {
var strVal string
if v.Kind() != reflect.String {
err = errs.ErrBadType
return
}
if strVal, err = interpolateString(v.String()); err != nil {
return
}
v.Set(reflect.ValueOf(strVal).Convert(v.Type()))
return
}
// interpolateStruct is used by Interpolate for structs. v should be a reflect.Value of a struct.
func interpolateStruct(v reflect.Value) (err error) {
var field reflect.StructField
var fieldVal reflect.Value
var wg sync.WaitGroup
var errChan chan error
var numJobs int
var doneChan chan bool = make(chan bool, 1)
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
var t reflect.Type = v.Type()
var kind reflect.Kind = t.Kind()
if kind != reflect.Struct {
err = errs.ErrBadType
return
}
numJobs = v.NumField()
wg.Add(numJobs)
errChan = make(chan error, numJobs)
for i := 0; i < v.NumField(); i++ {
field = t.Field(i)
fieldVal = v.Field(i)
go func(f reflect.StructField, fv reflect.Value) {
var fErr error
defer wg.Done()
if fErr = interpolateStructField(f, fv); fErr != nil {
errChan <- fErr
return
}
}(field, fieldVal)
}
go func() {
wg.Wait()
close(errChan)
doneChan <- true
}()
<-doneChan
for i := 0; i < numJobs; i++ {
if err = <-errChan; err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
return
}
// interpolateStructField interpolates a struct field.
func interpolateStructField(field reflect.StructField, v reflect.Value) (err error) {
var parsedTagOpts map[string]bool
if !v.CanSet() {
return
}
// Skip if explicitly instructed to do so.
parsedTagOpts = structutils.TagToBoolMap(field, StructTagInterpolate, structutils.TagMapTrim)
if parsedTagOpts["-"] {
return
}
if v.Kind() == reflect.Ptr {
err = interpolateStructField(field, v.Elem())
} else {
err = interpolateValue(v)
}
return
}
// interpolateValue is a dispatcher for a reflect value.
func interpolateValue(v reflect.Value) (err error) {
var kind reflect.Kind = v.Kind()
switch kind {
case reflect.Ptr:
if v.IsNil() || v.IsZero() || !v.IsValid() {
return
}
v = v.Elem()
if err = interpolateValue(v); err != nil {
return
}
case reflect.String:
if err = interpolateStringReflect(v); err != nil {
return
}
return
case reflect.Slice, reflect.Array:
if err = interpolateSlice(v); err != nil {
}
case reflect.Map:
if err = interpolateMap(v); err != nil {
return
}
case reflect.Struct:
if err = interpolateStruct(v); err != nil {
return
}
}
return
}

697
envs/funcs_staticenv.go Normal file
View File

@@ -0,0 +1,697 @@
package envs
import (
"os"
"reflect"
"strings"
"sync"
"r00t2.io/goutils/multierr"
"r00t2.io/goutils/structutils"
"r00t2.io/sysutils/errs"
"r00t2.io/sysutils/internal"
)
/*
DefEnv operates like Python's .get() method on dicts (maps);
if the environment variable specified by key does not exist/is not specified,
then the value specified by fallback will be returned instead
otherwise key's value is returned.
*/
func (s *StaticEnv) DefEnv(key, fallback string) (value string) {
var exists bool
if value, exists = os.LookupEnv(key); !exists {
value = fallback
}
return
}
// DefEnvBlank is like [DefEnv] but will ADDITIONALLY/ALSO apply fallback if key is *defined/exists but is an empty string*.
func (s *StaticEnv) DefEnvBlank(key, fallback string) (value string) {
value = DefEnv(key, fallback)
if value == "" {
value = fallback
}
return
}
// GetEnvErr returns the value of key if it exists. If it does not exist, err will be an [EnvErrNoVal].
func (s *StaticEnv) GetEnvErr(key string) (value string, err error) {
var exists bool
if value, exists = os.LookupEnv(key); !exists {
err = &EnvErrNoVal{
VarName: key,
}
return
}
return
}
/*
GetEnvErrNoBlank behaves exactly like [GetEnvErr] with the
additional stipulation that the value must not be empty.
An error for a value that is non-empty but whitespace only (e.g. VARNM="\t")
can be returned if ignoreWhitespace == true.
(If it is, an [EnvErrNoVal] will also be returned.)
*/
func (s *StaticEnv) GetEnvErrNoBlank(key string, ignoreWhitespace bool) (value string, err error) {
var exists bool
var e *EnvErrNoVal = &EnvErrNoVal{
VarName: key,
WasRequiredNonEmpty: true,
IgnoreWhitespace: ignoreWhitespace,
}
if value, exists = os.LookupEnv(key); !exists {
err = e
return
} else {
e.WasFound = true
e.WasWhitespace = (strings.TrimSpace(value) == "") && (value != "")
if ignoreWhitespace && e.WasWhitespace {
err = e
return
}
}
return
}
// GetEnvMap returns a map of all environment variables. All values are strings.
func (s *StaticEnv) GetEnvMap() (envVars map[string]string) {
var envList []string = os.Environ()
envVars = internal.EnvListToMap(envList)
return
}
/*
GetEnvMapNative returns a map of all environment variables, but attempts to "nativize" them.
All values are interfaces. It is up to the caller to typeswitch them to proper types.
Note that the PATH/Path environment variable (for *Nix and Windows, respectively) will be
a []string (as per [GetPathEnv]). No other env vars, even if they contain [os.PathListSeparator],
will be transformed to a slice or the like.
If an error occurs during parsing the path env var, it will be rendered as a string.
All number types will attempt to be their 64-bit version (i.e. int64, uint64, float64, etc.).
If a type cannot be determined for a value, its string form will be used
(as it would be found in [GetEnvMap]).
*/
func (s *StaticEnv) GetEnvMapNative() (envMap map[string]interface{}) {
var stringMap map[string]string = GetEnvMap()
envMap = internal.NativizeEnvMap(stringMap)
return
}
/*
GetFirst gets the first instance if populated/set occurrence of varNames.
For example, if you have three potential env vars, FOO, FOOBAR, FOOBARBAZ,
and want to follow the logic flow of:
1.) Check if FOO is set. If not,
2.) Check if FOOBAR is set. If not,
3.) Check if FOOBARBAZ is set.
Then this would be specified as:
GetFirst([]string{"FOO", "FOOBAR", "FOOBARBAZ"})
If val is "" and ok is true, this means that one of the specified variable names IS
set but is set to an empty value. If ok is false, none of the specified variables
are set.
It is a thin wrapper around [GetFirstWithRef].
*/
func (s *StaticEnv) GetFirst(varNames []string) (val string, ok bool) {
val, ok, _ = GetFirstWithRef(varNames)
return
}
/*
GetFirstWithRef behaves exactly like [GetFirst], but with an additional returned value, idx,
which specifies the index in varNames in which a set variable was found. e.g. if:
GetFirstWithRef([]string{"FOO", "FOOBAR", "FOOBAZ"})
is called and FOO is not set but FOOBAR is, idx will be 1.
If ok is false, idx will always be -1 and should be ignored.
*/
func (s *StaticEnv) GetFirstWithRef(varNames []string) (val string, ok bool, idx int) {
idx = -1
for i, vn := range varNames {
if HasEnv(vn) {
ok = true
idx = i
val = os.Getenv(vn)
return
}
}
return
}
// GetPathEnv returns a slice of the PATH variable's items.
func (s *StaticEnv) GetPathEnv() (pathList []string, err error) {
if pathList, err = internal.GetPathEnv(); err != nil {
return
}
return
}
/*
GetPidEnvMap will only work on *NIX-like systems with procfs.
It gets the environment variables of a given process' PID.
*/
func (s *StaticEnv) GetPidEnvMap(pid uint32) (envMap map[string]string, err error) {
if envMap, err = internal.GetPidEnvMap(pid); err != nil {
return
}
return
}
/*
GetPidEnvMapNative returns a map of all environment variables (like [GetEnvMapNative]), but attempts to "nativize" them.
All values are interfaces. It is up to the caller to typeswitch them to proper types.
See the documentation for [GetEnvMapNative] for details.
*/
func (s *StaticEnv) GetPidEnvMapNative(pid uint32) (envMap map[string]interface{}, err error) {
var stringMap map[string]string
if stringMap, err = internal.GetPidEnvMap(pid); err != nil {
return
}
envMap = internal.NativizeEnvMap(stringMap)
return
}
/*
HasEnv is much like [os.LookupEnv], but only returns a boolean indicating
if the environment variable key exists or not.
This is useful anywhere you may need to set a boolean in a func call
depending on the *presence* of an env var or not.
*/
func (s *StaticEnv) HasEnv(key string) (envIsSet bool) {
_, envIsSet = os.LookupEnv(key)
return
}
/*
Interpolate takes one of:
- a string (pointer only)
- a struct (pointer only)
- a map (applied to both keys *and* values)
- a slice
and performs variable substitution on strings from environment variables.
It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and,
if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%).
For structs, the tag name used can be changed by setting the [StructTagInterpolate]
variable in this submodule; the default is `envsub`.
If the tag value is "-", the field will be skipped.
For map fields within structs etc., the default is to apply interpolation to both keys and values.
All other tag value(s) are ignored.
For maps and slices, Interpolate will recurse into values (e.g. [][]string will work as expected).
If s is nil, no interpolation will be performed. No error will be returned.
If s is not a valid/supported type, no interpolation will be performed. No error will be returned.
*/
func (s *StaticEnv) Interpolate(inStr any) (err error) {
var ptrVal reflect.Value
var ptrType reflect.Type
var ptrKind reflect.Kind
var sVal reflect.Value = reflect.ValueOf(inStr)
var sType reflect.Type = sVal.Type()
var kind reflect.Kind = sType.Kind()
switch kind {
case reflect.Ptr:
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
return
}
ptrVal = sVal.Elem()
ptrType = ptrVal.Type()
ptrKind = ptrType.Kind()
if ptrKind == reflect.String {
err = s.interpolateStringReflect(ptrVal)
} else {
// Otherwise, it should be a struct ptr.
if ptrKind != reflect.Struct {
return
}
err = s.interpolateStruct(ptrVal)
}
case reflect.Map:
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
return
}
err = s.interpolateMap(sVal)
case reflect.Slice:
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
return
}
err = s.interpolateSlice(sVal)
/*
case reflect.Struct:
if sVal.IsZero() || !sVal.IsValid() {
return
}
err = interpolateStruct(sVal)
*/
}
return
}
/*
InterpolateString takes (a pointer to) a struct or string and performs variable substitution on it
from environment variables.
It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and,
if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%).
If s is nil, nothing will be done and err will be [errs.ErrNilPtr].
This is a standalone function that is much more performant than [Interpolate]
at the cost of rigidity.
*/
func (s *StaticEnv) InterpolateString(inStr *string) (err error) {
var newStr string
if inStr == nil {
err = errs.ErrNilPtr
return
}
if newStr, err = interpolateString(*inStr); err != nil {
return
}
*inStr = newStr
return
}
/*
Refresh refreshes the current environment.
This is called automatically on all other methods if
the [StaticEnv] was allocated with `dynamic` set to true.
*/
func (s *StaticEnv) Refresh() (err error) {
var ev []string
if !s.self && s.proc == nil {
// NO-OP; static mapping
return
}
if s.self {
ev = os.Environ()
} else if s.proc != nil {
if ev, err = s.proc.Environ(); err != nil {
return
}
}
s.envVars = internal.EnvListToMap(ev)
return
}
/*
Set sets variable key to value.
This only works under the following conditions:
- [SetEnv] was created from [NewEnvFromMap]
- [SetEnv] is from [Current]
- [SetEnv] is from [NewEnvFromPid] *and*
this process has PTRACE permissions/capabilities on/for
the target PID's process.
*/
func (s *StaticEnv) Set(key string, value string) {
var err error
s.lock.Lock()
defer s.lock.Unlock()
s.envVars[key] = value
if s.proc != nil {
if err = s.setProcVal(key, value); err != nil && s.strict {
panic(err)
}
}
return
}
// interpolateMap is used by [Interpolate] for maps. v should be a [reflect.Value] of a map.
func (s *StaticEnv) interpolateMap(v reflect.Value) (err error) {
var kVal reflect.Value
var vVal reflect.Value
var newMap reflect.Value
var wg sync.WaitGroup
var numJobs int
var errChan chan error
var doneChan chan bool = make(chan bool, 1)
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
var t reflect.Type = v.Type()
var kind reflect.Kind = t.Kind()
if kind != reflect.Map {
err = errs.ErrBadType
return
}
if v.IsNil() || v.IsZero() || !v.IsValid() {
return
}
numJobs = v.Len()
errChan = make(chan error, numJobs)
wg.Add(numJobs)
newMap = reflect.MakeMap(v.Type())
for _, kVal = range v.MapKeys() {
vVal = v.MapIndex(kVal)
go func(key, val reflect.Value) {
var mapErr error
var newKey reflect.Value
var newVal reflect.Value
newKey = reflect.New(key.Type()).Elem()
newVal = reflect.New(val.Type()).Elem()
newKey.Set(key.Convert(newKey.Type()))
newVal.Set(val.Convert(newVal.Type()))
defer wg.Done()
// key
if key.Kind() == reflect.String {
if mapErr = s.interpolateStringReflect(newKey); mapErr != nil {
errChan <- mapErr
return
}
} else {
if mapErr = s.interpolateValue(newKey); mapErr != nil {
errChan <- mapErr
return
}
}
// value
if val.Kind() == reflect.String {
if mapErr = s.interpolateStringReflect(newVal); mapErr != nil {
errChan <- mapErr
return
}
} else {
if mapErr = s.interpolateValue(newVal); mapErr != nil {
errChan <- mapErr
return
}
}
newMap.SetMapIndex(newKey.Convert(key.Type()), newVal.Convert(key.Type()))
}(kVal, vVal)
}
go func() {
wg.Wait()
close(errChan)
doneChan <- true
}()
<-doneChan
for i := 0; i < numJobs; i++ {
if err = <-errChan; err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
v.Set(newMap.Convert(v.Type()))
return
}
// interpolateSlice is used by [Interpolate] for slices and arrays. v should be a [reflect.Value] of a slice/array.
func (s *StaticEnv) interpolateSlice(v reflect.Value) (err error) {
var wg sync.WaitGroup
var errChan chan error
var numJobs int
var doneChan chan bool = make(chan bool, 1)
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
var t reflect.Type = v.Type()
var kind reflect.Kind = t.Kind()
switch kind {
case reflect.Slice:
if v.IsNil() || v.IsZero() || !v.IsValid() {
return
}
case reflect.Array:
if v.IsZero() || !v.IsValid() {
return
}
default:
err = errs.ErrBadType
return
}
numJobs = v.Len()
errChan = make(chan error, numJobs)
wg.Add(numJobs)
for i := 0; i < v.Len(); i++ {
go func(idx int) {
var sErr error
defer wg.Done()
if v.Index(idx).Kind() == reflect.String {
if sErr = s.interpolateStringReflect(v.Index(idx)); sErr != nil {
errChan <- sErr
return
}
} else {
if sErr = s.interpolateValue(v.Index(idx)); sErr != nil {
errChan <- sErr
return
}
}
}(i)
}
go func() {
wg.Wait()
close(errChan)
doneChan <- true
}()
<-doneChan
for i := 0; i < numJobs; i++ {
if err = <-errChan; err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
return
}
// interpolateStringReflect is used for structs/nested strings using reflection.
func (s *StaticEnv) interpolateStringReflect(v reflect.Value) (err error) {
var strVal string
if v.Kind() != reflect.String {
err = errs.ErrBadType
return
}
if strVal, err = interpolateString(v.String()); err != nil {
return
}
v.Set(reflect.ValueOf(strVal).Convert(v.Type()))
return
}
// interpolateStruct is used by [Interpolate] for structs. v should be a [reflect.Value] of a struct.
func (s *StaticEnv) interpolateStruct(v reflect.Value) (err error) {
var field reflect.StructField
var fieldVal reflect.Value
var wg sync.WaitGroup
var errChan chan error
var numJobs int
var doneChan chan bool = make(chan bool, 1)
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
var t reflect.Type = v.Type()
var kind reflect.Kind = t.Kind()
if kind != reflect.Struct {
err = errs.ErrBadType
return
}
numJobs = v.NumField()
wg.Add(numJobs)
errChan = make(chan error, numJobs)
for i := 0; i < v.NumField(); i++ {
field = t.Field(i)
fieldVal = v.Field(i)
go func(f reflect.StructField, fv reflect.Value) {
var fErr error
defer wg.Done()
if fErr = s.interpolateStructField(f, fv); fErr != nil {
errChan <- fErr
return
}
}(field, fieldVal)
}
go func() {
wg.Wait()
close(errChan)
doneChan <- true
}()
<-doneChan
for i := 0; i < numJobs; i++ {
if err = <-errChan; err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
return
}
// interpolateStructField interpolates a struct field.
func (s *StaticEnv) interpolateStructField(field reflect.StructField, v reflect.Value) (err error) {
var parsedTagOpts map[string]bool
if !v.CanSet() {
return
}
// Skip if explicitly instructed to do so.
parsedTagOpts = structutils.TagToBoolMap(field, StructTagInterpolate, structutils.TagMapTrim)
if parsedTagOpts["-"] {
return
}
if v.Kind() == reflect.Ptr {
err = s.interpolateStructField(field, v.Elem())
} else {
err = s.interpolateValue(v)
}
return
}
// interpolateValue is a dispatcher for a reflect value.
func (s *StaticEnv) interpolateValue(v reflect.Value) (err error) {
var kind reflect.Kind = v.Kind()
switch kind {
case reflect.Ptr:
if v.IsNil() || v.IsZero() || !v.IsValid() {
return
}
v = v.Elem()
if err = s.interpolateValue(v); err != nil {
return
}
case reflect.String:
if err = s.interpolateStringReflect(v); err != nil {
return
}
return
case reflect.Slice, reflect.Array:
if err = s.interpolateSlice(v); err != nil {
}
case reflect.Map:
if err = s.interpolateMap(v); err != nil {
return
}
case reflect.Struct:
if err = s.interpolateStruct(v); err != nil {
return
}
}
return
}

View File

@@ -0,0 +1,47 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
package envs
import (
"golang.org/x/sys/unix"
)
func (s *StaticEnv) platChecks() (err error) {
if s.proc != nil && !s.self {
// Check for ptrace caps/perms
if err = unix.PtraceAttach(int(s.proc.Pid)); err != nil {
return
}
if err = unix.PtraceDetach(int(s.proc.Pid)); err != nil {
return
}
}
return
}
func (s *StaticEnv) setProcVal(key, value string) (err error) {
if s.self {
if err = unix.Setenv(key, value); err != nil {
return
}
} else {
if err = unix.PtraceAttach(int(s.proc.Pid)); err != nil {
return
}
if err = unix.PtraceDetach(int(s.proc.Pid)); err != nil {
return
}
}
return
}
func (s *StaticEnv) unsetProcVal(key string) (err error) {
err = unix.Unsetenv(key)
return
}

View File

@@ -0,0 +1,26 @@
//go:build windows
package envs
import (
"golang.org/x/sys/windows"
)
func (s *StaticEnv) platChecks() (err error) {
return
}
func (s *StaticEnv) setProcVal(key, value string) (err error) {
err = windows.Setenv(key, value)
return
}
func (s *StaticEnv) unsetProcVal(key string) (err error) {
err = windows.Unsetenv(key)
return
}

View File

@@ -1,5 +1,11 @@
package envs
import (
"sync"
"github.com/shirou/gopsutil/v4/process"
)
type (
/*
EnvErrNoVal is an error containing the variable that does not exist
@@ -7,14 +13,29 @@ type (
*/
EnvErrNoVal struct {
// VarName is the variable name/key name originally specified in the function call.
VarName string `json:"var" toml:"VariableName" yaml:"Variable Name/Key" xml:"key,attr"`
VarName string `json:"var" toml:"VariableName" yaml:"Variable Name/Key" xml:"key,attr"`
// WasFound is only used for GetEnvErrNoBlank(). It is true if the variable was found/populated.
WasFound bool `json:"found" toml:"Found" yaml:"Found" xml:"found,attr"`
WasFound bool `json:"found" toml:"Found" yaml:"Found" xml:"found,attr"`
// WasRequiredNonEmpty indicates that this error was returned in a context where a variable was required to be non-empty (e.g. via GetEnvErrNoBlank()) but was empty.
WasRequiredNonEmpty bool `json:"reqd_non_empty" toml:"RequiredNonEmpty" yaml:"Required Non-Empty" xml:"reqNonEmpty,attr"`
WasRequiredNonEmpty bool `json:"reqd_non_empty" toml:"RequiredNonEmpty" yaml:"Required Non-Empty" xml:"reqNonEmpty,attr"`
// IgnoreWhitespace is true if the value was found but its evaluation was done against a whitestripped version.
IgnoreWhitespace bool `json:"ignore_ws" toml:"IgnoreWhitespace" yaml:"Ignore Whitespace" xml:"ignoreWhitespace,attr"`
IgnoreWhitespace bool `json:"ignore_ws" toml:"IgnoreWhitespace" yaml:"Ignore Whitespace" xml:"ignoreWhitespace,attr"`
// WasWhitespace is true if the value was whitespace-only.
WasWhitespace bool `json:"was_ws" toml:"WasWhitespace" yaml:"Was Whitespace Only" xml:"wasWhitespace,attr"`
WasWhitespace bool `json:"was_ws" toml:"WasWhitespace" yaml:"Was Whitespace Only" xml:"wasWhitespace,attr"`
}
/*
StaticEnv is an environment variable mapping that duplicates the normal functions of the standalone functions
but can be used for other processes, mock tests, etc.
(The standalone functions actually perform the same functions on the default StaticEnv as returned from [Current].)
*/
StaticEnv struct {
dynamic bool
strict bool
envVars map[string]string
self bool
proc *process.Process
lock sync.RWMutex
}
)

View File

@@ -1,87 +0,0 @@
package envs
import (
"strconv"
"strings"
"r00t2.io/sysutils/internal"
)
// envListToMap splits a []string of env var keypairs to a map.
func envListToMap(envs []string) (envMap map[string]string) {
var kv []string
var k, v string
envMap = make(map[string]string)
for _, ev := range envs {
kv = strings.SplitN(ev, "=", 2)
// I *think* SplitN does this for me, but...
if len(kv) == 1 {
kv = append(kv, "")
}
k = kv[0]
v = kv[1]
envMap[k] = v
}
return
}
// nativizeEnvMap returns a native-typed env map from a string version.
func nativizeEnvMap(stringMap map[string]string) (envMap map[string]interface{}) {
var pathVar string = internal.GetPathEnvName()
var err error
envMap = make(map[string]interface{})
for k, v := range stringMap {
// Check for PATH/Path - we handle this uniquely.
if k == pathVar {
if envMap[k], err = GetPathEnv(); err != nil {
envMap[k] = v
err = nil
}
continue
}
// It might be...
// a float
if reMaybeFloat.MatchString(v) {
if envMap[k], err = strconv.ParseFloat(v, 64); err == nil {
continue
}
err = nil
}
// an int
if reMaybeInt.MatchString(v) {
if envMap[k], err = strconv.Atoi(v); err == nil {
continue
}
err = nil
}
// a uint
if envMap[k], err = strconv.ParseUint(v, 10, 64); err == nil {
continue
} else {
err = nil
}
// a boolean
if envMap[k], err = strconv.ParseBool(v); err == nil {
continue
} else {
err = nil
}
// ok so... guess it's a string, then.
envMap[k] = v
}
return
}

View File

@@ -8,3 +8,9 @@ var (
ErrBadType error = errors.New("a bad type was passed")
ErrNilPtr error = errors.New("a nil pointer was passed")
)
var (
ErrHighPid error = errors.New("a provided PID exceeds the possible maximum")
// ErrInvalidNs indicates an invalid Linux namespace identifier format.
ErrInvalidNs error = errors.New("invalid namespace identifier")
)

View File

@@ -1,9 +0,0 @@
package errs
import (
"errors"
)
var (
ErrInvalidNs error = errors.New("invalid namespace identifier")
)

32
funcs.go Normal file
View File

@@ -0,0 +1,32 @@
package sysutils
import (
"strconv"
"strings"
"r00t2.io/sysutils/errs"
)
// NsToInode splits a Linux namespace identifier (e.g. `net:[12345]`) to its type (e.g. `net`) and inode (e.g. `12345`).
func NsToInode(ns string) (typ string, inode uint64, err error) {
var fields []string
fields = strings.SplitN(ns, ":", 2)
if len(fields) != 2 {
err = errs.ErrInvalidNs
return
}
fields[1] = strings.TrimPrefix(fields[1], "[")
fields[1] = strings.TrimSuffix(fields[1], "]")
if inode, err = strconv.ParseUint(fields[1], 10, 64); err != nil {
return
}
typ = fields[0]
return
}

View File

@@ -1,3 +1,5 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
package sysutils
// Checked consolidates all the provided checked functions.

View File

@@ -1,77 +0,0 @@
package sysutils
import (
"fmt"
"os"
"strconv"
"strings"
"golang.org/x/sys/unix"
"r00t2.io/sysutils/envs"
"r00t2.io/sysutils/errs"
)
// GetIDState returns current ID/elevation information. An IDState should *not* be explicitly created/defined.
func GetIDState() (ids IDState) {
var err error
ids.RUID, ids.EUID, ids.SUID = unix.Getresuid()
ids.uidsChecked = true
ids.RGID, ids.EGID, ids.SGID = unix.Getresgid()
ids.gidsChecked = true
ids.SudoEnvCmd = envs.HasEnv("SUDO_COMMAND")
ids.SudoEnvHome = envs.HasEnv("SUDO_HOME")
ids.SudoEnvGroup = envs.HasEnv("SUDO_GID")
ids.SudoEnvUser = envs.HasEnv("SUDO_UID") || envs.HasEnv("SUDO_USER")
if ids.SudoEnvCmd || ids.SudoEnvHome || ids.SudoEnvGroup || ids.SudoEnvUser {
ids.SudoEnvVars = true
}
ids.sudoChecked = true
// PID 1 will *always* be root, so that can return a false positive for sudo.
if os.Getppid() != 1 {
ids.stat = new(unix.Stat_t)
if err = unix.Stat(
fmt.Sprintf("/proc/%d/stat", os.Getppid()),
ids.stat,
); err != nil {
err = nil
} else {
ids.PPIDUidMatch = ids.RUID == int(ids.stat.Uid)
ids.ppidUidChecked = true
ids.PPIDGidMatch = ids.RGID == int(ids.stat.Gid)
ids.ppidGidChecked = true
}
} else {
ids.ppidUidChecked = true
ids.ppidGidChecked = true
}
return
}
// NsToInode splits a namespace identifier (e.g. `net:[12345]`) to its type (e.g. `net`) and inode (e.g. `12345`).
func NsToInode(ns string) (typ string, inode uint64, err error) {
var fields []string
fields = strings.SplitN(ns, ":", 2)
if len(fields) != 2 {
err = errs.ErrInvalidNs
return
}
fields[1] = strings.TrimPrefix(fields[1], "[")
fields[1] = strings.TrimSuffix(fields[1], "]")
if inode, err = strconv.ParseUint(fields[1], 10, 64); err != nil {
return
}
typ = fields[0]
return
}

158
funcs_nix.go Normal file
View File

@@ -0,0 +1,158 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
package sysutils
import (
"fmt"
`math`
"os"
`github.com/shirou/gopsutil/v3/process`
"golang.org/x/sys/unix"
"r00t2.io/sysutils/envs"
`r00t2.io/sysutils/errs`
`r00t2.io/sysutils/internal`
)
// GetIDState returns current ID/elevation information. An IDState should *not* be explicitly created/defined.
func GetIDState() (ids IDState) {
var err error
ids.RUID, ids.EUID, ids.SUID = unix.Getresuid()
ids.uidsChecked = true
ids.RGID, ids.EGID, ids.SGID = unix.Getresgid()
ids.gidsChecked = true
ids.SudoEnvCmd = envs.HasEnv(envSudoCmd)
ids.SudoEnvHome = envs.HasEnv(envSudoHome)
ids.SudoEnvGroup = envs.HasEnv(envSudoGrp)
ids.SudoEnvUser = envs.HasEnv(envSudoUid) || envs.HasEnv(envSudoUname)
if ids.SudoEnvCmd || ids.SudoEnvHome || ids.SudoEnvGroup || ids.SudoEnvUser {
ids.SudoEnvVars = true
}
ids.sudoChecked = true
// PID 1 will *always* be root, so that can return a false positive for sudo.
if os.Getppid() != 1 {
ids.stat = new(unix.Stat_t)
if err = unix.Stat(
fmt.Sprintf("/proc/%d/stat", os.Getppid()),
ids.stat,
); err != nil {
err = nil
} else {
ids.PPIDUidMatch = ids.RUID == int(ids.stat.Uid)
ids.ppidUidChecked = true
ids.PPIDGidMatch = ids.RGID == int(ids.stat.Gid)
ids.ppidGidChecked = true
}
} else {
ids.ppidUidChecked = true
ids.ppidGidChecked = true
}
return
}
// GetIDStateProc is like GetIDState but for an arbitrary PID.
func GetIDStateProc(pid uint32) (ids IDState, err error) {
var i32 int32
var ints []int32
var sudoUid bool
var sudoUname bool
var proc *process.Process
var envMap map[string]string
if pid > math.MaxInt32 {
err = errs.ErrHighPid
return
}
ids = IDState{
RUID: -1,
EUID: -1,
SUID: -1,
RGID: -1,
EGID: -1,
SGID: -1,
}
if proc, err = process.NewProcess(int32(pid)); err != nil {
return
}
// UIDs
if ints, err = proc.Uids(); err != nil {
return
}
if ints != nil && len(ints) > 0 {
if len(ints) >= 3 {
ids.SUID = int(ints[2])
}
if len(ints) >= 2 {
ids.EUID = int(ints[1])
}
if len(ints) >= 1 {
ids.RUID = int(ints[0])
}
}
ids.uidsChecked = true
// GIDs
if ints, err = proc.Gids(); err != nil {
return
}
if ints != nil && len(ints) > 0 {
if len(ints) >= 3 {
ids.SGID = int(ints[2])
}
if len(ints) >= 2 {
ids.EGID = int(ints[1])
}
if len(ints) >= 1 {
ids.SGID = int(ints[0])
}
}
ids.gidsChecked = true
// Sudo (env check)
if envMap, err = internal.GetPidEnvMap(uint32(pid)); err != nil {
return
}
_, ids.SudoEnvCmd = envMap[envSudoCmd]
_, ids.SudoEnvHome = envMap[envSudoHome]
_, ids.SudoEnvGroup = envMap[envSudoGrp]
_, sudoUid = envMap[envSudoUid]
_, sudoUname = envMap[envSudoUname]
ids.SudoEnvUser = sudoUid || sudoUname
if ids.SudoEnvCmd || ids.SudoEnvHome || ids.SudoEnvGroup || ids.SudoEnvUser {
ids.SudoEnvVars = true
}
ids.sudoChecked = true
// Sudo (PPID check)
if i32, err = proc.Ppid(); err != nil {
return
}
if i32 != 1 {
ids.stat = new(unix.Stat_t)
if err = unix.Stat(
fmt.Sprintf("/proc/%d/stat", i32),
ids.stat,
); err != nil {
err = nil
} else {
ids.PPIDUidMatch = ids.RUID == int(ids.stat.Uid)
ids.ppidUidChecked = true
ids.PPIDGidMatch = ids.SGID == int(ids.stat.Gid)
ids.ppidGidChecked = true
}
} else {
ids.ppidUidChecked = true
ids.ppidGidChecked = true
}
return
}

14
go.mod
View File

@@ -6,18 +6,20 @@ require (
github.com/davecgh/go-spew v1.1.1
github.com/djherbis/times v1.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/shirou/gopsutil/v4 v4.25.7
golang.org/x/sync v0.16.0
golang.org/x/sys v0.35.0
github.com/shirou/gopsutil/v3 v3.24.5
github.com/shirou/gopsutil/v4 v4.25.10
golang.org/x/sync v0.17.0
golang.org/x/sys v0.37.0
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8
r00t2.io/goutils v1.9.6
r00t2.io/goutils v1.10.3
)
require (
github.com/ebitengine/purego v0.8.4 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shoenig/go-m1cpu v0.1.7 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect

47
go.sum
View File

@@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -11,46 +11,39 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
github.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI0=
github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8 h1:FW42yWB1sGClqswyHIB68wo0+oPrav1IuQ+Tdy8Qp8E=
honnef.co/go/augeas v0.0.0-20161110001225-ca62e35ed6b8/go.mod h1:44w9OfBSQ9l3o59rc2w3AnABtE44bmtNnRMNC7z+oKE=
r00t2.io/goutils v1.9.2 h1:1rcDgJ3MorWVBmZSvLpbAUNC+J+ctRfJQq5Wliucjww=
r00t2.io/goutils v1.9.2/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=
r00t2.io/goutils v1.9.3 h1:pR9Ggu5JBpVjfrqNBrZg9bZpKan0TCcwt3MXrSdkhLo=
r00t2.io/goutils v1.9.3/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=
r00t2.io/goutils v1.9.4 h1:+Bm72mKhgXs6DRtU3P4sBjqUNwAKAFfdF9lx5bomwQY=
r00t2.io/goutils v1.9.4/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=
r00t2.io/goutils v1.9.5 h1:tIBtXKbGPLCkdhHZSESdTZ2QzC1e+8jDToNr/BauWe0=
r00t2.io/goutils v1.9.5/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=
r00t2.io/goutils v1.9.6/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=
r00t2.io/goutils v1.10.3 h1:GmEtsM/nrrVWooYJllXDRsgInobEinv2dn5ccU4zGAA=
r00t2.io/goutils v1.10.3/go.mod h1:76AxpXUeL10uFklxRB11kQsrtj2AKiNm8AwG1bNoBCA=

View File

@@ -1,8 +1,18 @@
package internal
import (
`regexp`
)
// OS-specific path environment variable name. The default is "PATH".
var (
pathEnvVarName map[string]string = map[string]string{
"windows": "Path",
}
)
// Compiled regex patterns.
var (
reMaybeInt *regexp.Regexp = regexp.MustCompile(`^(?P<sign>\+|-)[0-9]+$`)
reMaybeFloat *regexp.Regexp = regexp.MustCompile(`(?P<sign>\+|-)?[0-9]+\.[0-9]+$`)
)

160
internal/funcs.go Normal file
View File

@@ -0,0 +1,160 @@
package internal
import (
`bytes`
`errors`
`fmt`
`os`
"runtime"
"strconv"
"strings"
`r00t2.io/sysutils/paths`
)
// EnvListToMap splits a []string of env var keypairs to a map.
func EnvListToMap(envs []string) (envMap map[string]string) {
var kv []string
var k, v string
envMap = make(map[string]string)
for _, ev := range envs {
kv = strings.SplitN(ev, "=", 2)
// I *think* SplitN does this for me, but...
if len(kv) == 1 {
kv = append(kv, "")
}
k = kv[0]
v = kv[1]
envMap[k] = v
}
return
}
// GetPathEnv returns a slice of the PATH variable's items.
func GetPathEnv() (pathList []string, err error) {
var pathVar string = GetPathEnvName()
pathList = make([]string, 0)
for _, p := range strings.Split(os.Getenv(pathVar), string(os.PathListSeparator)) {
if err = paths.RealPath(&p); err != nil {
return
}
pathList = append(pathList, p)
}
return
}
/*
GetPidEnvMap will only work on *NIX-like systems with procfs.
It gets the environment variables of a given process' PID.
*/
func GetPidEnvMap(pid uint32) (envMap map[string]string, err error) {
var envBytes []byte
var envList []string
var envArr [][]byte
var procPath string
var exists bool
envMap = make(map[string]string)
procPath = fmt.Sprintf("/proc/%v/environ", pid)
if exists, err = paths.RealPathExists(&procPath); err != nil {
return
}
if !exists {
err = errors.New(fmt.Sprintf("information for pid %v does not exist", pid))
return
}
if envBytes, err = os.ReadFile(procPath); err != nil {
return
}
envArr = bytes.Split(envBytes, []byte{0x0})
envList = make([]string, len(envArr))
for idx, b := range envArr {
envList[idx] = string(b)
}
envMap = EnvListToMap(envList)
return
}
// GetPathEnvName gets the OS-specific path environment variable name.
func GetPathEnvName() (envVarName string) {
var ok bool
if envVarName, ok = pathEnvVarName[runtime.GOOS]; !ok {
// *NIX/the default.
envVarName = "PATH"
}
return
}
// NativizeEnvMap returns a native-typed env map from a string version.
func NativizeEnvMap(stringMap map[string]string) (envMap map[string]interface{}) {
var pathVar string = GetPathEnvName()
var err error
envMap = make(map[string]interface{})
for k, v := range stringMap {
// Check for PATH/Path - we handle this uniquely.
if k == pathVar {
if envMap[k], err = GetPathEnv(); err != nil {
envMap[k] = v
err = nil
}
continue
}
// It might be...
// a float
if reMaybeFloat.MatchString(v) {
if envMap[k], err = strconv.ParseFloat(v, 64); err == nil {
continue
}
err = nil
}
// an int
if reMaybeInt.MatchString(v) {
if envMap[k], err = strconv.Atoi(v); err == nil {
continue
}
err = nil
}
// a uint
if envMap[k], err = strconv.ParseUint(v, 10, 64); err == nil {
continue
} else {
err = nil
}
// a boolean
if envMap[k], err = strconv.ParseBool(v); err == nil {
continue
} else {
err = nil
}
// ok so... guess it's a string, then.
envMap[k] = v
}
return
}

1
internal/mem/consts.go Normal file
View File

@@ -0,0 +1 @@
package mem

81
internal/mem/funcs.go Normal file
View File

@@ -0,0 +1,81 @@
package mem
import (
"encoding/binary"
"unsafe"
"golang.org/x/sys/unix"
)
func getStdRIP(r *unix.PtraceRegs) (rip uint64) {
return
}
func getRIP(r *unix.PtraceRegs) uint64 {
// amd64
if binary.Size(unix.PtraceRegs{}) == 216 {
return r.Rip
}
// arm64
type regsArm64 struct{ Regs [18]uint64 }
return (*regsArm64)(unsafe.Pointer(r)).Regs[16] // PC
}
func setRIP(r *unix.PtraceRegs, v uint64) *unix.PtraceRegs {
if binary.Size(unix.PtraceRegs{}) == 216 {
r.Rip = v
} else {
type regsArm64 struct{ Regs [18]uint64 }
(*regsArm64)(unsafe.Pointer(r)).Regs[16] = v
}
return r
}
func getRSP(r *unix.PtraceRegs) uint64 {
if binary.Size(unix.PtraceRegs{}) == 216 {
return r.Rsp
}
type regsArm64 struct{ Regs [18]uint64 }
return (*regsArm64)(unsafe.Pointer(r)).Regs[17] // SP
}
func setRSP(r *unix.PtraceRegs, v uint64) *unix.PtraceRegs {
if binary.Size(unix.PtraceRegs{}) == 216 {
r.Rsp = v
} else {
type regsArm64 struct{ Regs [18]uint64 }
(*regsArm64)(unsafe.Pointer(r)).Regs[17] = v
}
return r
}
func setArg0(r *unix.PtraceRegs, v uint64) *unix.PtraceRegs {
if binary.Size(unix.PtraceRegs{}) == 216 {
r.Rdi = v
} else {
type regsArm64 struct{ Regs [18]uint64 }
(*regsArm64)(unsafe.Pointer(r)).Regs[0] = v // X0
}
return r
}
func setArg1(r *unix.PtraceRegs, v uint64) *unix.PtraceRegs {
if binary.Size(unix.PtraceRegs{}) == 216 {
r.Rsi = v
} else {
type regsArm64 struct{ Regs [18]uint64 }
(*regsArm64)(unsafe.Pointer(r)).Regs[1] = v // X1
}
return r
}
func setArg2(r *unix.PtraceRegs, v uint64) *unix.PtraceRegs {
if binary.Size(unix.PtraceRegs{}) == 216 {
r.Rdx = v
} else {
type regsArm64 struct{ Regs [18]uint64 }
(*regsArm64)(unsafe.Pointer(r)).Regs[2] = v // X2
}
return r
}

View File

@@ -1,18 +0,0 @@
package internal
import (
`runtime`
)
// GetPathEnvName gets the OS-specific path environment variable name.
func GetPathEnvName() (envVarName string) {
var ok bool
if envVarName, ok = pathEnvVarName[runtime.GOOS]; !ok {
// *NIX/the default.
envVarName = "PATH"
}
return
}

View File

@@ -1,3 +1,5 @@
//go:build !(windows || plan9 || wasip1 || js || ios)
package sysutils
import (