29 Commits

Author SHA1 Message Date
b612 d93a851d1b feat: 完善 staros 系统能力并更新 wincmd 发布版依赖
- 重构 sysconf 为文档模型 INI Parser 与 Config Framework
- 强化 hosts 解析、插入校验、写回与异常输入处理
- 完善 StarCmd 生命周期、等待 API、流式输出与 IO 重定向
- 扩展跨平台文件时间、文件锁、内存、进程与网络能力
- 将 Windows 进程适配更新到 b612.me/wincmd v0.1.0
- 移除本地 wincmd/win32api replace,改用发布版依赖
- 将最低 Go 版本提升到 1.18
- 补充 hosts、sysconf、FileLock、StarCmd 与平台适配回归测试
2026-06-09 18:10:19 +08:00
b612 0b6373e9e3 Merge branch 'master' of git.b612.me:b612/staros 2024-08-30 15:03:24 +08:00
b612 0e7072d72a update go.mod 2024-08-30 15:02:53 +08:00
b612 c1b5b0c2c5 update host file 2024-04-10 15:52:57 +08:00
b612 a1fdeb62fc update host file 2024-04-10 15:19:25 +08:00
b612 bae1b83843 update 2024-04-10 13:10:41 +08:00
b612 4190d59159 update host file 2024-04-10 11:19:04 +08:00
b612 42ad19e798 update go.mod 2024-03-10 14:21:37 +08:00
b612 232bc3835e re-support go 1.16 2023-02-11 17:53:24 +08:00
b612 1333eb85bb adjust linux network speed monitor 2023-02-03 13:54:13 +08:00
b612 dae84c0a85 update go mod 2023-02-03 13:36:41 +08:00
b612 ed85eb7616 update go mod and fix filelock error on darwin 2022-03-18 17:22:38 +08:00
b612 d8c0d86ca0 go mod support 2022-03-14 11:40:14 +08:00
b612 bbd85885df add beep function 2022-01-04 14:18:38 +08:00
b612 c615c4bb00 vendor update 2021-11-12 15:58:01 +08:00
b612 5353429b8c shared lock support 2021-09-01 13:52:43 +08:00
b612 cf453821ef new feature add 2021-09-01 11:01:51 +08:00
b612 3501166592 bug fix 2021-07-07 10:39:07 +08:00
Starainrt d981a03b14 bug fix & netconn add 2021-06-04 10:44:53 +08:00
Starainrt fd5cf2c1f6 csv support 2021-05-17 15:09:04 +08:00
Starainrt c3ca057b96 add ini config parse and fix bug:math problem 2021-03-17 11:16:28 +08:00
b612 c3a165e64a bug fix:struct error while get file creation date 2020-12-22 09:27:55 +08:00
b612 ad5c18667a add mathmatic calc 2020-12-21 17:19:41 +08:00
b612 cdf3a18981 bug fix 2020-11-27 11:20:23 +08:00
b612 6de9af62cc fix bug while use common user 2020-11-26 16:14:12 +08:00
b612 bca0d42559 fix bug while use common user 2020-11-26 15:54:46 +08:00
b612 26da8c8b88 fix bug that darwin cannot compile due to syscall_t error 2020-08-21 11:17:48 +08:00
b612 5c2cce2da2 cli bug fix 2020-07-27 14:19:05 +08:00
b612 a94fc8d19d new feature add 2020-07-17 09:56:23 +08:00
51 changed files with 12693 additions and 1361 deletions
+2
View File
@@ -0,0 +1,2 @@
.idea
.vscode
+119
View File
@@ -0,0 +1,119 @@
# staros
`staros` is a cgo-free Go package for small cross-platform OS utilities.
The package keeps compatibility with existing APIs, but platform-dependent functions should prefer explicit error-returning variants where available. Unsupported platform implementations return `ERR_UNSUPPORTED` instead of silently pretending to work.
## Go Version
`go.mod` declares `go 1.18`. This release targets Go 1.18 or newer and no longer promises Go 1.16/1.17 compatibility. The release gate includes the current local Go toolchain plus cross-platform compile checks.
## Platform Support
| Area | Linux | Windows | Darwin |
| --- | --- | --- | --- |
| Basic path checks: `Exists`, `IsFile`, `IsFolder` | Supported | Supported | Supported |
| File locks: `FileLock` | `flock` | Win32 lock API | `flock` |
| File timestamps: `GetFileCreationTime`, `GetFileAccessTime`, `SetFileTimesE` | ctime/access/modtime via `x/sys/unix` | creation/access/modtime via `x/sys/windows` | birth/access/modtime via stdlib/syscall |
| Memory: `Memory` | `/proc` and `syscall.Sysinfo` | Win32 `GlobalMemoryStatusEx` | `vm_stat` and `sysctl` |
| Disk: `DiskUsageE` | `Statfs` | `GetDiskFreeSpaceExW` | `Statfs` |
| OS identity: `IsRoot`, `Whoami` | Supported | `IsRoot` supported, `Whoami` returns `ERR_UNSUPPORTED` | Supported |
| CPU usage: `CpuUsage`, `CpuUsageByPid` | `/proc/stat` and `/proc/<pid>/stat` | Stub returns `0` | Stub returns `0` |
| Process query: `FindProcess*` | `/proc` backed | `wincmd` backed basic fields | Returns `ERR_UNSUPPORTED` |
| Process launch: `Command`, `CommandContext`, `Start` | Supported | Supported | Supported |
| Process lifecycle: `ReleaseE` | Starts through normal lifecycle with `Setsid` | Starts through normal lifecycle | Returns `ERR_UNSUPPORTED` |
| Process detach: `DetachE` | Start-before-Start only, then `Process.Release` | Start-before-Start only, then `Process.Release` | Returns `ERR_UNSUPPORTED` |
| Run as user: `SetRunUserE`, `DaemonWithUser` | Supported | Returns `ERR_UNSUPPORTED` | Returns `ERR_UNSUPPORTED` |
| Keep capabilities: `SetKeepCaps`, `StarCmd.SetKeepCaps` | Package helper uses Linux `prctl`; command helper preserves current caps via `AmbientCaps` | Returns `ERR_UNSUPPORTED` | Returns `ERR_UNSUPPORTED` |
| Network adapters/speeds/connections | `/proc/net` backed | IP Helper adapter counters and TCP/UDP owner-pid tables; no Unix socket/inode fields | Returns `ERR_UNSUPPORTED` |
| Beep | PC speaker or terminal bell fallback | Win32 `Beep` | `osascript` or terminal bell fallback |
## API Semantics
- `ERR_UNSUPPORTED` means the symbol exists for build compatibility but the current OS implementation is intentionally unavailable.
- `ReleaseE` preserves `StarCmd` lifecycle observation: `Stopped()` still closes after the process exits and `ExitCode()` is populated when available. The historical misspelled `Stoped()` method remains as a deprecated compatibility alias.
- `DetachE` is a true detached start path and must be called before `Start`; calling it after `Start` returns an already-started error to avoid racing `Process.Release()` with the internal `Wait()`.
- `Wait`, `WaitContext`, and `WaitTimeout` provide explicit lifecycle wait helpers that return the final process wait error. `Stopped()` remains available when callers only need a close signal.
- On Linux, the package-level `SetKeepCaps()` helper applies `prctl(PR_SET_KEEPCAPS)` to the current process. `StarCmd.SetKeepCaps()` is different: it snapshots the current capability set and configures the child command's `SysProcAttr.AmbientCaps` before `Start`.
- `StdoutChan`, `StderrChan`, and `OutputChan` provide best-effort streaming observation for future output chunks. They close with `Stopped()` and do not replace the existing full-output capture methods.
- `RedirectStdout`, `RedirectStderr`, `RedirectOutput`, and `RedirectStdin` configure process IO before `Start`. File helpers such as `RedirectStdoutFile` and `RedirectStdinFile` open the file and close it after the process reaches its final state.
- `WriteStdinE` and `WriteStdinStringE` write raw stdin data without appending a newline. `WriteStdinLineE` and the legacy `WriteCmdE` append one newline.
- Legacy methods that do not return errors are kept for compatibility; prefer the `*E` variants for new code.
## sysconf Migration
The `sysconf` package now uses a document-backed INI model instead of the old `SysConf` struct shape. This is a breaking API change: callers should migrate to `NewIni`, `NewLinuxConf`, `Document`, `Section`, and `Entry` directly instead of relying on a field-compatible wrapper.
Minimal migration rules:
- Replace parser setup through `SysConf` fields with constructors: use `sysconf.NewIni()` for sectioned INI files, or `sysconf.NewLinuxConf(equal)` for flat Linux-style config files.
- Replace direct segment/key data mutation with section methods: `ini.Section(name)`, `ini.Set(section, key, value)`, `sec.Set`, `sec.SetAll`, `sec.AddValue`, `sec.Delete`, and `ini.DeleteSection`.
- Replace single-value assumptions with duplicate-aware reads where needed: `ini.Get`/`sec.Get` returns the first value, while `ini.GetAll`/`sec.GetAll` returns all repeated keys.
- Replace manual struct binding with `ini.Unmarshal(&dst)` and `ini.Marshal(src)` using `seg` and `key` tags.
- Use `ini.Build()` or `ini.Save(path)` for write-back. Unchanged parsed lines keep their original formatting, while changed entries are rebuilt from the new model.
Example:
```go
ini := sysconf.NewIni()
if err := ini.Parse(data); err != nil {
return err
}
app := ini.Section("app")
if app == nil {
app = ini.AddSection("app")
}
_ = app.SetInt("port", 9090, "")
_ = app.SetAll("feature", []string{"stable", "audit"}, "")
out := ini.Build()
```
INI parser capabilities:
- `NewIni()` parses common sectioned INI files with `=` and `:` key/value delimiters, `#` and `;` comments, section header comments, quoted values, no-value keys, duplicate keys, duplicate sections, and backslash line continuation.
- Inline comments require whitespace before the comment marker, so values such as URLs or fragments containing `#` are not truncated accidentally.
- `NewIniWithProfiles(...)`, `StrictINIProfile()`, and `LinuxConfProfile(equal)` let callers pin parser behavior for strict sectioned INI or flat Linux-style config files without mutating parser fields ad hoc.
- `Document.Strict` can be enabled when callers want malformed input to return a `ParseError` with line and column information instead of preserving unknown lines as raw content.
- Write-back is lossless for unchanged parsed lines. Changed values that would otherwise be misread as comments, leading/trailing whitespace, tabs, or newlines are emitted as quoted values.
Config framework capabilities:
- `sysconf.NewConfig()`, `sysconf.LoadConfig(&dst, files, ...)`, and `sysconf.LoadConfigSources(&dst, sources, ...)` load one or more INI sources in order; later sources override earlier values for the same section/key.
- `sysconf.RequiredFile(path)` and `sysconf.OptionalFile(path)` declare whether a missing file should fail loading or be skipped. `sysconf.BytesSource(name, data)` and `sysconf.StringSource(name, data)` support in-memory overlays for tests, generated defaults, and embedded configs.
- `Config` exposes direct access and write-back helpers: `Get`, `GetAll`, error-returning typed getters such as `GetIntE` / `GetBoolE` / `GetDurationE`, `Has`, `Set`, `SetAll`, `Delete`, `Build`, `Save`, and `SaveAtomic`.
- Struct binding uses `seg`, `key`, `default`, `env`, `split`, and `required` tags. Nested structs inherit their parent `seg` tag, while `env:"-"` disables environment overrides for a field.
- Environment overrides are opt-in through `WithEnvPrefix` / `WithEnvLookup`; generated names normalize section and key names to uppercase underscore form, such as `APP_SERVER_PORT`.
- Binding supports strings, bools, signed/unsigned integers, floats, `time.Duration`, `encoding.TextUnmarshaler`, slices, arrays, and `map[string]T` for scalar `T`.
- Repeated INI keys bind naturally to slices. When a single value should expand into multiple collection items, add an explicit `split` tag such as `split:","`, `split:"|"`, or `split:"csv"`.
- `Config.SetStruct(src)` writes a config struct back into the current document, using repeated keys for slices/arrays and sorted `key=value` repeated entries for maps.
- `sysconf.DescribeConfig(src)` exports struct tag metadata as `ConfigFieldInfo` records, and `sysconf.SampleConfig(src)` builds a sample INI from defaults, current struct values, required placeholders, or type zero values without mutating the source struct.
- `Config.SectionNames()`, `Config.Keys(section)`, `Config.Flatten()`, and `Config.FlattenEntries()` expose sorted section/key discovery, duplicate-aware flattened values, and structured section/key/value entries for diagnostics, tests, and lightweight config export.
- `ConfigError` and `ConfigSourceError` include structured metadata and unwrap their underlying parse, file, or conversion errors for `errors.Is` / `errors.As`.
- A config struct can implement `Validate() error`; validation runs after defaults, file values, env overrides, required checks, and type binding.
Example:
```go
type AppConfig struct {
App struct {
Name string `key:"name" required:"true"`
Port int `key:"port" default:"8080"`
Timeout time.Duration `key:"timeout" default:"5s"`
Tags []string `key:"tag" env:"APP_TAGS"`
} `seg:"app"`
}
var cfg AppConfig
_, err := sysconf.LoadConfigSources(&cfg, []sysconf.ConfigSource{
sysconf.RequiredFile("/etc/app.ini"),
sysconf.OptionalFile("/etc/app.local.ini"),
}, sysconf.WithEnvPrefix("APP"))
```
The current framework is intentionally local-file focused. It does not include hot reload, remote configuration centers, secret managers, or a schema DSL.
## Scope
This package is intentionally small and cgo-free. Functionality that duplicates broad system inventory packages should stay frozen unless it improves cross-platform semantics, error observability, or compatibility for existing callers.
+28
View File
@@ -0,0 +1,28 @@
// +build darwin
package staros
import (
"os"
"os/exec"
)
var (
// DefaultFreq - frequency, in Hz, middle A
DefaultFreq = 0.0
// DefaultDuration - duration in milliseconds
DefaultDuration = 0
)
// Beep beeps the PC speaker (https://en.wikipedia.org/wiki/PC_speaker).
func Beep(freq float64, duration int) error {
osa, err := exec.LookPath("osascript")
if err != nil {
// Output the only beep we can
_, err = os.Stdout.Write([]byte{7})
return err
}
cmd := exec.Command(osa, "-e", `beep`)
return cmd.Run()
}
+138
View File
@@ -0,0 +1,138 @@
// +build linux
package staros
import (
"errors"
"os"
"syscall"
"time"
"unsafe"
)
// Constants
const (
// This number represents the fixed frequency of the original PC XT's timer chip, which is approximately 1.193 MHz. This number
// is divided with the desired frequency to obtain a counter value, that is subsequently fed into the timer chip, tied to the PC speaker.
clockTickRate = 1193180
// linux/kd.h, start sound generation (0 for off)
kiocsound = 0x4B2F
// linux/input-event-codes.h
evSnd = 0x12 // Event type
sndTone = 0x02 // Sound
)
var (
// DefaultFreq - frequency, in Hz, middle A
DefaultFreq = 440.0
// DefaultDuration - duration in milliseconds
DefaultDuration = 200
)
// inputEvent represents linux/input.h event structure.
type inputEvent struct {
Time syscall.Timeval // time in seconds since epoch at which event occurred
Type uint16 // event type
Code uint16 // event code related to the event type
Value int32 // event value related to the event type
}
// ioctl system call manipulates the underlying device parameters of special files.
func ioctl(fd, name, data uintptr) error {
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, name, data)
if e != 0 {
return e
}
return nil
}
// Beep beeps the PC speaker (https://en.wikipedia.org/wiki/PC_speaker).
//
// On Linux it needs permission to access `/dev/tty0` or `/dev/input/by-path/platform-pcspkr-event-spkr` files for writing,
// and `pcspkr` module must be loaded. User must be in correct groups, usually `input` and/or `tty`.
//
// If it can not open device files, it will fallback to sending Bell character (https://en.wikipedia.org/wiki/Bell_character).
// For bell character in X11 terminals you can enable bell with `xset b on`. For console check `setterm` and `--blength` or `--bfreq` options.
//
// On macOS this just sends bell character. Enable `Audible bell` in Terminal --> Preferences --> Settings --> Advanced.
//
// On Windows it uses Beep function via syscall.
//
// On Web it plays hard coded beep sound.
func Beep(freq float64, duration int) error {
if freq == 0 {
freq = DefaultFreq
} else if freq > 20000 {
freq = 20000
} else if freq < 0 {
freq = DefaultFreq
}
if duration == 0 {
duration = DefaultDuration
}
period := int(float64(clockTickRate) / freq)
var evdev bool
f, err := os.OpenFile("/dev/tty0", os.O_WRONLY, 0644)
if err != nil {
e := err
f, err = os.OpenFile("/dev/input/by-path/platform-pcspkr-event-spkr", os.O_WRONLY, 0644)
if err != nil {
e = errors.New("beeep: " + e.Error() + "; " + err.Error())
// Output the only beep we can
_, err = os.Stdout.Write([]byte{7})
if err != nil {
return errors.New(e.Error() + "; " + err.Error())
}
return nil
}
evdev = true
}
defer f.Close()
if evdev { // Use Linux evdev API
ev := inputEvent{}
ev.Type = evSnd
ev.Code = sndTone
ev.Value = int32(freq)
d := *(*[unsafe.Sizeof(ev)]byte)(unsafe.Pointer(&ev))
// Start beep
f.Write(d[:])
time.Sleep(time.Duration(duration) * time.Millisecond)
ev.Value = 0
d = *(*[unsafe.Sizeof(ev)]byte)(unsafe.Pointer(&ev))
// Stop beep
f.Write(d[:])
} else { // Use ioctl
// Start beep
err = ioctl(f.Fd(), kiocsound, uintptr(period))
if err != nil {
return err
}
time.Sleep(time.Duration(duration) * time.Millisecond)
// Stop beep
err = ioctl(f.Fd(), kiocsound, uintptr(0))
if err != nil {
return err
}
}
return nil
}
+37
View File
@@ -0,0 +1,37 @@
package staros
import (
"fmt"
"testing"
"time"
)
const (
rat float64 = 1.059463094 //2^(1/12)
C float64 = 493.8833013 * rat
CU = C * rat * rat
D = CU * rat
DU = D * rat
E = DU * rat
F = E * rat
FU = F * rat
G = FU * rat
GU = G * rat
A = GU * rat
AU = A * rat
B = AU * rat
)
func beepMusic(qual ...float64) {
for _, v := range qual {
fmt.Println(v)
Beep(v, 700)
time.Sleep(time.Millisecond * 1000)
}
}
func Test_Music(t *testing.T) {
beepMusic(G, D, A, AU, A, G, F, D, DU, D, C, D, AU/2, C, G/2, C, D)
time.Sleep(time.Second * 3)
beepMusic(D,AU,A,G,A,D*2,F*2,G*2,F*2,D*2,D*2,C*2,D*2,DU*2,D*2,AU,A,E,G,FU)
}
+41
View File
@@ -0,0 +1,41 @@
// +build windows
package staros
import (
"syscall"
)
var (
// DefaultFreq - frequency, in Hz, middle A
DefaultFreq = 587.0
// DefaultDuration - duration in milliseconds
DefaultDuration = 500
)
// Beep beeps the PC speaker (https://en.wikipedia.org/wiki/PC_speaker).
func Beep(freq float64, duration int) error {
if freq == 0 {
freq = DefaultFreq
} else if freq > 32767 {
freq = 32767
} else if freq < 37 {
freq = DefaultFreq
}
if duration == 0 {
duration = DefaultDuration
}
kernel32, _ := syscall.LoadLibrary("kernel32.dll")
beep32, _ := syscall.GetProcAddress(kernel32, "Beep")
defer syscall.FreeLibrary(kernel32)
_, _, e := syscall.Syscall(uintptr(beep32), uintptr(2), uintptr(int(freq)), uintptr(duration), 0)
if e != 0 {
return e
}
return nil
}
+21 -3
View File
@@ -1,6 +1,24 @@
package staros package staros
import "os" import (
"errors"
"os"
)
var ERR_ALREADY_LOCKED = errors.New("ALREADY LOCKED")
var ERR_TIMEOUT = errors.New("TIME OUT")
var ERR_UNSUPPORTED = errors.New("UNSUPPORTED")
var errNilFile = errors.New("nil file")
var errNilFileInfo = errors.New("nil file info")
var errUnsupportedFileInfo = errors.New("unsupported file info")
var errFileLockNotLocked = errors.New("file lock is not locked")
func NewFileLock(filepath string) FileLock {
return FileLock{
filepath: filepath,
}
}
// 检测文件/文件夹是否存在 // 检测文件/文件夹是否存在
func Exists(path string) bool { func Exists(path string) bool {
@@ -12,7 +30,7 @@ func Exists(path string) bool {
} }
// IsFile 返回给定文件地址是否是一个文件, // IsFile 返回给定文件地址是否是一个文件,
//True为是一个文件,False为不是文件或路径无效 // True为是一个文件,False为不是文件或路径无效
func IsFile(fpath string) bool { func IsFile(fpath string) bool {
s, err := os.Stat(fpath) s, err := os.Stat(fpath)
if err != nil { if err != nil {
@@ -22,7 +40,7 @@ func IsFile(fpath string) bool {
} }
// IsFolder 返回给定文件地址是否是一个文件夹, // IsFolder 返回给定文件地址是否是一个文件夹,
//True为是一个文件夹,False为不是文件夹或路径无效 // True为是一个文件夹,False为不是文件夹或路径无效
func IsFolder(fpath string) bool { func IsFolder(fpath string) bool {
s, err := os.Stat(fpath) s, err := os.Stat(fpath)
if err != nil { if err != nil {
+176
View File
@@ -0,0 +1,176 @@
//go:build darwin
// +build darwin
package staros
import (
"os"
"syscall"
"time"
)
type FileLock struct {
fd int
filepath string
locked bool
}
func (f *FileLock) openFileForLock() error {
fd, err := syscall.Open(f.filepath, syscall.O_CREAT|syscall.O_RDONLY, 0600)
if err != nil {
return err
}
f.fd = fd
return nil
}
func (f *FileLock) Lock(Exclusive bool) error {
var lockType int
if Exclusive {
lockType = syscall.LOCK_EX
} else {
lockType = syscall.LOCK_SH
}
return f.lockWithFlags(lockType)
}
func (f *FileLock) LockNoBlocking(Exclusive bool) error {
var lockType int
if Exclusive {
lockType = syscall.LOCK_EX
} else {
lockType = syscall.LOCK_SH
}
return f.lockWithFlags(lockType | syscall.LOCK_NB)
}
func (f *FileLock) lockWithFlags(lockType int) error {
if f.locked {
return ERR_ALREADY_LOCKED
}
if err := f.openFileForLock(); err != nil {
return err
}
err := syscall.Flock(f.fd, lockType)
if err != nil {
_ = syscall.Close(f.fd)
f.fd = 0
if err == syscall.EWOULDBLOCK {
return ERR_ALREADY_LOCKED
}
return err
}
f.locked = true
return nil
}
func (f *FileLock) Unlock() error {
if f == nil || !f.locked {
return errFileLockNotLocked
}
err := syscall.Flock(f.fd, syscall.LOCK_UN)
if err != nil {
return err
}
if err := syscall.Close(f.fd); err != nil {
return err
}
f.fd = 0
f.locked = false
return nil
}
func (f *FileLock) LockWithTimeout(tm time.Duration, Exclusive bool) error {
if f.locked {
return ERR_ALREADY_LOCKED
}
var lockType int
if Exclusive {
lockType = syscall.LOCK_EX
} else {
lockType = syscall.LOCK_SH
}
if tm < 0 {
return f.Lock(Exclusive)
}
deadline := time.Now().Add(tm)
for {
err := f.lockWithFlags(lockType | syscall.LOCK_NB)
if err == nil {
return nil
}
if err != ERR_ALREADY_LOCKED {
return err
}
if !time.Now().Before(deadline) {
return ERR_TIMEOUT
}
sleep := time.Millisecond * 10
if remaining := time.Until(deadline); remaining < sleep {
sleep = remaining
}
if sleep > 0 {
time.Sleep(sleep)
}
}
}
func timespecToTime(ts syscall.Timespec) time.Time {
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
}
func GetFileCreationTime(fileinfo os.FileInfo) time.Time {
if fileinfo == nil {
return time.Time{}
}
if stat, ok := fileinfo.Sys().(*syscall.Stat_t); ok && stat != nil {
return timespecToTime(stat.Birthtimespec)
}
return time.Time{}
}
func GetFileAccessTime(fileinfo os.FileInfo) time.Time {
if fileinfo == nil {
return time.Time{}
}
if stat, ok := fileinfo.Sys().(*syscall.Stat_t); ok && stat != nil {
return timespecToTime(stat.Atimespec)
}
return time.Time{}
}
func SetFileTimes(file *os.File, info os.FileInfo) {
_ = SetFileTimesE(file, info)
}
func SetFileTimesbyTime(file *os.File) {
_ = SetFileTimesbyTimeE(file)
}
func SetFileTimesE(file *os.File, info os.FileInfo) error {
if file == nil {
return errNilFile
}
if info == nil {
return errNilFileInfo
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok || stat == nil {
return errUnsupportedFileInfo
}
atime := timespecToTime(stat.Atimespec)
mtime := info.ModTime()
return os.Chtimes(file.Name(), atime, mtime)
}
func SetFileTimesByTimeE(file *os.File) error {
return SetFileTimesbyTimeE(file)
}
func SetFileTimesbyTimeE(file *os.File) error {
if file == nil {
return errNilFile
}
now := time.Now()
return os.Chtimes(file.Name(), now, now)
}
+139
View File
@@ -0,0 +1,139 @@
package staros
import (
"errors"
"os"
"path/filepath"
"runtime"
"testing"
"time"
)
func Test_FileLock(t *testing.T) {
filename := filepath.Join(t.TempDir(), "test.file")
lock := NewFileLock(filename)
lock2 := NewFileLock(filename)
if err := lock.LockNoBlocking(false); err != nil {
t.Fatal(err)
}
if err := lock2.LockNoBlocking(false); err != nil {
t.Fatal(err)
}
if err := lock.Unlock(); err != nil {
t.Fatal(err)
}
if err := lock2.Unlock(); err != nil {
t.Fatal(err)
}
if err := lock2.LockNoBlocking(true); err != nil {
t.Fatal(err)
}
if err := lock2.Unlock(); err != nil {
t.Fatal(err)
}
_ = os.Remove(filename)
}
func TestFileLockExclusiveConflictTimeout(t *testing.T) {
filename := filepath.Join(t.TempDir(), "timeout.file")
lock := NewFileLock(filename)
contender := NewFileLock(filename)
if err := lock.Lock(true); err != nil {
t.Fatal(err)
}
defer lock.Unlock()
if err := contender.LockNoBlocking(true); !errors.Is(err, ERR_ALREADY_LOCKED) {
if err == nil {
_ = contender.Unlock()
}
t.Fatalf("expected non-blocking exclusive lock conflict, got %v", err)
}
start := time.Now()
if err := contender.LockWithTimeout(50*time.Millisecond, true); !errors.Is(err, ERR_TIMEOUT) {
if err == nil {
_ = contender.Unlock()
}
t.Fatalf("expected exclusive lock timeout, got %v", err)
}
if elapsed := time.Since(start); elapsed > time.Second {
t.Fatalf("lock timeout took too long: %s", elapsed)
}
if err := lock.Unlock(); err != nil {
t.Fatal(err)
}
if err := contender.LockWithTimeout(time.Second, true); err != nil {
t.Fatalf("expected lock after owner unlock, got %v", err)
}
if err := contender.Unlock(); err != nil {
t.Fatal(err)
}
}
func TestFileLockUnlockWithoutSuccessfulLock(t *testing.T) {
filename := filepath.Join(t.TempDir(), "unlock-state.file")
lock := NewFileLock(filename)
if err := lock.Unlock(); !errors.Is(err, errFileLockNotLocked) {
t.Fatalf("expected unlock without lock error, got %v", err)
}
owner := NewFileLock(filename)
contender := NewFileLock(filename)
if err := owner.Lock(true); err != nil {
t.Fatal(err)
}
defer owner.Unlock()
if err := contender.LockNoBlocking(true); !errors.Is(err, ERR_ALREADY_LOCKED) {
if err == nil {
_ = contender.Unlock()
}
t.Fatalf("expected lock conflict, got %v", err)
}
if err := contender.Unlock(); !errors.Is(err, errFileLockNotLocked) {
t.Fatalf("failed lock attempt should not be unlockable, got %v", err)
}
if err := owner.Unlock(); err != nil {
t.Fatal(err)
}
}
func TestFileLockRejectsSecondLockOnSameObject(t *testing.T) {
filename := filepath.Join(t.TempDir(), "same-object.file")
lock := NewFileLock(filename)
if err := lock.Lock(true); err != nil {
t.Fatal(err)
}
defer lock.Unlock()
for name, fn := range map[string]func() error{
"Lock": func() error { return lock.Lock(true) },
"LockNoBlocking": func() error { return lock.LockNoBlocking(true) },
"LockWithTimeout": func() error { return lock.LockWithTimeout(time.Second, true) },
} {
if err := fn(); !errors.Is(err, ERR_ALREADY_LOCKED) {
if err == nil {
_ = lock.Unlock()
}
t.Fatalf("expected %s on same lock object to reject second lock, got %v", name, err)
}
}
}
func TestGetFileCreationTimeLinuxUnavailable(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("linux-only creation time fallback")
}
filename := filepath.Join(t.TempDir(), "creation-time.file")
if err := os.WriteFile(filename, []byte("demo"), 0o644); err != nil {
t.Fatal(err)
}
info, err := os.Stat(filename)
if err != nil {
t.Fatal(err)
}
if got := GetFileCreationTime(info); !got.IsZero() {
t.Fatalf("linux FileInfo should not report synthetic creation time, got %s", got)
}
}
+184
View File
@@ -0,0 +1,184 @@
//go:build linux
// +build linux
package staros
import (
"golang.org/x/sys/unix"
"os"
"syscall"
"time"
)
type FileLock struct {
fd int
filepath string
locked bool
}
func timespecToTime(ts syscall.Timespec) time.Time {
return time.Unix(int64(ts.Sec), int64(ts.Nsec))
}
func GetFileCreationTime(fileinfo os.FileInfo) time.Time {
if fileinfo == nil {
return time.Time{}
}
// Linux os.FileInfo/syscall.Stat_t does not expose a stable birth time.
// Returning ctime here would be wrong because it tracks inode changes.
return time.Time{}
}
func GetFileAccessTime(fileinfo os.FileInfo) time.Time {
if fileinfo == nil {
return time.Time{}
}
if stat, ok := fileinfo.Sys().(*syscall.Stat_t); ok && stat != nil {
return timespecToTime(stat.Atim)
}
return time.Time{}
}
func SetFileTimes(file *os.File, info os.FileInfo) {
_ = SetFileTimesE(file, info)
}
func SetFileTimesbyTime(file *os.File) {
_ = SetFileTimesbyTimeE(file)
}
func SetFileTimesE(file *os.File, info os.FileInfo) error {
if file == nil {
return errNilFile
}
if info == nil {
return errNilFileInfo
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok || stat == nil {
return errUnsupportedFileInfo
}
atime := timespecToTime(stat.Atim)
mtime := info.ModTime()
return setFileTimes(file.Name(), atime, mtime)
}
func SetFileTimesByTimeE(file *os.File) error {
return SetFileTimesbyTimeE(file)
}
func SetFileTimesbyTimeE(file *os.File) error {
if file == nil {
return errNilFile
}
now := time.Now()
return setFileTimes(file.Name(), now, now)
}
func setFileTimes(path string, atime, mtime time.Time) error {
ts := [2]unix.Timespec{
unix.NsecToTimespec(atime.UnixNano()),
unix.NsecToTimespec(mtime.UnixNano()),
}
return unix.UtimesNanoAt(unix.AT_FDCWD, path, ts[:], unix.AT_SYMLINK_NOFOLLOW)
}
func (f *FileLock) openFileForLock() error {
fd, err := syscall.Open(f.filepath, syscall.O_CREAT|syscall.O_RDONLY, 0600)
if err != nil {
return err
}
f.fd = fd
return nil
}
func (f *FileLock) Lock(Exclusive bool) error {
var lockType int
if Exclusive {
lockType = syscall.LOCK_EX
} else {
lockType = syscall.LOCK_SH
}
return f.lockWithFlags(lockType)
}
func (f *FileLock) LockNoBlocking(Exclusive bool) error {
var lockType int
if Exclusive {
lockType = syscall.LOCK_EX
} else {
lockType = syscall.LOCK_SH
}
return f.lockWithFlags(lockType | syscall.LOCK_NB)
}
func (f *FileLock) lockWithFlags(lockType int) error {
if f.locked {
return ERR_ALREADY_LOCKED
}
if err := f.openFileForLock(); err != nil {
return err
}
err := syscall.Flock(f.fd, lockType)
if err != nil {
_ = syscall.Close(f.fd)
f.fd = 0
if err == syscall.EWOULDBLOCK {
return ERR_ALREADY_LOCKED
}
return err
}
f.locked = true
return nil
}
func (f *FileLock) Unlock() error {
if f == nil || !f.locked {
return errFileLockNotLocked
}
err := syscall.Flock(f.fd, syscall.LOCK_UN)
if err != nil {
return err
}
if err := syscall.Close(f.fd); err != nil {
return err
}
f.fd = 0
f.locked = false
return nil
}
func (f *FileLock) LockWithTimeout(tm time.Duration, Exclusive bool) error {
if f.locked {
return ERR_ALREADY_LOCKED
}
var lockType int
if Exclusive {
lockType = syscall.LOCK_EX
} else {
lockType = syscall.LOCK_SH
}
if tm < 0 {
return f.Lock(Exclusive)
}
deadline := time.Now().Add(tm)
for {
err := f.lockWithFlags(lockType | syscall.LOCK_NB)
if err == nil {
return nil
}
if err != ERR_ALREADY_LOCKED {
return err
}
if !time.Now().Before(deadline) {
return ERR_TIMEOUT
}
sleep := time.Millisecond * 10
if remaining := time.Until(deadline); remaining < sleep {
sleep = remaining
}
if sleep > 0 {
time.Sleep(sleep)
}
}
}
+217
View File
@@ -0,0 +1,217 @@
//go:build windows
// +build windows
package staros
import (
"b612.me/win32api"
"golang.org/x/sys/windows"
"os"
"syscall"
"time"
)
type FileLock struct {
filepath string
handle win32api.HANDLE
locked bool
}
func GetFileCreationTime(fileinfo os.FileInfo) time.Time {
if fileinfo == nil {
return time.Time{}
}
if data, ok := fileinfo.Sys().(*syscall.Win32FileAttributeData); ok && data != nil {
return time.Unix(0, data.CreationTime.Nanoseconds())
}
return time.Time{}
}
func GetFileAccessTime(fileinfo os.FileInfo) time.Time {
if fileinfo == nil {
return time.Time{}
}
if data, ok := fileinfo.Sys().(*syscall.Win32FileAttributeData); ok && data != nil {
return time.Unix(0, data.LastAccessTime.Nanoseconds())
}
return time.Time{}
}
func SetFileTimes(file *os.File, info os.FileInfo) {
_ = SetFileTimesE(file, info)
}
func SetFileTimesbyTime(file *os.File) {
_ = SetFileTimesbyTimeE(file)
}
func SetFileTimesE(file *os.File, info os.FileInfo) error {
if file == nil {
return errNilFile
}
if info == nil {
return errNilFileInfo
}
data, ok := info.Sys().(*syscall.Win32FileAttributeData)
if !ok || data == nil {
return errUnsupportedFileInfo
}
ctime := time.Unix(0, data.CreationTime.Nanoseconds())
atime := time.Unix(0, data.LastAccessTime.Nanoseconds())
mtime := info.ModTime()
return setFileTimes(file.Name(), ctime, atime, mtime)
}
func SetFileTimesByTimeE(file *os.File) error {
return SetFileTimesbyTimeE(file)
}
func SetFileTimesbyTimeE(file *os.File) error {
if file == nil {
return errNilFile
}
now := time.Now()
return setFileTimes(file.Name(), now, now, now)
}
func setFileTimes(path string, ctime, atime, mtime time.Time) error {
path16, err := windows.UTF16PtrFromString(path)
if err != nil {
return err
}
handle, err := windows.CreateFile(
path16,
windows.FILE_WRITE_ATTRIBUTES,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
nil,
windows.OPEN_EXISTING,
windows.FILE_FLAG_BACKUP_SEMANTICS,
0,
)
if err != nil {
return err
}
defer windows.CloseHandle(handle)
ctimeFt := windows.NsecToFiletime(ctime.UnixNano())
atimeFt := windows.NsecToFiletime(atime.UnixNano())
mtimeFt := windows.NsecToFiletime(mtime.UnixNano())
return windows.SetFileTime(handle, &ctimeFt, &atimeFt, &mtimeFt)
}
func (f *FileLock) openFileForLock() error {
name, err := syscall.UTF16PtrFromString(f.filepath)
if err != nil {
return err
}
handle, err := syscall.CreateFile(
name,
syscall.GENERIC_READ,
syscall.FILE_SHARE_READ,
nil,
syscall.OPEN_ALWAYS,
syscall.FILE_FLAG_OVERLAPPED|0x00000080,
0)
if err != nil {
return err
}
f.handle = win32api.HANDLE(handle)
return nil
}
func (f *FileLock) lockForTimeout(timeout time.Duration, lockType win32api.DWORD) error {
if f.locked {
return ERR_ALREADY_LOCKED
}
var err error
if err = f.openFileForLock(); err != nil {
return err
}
event, err := win32api.CreateEventW(nil, true, false, nil)
if err != nil {
_ = f.closeHandle()
return err
}
myEvent := &syscall.Overlapped{HEvent: syscall.Handle(event)}
defer syscall.CloseHandle(myEvent.HEvent)
_, err = win32api.LockFileEx(f.handle, lockType, 0, 1, 0, myEvent)
if err == nil {
f.locked = true
return nil
}
if err != syscall.ERROR_IO_PENDING {
_ = f.closeHandle()
return err
}
millis := uint32(syscall.INFINITE)
if timeout >= 0 {
millis = uint32(timeout.Nanoseconds() / 1000000)
}
s, err := syscall.WaitForSingleObject(myEvent.HEvent, millis)
switch s {
case syscall.WAIT_OBJECT_0:
// success!
f.locked = true
return nil
case syscall.WAIT_TIMEOUT:
_ = f.closeHandle()
return ERR_TIMEOUT
default:
_ = f.closeHandle()
return err
}
}
func (f *FileLock) Lock(Exclusive bool) error {
var lockType win32api.DWORD
if Exclusive {
lockType = win32api.LOCKFILE_EXCLUSIVE_LOCK
} else {
lockType = 0
}
return f.lockForTimeout(-1, lockType)
}
func (f *FileLock) LockWithTimeout(tm time.Duration, Exclusive bool) error {
var lockType win32api.DWORD
if Exclusive {
lockType = win32api.LOCKFILE_EXCLUSIVE_LOCK
} else {
lockType = 0
}
return f.lockForTimeout(tm, lockType)
}
func (f *FileLock) LockNoBlocking(Exclusive bool) error {
var lockType win32api.DWORD
if Exclusive {
lockType = win32api.LOCKFILE_EXCLUSIVE_LOCK
} else {
lockType = 0
}
return f.lockForTimeout(0, lockType|win32api.LOCKFILE_FAIL_IMMEDIATELY)
}
func (f *FileLock) Unlock() error {
if f == nil || !f.locked {
return errFileLockNotLocked
}
if err := f.closeHandle(); err != nil {
return err
}
f.locked = false
return nil
}
func (f *FileLock) closeHandle() error {
if f == nil || f.handle == 0 {
return nil
}
err := syscall.Close(syscall.Handle(f.handle))
if err != nil {
return err
}
f.handle = 0
return nil
}
+15
View File
@@ -0,0 +1,15 @@
module b612.me/staros
go 1.18
require (
b612.me/win32api v0.0.4
b612.me/wincmd v0.1.0
golang.org/x/sys v0.24.0
)
require (
b612.me/stario v0.0.11 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/term v0.23.0 // indirect
)
+75
View File
@@ -0,0 +1,75 @@
b612.me/stario v0.0.11 h1:H5SN5G36ZlW7Lu5co3CWK59eHVJduqHSa9a29Cx5ExQ=
b612.me/stario v0.0.11/go.mod h1:1Owmu9jzKWgs4VsmeI8YWlGwLrCwPNM/bYpxkyn+MMk=
b612.me/win32api v0.0.4 h1:V3LgCTbl8UF0Tb1UJDXl8+F/404yLA0XtC/131KmQ7c=
b612.me/win32api v0.0.4/go.mod h1:sj66sFJDKElEjOR+0YhdSW6b4kq4jsXu4T5/Hnpyot0=
b612.me/wincmd v0.1.0 h1:hLOvoIvsPhesb7XbN0l+pcfu4YNWog7YYw11MAkOiDs=
b612.me/wincmd v0.1.0/go.mod h1:kSUbCBCBciJQZi8V2gP78ZEtt8yUHaLatl/5X+V+4Fc=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+1308 -164
View File
File diff suppressed because it is too large Load Diff
+723 -3
View File
@@ -2,11 +2,731 @@ package hosts
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"strings"
"testing" "testing"
) )
func Test_Hosts(t *testing.T) { func Test_Hosts(t *testing.T) {
//RemoveHostbyIp("192.168.222.33") var h = NewHosts()
Parse() tmpDir := t.TempDir()
fmt.Println(GetAllListbyIp()) err := h.Parse("./test_hosts.txt")
if err != nil {
t.Error(err)
}
next := h.firstUid
for next != 0 {
node, _ := h.GetNode(next)
fmt.Printf("Last %d, Next: %d, IP: %s, Hosts: %s, Comment: %s\n", node.LastUID(), node.NextUID(), node.IP(), node.Hosts(), node.Comment())
next = node.NextUID()
}
data := h.ListHostsByIP("11.22.33.44")
if len(data) != 2 {
t.Error("Expected 2, got ", len(data))
} else {
t.Log(data)
}
data = h.ListIPsByHost("dns.b612.me")
if len(data) < 1 || data[0] != "4.5.6.7" {
t.Error("Expected 4.5.6.7, got ", data)
} else {
t.Log(data)
}
err = h.RemoveHosts("dns.b612.me")
if err != nil {
t.Error(err)
}
data = h.ListIPsByHost("dns.b612.me")
if len(data) > 0 {
t.Error("Expected 0, got ", len(data))
} else {
t.Log(data)
}
err = h.RemoveHosts("test.dns.set.b612.me")
if err != nil {
t.Error(err)
}
data = h.ListIPsByHost("remove.b612.me")
if len(data) < 1 || data[0] != "11.22.33.44" {
t.Error("Expected 11.22.33.44, got ", data)
} else {
t.Log(data)
}
nodes := h.ListByIP("11.22.33.44")
if nodes == nil {
t.Error("Expected not nil, got ", nodes)
} else {
t.Log(nodes)
}
nodes[0].AddHosts("hello.b612.me")
err = h.UpdateNode(nodes[0])
if err != nil {
t.Error(err)
}
data = h.ListIPsByHost("hello.b612.me")
if len(data) < 1 || data[0] != "11.22.33.44" {
t.Error("Not Expected Data", data)
} else {
t.Log(data)
}
insertNode := new(HostNode)
insertNode.SetIP("11.11.11.11")
insertNode.SetHosts("insert.b612.me")
insertNode.SetComment("Insert Node")
insertNode.SetNextUID(nodes[0].UID())
insertNode.SetLastUID(nodes[0].LastUID())
err = h.InsertNode(insertNode)
if err != nil {
t.Error(err)
}
data = h.ListIPsByHost("insert.b612.me")
if len(data) < 1 || data[0] != "11.11.11.11" {
t.Error("Expected 11.11.11.11 got ", data)
} else {
t.Log(data)
}
err = h.SaveAs(filepath.Join(tmpDir, "test_hosts_01.txt"))
if err != nil {
t.Error(err)
}
err = h.DeleteNode(insertNode)
if err != nil {
t.Error(err)
}
data = h.ListIPsByHost("insert.b612.me")
if len(data) > 0 {
t.Error("Expected 0 got ", data)
} else {
t.Log(data)
}
for i := 0; i < 100; i++ {
err = h.RemoveHosts("release-ftpd")
if err != nil {
t.Error(err)
}
err = h.AddHosts("2.3.4.9", "release-ftpd")
if err != nil {
t.Error(err)
}
}
err = h.SetHostIPs("ssh.b612.me", "9.9.9.9")
if err != nil {
t.Error(err)
}
data = h.ListIPsByHost("ssh.b612.me")
if len(data) == 0 {
t.Error("Expected 1 got ", data)
} else {
t.Log(data)
}
err = h.SetIPHosts("10.10.10.10", "ssh.b612.me", "ssr.b612.me")
if len(data) == 0 {
t.Error("Expected 1 got ", data)
}
err = h.SaveAs(filepath.Join(tmpDir, "test_hosts_02.txt"))
if err != nil {
t.Error(err)
}
}
func BenchmarkAddHosts(b *testing.B) {
var h = NewHosts()
err := h.Parse("./test_hosts.txt")
if err != nil {
b.Error(err)
}
for i := 0; i < b.N; i++ {
err = h.AddHosts("1.3.4.5", "test.b612.me")
if err != nil {
b.Error(err)
}
}
}
func TestParseHandlesEmptyAndNoTrailingNewline(t *testing.T) {
t.Run("empty file", func(t *testing.T) {
h := NewHosts()
path := filepath.Join(t.TempDir(), "hosts.empty")
if err := os.WriteFile(path, nil, 0o644); err != nil {
t.Fatal(err)
}
if err := h.Parse(path); err != nil {
t.Fatal(err)
}
if got := h.List(); len(got) != 0 {
t.Fatalf("expected empty hosts list, got %d entries", len(got))
}
})
t.Run("last line without newline", func(t *testing.T) {
h := NewHosts()
path := filepath.Join(t.TempDir(), "hosts.nonewline")
if err := os.WriteFile(path, []byte("1.2.3.4 example.test"), 0o644); err != nil {
t.Fatal(err)
}
if err := h.Parse(path); err != nil {
t.Fatal(err)
}
if got := h.ListIPsByHost("example.test"); len(got) != 1 || got[0] != "1.2.3.4" {
t.Fatalf("expected last line to be parsed, got %v", got)
}
node, err := h.GetLatestNode()
if err != nil {
t.Fatal(err)
}
if node.NextUID() != 0 {
t.Fatalf("expected last node next uid 0, got %d", node.NextUID())
}
})
}
func TestAddHostsAndAddNodeWorkOnEmptyModel(t *testing.T) {
t.Run("add hosts", func(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "example.test"); err != nil {
t.Fatal(err)
}
if got := h.ListFirstIPByHost("example.test"); got != "1.2.3.4" {
t.Fatalf("expected inserted host ip, got %q", got)
}
out, err := h.Build()
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(out), "1.2.3.4 example.test") {
t.Fatalf("unexpected build output: %q", out)
}
})
t.Run("add node", func(t *testing.T) {
h := NewHosts()
node := &HostNode{}
node.SetIP("5.6.7.8")
node.SetHosts("node.test")
if err := h.AddNode(node); err != nil {
t.Fatal(err)
}
if node.UID() == 0 {
t.Fatal("expected node uid to be assigned")
}
if got := h.ListFirstIPByHost("node.test"); got != "5.6.7.8" {
t.Fatalf("expected inserted node ip, got %q", got)
}
})
}
func TestInsertNodeByDataInsertsAndLinksNode(t *testing.T) {
h := NewHosts()
path := filepath.Join(t.TempDir(), "hosts.insert")
if err := os.WriteFile(path, []byte("2.2.2.2 anchor.test\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := h.Parse(path); err != nil {
t.Fatal(err)
}
anchor, err := h.GetFirstNode()
if err != nil {
t.Fatal(err)
}
if err := h.InsertNodeByData(anchor, true, "before", "1.1.1.1", "before.test"); err != nil {
t.Fatal(err)
}
if err := h.InsertNodeByData(anchor, false, "after", "3.3.3.3", "after.test"); err != nil {
t.Fatal(err)
}
nodes := h.List()
if len(nodes) != 3 {
t.Fatalf("expected 3 nodes after insert, got %d", len(nodes))
}
if nodes[0].IP() != "1.1.1.1" || nodes[1].IP() != "2.2.2.2" || nodes[2].IP() != "3.3.3.3" {
t.Fatalf("unexpected node order: %q, %q, %q", nodes[0].IP(), nodes[1].IP(), nodes[2].IP())
}
if got := h.ListFirstIPByHost("before.test"); got != "1.1.1.1" {
t.Fatalf("expected before node to be indexed, got %q", got)
}
if got := h.ListFirstIPByHost("after.test"); got != "3.3.3.3" {
t.Fatalf("expected after node to be indexed, got %q", got)
}
if nodes[0].NextUID() != nodes[1].UID() || nodes[1].LastUID() != nodes[0].UID() {
t.Fatalf("before/anchor linkage broken: before.next=%d anchor.uid=%d anchor.last=%d", nodes[0].NextUID(), nodes[1].UID(), nodes[1].LastUID())
}
if nodes[1].NextUID() != nodes[2].UID() || nodes[2].LastUID() != nodes[1].UID() {
t.Fatalf("anchor/after linkage broken: anchor.next=%d after.uid=%d after.last=%d", nodes[1].NextUID(), nodes[2].UID(), nodes[2].LastUID())
}
}
func TestInsertNodeByDataRejectsNilAnchor(t *testing.T) {
h := NewHosts()
path := filepath.Join(t.TempDir(), "hosts.insert.nil")
if err := os.WriteFile(path, []byte("2.2.2.2 anchor.test\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := h.Parse(path); err != nil {
t.Fatal(err)
}
if err := h.InsertNodeByData(nil, true, "before", "1.1.1.1", "before.test"); err == nil {
t.Fatal("expected nil anchor error")
}
}
func TestSetIPHostsUpdatesReverseIndex(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
t.Fatal(err)
}
if err := h.SetIPHosts("1.2.3.4", "new.test"); err != nil {
t.Fatal(err)
}
if got := h.ListIPsByHost("new.test"); len(got) != 1 || got[0] != "1.2.3.4" {
t.Fatalf("expected new reverse index, got %v", got)
}
if got := h.ListIPsByHost("old.test"); len(got) != 0 {
t.Fatalf("expected old reverse index to be removed, got %v", got)
}
}
func TestSetIPHostsDeduplicatesHosts(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
t.Fatal(err)
}
if err := h.SetIPHosts("1.2.3.4", "new.test", "new.test"); err != nil {
t.Fatal(err)
}
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "new.test" {
t.Fatalf("expected deduplicated ip mapping, got %v", got)
}
if got := h.ListIPsByHost("new.test"); len(got) != 1 || got[0] != "1.2.3.4" {
t.Fatalf("expected deduplicated reverse index, got %v", got)
}
}
func TestSetIPHostsReplacesMultipleSameIPNodes(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "first.test"); err != nil {
t.Fatal(err)
}
if err := h.AddHosts("1.2.3.4", "second.test"); err != nil {
t.Fatal(err)
}
if err := h.AddHosts("5.6.7.8", "tail.test"); err != nil {
t.Fatal(err)
}
if err := h.SetIPHosts("1.2.3.4", "new.test"); err != nil {
t.Fatal(err)
}
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "new.test" {
t.Fatalf("expected replaced same-ip mappings, got %v", got)
}
if got := h.ListIPsByHost("first.test"); len(got) != 0 {
t.Fatalf("expected first old host to disappear, got %v", got)
}
if got := h.ListIPsByHost("second.test"); len(got) != 0 {
t.Fatalf("expected second old host to disappear, got %v", got)
}
if got := h.ListFirstIPByHost("tail.test"); got != "5.6.7.8" {
t.Fatalf("expected tail node to remain linked, got %q", got)
}
nodes := h.List()
if len(nodes) != 2 || nodes[0].IP() != "5.6.7.8" || nodes[1].IP() != "1.2.3.4" {
t.Fatalf("unexpected node list after SetIPHosts: %#v", nodes)
}
if nodes[0].NextUID() != nodes[1].UID() || nodes[1].LastUID() != nodes[0].UID() {
t.Fatalf("remaining node linkage broken: first.next=%d second.uid=%d second.last=%d", nodes[0].NextUID(), nodes[1].UID(), nodes[1].LastUID())
}
}
func TestSetHostIPsReplacesMappingsInOneOperation(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
t.Fatal(err)
}
if err := h.SetHostIPs("old.test", "2.2.2.2", "3.3.3.3"); err != nil {
t.Fatal(err)
}
if got := h.ListIPsByHost("old.test"); len(got) != 2 || got[0] != "2.2.2.2" || got[1] != "3.3.3.3" {
t.Fatalf("expected replaced host ip mappings, got %v", got)
}
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 0 {
t.Fatalf("expected old ip mapping to be removed, got %v", got)
}
}
func TestSetIPHostsRejectsInvalidInputWithoutMutating(t *testing.T) {
tests := []struct {
name string
ip string
hosts []string
}{
{name: "bad ip", ip: "bad-ip", hosts: []string{"new.test"}},
{name: "empty host", ip: "1.2.3.4", hosts: []string{""}},
{name: "comment host", ip: "1.2.3.4", hosts: []string{"#bad.test"}},
{name: "missing host", ip: "1.2.3.4"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
t.Fatal(err)
}
if err := h.SetIPHosts(tt.ip, tt.hosts...); err == nil {
t.Fatal("expected invalid SetIPHosts input to fail")
}
if got := h.ListIPsByHost("old.test"); len(got) != 1 || got[0] != "1.2.3.4" {
t.Fatalf("old host mapping should remain after failed SetIPHosts, got %v", got)
}
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "old.test" {
t.Fatalf("ip index should remain after failed SetIPHosts, got %v", got)
}
if got := h.ListIPsByHost("new.test"); len(got) != 0 {
t.Fatalf("failed SetIPHosts should not add new host, got %v", got)
}
})
}
}
func TestSetHostIPsRejectsInvalidInputWithoutMutating(t *testing.T) {
tests := []struct {
name string
host string
ips []string
}{
{name: "empty host", host: "", ips: []string{"2.2.2.2"}},
{name: "comment host", host: "#old.test", ips: []string{"2.2.2.2"}},
{name: "bad ip", host: "old.test", ips: []string{"2.2.2.2", "bad-ip"}},
{name: "missing ip", host: "old.test"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
t.Fatal(err)
}
if err := h.SetHostIPs(tt.host, tt.ips...); err == nil {
t.Fatal("expected invalid SetHostIPs input to fail")
}
if got := h.ListIPsByHost("old.test"); len(got) != 1 || got[0] != "1.2.3.4" {
t.Fatalf("old host mapping should remain after failed SetHostIPs, got %v", got)
}
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "old.test" {
t.Fatalf("ip index should remain after failed SetHostIPs, got %v", got)
}
if got := h.ListHostsByIP("2.2.2.2"); len(got) != 0 {
t.Fatalf("failed SetHostIPs should not add partial ip mapping, got %v", got)
}
})
}
}
func TestRemoveIPHostsKeepsSameIPOtherNodes(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "first.test"); err != nil {
t.Fatal(err)
}
if err := h.AddHosts("1.2.3.4", "second.test"); err != nil {
t.Fatal(err)
}
if err := h.RemoveIPHosts("1.2.3.4", "first.test"); err != nil {
t.Fatal(err)
}
if got := h.ListIPsByHost("first.test"); len(got) != 0 {
t.Fatalf("expected removed host to disappear, got %v", got)
}
if got := h.ListIPsByHost("second.test"); len(got) != 1 || got[0] != "1.2.3.4" {
t.Fatalf("expected same-ip sibling node to stay indexed, got %v", got)
}
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "second.test" {
t.Fatalf("expected ip index to keep sibling host, got %v", got)
}
}
func TestRemoveHostsKeepsSameIPOtherNodes(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "first.test"); err != nil {
t.Fatal(err)
}
if err := h.AddHosts("1.2.3.4", "second.test"); err != nil {
t.Fatal(err)
}
if err := h.RemoveHosts("first.test"); err != nil {
t.Fatal(err)
}
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "second.test" {
t.Fatalf("expected ip index to keep sibling host after RemoveHosts, got %v", got)
}
}
func TestRemoveIPsUnlinksAdjacentNodes(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "first.test"); err != nil {
t.Fatal(err)
}
if err := h.AddHosts("1.2.3.4", "second.test"); err != nil {
t.Fatal(err)
}
if err := h.AddHosts("5.6.7.8", "tail.test"); err != nil {
t.Fatal(err)
}
if err := h.RemoveIPs("1.2.3.4"); err != nil {
t.Fatal(err)
}
nodes := h.List()
if len(nodes) != 1 || nodes[0].IP() != "5.6.7.8" || nodes[0].LastUID() != 0 || nodes[0].NextUID() != 0 {
t.Fatalf("expected only tail node with clean links, got %#v", nodes)
}
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 0 {
t.Fatalf("expected removed ip index to be empty, got %v", got)
}
if got := h.ListFirstIPByHost("tail.test"); got != "5.6.7.8" {
t.Fatalf("expected tail reverse index to remain, got %q", got)
}
}
func TestAddHostsRejectsInvalidInput(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("not-an-ip", "bad.test"); err == nil {
t.Fatal("expected invalid ip error")
}
if got := h.ListHostsByIP("not-an-ip"); len(got) != 0 {
t.Fatalf("invalid ip should not be indexed, got %v", got)
}
if err := h.AddHosts("1.2.3.4", ""); err == nil {
t.Fatal("expected empty host error")
}
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 0 {
t.Fatalf("empty host should not be indexed, got %v", got)
}
}
func TestInsertNodeByDataRejectsInvalidHostDataWithoutMutating(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("2.2.2.2", "anchor.test"); err != nil {
t.Fatal(err)
}
anchor, err := h.GetFirstNode()
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
ip string
hosts []string
}{
{name: "bad ip", ip: "not-an-ip", hosts: []string{"bad.test"}},
{name: "empty host", ip: "1.1.1.1", hosts: []string{""}},
{name: "comment host", ip: "1.1.1.1", hosts: []string{"#bad.test"}},
{name: "missing host", ip: "1.1.1.1"},
}
for _, tt := range tests {
if err := h.InsertNodeByData(anchor, false, "", tt.ip, tt.hosts...); err == nil {
t.Fatalf("%s: expected error", tt.name)
}
}
nodes := h.List()
if len(nodes) != 1 || nodes[0].IP() != "2.2.2.2" {
t.Fatalf("invalid insert should not mutate node list: %#v", nodes)
}
if got := h.ListHostsByIP("1.1.1.1"); len(got) != 0 {
t.Fatalf("invalid insert should not mutate ip index: %v", got)
}
if err := h.InsertNodeByData(anchor, true, "comment-only", ""); err != nil {
t.Fatalf("comment-only insert should remain valid: %v", err)
}
nodes = h.List()
if len(nodes) != 2 || !nodes[0].OnlyComment() || nodes[1].IP() != "2.2.2.2" {
t.Fatalf("comment-only insert mismatch: %#v", nodes)
}
}
func TestInsertNodeByDataRejectsEmptyNodeWithoutMutating(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("2.2.2.2", "anchor.test"); err != nil {
t.Fatal(err)
}
anchor, err := h.GetFirstNode()
if err != nil {
t.Fatal(err)
}
if err := h.InsertNodeByData(anchor, true, "", ""); err == nil {
t.Fatal("expected empty insert to fail")
}
nodes := h.List()
if len(nodes) != 1 || nodes[0].IP() != "2.2.2.2" {
t.Fatalf("empty insert should not mutate node list: %#v", nodes)
}
out, err := h.Build()
if err != nil {
t.Fatal(err)
}
if got, want := string(out), "2.2.2.2 anchor.test"+lineBreaker; got != want {
t.Fatalf("empty insert should not change output: got %q want %q", got, want)
}
}
func TestEmptyHostsBuildAndSaveAs(t *testing.T) {
h := NewHosts()
path := filepath.Join(t.TempDir(), "hosts.empty")
if err := os.WriteFile(path, nil, 0o644); err != nil {
t.Fatal(err)
}
if err := h.Parse(path); err != nil {
t.Fatal(err)
}
out, err := h.Build()
if err != nil {
t.Fatal(err)
}
if len(out) != 0 {
t.Fatalf("expected empty build output, got %q", out)
}
outPath := filepath.Join(t.TempDir(), "hosts.out")
if err := h.SaveAs(outPath); err != nil {
t.Fatal(err)
}
saved, err := os.ReadFile(outPath)
if err != nil {
t.Fatal(err)
}
if len(saved) != 0 {
t.Fatalf("expected empty saved file, got %q", saved)
}
}
func TestParsePreservesBlankAndRawLines(t *testing.T) {
h := NewHosts()
path := filepath.Join(t.TempDir(), "hosts.raw")
input := []byte("127.0.0.1 localhost\n\nbadline\n# tail comment\n")
if err := os.WriteFile(path, input, 0o644); err != nil {
t.Fatal(err)
}
if err := h.Parse(path); err != nil {
t.Fatal(err)
}
out, err := h.Build()
if err != nil {
t.Fatal(err)
}
got := string(out)
if !strings.Contains(got, "127.0.0.1 localhost"+lineBreaker+lineBreaker+"badline"+lineBreaker) {
t.Fatalf("expected blank/raw lines to be preserved, got %q", got)
}
if !strings.Contains(got, "# tail comment"+lineBreaker) {
t.Fatalf("expected comment line to be preserved, got %q", got)
}
}
func TestHostAccessorsReturnDetachedCopies(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "example.test"); err != nil {
t.Fatal(err)
}
node, err := h.GetFirstNode()
if err != nil {
t.Fatal(err)
}
node.SetIP("9.9.9.9")
node.SetHosts("mutated.test")
if got := h.ListFirstIPByHost("example.test"); got != "1.2.3.4" {
t.Fatalf("detached copy mutated internal host index: %q", got)
}
if got := h.ListFirstIPByHost("mutated.test"); got != "" {
t.Fatalf("detached copy should not create new host index: %q", got)
}
out, err := h.Build()
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(out), "9.9.9.9 mutated.test") {
t.Fatalf("detached copy leaked into build output: %q", out)
}
}
func TestUpdateNodeRejectsInvalidMutationAndPreservesState(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "example.test"); err != nil {
t.Fatal(err)
}
node, err := h.GetFirstNode()
if err != nil {
t.Fatal(err)
}
node.SetIP("bad-ip")
if err := h.UpdateNode(node); err == nil {
t.Fatal("expected invalid update to fail")
}
if got := h.ListFirstIPByHost("example.test"); got != "1.2.3.4" {
t.Fatalf("failed update should preserve previous index, got %q", got)
}
out, err := h.Build()
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(out), "1.2.3.4 example.test") || strings.Contains(string(out), "bad-ip") {
t.Fatalf("failed update should preserve previous output, got %q", out)
}
}
func TestUpdateNodeCommentOnlyStateReindexesCleanly(t *testing.T) {
h := NewHosts()
if err := h.AddHosts("1.2.3.4", "example.test"); err != nil {
t.Fatal(err)
}
node, err := h.GetFirstNode()
if err != nil {
t.Fatal(err)
}
node.SetIP("")
node.SetHosts()
node.SetComment("note")
if err := h.UpdateNode(node); err != nil {
t.Fatalf("comment-only update failed: %v", err)
}
if got := h.ListByIP(""); len(got) != 0 {
t.Fatalf("comment-only node should not be indexed under empty ip: %#v", got)
}
updated, err := h.GetNode(node.UID())
if err != nil {
t.Fatal(err)
}
if !updated.OnlyComment() {
t.Fatalf("comment-only node should keep onlyComment state: %#v", updated)
}
node = updated
node.SetIP("2.2.2.2")
node.SetHosts("restored.test")
if err := h.UpdateNode(node); err != nil {
t.Fatalf("restoring host entry failed: %v", err)
}
updated, err = h.GetNode(node.UID())
if err != nil {
t.Fatal(err)
}
if updated.OnlyComment() {
t.Fatalf("host entry should clear onlyComment after restore: %#v", updated)
}
if got := h.ListFirstIPByHost("restored.test"); got != "2.2.2.2" {
t.Fatalf("restored host index mismatch: %q", got)
}
if got := h.ListByIP(""); len(got) != 0 {
t.Fatalf("restored host should still avoid empty ip index: %#v", got)
}
} }
+23
View File
@@ -0,0 +1,23 @@
#hosts This file describes a number of hostname-to-address
#mappings for the TCP/IP subsystem. It is mostly
#used at boot time, when no name servers are running.
#On small systems, this file can be used instead of a
#"named" name server.
#Syntax:
#IP-Address Full-Qualifie
#special IPv6 addre
127.0.0.1 localhost
127.0.0.1 b612
8.8.8.8 ssh.b612.me
#special IPv6 addresses
::1 localhost ipv6-localhost ipv6-loopback
fe00::0 ipv6-localnet
ff00::0 ipv6-mcastprefix
ff02::1 ipv6-allnodes
ff02::2 ipv6-allrouters
ff02::3 ipv6-allhosts
1.2.3.4 ssh.b612.me
4.5.6.7 dns.b612.me
8.9.10.11 release-ftpd
11.22.33.44 test.dns.set.b612.me remove.b612.me
4.5.6.7 game.b612.me
+403
View File
@@ -0,0 +1,403 @@
package staros
import (
"fmt"
"math"
"strconv"
"strings"
"unicode"
)
// Calc evaluates a small frozen arithmetic expression language kept for
// compatibility with older staros callers.
func Calc(expr string) (float64, error) {
parser := calcParser{input: strings.ToLower(strings.TrimSpace(expr))}
if parser.input == "" {
return 0, fmt.Errorf("empty expression")
}
value, err := parser.parseExpression()
if err != nil {
return 0, err
}
parser.skipSpace()
if !parser.done() {
return 0, fmt.Errorf("unexpected token %q at position %d", parser.peek(), parser.pos)
}
return normalizeCalcFloat(value), nil
}
type calcParser struct {
input string
pos int
}
func (p *calcParser) parseExpression() (float64, error) {
return p.parseAddSub()
}
func (p *calcParser) parseAddSub() (float64, error) {
left, err := p.parseMulDiv()
if err != nil {
return 0, err
}
for {
p.skipSpace()
switch p.peek() {
case '+':
p.pos++
right, err := p.parseMulDiv()
if err != nil {
return 0, err
}
left += right
case '-':
p.pos++
right, err := p.parseMulDiv()
if err != nil {
return 0, err
}
left -= right
default:
return left, nil
}
}
}
func (p *calcParser) parseMulDiv() (float64, error) {
left, err := p.parsePower()
if err != nil {
return 0, err
}
for {
p.skipSpace()
switch p.peek() {
case '*':
p.pos++
right, err := p.parsePower()
if err != nil {
return 0, err
}
left *= right
case '/':
p.pos++
right, err := p.parsePower()
if err != nil {
return 0, err
}
if right == 0 {
return 0, fmt.Errorf("divisor cannot be 0")
}
left /= right
default:
return left, nil
}
}
}
func (p *calcParser) parsePower() (float64, error) {
left, err := p.parseUnary()
if err != nil {
return 0, err
}
p.skipSpace()
if p.peek() != '^' {
return left, nil
}
p.pos++
right, err := p.parsePower()
if err != nil {
return 0, err
}
return math.Pow(left, right), nil
}
func (p *calcParser) parseUnary() (float64, error) {
p.skipSpace()
switch p.peek() {
case '+':
p.pos++
return p.parseUnary()
case '-':
p.pos++
value, err := p.parseUnary()
if err != nil {
return 0, err
}
return -value, nil
default:
return p.parsePrimary()
}
}
func (p *calcParser) parsePrimary() (float64, error) {
p.skipSpace()
if p.done() {
return 0, fmt.Errorf("unexpected end of expression")
}
ch := p.peek()
switch {
case ch == '(':
p.pos++
value, err := p.parseExpression()
if err != nil {
return 0, err
}
p.skipSpace()
if p.peek() != ')' {
return 0, fmt.Errorf("missing ')' at position %d", p.pos)
}
p.pos++
return value, nil
case isCalcNumberStart(p.input, p.pos):
return p.parseNumber()
case isCalcIdentStart(ch):
return p.parseIdentifier()
default:
return 0, fmt.Errorf("unexpected token %q at position %d", ch, p.pos)
}
}
func (p *calcParser) parseNumber() (float64, error) {
start := p.pos
seenDot := false
seenExp := false
for !p.done() {
ch := p.peek()
switch {
case ch >= '0' && ch <= '9':
p.pos++
case ch == '.' && !seenDot && !seenExp:
seenDot = true
p.pos++
case (ch == 'e') && !seenExp:
seenExp = true
p.pos++
if !p.done() && (p.peek() == '+' || p.peek() == '-') {
p.pos++
}
default:
value, err := strconv.ParseFloat(p.input[start:p.pos], 64)
if err != nil {
return 0, fmt.Errorf("invalid number %q at position %d", p.input[start:p.pos], start)
}
return value, nil
}
}
value, err := strconv.ParseFloat(p.input[start:p.pos], 64)
if err != nil {
return 0, fmt.Errorf("invalid number %q at position %d", p.input[start:p.pos], start)
}
return value, nil
}
func (p *calcParser) parseIdentifier() (float64, error) {
start := p.pos
for !p.done() && isCalcIdent(p.peek()) {
p.pos++
}
name := p.input[start:p.pos]
p.skipSpace()
if p.peek() != '(' {
value, ok := calcConstant(name)
if !ok {
return 0, fmt.Errorf("unknown identifier %q at position %d", name, start)
}
return value, nil
}
p.pos++
args, err := p.parseArguments(name)
if err != nil {
return 0, err
}
return calcFunction(name, args)
}
func (p *calcParser) parseArguments(name string) ([]float64, error) {
p.skipSpace()
if p.peek() == ')' {
p.pos++
return nil, nil
}
var args []float64
for {
arg, err := p.parseExpression()
if err != nil {
return nil, err
}
args = append(args, arg)
p.skipSpace()
if p.peek() != ',' {
break
}
p.pos++
p.skipSpace()
if p.peek() == ')' {
return nil, fmt.Errorf("missing argument for function %q", name)
}
}
if p.peek() != ')' {
return nil, fmt.Errorf("missing ')' after function %q", name)
}
p.pos++
return args, nil
}
func (p *calcParser) skipSpace() {
for !p.done() && unicode.IsSpace(rune(p.peek())) {
p.pos++
}
}
func (p *calcParser) done() bool {
return p.pos >= len(p.input)
}
func (p *calcParser) peek() byte {
if p.done() {
return 0
}
return p.input[p.pos]
}
func isCalcNumberStart(input string, pos int) bool {
ch := input[pos]
if ch >= '0' && ch <= '9' {
return true
}
return ch == '.' && pos+1 < len(input) && input[pos+1] >= '0' && input[pos+1] <= '9'
}
func isCalcIdentStart(ch byte) bool {
return ch >= 'a' && ch <= 'z'
}
func isCalcIdent(ch byte) bool {
return (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '_'
}
func calcConstant(name string) (float64, bool) {
switch name {
case "pi":
return math.Pi, true
case "e":
return math.E, true
default:
return 0, false
}
}
func calcFunction(name string, args []float64) (float64, error) {
argCount := len(args)
if !calcFunctionArgCountValid(name, argCount) {
return 0, fmt.Errorf("function %q accepts %s, got %d", name, calcFunctionArgSpec(name), argCount)
}
switch name {
case "sin":
return math.Sin(args[0]), nil
case "cos":
return math.Cos(args[0]), nil
case "tan":
return math.Tan(args[0]), nil
case "sinh":
return math.Sinh(args[0]), nil
case "cosh":
return math.Cosh(args[0]), nil
case "tanh":
return math.Tanh(args[0]), nil
case "abs":
return math.Abs(args[0]), nil
case "arcsin", "asin":
return math.Asin(args[0]), nil
case "arccos", "acos":
return math.Acos(args[0]), nil
case "arctan", "atan":
return math.Atan(args[0]), nil
case "sqrt":
return math.Sqrt(args[0]), nil
case "cbrt":
return math.Cbrt(args[0]), nil
case "exp":
return math.Exp(args[0]), nil
case "loge", "ln":
return math.Log(args[0]), nil
case "log":
return math.Log10(args[0]), nil
case "log10":
return math.Log10(args[0]), nil
case "log2":
return math.Log2(args[0]), nil
case "floor":
return math.Floor(args[0]), nil
case "ceil":
return math.Ceil(args[0]), nil
case "round":
return math.Round(args[0]), nil
case "trunc":
return math.Trunc(args[0]), nil
case "rad":
return args[0] * math.Pi / 180.0, nil
case "deg":
return args[0] * 180.0 / math.Pi, nil
case "pow":
return math.Pow(args[0], args[1]), nil
case "hypot":
return math.Hypot(args[0], args[1]), nil
case "min":
result := args[0]
for _, arg := range args[1:] {
if arg < result {
result = arg
}
}
return result, nil
case "max":
result := args[0]
for _, arg := range args[1:] {
if arg > result {
result = arg
}
}
return result, nil
default:
return 0, fmt.Errorf("unknown function %q", name)
}
}
func calcFunctionArgCountValid(name string, count int) bool {
switch name {
case "pow", "hypot":
return count == 2
case "min", "max":
return count >= 1
case "sin", "cos", "tan", "sinh", "cosh", "tanh",
"abs", "arcsin", "asin", "arccos", "acos", "arctan", "atan",
"sqrt", "cbrt", "exp", "loge", "ln", "log", "log10", "log2",
"floor", "ceil", "round", "trunc", "rad", "deg":
return count == 1
default:
return true
}
}
func calcFunctionArgSpec(name string) string {
switch name {
case "pow", "hypot":
return "exactly two arguments"
case "min", "max":
return "at least one argument"
default:
return "exactly one argument"
}
}
func normalizeCalcFloat(value float64) float64 {
text := strconv.FormatFloat(value, 'g', 15, 64)
out, err := strconv.ParseFloat(text, 64)
if err != nil {
return value
}
if out == 0 {
return 0
}
return out
}
+61
View File
@@ -0,0 +1,61 @@
package staros
import (
"math"
"testing"
)
func TestCalcCompatibilityExpressions(t *testing.T) {
tests := []struct {
expr string
want float64
}{
{"1+2*3", 7},
{"(1+2)*3", 9},
{"60*60*24", 86400},
{"-1+2", 1},
{"sqrt(4)+abs(-3)", 5},
{"sin(pi/2)", 1},
{"arcsin(1)", math.Pi / 2},
{"asin(1)", math.Pi / 2},
{"loge(e)", 1},
{"ln(e)", 1},
{"log10(100)+log2(8)", 5},
{"floor(1.9)+ceil(1.1)+round(1.5)+trunc(1.9)", 6},
{"1.2e3+3", 1203},
{"pow(2,3)+hypot(3,4)", 13},
{"min(3,1,2)+max(3,1,2)", 4},
{"log(100)+rad(180)/pi+deg(pi)/180", 4},
{"cbrt(27)+exp(0)", 4},
{"sinh(0)+cosh(0)+tanh(0)", 1},
}
for _, tt := range tests {
got, err := Calc(tt.expr)
if err != nil {
t.Fatalf("Calc(%q) failed: %v", tt.expr, err)
}
if math.Abs(got-tt.want) > 1e-12 {
t.Fatalf("Calc(%q)=%v, want %v", tt.expr, got, tt.want)
}
}
}
func TestCalcRejectsInvalidExpressions(t *testing.T) {
tests := []string{
"",
"1/",
"(1+2",
"1/0",
"unknown(1)",
"min()",
"pow(2)",
"pow(2,3,4)",
"sqrt(1,2)",
"pi()",
}
for _, expr := range tests {
if got, err := Calc(expr); err == nil {
t.Fatalf("Calc(%q)=%v, expected error", expr, got)
}
}
}
+102
View File
@@ -0,0 +1,102 @@
//go:build darwin
// +build darwin
package staros
import (
"encoding/binary"
"fmt"
"golang.org/x/sys/unix"
"os/exec"
"strconv"
"strings"
"unsafe"
)
// Memory 系统内存信息
func Memory() (MemStatus, error) {
return darwinMemory()
}
type swapUsage struct {
Total uint64
Avail uint64
Used uint64
Pagesize int32
Encrypted bool
}
func darwinMemory() (MemStatus, error) {
var err error
var res MemStatus
vm_stat, err := exec.LookPath("vm_stat")
if err != nil {
return res, err
}
out, err := exec.Command(vm_stat).CombinedOutput()
if err != nil {
return res, err
}
totalString, err := unix.Sysctl("hw.memsize")
if err != nil {
return res, err
}
// unix.sysctl() helpfully assumes the result is a null-terminated string and
// removes the last byte of the result if it's 0 :/
totalString += "\x00"
res.All = uint64(binary.LittleEndian.Uint64([]byte(totalString)))
lines := strings.Split(string(out), "\n")
pagesize := uint64(unix.Getpagesize())
for _, line := range lines {
fields := strings.Split(line, ":")
if len(fields) < 2 {
continue
}
key := strings.TrimSpace(fields[0])
value := strings.Trim(fields[1], " .")
switch key {
case "Pages free":
free, e := strconv.ParseUint(value, 10, 64)
if e != nil {
err = e
}
res.Free = free * pagesize
case "Pages inactive":
inactive, e := strconv.ParseUint(value, 10, 64)
if e != nil {
err = e
}
res.Available = inactive * pagesize
case "Pages active":
active, e := strconv.ParseUint(value, 10, 64)
if e != nil {
err = e
}
_ = active * pagesize
case "Pages wired down":
wired, e := strconv.ParseUint(value, 10, 64)
if e != nil {
err = e
}
_ = wired * pagesize
}
}
res.Available += res.Free
res.Used = res.All - res.Available
//swap
value, err := unix.SysctlRaw("vm.swapusage")
if err != nil {
return res, err
}
if len(value) != 32 {
return res, fmt.Errorf("unexpected output of sysctl vm.swapusage: %v (len: %d)", value, len(value))
}
swap := (*swapUsage)(unsafe.Pointer(&value[0]))
res.SwapAll = swap.Total
res.SwapUsed = swap.Used
res.SwapFree = swap.Avail
return res, err
}
+56
View File
@@ -0,0 +1,56 @@
//go:build linux
// +build linux
package staros
import (
"io/ioutil"
"strconv"
"strings"
"syscall"
)
// Memory 系统内存信息
func Memory() (MemStatus, error) {
var mem MemStatus
ram := new(syscall.Sysinfo_t)
if err := syscall.Sysinfo(ram); err != nil {
return mem, err
}
unit := uint64(ram.Unit)
if unit == 0 {
unit = 1
}
mem.All = uint64(ram.Totalram) * unit
mem.BuffCache = uint64(ram.Bufferram) * unit
mem.Free = uint64(ram.Freeram) * unit
mem.Shared = uint64(ram.Sharedram) * unit
mem.Available = mem.Free + mem.Shared + mem.BuffCache
if available, ok := linuxMemAvailable(); ok {
mem.Available = available
}
mem.SwapAll = uint64(ram.Totalswap) * unit
mem.SwapFree = uint64(ram.Freeswap) * unit
mem.SwapUsed = uint64(mem.SwapAll - mem.SwapFree)
mem.Used = uint64(mem.All - mem.Free)
return mem, nil
}
func linuxMemAvailable() (uint64, bool) {
data, err := ioutil.ReadFile("/proc/meminfo")
if err != nil {
return 0, false
}
for _, line := range strings.Split(string(data), "\n") {
fields := strings.Fields(line)
if len(fields) < 2 || fields[0] != "MemAvailable:" {
continue
}
value, err := strconv.ParseUint(fields[1], 10, 64)
if err != nil {
return 0, false
}
return value * 1024, true
}
return 0, false
}
+28
View File
@@ -0,0 +1,28 @@
//go:build windows
// +build windows
package staros
import "b612.me/win32api"
// Memory 系统内存信息
func Memory() (MemStatus, error) {
var mem MemStatus
ram := new(win32api.MEMORYSTATUSEX)
_, err := win32api.GlobalMemoryStatusEx(ram)
if err != nil {
return mem, err
}
mem.All = uint64(ram.UllTotalPhys)
mem.Free = uint64(ram.UllAvailPhys)
mem.Available = uint64(ram.UllAvailPhys)
mem.Used = uint64(mem.All - mem.Free)
mem.SwapAll = uint64(ram.UllTotalPageFile)
mem.SwapFree = uint64(ram.UllAvailPageFile)
mem.SwapUsed = mem.SwapAll - mem.SwapFree
mem.VirtualAll = uint64(ram.UllTotalVirtual)
mem.VirtualAvail = uint64(ram.UllAvailVirtual)
mem.VirtualUsed = mem.VirtualAll - mem.VirtualAvail
mem.AvailExtended = uint64(ram.UllAvailExtendedVirtual)
return mem, nil
}
+30
View File
@@ -0,0 +1,30 @@
//go:build darwin
// +build darwin
package staros
import "time"
func NetUsage() ([]NetAdapter, error) {
return nil, ERR_UNSUPPORTED
}
func NetUsageByname(name string) (NetAdapter, error) {
return NetAdapter{}, ERR_UNSUPPORTED
}
func NetSpeeds(duration time.Duration) ([]NetSpeed, error) {
return nil, ERR_UNSUPPORTED
}
func NetSpeedsByName(duration time.Duration, name string) (NetSpeed, error) {
return NetSpeed{}, ERR_UNSUPPORTED
}
func NetConnections(analysePid bool, types string) ([]NetConn, error) {
return nil, ERR_UNSUPPORTED
}
func GetInodeMap() (map[string]int64, error) {
return nil, ERR_UNSUPPORTED
}
+67
View File
@@ -0,0 +1,67 @@
//go:build linux
// +build linux
package staros
import (
"testing"
"time"
)
func Test_TrimSpace(t *testing.T) {
}
func TestAnalyseNetFilesSkipsShortLines(t *testing.T) {
data := []byte("sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\nshort\n")
res, err := analyseNetFiles(data, nil, "tcp")
if err != nil {
t.Fatal(err)
}
if len(res) != 0 {
t.Fatalf("expected no parsed connections, got %d", len(res))
}
}
func TestNetSpeedsRejectsInvalidDuration(t *testing.T) {
_, err := NetSpeeds(0)
if err == nil {
t.Fatal("expected invalid duration error")
}
_, err = NetSpeeds(-time.Second)
if err == nil {
t.Fatal("expected invalid duration error")
}
}
func TestUniqueStrings(t *testing.T) {
got := uniqueStrings([]string{"tcp", "udp", "tcp"})
if len(got) != 2 {
t.Fatalf("expected 2 unique values, got %d", len(got))
}
if got[0] != "tcp" || got[1] != "udp" {
t.Fatalf("unexpected order: %#v", got)
}
}
func TestParseProcStatusKB(t *testing.T) {
if got := parseProcStatusKB("12 kB"); got != 12*1024 {
t.Fatalf("expected 12288, got %d", got)
}
if got := parseProcStatusKB(""); got != 0 {
t.Fatalf("expected 0, got %d", got)
}
}
func TestProcStartTimeFromStatHandlesProcessNameWithSpacesAndParens(t *testing.T) {
stat := []byte("42 (name with ) parens) S 1 1 1 0 -1 4194560 0 0 0 0 0 0 0 0 20 0 1 0 12345")
got, ok := procStartTimeFromStat(stat)
if !ok {
t.Fatal("expected proc stat start time to parse")
}
ticks := int64(clockTicks())
want := time.Unix(StartTime().Unix()+12345/ticks, (12345%ticks)*int64(time.Second)/ticks)
if !got.Equal(want) {
t.Fatalf("unexpected start time: got %s want %s", got, want)
}
}
+390
View File
@@ -0,0 +1,390 @@
//go:build linux
// +build linux
package staros
import (
"errors"
"io/ioutil"
"os"
"strconv"
"strings"
"time"
)
func NetUsage() ([]NetAdapter, error) {
data, err := ioutil.ReadFile("/proc/net/dev")
if err != nil {
return nil, err
}
sps := strings.Split(strings.TrimSpace(string(data)), "\n")
if len(sps) < 3 {
return nil, errors.New("No Adaptor")
}
var res []NetAdapter
netLists := sps[2:]
for _, v := range netLists {
parts := strings.SplitN(strings.TrimSpace(v), ":", 2)
if len(parts) != 2 {
continue
}
card := strings.Fields(parts[1])
if len(card) < 16 {
continue
}
name := strings.TrimSpace(parts[0])
recvBytes, err := strconv.ParseUint(card[0], 10, 64)
if err != nil {
continue
}
sendBytes, err := strconv.ParseUint(card[8], 10, 64)
if err != nil {
continue
}
res = append(res, NetAdapter{name, uint64(recvBytes), uint64(sendBytes)})
}
if len(res) == 0 {
return nil, errors.New("No Adaptor")
}
return res, nil
}
func NetUsageByname(name string) (NetAdapter, error) {
ada, err := NetUsage()
if err != nil {
return NetAdapter{}, err
}
for _, v := range ada {
if v.Name == name {
return v, nil
}
}
return NetAdapter{}, errors.New("Not Found")
}
func NetSpeeds(duration time.Duration) ([]NetSpeed, error) {
if duration <= 0 {
return nil, errors.New("duration must be positive")
}
list1, err := NetUsage()
if err != nil {
return nil, err
}
time.Sleep(duration)
list2, err := NetUsage()
if err != nil {
return nil, err
}
byName := make(map[string]NetAdapter, len(list2))
for _, item := range list2 {
byName[item.Name] = item
}
var res []NetSpeed
for _, v := range list1 {
next, ok := byName[v.Name]
if !ok {
continue
}
var recvDelta, sendDelta uint64
if next.RecvBytes >= v.RecvBytes {
recvDelta = next.RecvBytes - v.RecvBytes
}
if next.SendBytes >= v.SendBytes {
sendDelta = next.SendBytes - v.SendBytes
}
recv := float64(recvDelta) / duration.Seconds()
send := float64(sendDelta) / duration.Seconds()
res = append(res, NetSpeed{
Name: v.Name,
RecvSpeeds: recv,
SendSpeeds: send,
RecvBytes: next.RecvBytes,
SendBytes: next.SendBytes,
})
}
if len(res) == 0 {
return nil, errors.New("NetWork Adaptor Num Not ok")
}
return res, nil
}
func NetSpeedsByName(duration time.Duration, name string) (NetSpeed, error) {
ada, err := NetSpeeds(duration)
if err != nil {
return NetSpeed{}, err
}
for _, v := range ada {
if v.Name == name {
return v, nil
}
}
return NetSpeed{}, errors.New("Not Found")
}
// NetConnections return all TCP/UDP/UNIX DOMAIN SOCKET Connections
// if your uid != 0 ,and analysePid==true ,you should have CAP_SYS_PRTACE and CAP_DAC_OVERRIDE/CAP_DAC_READ_SEARCH Caps
func NetConnections(analysePid bool, types string) ([]NetConn, error) {
var result []NetConn
var inodeMap map[string]int64
var err error
var fileList []string
types = strings.ToLower(types)
if types == "" || strings.Contains(types, "all") {
fileList = []string{
"/proc/net/tcp",
"/proc/net/tcp6",
"/proc/net/udp",
"/proc/net/udp6",
"/proc/net/unix",
}
} else {
if strings.Contains(types, "tcp") {
fileList = append(fileList, "/proc/net/tcp", "/proc/net/tcp6")
}
if strings.Contains(types, "udp") {
fileList = append(fileList, "/proc/net/udp", "/proc/net/udp6")
}
if strings.Contains(types, "unix") {
fileList = append(fileList, "/proc/net/unix")
}
}
fileList = uniqueStrings(fileList)
if len(fileList) == 0 {
return nil, errors.New("unsupported net connection type")
}
if analysePid {
inodeMap, err = GetInodeMap()
if err != nil {
inodeMap = nil
}
}
for _, file := range fileList {
data, err := ioutil.ReadFile(file)
if err != nil {
if os.IsNotExist(err) {
continue
}
return result, err
}
tmpRes, err := analyseNetFiles(data, inodeMap, file[strings.LastIndex(file, "/")+1:])
if err != nil {
return result, err
}
result = append(result, tmpRes...)
}
return result, nil
}
func uniqueStrings(items []string) []string {
if len(items) == 0 {
return nil
}
seen := make(map[string]struct{}, len(items))
res := make([]string, 0, len(items))
for _, item := range items {
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
res = append(res, item)
}
return res
}
func GetInodeMap() (map[string]int64, error) {
res := make(map[string]int64)
paths, err := ioutil.ReadDir("/proc")
if err != nil {
return nil, err
}
for _, v := range paths {
if v.IsDir() && Exists("/proc/"+v.Name()+"/fd") {
fds, err := ioutil.ReadDir("/proc/" + v.Name() + "/fd")
if err != nil && Exists("/proc/"+v.Name()+"/fd") {
return nil, err
}
for _, fd := range fds {
socket, err := os.Readlink("/proc/" + v.Name() + "/fd/" + fd.Name())
if err != nil {
continue
}
if !strings.Contains(socket, "socket") {
continue
}
start := strings.Index(socket, "[")
if start < 0 {
continue
}
pid, err := strconv.ParseInt(v.Name(), 10, 64)
if err != nil {
break
}
res[socket[start+1:len(socket)-1]] = pid
}
}
}
return res, err
}
func analyseNetFiles(data []byte, inodeMap map[string]int64, typed string) ([]NetConn, error) {
if typed == "unix" {
return analyseUnixFiles(data, inodeMap, typed)
}
var result []NetConn
strdata := strings.TrimSpace(string(data))
strdata = remainOne(strdata, " ", " ")
csvData := strings.Split(strdata, "\n")
pidMap := make(map[int64]*Process)
for line, lineData := range csvData {
if line == 0 {
continue
}
v := strings.Split(strings.TrimSpace(lineData), " ")
if len(v) < 10 {
continue
}
var res NetConn
ip, port, err := parseHexIpPort(v[1])
if err != nil {
return result, err
}
res.LocalAddr = ip
res.LocalPort = port
ip, port, err = parseHexIpPort(v[2])
if err != nil {
return result, err
}
res.RemoteAddr = ip
res.RemotePort = port
//connection state
if strings.Contains(typed, "tcp") {
state, err := strconv.ParseInt(strings.TrimSpace(v[3]), 16, 64)
if err != nil {
return result, err
}
if state >= 0 && int(state) < len(TCP_STATE) {
res.Status = TCP_STATE[state]
} else {
res.Status = TCP_STATE[TCP_UNKNOWN]
}
}
txrx_queue := strings.Split(strings.TrimSpace(v[4]), ":")
if len(txrx_queue) != 2 {
return result, errors.New("not a valid net file")
}
tx_queue, err := strconv.ParseInt(txrx_queue[0], 16, 64)
if err != nil {
return result, err
}
res.TX_Queue = tx_queue
rx_queue, err := strconv.ParseInt(txrx_queue[1], 16, 64)
if err != nil {
return result, err
}
res.RX_Queue = rx_queue
timer := strings.Split(strings.TrimSpace(v[5]), ":")
if len(timer) != 2 {
return result, errors.New("not a valid net file")
}
switch timer[0] {
case "00":
res.TimerActive = "NO_TIMER"
case "01":
//重传定时器
res.TimerActive = "RETRANSMIT"
case "02":
//连接定时器、FIN_WAIT_2定时器或TCP保活定时器
res.TimerActive = "KEEPALIVE"
case "03":
//TIME_WAIT定时器
res.TimerActive = "TIME_WAIT"
case "04":
//持续定时器
res.TimerActive = "ZERO_WINDOW_PROBE"
default:
res.TimerActive = "UNKNOWN"
}
timerJif, err := strconv.ParseInt(timer[1], 16, 64)
if err != nil {
return result, err
}
res.TimerJiffies = timerJif
timerCnt, err := strconv.ParseInt(strings.TrimSpace(v[6]), 16, 64)
if err != nil {
return result, err
}
res.RtoTimer = timerCnt
res.Uid, err = strconv.ParseInt(v[7], 10, 64)
if err != nil {
return result, err
}
res.Inode = v[9]
if inodeMap != nil && len(inodeMap) > 0 {
var ok bool
res.Pid, ok = inodeMap[res.Inode]
if !ok {
res.Pid = -1
} else {
_, ok := pidMap[res.Pid]
if !ok {
tmp, err := FindProcessByPid(res.Pid)
if err != nil {
pidMap[res.Pid] = nil
} else {
pidMap[res.Pid] = &tmp
}
}
res.Process = pidMap[res.Pid]
}
}
res.Typed = typed
result = append(result, res)
}
return result, nil
}
func analyseUnixFiles(data []byte, inodeMap map[string]int64, typed string) ([]NetConn, error) {
var result []NetConn
strdata := strings.TrimSpace(string(data))
strdata = remainOne(strdata, " ", " ")
csvData := strings.Split(strdata, "\n")
pidMap := make(map[int64]*Process)
for line, lineData := range csvData {
if line == 0 {
continue
}
v := strings.Split(strings.TrimSpace(lineData), " ")
if len(v) < 7 {
continue
}
var res NetConn
res.Inode = v[6]
if len(v) == 8 {
res.Socket = v[7]
}
if inodeMap != nil && len(inodeMap) > 0 {
var ok bool
res.Pid, ok = inodeMap[res.Inode]
if !ok {
res.Pid = -1
} else {
_, ok := pidMap[res.Pid]
if !ok || pidMap[res.Pid] == nil {
tmp, err := FindProcessByPid(res.Pid)
if err != nil {
pidMap[res.Pid] = nil
} else {
pidMap[res.Pid] = &tmp
}
}
if pidMap[res.Pid] != nil {
res.Uid = int64(pidMap[res.Pid].RUID)
res.Process = pidMap[res.Pid]
}
}
}
res.Typed = typed
result = append(result, res)
}
return result, nil
}
+314
View File
@@ -0,0 +1,314 @@
//go:build windows
// +build windows
package staros
import (
"errors"
"net"
"strconv"
"strings"
"syscall"
"time"
"b612.me/win32api"
)
const windowsErrorNotSupported syscall.Errno = 50
func NetUsage() ([]NetAdapter, error) {
rows, err := win32api.GetIfTable2()
if err != nil {
return nil, err
}
res := make([]NetAdapter, 0, len(rows))
for _, row := range rows {
name := windowsInterfaceName(row)
if name == "" {
continue
}
res = append(res, NetAdapter{
Name: name,
RecvBytes: row.InOctets,
SendBytes: row.OutOctets,
})
}
if len(res) == 0 {
return nil, errors.New("No Adaptor")
}
return res, nil
}
func NetUsageByname(name string) (NetAdapter, error) {
ada, err := NetUsage()
if err != nil {
return NetAdapter{}, err
}
for _, v := range ada {
if v.Name == name {
return v, nil
}
}
return NetAdapter{}, errors.New("Not Found")
}
func NetSpeeds(duration time.Duration) ([]NetSpeed, error) {
if duration <= 0 {
return nil, errors.New("duration must be positive")
}
list1, err := NetUsage()
if err != nil {
return nil, err
}
time.Sleep(duration)
list2, err := NetUsage()
if err != nil {
return nil, err
}
byName := make(map[string]NetAdapter, len(list2))
for _, item := range list2 {
byName[item.Name] = item
}
res := make([]NetSpeed, 0, len(list1))
for _, v := range list1 {
next, ok := byName[v.Name]
if !ok {
continue
}
var recvDelta, sendDelta uint64
if next.RecvBytes >= v.RecvBytes {
recvDelta = next.RecvBytes - v.RecvBytes
}
if next.SendBytes >= v.SendBytes {
sendDelta = next.SendBytes - v.SendBytes
}
res = append(res, NetSpeed{
Name: v.Name,
RecvSpeeds: float64(recvDelta) / duration.Seconds(),
SendSpeeds: float64(sendDelta) / duration.Seconds(),
RecvBytes: next.RecvBytes,
SendBytes: next.SendBytes,
})
}
if len(res) == 0 {
return nil, errors.New("NetWork Adaptor Num Not ok")
}
return res, nil
}
func NetSpeedsByName(duration time.Duration, name string) (NetSpeed, error) {
ada, err := NetSpeeds(duration)
if err != nil {
return NetSpeed{}, err
}
for _, v := range ada {
if v.Name == name {
return v, nil
}
}
return NetSpeed{}, errors.New("Not Found")
}
// NetConnections return all TCP/UDP/UNIX DOMAIN SOCKET Connections
// if your uid != 0 ,and analysePid==true ,you should have CAP_SYS_PRTACE and CAP_DAC_OVERRIDE/CAP_DAC_READ_SEARCH Caps
func NetConnections(analysePid bool, types string) ([]NetConn, error) {
wantTCP, wantUDP, err := windowsNetConnectionTypes(types)
if err != nil {
return nil, err
}
result := make([]NetConn, 0)
processCache := make(map[int64]*Process)
if wantTCP {
result, err = appendWindowsTCPConnections(result, analysePid, processCache)
if err != nil {
return result, err
}
}
if wantUDP {
result, err = appendWindowsUDPConnections(result, analysePid, processCache)
if err != nil {
return result, err
}
}
return result, nil
}
func GetInodeMap() (map[string]int64, error) {
return nil, ERR_UNSUPPORTED
}
func windowsInterfaceName(row win32api.MIB_IF_ROW2) string {
name := strings.TrimSpace(syscall.UTF16ToString(row.Alias[:]))
if name != "" {
return name
}
name = strings.TrimSpace(syscall.UTF16ToString(row.Description[:]))
if name != "" {
return name
}
if row.InterfaceIndex != 0 {
return "if" + strconv.FormatUint(uint64(row.InterfaceIndex), 10)
}
if row.InterfaceLuid != 0 {
return "luid" + strconv.FormatUint(row.InterfaceLuid, 10)
}
return ""
}
func windowsNetConnectionTypes(types string) (wantTCP, wantUDP bool, err error) {
normalized := strings.ToLower(strings.TrimSpace(types))
if strings.Contains(normalized, "unix") {
return false, false, ERR_UNSUPPORTED
}
if normalized == "" || strings.Contains(normalized, "all") {
return true, true, nil
}
if strings.Contains(normalized, "tcp") {
wantTCP = true
}
if strings.Contains(normalized, "udp") {
wantUDP = true
}
if !wantTCP && !wantUDP {
return false, false, errors.New("unsupported net connection type")
}
return wantTCP, wantUDP, nil
}
func appendWindowsTCPConnections(result []NetConn, analysePid bool, processCache map[int64]*Process) ([]NetConn, error) {
rows4, err := win32api.GetExtendedTcp4Table(false, win32api.TCP_TABLE_OWNER_PID_ALL)
if err != nil {
return result, err
}
for _, row := range rows4 {
conn := NetConn{
LocalAddr: windowsIPv4FromDWORD(row.LocalAddr),
LocalPort: int(row.LocalPortHost()),
RemoteAddr: windowsIPv4FromDWORD(row.RemoteAddr),
RemotePort: int(row.RemotePortHost()),
Status: windowsTCPState(row.State),
Typed: "tcp",
}
attachWindowsProcess(&conn, row.OwningPid, analysePid, processCache)
result = append(result, conn)
}
rows6, err := win32api.GetExtendedTcp6Table(false, win32api.TCP_TABLE_OWNER_PID_ALL)
if err != nil {
if isOptionalWindowsNetTableError(err) {
return result, nil
}
return result, err
}
for _, row := range rows6 {
conn := NetConn{
LocalAddr: net.IP(row.LocalAddr[:]).String(),
LocalPort: int(row.LocalPortHost()),
RemoteAddr: net.IP(row.RemoteAddr[:]).String(),
RemotePort: int(row.RemotePortHost()),
Status: windowsTCPState(row.State),
Typed: "tcp6",
}
attachWindowsProcess(&conn, row.OwningPid, analysePid, processCache)
result = append(result, conn)
}
return result, nil
}
func appendWindowsUDPConnections(result []NetConn, analysePid bool, processCache map[int64]*Process) ([]NetConn, error) {
rows4, err := win32api.GetExtendedUdp4Table(false, win32api.UDP_TABLE_OWNER_PID)
if err != nil {
return result, err
}
for _, row := range rows4 {
conn := NetConn{
LocalAddr: windowsIPv4FromDWORD(row.LocalAddr),
LocalPort: int(row.LocalPortHost()),
Typed: "udp",
}
attachWindowsProcess(&conn, row.OwningPid, analysePid, processCache)
result = append(result, conn)
}
rows6, err := win32api.GetExtendedUdp6Table(false, win32api.UDP_TABLE_OWNER_PID)
if err != nil {
if isOptionalWindowsNetTableError(err) {
return result, nil
}
return result, err
}
for _, row := range rows6 {
conn := NetConn{
LocalAddr: net.IP(row.LocalAddr[:]).String(),
LocalPort: int(row.LocalPortHost()),
Typed: "udp6",
}
attachWindowsProcess(&conn, row.OwningPid, analysePid, processCache)
result = append(result, conn)
}
return result, nil
}
func attachWindowsProcess(conn *NetConn, pid uint32, analysePid bool, processCache map[int64]*Process) {
if conn == nil || !analysePid {
return
}
conn.Pid = int64(pid)
if conn.Pid <= 0 {
return
}
if proc, ok := processCache[conn.Pid]; ok {
conn.Process = proc
return
}
proc, err := FindProcessByPid(conn.Pid)
if err != nil {
processCache[conn.Pid] = nil
return
}
processCache[conn.Pid] = &proc
conn.Process = &proc
}
func windowsIPv4FromDWORD(addr uint32) string {
return net.IPv4(byte(addr), byte(addr>>8), byte(addr>>16), byte(addr>>24)).String()
}
func windowsTCPState(state win32api.MIB_TCP_STATE) string {
switch state {
case win32api.MIB_TCP_STATE_CLOSED:
return TCP_STATE[TCP_CLOSE]
case win32api.MIB_TCP_STATE_LISTEN:
return TCP_STATE[TCP_LISTEN]
case win32api.MIB_TCP_STATE_SYN_SENT:
return TCP_STATE[TCP_SYN_SENT]
case win32api.MIB_TCP_STATE_SYN_RCVD:
return TCP_STATE[TCP_SYN_RECV]
case win32api.MIB_TCP_STATE_ESTAB:
return TCP_STATE[TCP_ESTABLISHED]
case win32api.MIB_TCP_STATE_FIN_WAIT1:
return TCP_STATE[TCP_FIN_WAIT1]
case win32api.MIB_TCP_STATE_FIN_WAIT2:
return TCP_STATE[TCP_FIN_WAIT2]
case win32api.MIB_TCP_STATE_CLOSE_WAIT:
return TCP_STATE[TCP_CLOSE_WAIT]
case win32api.MIB_TCP_STATE_CLOSING:
return TCP_STATE[TCP_CLOSING]
case win32api.MIB_TCP_STATE_LAST_ACK:
return TCP_STATE[TCP_LAST_ACK]
case win32api.MIB_TCP_STATE_TIME_WAIT:
return TCP_STATE[TCP_TIME_WAIT]
case win32api.MIB_TCP_STATE_DELETE_TCB:
return "TCP_DELETE_TCB"
default:
return TCP_STATE[TCP_UNKNOWN]
}
}
func isOptionalWindowsNetTableError(err error) bool {
if errno, ok := err.(syscall.Errno); ok {
return errno == windowsErrorNotSupported
}
return false
}
+121
View File
@@ -0,0 +1,121 @@
//go:build windows
// +build windows
package staros
import (
"errors"
"syscall"
"testing"
"b612.me/win32api"
)
func TestWindowsNetConnectionTypes(t *testing.T) {
tcp, udp, err := windowsNetConnectionTypes("")
if err != nil {
t.Fatal(err)
}
if !tcp || !udp {
t.Fatalf("empty types should request tcp and udp, got tcp=%v udp=%v", tcp, udp)
}
tcp, udp, err = windowsNetConnectionTypes("all")
if err != nil {
t.Fatal(err)
}
if !tcp || !udp {
t.Fatalf("all types should request tcp and udp, got tcp=%v udp=%v", tcp, udp)
}
tcp, udp, err = windowsNetConnectionTypes("tcp")
if err != nil {
t.Fatal(err)
}
if !tcp || udp {
t.Fatalf("tcp types mismatch: tcp=%v udp=%v", tcp, udp)
}
tcp, udp, err = windowsNetConnectionTypes("TCP,UDP")
if err != nil {
t.Fatal(err)
}
if !tcp || !udp {
t.Fatalf("mixed tcp/udp types mismatch: tcp=%v udp=%v", tcp, udp)
}
tcp, udp, err = windowsNetConnectionTypes("udp")
if err != nil {
t.Fatal(err)
}
if tcp || !udp {
t.Fatalf("udp types mismatch: tcp=%v udp=%v", tcp, udp)
}
if _, _, err = windowsNetConnectionTypes("unix"); !errors.Is(err, ERR_UNSUPPORTED) {
t.Fatalf("unix should be unsupported on windows, got %v", err)
}
if _, _, err = windowsNetConnectionTypes("tcp,unix"); !errors.Is(err, ERR_UNSUPPORTED) {
t.Fatalf("mixed unix request should be unsupported on windows, got %v", err)
}
if _, _, err = windowsNetConnectionTypes("all,unix"); !errors.Is(err, ERR_UNSUPPORTED) {
t.Fatalf("all plus unix request should be unsupported on windows, got %v", err)
}
if _, _, err = windowsNetConnectionTypes("raw"); err == nil {
t.Fatal("unknown type should return error")
}
}
func TestWindowsIPv4FromDWORD(t *testing.T) {
if got := windowsIPv4FromDWORD(0x0100007f); got != "127.0.0.1" {
t.Fatalf("unexpected localhost conversion: %s", got)
}
}
func TestWindowsTCPState(t *testing.T) {
cases := map[win32api.MIB_TCP_STATE]string{
win32api.MIB_TCP_STATE_ESTAB: TCP_STATE[TCP_ESTABLISHED],
win32api.MIB_TCP_STATE_LISTEN: TCP_STATE[TCP_LISTEN],
win32api.MIB_TCP_STATE_SYN_SENT: TCP_STATE[TCP_SYN_SENT],
win32api.MIB_TCP_STATE_SYN_RCVD: TCP_STATE[TCP_SYN_RECV],
win32api.MIB_TCP_STATE_FIN_WAIT1: TCP_STATE[TCP_FIN_WAIT1],
win32api.MIB_TCP_STATE_FIN_WAIT2: TCP_STATE[TCP_FIN_WAIT2],
win32api.MIB_TCP_STATE_TIME_WAIT: TCP_STATE[TCP_TIME_WAIT],
win32api.MIB_TCP_STATE_CLOSED: TCP_STATE[TCP_CLOSE],
win32api.MIB_TCP_STATE_CLOSE_WAIT: TCP_STATE[TCP_CLOSE_WAIT],
win32api.MIB_TCP_STATE_LAST_ACK: TCP_STATE[TCP_LAST_ACK],
win32api.MIB_TCP_STATE_CLOSING: TCP_STATE[TCP_CLOSING],
}
for state, want := range cases {
if got := windowsTCPState(state); got != want {
t.Fatalf("state %d mismatch: got=%s want=%s", state, got, want)
}
}
if got := windowsTCPState(win32api.MIB_TCP_STATE(0)); got != TCP_STATE[TCP_UNKNOWN] {
t.Fatalf("unknown state mismatch: %s", got)
}
}
func TestIsOptionalWindowsNetTableError(t *testing.T) {
if !isOptionalWindowsNetTableError(windowsErrorNotSupported) {
t.Fatal("ERROR_NOT_SUPPORTED should be optional")
}
if isOptionalWindowsNetTableError(syscall.EINVAL) {
t.Fatal("EINVAL should not be optional")
}
}
func TestAttachWindowsProcess(t *testing.T) {
conn := NetConn{}
cache := map[int64]*Process{}
attachWindowsProcess(&conn, 123, false, cache)
if conn.Pid != 0 || conn.Process != nil {
t.Fatalf("analysePid=false should not populate process fields: %#v", conn)
}
conn = NetConn{}
attachWindowsProcess(&conn, 0, true, cache)
if conn.Pid != 0 || conn.Process != nil {
t.Fatalf("pid 0 should not populate process fields: %#v", conn)
}
}
+29 -17
View File
@@ -1,19 +1,34 @@
package staros package staros
import ( import (
"fmt"
"os/user" "os/user"
"strconv" "strconv"
) )
func parseUint32Identity(kind, raw string) (uint32, error) {
value, err := strconv.ParseUint(raw, 10, 32)
if err != nil {
return 0, fmt.Errorf("parse %s %q: %w", kind, raw, err)
}
return uint32(value), nil
}
// GetUidGid // GetUidGid
func GetUidGid(uname string) (uint32, uint32, string, error) { func GetUidGid(uname string) (uint32, uint32, string, error) {
usr, err := user.Lookup(uname) usr, err := user.Lookup(uname)
if err != nil { if err != nil {
return 0, 0, "", err return 0, 0, "", err
} }
uidInt, _ := strconv.Atoi(usr.Uid) uid, err := parseUint32Identity("uid", usr.Uid)
gidInt, _ := strconv.Atoi(usr.Gid) if err != nil {
return uint32(uidInt), uint32(gidInt), usr.HomeDir, nil return 0, 0, "", err
}
gid, err := parseUint32Identity("gid", usr.Gid)
if err != nil {
return 0, 0, "", err
}
return uid, gid, usr.HomeDir, nil
} }
// GetUid // GetUid
@@ -22,26 +37,23 @@ func GetUid(uname string) (uint32, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
uidInt, _ := strconv.Atoi(usr.Uid) return parseUint32Identity("uid", usr.Uid)
return uint32(uidInt), nil
} }
// GetGid // GetGid
func GetGid(uname string) (uint32, error) { func GetGid(uname string) (uint32, error) {
usr, err := user.LookupGroup(uname)
if err != nil {
return 0, err
}
gidInt, _ := strconv.Atoi(usr.Gid)
return uint32(gidInt), nil
}
// GetGidByName
func GetGidByName(uname string) (uint32, error) {
usr, err := user.Lookup(uname) usr, err := user.Lookup(uname)
if err != nil { if err != nil {
return 0, err return 0, err
} }
uidInt, _ := strconv.Atoi(usr.Gid) return parseUint32Identity("gid", usr.Gid)
return uint32(uidInt), nil }
// GetGidByName
func GetGidByName(uname string) (uint32, error) {
usr, err := user.LookupGroup(uname)
if err != nil {
return 0, err
}
return parseUint32Identity("gid", usr.Gid)
} }
+69
View File
@@ -0,0 +1,69 @@
//go:build darwin
// +build darwin
package staros
import (
"os/user"
"strconv"
"syscall"
"time"
)
// StartTime is not implemented on Darwin yet.
func StartTime() time.Time {
return time.Time{}
}
// IsRoot 当前是否是管理员用户
func IsRoot() bool {
uid, err := user.Current()
return err == nil && uid.Uid == "0"
}
func Whoami() (uid, gid int, uname, gname, home string, err error) {
var me *user.User
var group *user.Group
me, err = user.Current()
if err != nil {
return
}
uid, _ = strconv.Atoi(me.Uid)
gid, _ = strconv.Atoi(me.Gid)
home = me.HomeDir
uname = me.Username
group, err = user.LookupGroupId(me.Gid)
if err != nil {
return
}
gname = group.Name
return
}
func CpuUsageByPid(pid int, sleep time.Duration) float64 {
return 0
}
func CpuUsage(sleep time.Duration) float64 {
return 0
}
func DiskUsage(path string) (disk DiskStatus) {
disk, _ = DiskUsageE(path)
return
}
func DiskUsageE(path string) (disk DiskStatus, err error) {
if path == "" {
path = "."
}
fs := syscall.Statfs_t{}
if err = syscall.Statfs(path, &fs); err != nil {
return
}
disk.All = fs.Blocks * uint64(fs.Bsize)
disk.Free = fs.Bfree * uint64(fs.Bsize)
disk.Available = fs.Bavail * uint64(fs.Bsize)
disk.Used = disk.All - disk.Free
return
}
-121
View File
@@ -1,121 +0,0 @@
// +build linux darwin
package staros
import (
"fmt"
"io/ioutil"
"os/user"
"strconv"
"strings"
"syscall"
"time"
)
// StartTime 开机时间
func StartTime() time.Time {
tmp, _ := readAsString("/proc/stat")
data := splitBy(ReplaceByte9(tmp), " ")
btime, _ := strconv.ParseInt(strings.TrimSpace(data["btime"]), 10, 64)
return time.Unix(btime, 0)
}
// IsRoot 当前是否是管理员用户
func IsRoot() bool {
uid, _ := user.Current()
if uid.Uid == "0" {
return true
}
return false
}
// Memory 系统内存信息
func Memory() MemStatus {
var mem MemStatus
ram := new(syscall.Sysinfo_t)
if err := syscall.Sysinfo(ram); err != nil {
return mem
}
mem.All = ram.Totalram
mem.BuffCache = ram.Bufferram
mem.Free = ram.Freeram
mem.Shared = ram.Sharedram
mem.Available = ram.Freeram + ram.Sharedram + ram.Bufferram
mem.SwapAll = ram.Totalswap
mem.SwapFree = ram.Freeswap
mem.SwapUsed = mem.SwapAll - mem.SwapFree
mem.Used = mem.All - mem.Free
return mem
}
func Whoami() (uid, gid int, uname, gname, home string, err error) {
var me *user.User
var gup *user.Group
me, err = user.Current()
if err != nil {
return
}
uid, _ = strconv.Atoi(me.Uid)
gid, _ = strconv.Atoi(me.Uid)
home = me.HomeDir
uname = me.Username
gup, err = user.LookupGroupId(me.Gid)
if err != nil {
return
}
gname = gup.Name
return
}
func getCPUSample() (idle, total uint64) {
contents, err := ioutil.ReadFile("/proc/stat")
if err != nil {
return
}
lines := strings.Split(string(contents), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if fields[0] == "cpu" {
numFields := len(fields)
for i := 1; i < numFields; i++ {
val, err := strconv.ParseUint(fields[i], 10, 64)
if err != nil {
fmt.Println("Error: ", i, fields[i], err)
}
total += val // tally up all the numbers to get total ticks
if i == 4 { // idle is the 5th field in the cpu line
idle = val
}
}
return
}
}
return
}
// CpuUsage 获取CPU使用量
func CpuUsage(sleep time.Duration) float64 {
idle0, total0 := getCPUSample()
time.Sleep(sleep)
idle1, total1 := getCPUSample()
idleTicks := float64(idle1 - idle0)
totalTicks := float64(total1 - total0)
cpuUsage := 100 * (totalTicks - idleTicks) / totalTicks
return cpuUsage
//fmt.Printf("CPU usage is %f%% [busy: %f, total: %f]\n", cpuUsage, totalTicks-idleTicks, totalTicks)
}
func DiskUsage(path string) (disk DiskStatus) {
fs := syscall.Statfs_t{}
err := syscall.Statfs(path, &fs)
if err != nil {
return
}
disk.All = fs.Blocks * uint64(fs.Bsize)
disk.Free = fs.Bfree * uint64(fs.Bsize)
disk.Available = fs.Bavail * uint64(fs.Bsize)
disk.Used = disk.All - disk.Free
return
}
+102 -3
View File
@@ -1,11 +1,110 @@
package staros package staros
import ( import (
"fmt" "errors"
"os/user"
"strconv"
"testing" "testing"
"time"
) )
func Test_Disk(t *testing.T) { func Test_Disk(t *testing.T) {
a := DiskUsage("c:") disk, err := DiskUsageE(".")
fmt.Println(a) if err != nil {
t.Fatal(err)
}
if disk.All == 0 {
t.Fatal("expected non-zero total disk size")
}
if disk.Used+disk.Free != disk.All {
t.Fatalf("expected used + free == all, got used=%d free=%d all=%d", disk.Used, disk.Free, disk.All)
}
}
func TestCpuUsageDoesNotPanic(t *testing.T) {
_ = CpuUsage(time.Millisecond)
}
func TestWhoamiGID(t *testing.T) {
_, gid, _, _, _, err := Whoami()
if errors.Is(err, ERR_UNSUPPORTED) {
t.Skip(err)
}
if err != nil {
t.Fatal(err)
}
current, err := user.Current()
if err != nil {
t.Fatal(err)
}
expected, err := strconv.Atoi(current.Gid)
if err != nil {
t.Fatal(err)
}
if gid != expected {
t.Fatalf("expected gid %d, got %d", expected, gid)
}
}
func TestIdentityLookupFunctions(t *testing.T) {
current, err := user.Current()
if err != nil {
t.Skipf("user.Current unavailable: %v", err)
}
wantUID, uidErr := strconv.ParseUint(current.Uid, 10, 32)
wantGID, gidErr := strconv.ParseUint(current.Gid, 10, 32)
uid, gid, home, err := GetUidGid(current.Username)
if uidErr == nil && gidErr == nil {
if err != nil {
t.Fatalf("GetUidGid failed: %v", err)
}
if uid != uint32(wantUID) || gid != uint32(wantGID) || home != current.HomeDir {
t.Fatalf("GetUidGid mismatch: uid=%d gid=%d home=%q", uid, gid, home)
}
} else if err == nil {
t.Fatalf("GetUidGid should reject non-numeric ids: uid=%q gid=%q", current.Uid, current.Gid)
}
uid, err = GetUid(current.Username)
if uidErr == nil {
if err != nil {
t.Fatalf("GetUid failed: %v", err)
}
if uid != uint32(wantUID) {
t.Fatalf("GetUid mismatch: got=%d want=%d", uid, wantUID)
}
} else if err == nil {
t.Fatalf("GetUid should reject non-numeric uid %q", current.Uid)
}
gid, err = GetGid(current.Username)
if gidErr == nil {
if err != nil {
t.Fatalf("GetGid failed: %v", err)
}
if gid != uint32(wantGID) {
t.Fatalf("GetGid mismatch: got=%d want=%d", gid, wantGID)
}
} else if err == nil {
t.Fatalf("GetGid should reject non-numeric gid %q", current.Gid)
}
group, err := user.LookupGroupId(current.Gid)
if err != nil {
t.Skipf("user.LookupGroupId unavailable: %v", err)
}
groupID, groupErr := strconv.ParseUint(group.Gid, 10, 32)
gotGroupID, err := GetGidByName(group.Name)
if groupErr == nil {
if err != nil {
t.Fatalf("GetGidByName failed: %v", err)
}
if gotGroupID != uint32(groupID) {
t.Fatalf("GetGidByName mismatch: got=%d want=%d", gotGroupID, groupID)
}
} else if err == nil {
t.Fatalf("GetGidByName should reject non-numeric gid %q", group.Gid)
}
} }
+247
View File
@@ -0,0 +1,247 @@
//go:build linux
// +build linux
package staros
import (
"bytes"
"encoding/binary"
"errors"
"io/ioutil"
"os"
"os/user"
"strconv"
"strings"
"sync"
"syscall"
"time"
"unsafe"
)
var (
clockTicksOnce sync.Once
clockTicksValue uint64 = 100
)
// StartTime 开机时间
func StartTime() time.Time {
tmp, _ := readAsString("/proc/stat")
data := splitBy(ReplaceByte9(tmp), " ")
btime, _ := strconv.ParseInt(strings.TrimSpace(data["btime"]), 10, 64)
return time.Unix(btime, 0)
}
// IsRoot 当前是否是管理员用户
func IsRoot() bool {
uid, err := user.Current()
return err == nil && uid != nil && uid.Uid == "0"
}
func Whoami() (uid, gid int, uname, gname, home string, err error) {
var me *user.User
var gup *user.Group
me, err = user.Current()
if err != nil {
return
}
uid, _ = strconv.Atoi(me.Uid)
gid, _ = strconv.Atoi(me.Gid)
home = me.HomeDir
uname = me.Username
gup, err = user.LookupGroupId(me.Gid)
if err != nil {
return
}
gname = gup.Name
return
}
func clockTicks() uint64 {
clockTicksOnce.Do(initClockTicks)
if clockTicksValue == 0 {
return 100
}
return clockTicksValue
}
func initClockTicks() {
ticks, err := readClockTicksFromAuxv()
if err != nil || ticks == 0 {
return
}
clockTicksValue = ticks
}
func readClockTicksFromAuxv() (uint64, error) {
data, err := os.ReadFile("/proc/self/auxv")
if err != nil {
return 0, err
}
wordSize := int(unsafe.Sizeof(uintptr(0)))
if wordSize != 4 && wordSize != 8 {
return 0, errors.New("unsupported pointer size")
}
order := nativeEndian()
entrySize := wordSize * 2
for offset := 0; offset+entrySize <= len(data); offset += entrySize {
key := readAuxvWord(data[offset:offset+wordSize], order)
val := readAuxvWord(data[offset+wordSize:offset+entrySize], order)
if key == 0 {
break
}
if key == 17 {
return val, nil
}
}
return 0, errors.New("AT_CLKTCK not found")
}
func readAuxvWord(data []byte, order binary.ByteOrder) uint64 {
if len(data) == 4 {
return uint64(order.Uint32(data))
}
return order.Uint64(data)
}
func nativeEndian() binary.ByteOrder {
var n uint16 = 1
if *(*byte)(unsafe.Pointer(&n)) == 1 {
return binary.LittleEndian
}
return binary.BigEndian
}
func cpuUsageOverDuration(delta float64, sleep time.Duration) float64 {
if delta < 0 || sleep <= 0 {
return 0
}
seconds := sleep.Seconds()
if seconds <= 0 {
return 0
}
return delta / seconds * 100
}
func cpuUsagePercent(busyTicks, totalTicks float64) float64 {
if busyTicks < 0 || totalTicks <= 0 {
return 0
}
return 100 * busyTicks / totalTicks
}
func getCPUSample() (idle, total uint64) {
contents, err := ioutil.ReadFile("/proc/stat")
if err != nil {
return
}
lines := strings.Split(string(contents), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) == 0 {
continue
}
if fields[0] == "cpu" {
numFields := len(fields)
for i := 1; i < numFields; i++ {
val, err := strconv.ParseUint(fields[i], 10, 64)
if err != nil {
continue
}
total += val // tally up all the numbers to get total ticks
if i == 4 || i == 5 { // idle is the 5th field in the cpu line
idle += val
}
}
return
}
}
return
}
func splitProcStat(content []byte) []string {
nameStart := bytes.IndexByte(content, '(')
nameEnd := bytes.LastIndexByte(content, ')')
restFields := strings.Fields(string(content[nameEnd+2:])) // +2 skip ') '
name := content[nameStart+1 : nameEnd]
pid := strings.TrimSpace(string(content[:nameStart]))
fields := make([]string, 3, len(restFields)+3)
fields[1] = string(pid)
fields[2] = string(name)
fields = append(fields, restFields...)
return fields
}
func getCPUSampleByPid(pid int) float64 {
contents, err := ioutil.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat")
if err != nil {
return 0
}
fields := splitProcStat(contents)
utime, err := strconv.ParseFloat(fields[14], 64)
if err != nil {
return 0
}
stime, err := strconv.ParseFloat(fields[15], 64)
if err != nil {
return 0
}
// There is no such thing as iotime in stat file. As an approximation, we
// will use delayacct_blkio_ticks (aggregated block I/O delays, as per Linux
// docs). Note: I am assuming at least Linux 2.6.18
var iotime float64
if len(fields) > 42 {
iotime, err = strconv.ParseFloat(fields[42], 64)
if err != nil {
iotime = 0 // Ancient linux version, most likely
}
} else {
iotime = 0 // e.g. SmartOS containers
}
ticks := float64(clockTicks())
return utime/ticks + stime/ticks + iotime/ticks
}
func CpuUsageByPid(pid int, sleep time.Duration) float64 {
if sleep <= 0 {
return 0
}
total1 := getCPUSampleByPid(pid)
time.Sleep(sleep)
total2 := getCPUSampleByPid(pid)
return cpuUsageOverDuration(total2-total1, sleep)
}
// CpuUsage 获取CPU使用量
func CpuUsage(sleep time.Duration) float64 {
if sleep <= 0 {
return 0
}
idle0, total0 := getCPUSample()
time.Sleep(sleep)
idle1, total1 := getCPUSample()
idleTicks := float64(idle1 - idle0)
totalTicks := float64(total1 - total0)
cpuUsage := cpuUsagePercent(totalTicks-idleTicks, totalTicks)
return cpuUsage
//fmt.Printf("CPU usage is %f%% [busy: %f, total: %f]\n", cpuUsage, totalTicks-idleTicks, totalTicks)
}
func DiskUsage(path string) (disk DiskStatus) {
disk, _ = DiskUsageE(path)
return
}
func DiskUsageE(path string) (disk DiskStatus, err error) {
if path == "" {
path = "."
}
fs := syscall.Statfs_t{}
if err = syscall.Statfs(path, &fs); err != nil {
return
}
disk.All = fs.Blocks * uint64(fs.Bsize)
disk.Free = fs.Bfree * uint64(fs.Bsize)
disk.Available = fs.Bavail * uint64(fs.Bsize)
disk.Used = disk.All - disk.Free
return
}
+36
View File
@@ -0,0 +1,36 @@
//go:build linux
// +build linux
package staros
import (
"testing"
"time"
)
func TestCPUUsageOverDurationGuardsZeroOrNegativeWindow(t *testing.T) {
if got := cpuUsageOverDuration(10, 0); got != 0 {
t.Fatalf("expected zero-window cpu usage to clamp to 0, got %v", got)
}
if got := cpuUsageOverDuration(10, -time.Millisecond); got != 0 {
t.Fatalf("expected negative-window cpu usage to clamp to 0, got %v", got)
}
if got := cpuUsageOverDuration(-1, time.Second); got != 0 {
t.Fatalf("expected negative delta cpu usage to clamp to 0, got %v", got)
}
if got := cpuUsageOverDuration(0.5, time.Second); got != 50 {
t.Fatalf("expected normal cpu usage calculation, got %v", got)
}
}
func TestCPUUsagePercentGuardsInvalidSamples(t *testing.T) {
if got := cpuUsagePercent(1, 0); got != 0 {
t.Fatalf("expected zero total ticks to clamp to 0, got %v", got)
}
if got := cpuUsagePercent(-1, 4); got != 0 {
t.Fatalf("expected negative busy ticks to clamp to 0, got %v", got)
}
if got := cpuUsagePercent(1, 4); got != 25 {
t.Fatalf("expected normal cpu percent calculation, got %v", got)
}
}
+24 -31
View File
@@ -1,9 +1,9 @@
//go:build windows
// +build windows // +build windows
package staros package staros
import ( import (
"log"
"syscall" "syscall"
"time" "time"
"unsafe" "unsafe"
@@ -27,47 +27,40 @@ func IsRoot() bool {
return wincmd.Isas() return wincmd.Isas()
} }
// Memory 系统内存信息 func DiskUsage(path string) (disk DiskStatus) {
func Memory() MemStatus { disk, _ = DiskUsageE(path)
var mem MemStatus return
ram := new(win32api.MEMORYSTATUSEX)
win32api.GlobalMemoryStatusEx(ram)
mem.All = uint64(ram.UllTotalPhys)
mem.Free = uint64(ram.UllAvailPhys)
mem.Available = uint64(ram.UllAvailPhys)
mem.Used = uint64(mem.All - mem.Free)
mem.SwapAll = uint64(ram.UllTotalPageFile)
mem.SwapFree = uint64(ram.UllAvailPageFile)
mem.SwapUsed = mem.SwapAll - mem.SwapFree
mem.VirtualAll = uint64(mem.VirtualAll)
mem.VirtualAvail = uint64(mem.VirtualAvail)
mem.VirtualUsed = mem.VirtualAll - mem.VirtualUsed
return mem
} }
func DiskUsage(path string) (disk DiskStatus) { func DiskUsageE(path string) (disk DiskStatus, err error) {
kernel32, err := syscall.LoadLibrary("Kernel32.dll") if path == "" {
if err != nil { path = "."
log.Panic(err)
} }
defer syscall.FreeLibrary(kernel32)
GetDiskFreeSpaceEx, err := syscall.GetProcAddress(syscall.Handle(kernel32), "GetDiskFreeSpaceExW")
if err != nil {
log.Panic(err)
}
lpFreeBytesAvailable := int64(0) lpFreeBytesAvailable := int64(0)
lpTotalNumberOfBytes := int64(0) lpTotalNumberOfBytes := int64(0)
lpTotalNumberOfFreeBytes := int64(0) lpTotalNumberOfFreeBytes := int64(0)
syscall.Syscall6(uintptr(GetDiskFreeSpaceEx), 4,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("C:"))), path16, err := syscall.UTF16PtrFromString(path)
if err != nil {
return
}
r1, _, callErr := syscall.NewLazyDLL("kernel32.dll").NewProc("GetDiskFreeSpaceExW").Call(
uintptr(unsafe.Pointer(path16)),
uintptr(unsafe.Pointer(&lpFreeBytesAvailable)), uintptr(unsafe.Pointer(&lpFreeBytesAvailable)),
uintptr(unsafe.Pointer(&lpTotalNumberOfBytes)), uintptr(unsafe.Pointer(&lpTotalNumberOfBytes)),
uintptr(unsafe.Pointer(&lpTotalNumberOfFreeBytes)), 0, 0) uintptr(unsafe.Pointer(&lpTotalNumberOfFreeBytes)),
)
if r1 == 0 {
err = callErr
return
}
disk.Free = uint64(lpTotalNumberOfFreeBytes) disk.Free = uint64(lpTotalNumberOfFreeBytes)
disk.Used = uint64(lpTotalNumberOfBytes - lpTotalNumberOfFreeBytes) disk.Used = uint64(lpTotalNumberOfBytes - lpTotalNumberOfFreeBytes)
disk.All = uint64(lpTotalNumberOfBytes) disk.All = uint64(lpTotalNumberOfBytes)
disk.Available = uint64(lpFreeBytesAvailable) disk.Available = uint64(lpFreeBytesAvailable)
return return
} }
func CpuUsage(sleep time.Duration) float64 {
return 0
}
+928
View File
@@ -0,0 +1,928 @@
package staros
import (
"bytes"
"context"
"errors"
"io"
"os"
"os/exec"
"sync"
"sync/atomic"
"time"
)
var errNilCommand = errors.New("nil command")
var errCommandStdinUnavailable = errors.New("command stdin is not available")
var errCommandProcessNotStarted = errors.New("command process is not started")
var errCommandAlreadyStarted = errors.New("command already started")
var errCommandAlreadyReleased = errors.New("command already released")
var errCommandStdinClosed = errors.New("command stdin is closed")
var errCommandAlreadyDetached = errors.New("command already detached")
var errCommandDetached = errors.New("command already detached")
var errCommandRedirectNil = errors.New("command redirect target is nil")
const starCmdUnknownExitCode = -999
const starCmdStreamBuffer = 64
type starCmdStream int
const (
starCmdStdout starCmdStream = iota
starCmdStderr
)
// StarCmdOutputStream identifies which process stream produced a chunk.
type StarCmdOutputStream int
const (
StarCmdOutputStdout StarCmdOutputStream = iota
StarCmdOutputStderr
)
// StarCmdOutput is a streamed stdout/stderr chunk.
type StarCmdOutput struct {
Stream StarCmdOutputStream
Data []byte
}
type starCmdWriter struct {
cmd *StarCmd
stream starCmdStream
}
func (writer starCmdWriter) Write(data []byte) (int, error) {
if writer.cmd == nil {
return 0, errNilCommand
}
writer.cmd.lock.Lock()
writer.cmd.ensureBuffers()
var redirect io.Writer
switch writer.stream {
case starCmdStdout:
if _, err := writer.cmd.stdoutBuf.Write(data); err != nil {
writer.cmd.lock.Unlock()
return 0, err
}
writer.cmd.stdout = append(writer.cmd.stdout, data...)
writer.cmd.publishStreamLocked(starCmdStdout, data)
redirect = writer.cmd.stdoutRedirect
case starCmdStderr:
if _, err := writer.cmd.stderrBuf.Write(data); err != nil {
writer.cmd.lock.Unlock()
return 0, err
}
writer.cmd.errout = append(writer.cmd.errout, data...)
writer.cmd.publishStreamLocked(starCmdStderr, data)
redirect = writer.cmd.stderrRedirect
default:
writer.cmd.lock.Unlock()
return 0, errors.New("unknown command stream")
}
writer.cmd.lock.Unlock()
if redirect != nil {
writer.cmd.redirectLock.Lock()
n, err := redirect.Write(data)
writer.cmd.redirectLock.Unlock()
if err != nil {
return n, err
}
if n != len(data) {
return n, io.ErrShortWrite
}
}
return len(data), nil
}
//StarCmd Is Here
type StarCmd struct {
CMD *exec.Cmd
infile io.WriteCloser
inclosed bool
running int32
started int32
released int32
detached int32
//Store AlL of the Standed Outputs
stdout []byte
//Store All of the Standed Errors
errout []byte
runerr error
exitcode int
stdoutBuf *bytes.Buffer
stderrBuf *bytes.Buffer
lock sync.Mutex
prewrite []string
prewritetime time.Duration
stopctxfunc context.CancelFunc
stopctx context.Context
doneOnce sync.Once
done chan struct{}
resultOnce sync.Once
resultDone chan struct{}
stdoutStream []chan []byte
stderrStream []chan []byte
outputStream []chan StarCmdOutput
streamClosed bool
stdoutRedirect io.Writer
stderrRedirect io.Writer
redirectLock sync.Mutex
closeAfter []io.Closer
}
func (starcli *StarCmd) ensureBuffers() {
if starcli.stdoutBuf == nil {
starcli.stdoutBuf = bytes.NewBuffer(make([]byte, 0))
}
if starcli.stderrBuf == nil {
starcli.stderrBuf = bytes.NewBuffer(make([]byte, 0))
}
}
func (starcli *StarCmd) ensureStopContext() {
if starcli.stopctx == nil || starcli.stopctxfunc == nil {
starcli.stopctx, starcli.stopctxfunc = context.WithCancel(context.Background())
}
if starcli.done == nil {
starcli.done = make(chan struct{})
}
if starcli.resultDone == nil {
starcli.resultDone = make(chan struct{})
}
}
func (starcli *StarCmd) ensureResultDone() <-chan struct{} {
if starcli == nil {
closed := make(chan struct{})
close(closed)
return closed
}
starcli.lock.Lock()
starcli.ensureStopContext()
done := starcli.resultDone
starcli.lock.Unlock()
return done
}
func (starcli *StarCmd) signalResultDone() {
if starcli == nil {
return
}
starcli.resultOnce.Do(func() {
starcli.lock.Lock()
starcli.ensureStopContext()
done := starcli.resultDone
starcli.lock.Unlock()
close(done)
})
}
func (starcli *StarCmd) finish() {
if starcli == nil {
return
}
starcli.setRunning(false)
starcli.signalResultDone()
if starcli.stopctxfunc != nil {
starcli.stopctxfunc()
}
starcli.doneOnce.Do(func() {
starcli.lock.Lock()
done := starcli.done
stdoutStream := starcli.stdoutStream
stderrStream := starcli.stderrStream
outputStream := starcli.outputStream
infile := starcli.infile
closeAfter := starcli.closeAfter
starcli.stdoutStream = nil
starcli.stderrStream = nil
starcli.outputStream = nil
starcli.closeAfter = nil
starcli.streamClosed = true
if infile != nil && !starcli.inclosed {
starcli.inclosed = true
} else {
infile = nil
}
starcli.lock.Unlock()
for _, stream := range stdoutStream {
close(stream)
}
for _, stream := range stderrStream {
close(stream)
}
for _, stream := range outputStream {
close(stream)
}
if infile != nil {
_ = infile.Close()
}
for _, closer := range closeAfter {
_ = closer.Close()
}
if done != nil {
close(done)
}
})
}
func (starcli *StarCmd) publishStreamLocked(stream starCmdStream, data []byte) {
if starcli == nil || starcli.streamClosed {
return
}
switch stream {
case starCmdStdout:
for _, receiver := range starcli.stdoutStream {
select {
case receiver <- append([]byte(nil), data...):
default:
}
}
for _, receiver := range starcli.outputStream {
select {
case receiver <- StarCmdOutput{Stream: StarCmdOutputStdout, Data: append([]byte(nil), data...)}:
default:
}
}
case starCmdStderr:
for _, receiver := range starcli.stderrStream {
select {
case receiver <- append([]byte(nil), data...):
default:
}
}
for _, receiver := range starcli.outputStream {
select {
case receiver <- StarCmdOutput{Stream: StarCmdOutputStderr, Data: append([]byte(nil), data...)}:
default:
}
}
}
}
func (starcli *StarCmd) registerByteStream(selectStream starCmdStream) <-chan []byte {
stream := make(chan []byte, starCmdStreamBuffer)
if starcli == nil {
close(stream)
return stream
}
starcli.lock.Lock()
if starcli.streamClosed {
close(stream)
} else {
switch selectStream {
case starCmdStdout:
starcli.stdoutStream = append(starcli.stdoutStream, stream)
case starCmdStderr:
starcli.stderrStream = append(starcli.stderrStream, stream)
default:
close(stream)
}
}
starcli.lock.Unlock()
return stream
}
func (starcli *StarCmd) ensureConfigurable() error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
if atomic.LoadInt32(&starcli.started) != 0 {
return errCommandAlreadyStarted
}
if atomic.LoadInt32(&starcli.detached) != 0 {
return errCommandDetached
}
return nil
}
func Command(command string, args ...string) (*StarCmd, error) {
return newStarCmd(exec.Command(command, args...))
}
func CommandContext(ctx context.Context, command string, args ...string) (*StarCmd, error) {
return newStarCmd(exec.CommandContext(ctx, command, args...))
}
func newStarCmd(cmd *exec.Cmd) (*StarCmd, error) {
var err error
shell := &StarCmd{
CMD: cmd,
prewritetime: time.Millisecond * 200,
stdoutBuf: bytes.NewBuffer(make([]byte, 0)),
stderrBuf: bytes.NewBuffer(make([]byte, 0)),
done: make(chan struct{}),
resultDone: make(chan struct{}),
exitcode: starCmdUnknownExitCode,
}
shell.stopctx, shell.stopctxfunc = context.WithCancel(context.Background())
shell.infile, err = shell.CMD.StdinPipe()
if err != nil {
return shell, err
}
shell.CMD.Stdout = starCmdWriter{cmd: shell, stream: starCmdStdout}
shell.CMD.Stderr = starCmdWriter{cmd: shell, stream: starCmdStderr}
return shell, nil
}
func (starcli *StarCmd) NowLineOutput() (string, error) {
if starcli == nil {
return "", errNilCommand
}
starcli.lock.Lock()
defer starcli.lock.Unlock()
starcli.ensureBuffers()
buf, _ := starcli.stdoutBuf.ReadBytes('\n')
buferr, _ := starcli.stderrBuf.ReadBytes(byte('\n'))
if len(buferr) != 0 {
return string(buf), errors.New(string(buferr))
}
return string(buf), nil
}
func (starcli *StarCmd) NowLineStdOut() string {
if starcli == nil {
return ""
}
starcli.lock.Lock()
defer starcli.lock.Unlock()
starcli.ensureBuffers()
buf, _ := starcli.stdoutBuf.ReadBytes('\n')
return string(buf)
}
func (starcli *StarCmd) NowLineStdErr() error {
if starcli == nil {
return errNilCommand
}
starcli.lock.Lock()
defer starcli.lock.Unlock()
starcli.ensureBuffers()
buferr, _ := starcli.stderrBuf.ReadBytes(byte('\n'))
if len(buferr) != 0 {
return errors.New(string(buferr))
}
return nil
}
func (starcli *StarCmd) NowAllOutput() (string, error) {
if starcli == nil {
return "", errNilCommand
}
var outstr string
starcli.lock.Lock()
defer starcli.lock.Unlock()
starcli.ensureBuffers()
buf := make([]byte, starcli.stdoutBuf.Len())
n, _ := starcli.stdoutBuf.Read(buf)
runerr := starcli.runerr
if n != 0 {
outstr = string(buf[:n])
}
if runerr != nil {
return outstr, runerr
}
buf = make([]byte, starcli.stderrBuf.Len())
n, _ = starcli.stderrBuf.Read(buf)
if n != 0 {
return outstr, errors.New(string(buf[:n]))
}
return outstr, nil
}
func (starcli *StarCmd) NowStdOut() string {
if starcli == nil {
return ""
}
var outstr string
starcli.lock.Lock()
defer starcli.lock.Unlock()
starcli.ensureBuffers()
buf := make([]byte, starcli.stdoutBuf.Len())
n, _ := starcli.stdoutBuf.Read(buf)
if n != 0 {
outstr = string(buf[:n])
}
return outstr
}
func (starcli *StarCmd) NowStdErr() error {
if starcli == nil {
return errNilCommand
}
starcli.lock.Lock()
defer starcli.lock.Unlock()
starcli.ensureBuffers()
buf := make([]byte, starcli.stderrBuf.Len())
n, _ := starcli.stderrBuf.Read(buf)
if n != 0 {
return errors.New(string(buf[:n]))
}
return nil
}
func (starcli *StarCmd) AllOutPut() (string, error) {
if starcli == nil {
return "", errNilCommand
}
starcli.lock.Lock()
defer starcli.lock.Unlock()
err := starcli.runerr
if err == nil && len(starcli.errout) != 0 {
err = errors.New(string(starcli.errout))
}
return string(starcli.stdout), err
}
func (starcli *StarCmd) AllStdOut() string {
if starcli == nil {
return ""
}
starcli.lock.Lock()
defer starcli.lock.Unlock()
return string(starcli.stdout)
}
func (starcli *StarCmd) AllStdErr() error {
if starcli == nil {
return errNilCommand
}
starcli.lock.Lock()
defer starcli.lock.Unlock()
err := starcli.runerr
if err == nil && len(starcli.errout) != 0 {
err = errors.New(string(starcli.errout))
}
return err
}
func (starcli *StarCmd) setRunning(alive bool) {
if starcli == nil {
return
}
if alive {
atomic.StoreInt32(&starcli.running, 1)
return
}
atomic.StoreInt32(&starcli.running, 0)
}
func (starcli *StarCmd) Start() error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
if atomic.LoadInt32(&starcli.detached) != 0 {
return errCommandDetached
}
starcli.lock.Lock()
starcli.ensureBuffers()
starcli.ensureStopContext()
starcli.lock.Unlock()
if !atomic.CompareAndSwapInt32(&starcli.started, 0, 1) {
return errCommandAlreadyStarted
}
if err := starcli.CMD.Start(); err != nil {
starcli.lock.Lock()
starcli.runerr = err
starcli.exitcode = -1
starcli.lock.Unlock()
starcli.signalResultDone()
starcli.finish()
return err
}
starcli.setRunning(true)
go func() {
err := starcli.CMD.Wait()
if err != nil {
starcli.lock.Lock()
starcli.runerr = err
starcli.lock.Unlock()
}
if starcli.CMD.ProcessState != nil {
starcli.lock.Lock()
starcli.exitcode = starcli.CMD.ProcessState.ExitCode()
starcli.lock.Unlock()
}
starcli.signalResultDone()
starcli.finish()
}()
go func(ctx context.Context) {
starcli.lock.Lock()
prewrite := append([]string(nil), starcli.prewrite...)
prewritetime := starcli.prewritetime
starcli.lock.Unlock()
for _, v := range prewrite {
select {
case <-ctx.Done():
return
default:
}
_ = starcli.WriteCmdE(v)
time.Sleep(prewritetime)
}
}(starcli.stopctx)
return nil
}
func (starcli *StarCmd) IsRunning() bool {
if starcli == nil {
return false
}
return 0 != atomic.LoadInt32(&starcli.running)
}
func (starcli *StarCmd) runError() error {
if starcli == nil {
return errNilCommand
}
starcli.lock.Lock()
defer starcli.lock.Unlock()
return starcli.runerr
}
func (starcli *StarCmd) ensureWaitable() error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
if atomic.LoadInt32(&starcli.started) == 0 {
return errCommandProcessNotStarted
}
return nil
}
// Stopped returns a channel that is closed after the command reaches its final state.
func (starcli *StarCmd) Stopped() <-chan struct{} {
if starcli == nil {
closed := make(chan struct{})
close(closed)
return closed
}
starcli.lock.Lock()
starcli.ensureStopContext()
done := starcli.done
starcli.lock.Unlock()
return done
}
// Stoped returns a channel that is closed after the command reaches its final state.
//
// Deprecated: use Stopped.
func (starcli *StarCmd) Stoped() <-chan struct{} {
return starcli.Stopped()
}
// Wait blocks until the command reaches its final state and returns the process wait error.
func (starcli *StarCmd) Wait() error {
if err := starcli.ensureWaitable(); err != nil {
return err
}
<-starcli.ensureResultDone()
return starcli.runError()
}
// WaitContext blocks until the command reaches its final state or ctx is done.
func (starcli *StarCmd) WaitContext(ctx context.Context) error {
if err := starcli.ensureWaitable(); err != nil {
return err
}
if ctx == nil {
return starcli.Wait()
}
resultDone := starcli.ensureResultDone()
select {
case <-resultDone:
return starcli.runError()
default:
}
select {
case <-resultDone:
return starcli.runError()
case <-ctx.Done():
select {
case <-resultDone:
return starcli.runError()
default:
}
return ctx.Err()
}
}
// WaitTimeout blocks until the command reaches its final state or tm elapses.
func (starcli *StarCmd) WaitTimeout(tm time.Duration) error {
if err := starcli.ensureWaitable(); err != nil {
return err
}
if tm <= 0 {
select {
case <-starcli.ensureResultDone():
return starcli.runError()
default:
return ERR_TIMEOUT
}
}
timer := time.NewTimer(tm)
defer timer.Stop()
resultDone := starcli.ensureResultDone()
select {
case <-resultDone:
return starcli.runError()
case <-timer.C:
select {
case <-resultDone:
return starcli.runError()
default:
return ERR_TIMEOUT
}
}
}
// StdoutChan returns a channel that receives future stdout chunks until Stopped closes.
func (starcli *StarCmd) StdoutChan() <-chan []byte {
return starcli.registerByteStream(starCmdStdout)
}
// StderrChan returns a channel that receives future stderr chunks until Stopped closes.
func (starcli *StarCmd) StderrChan() <-chan []byte {
return starcli.registerByteStream(starCmdStderr)
}
// OutputChan returns a channel that receives future stdout and stderr chunks until Stopped closes.
func (starcli *StarCmd) OutputChan() <-chan StarCmdOutput {
stream := make(chan StarCmdOutput, starCmdStreamBuffer)
if starcli == nil {
close(stream)
return stream
}
starcli.lock.Lock()
if starcli.streamClosed {
close(stream)
} else {
starcli.outputStream = append(starcli.outputStream, stream)
}
starcli.lock.Unlock()
return stream
}
// RedirectStdout mirrors stdout into writer while keeping StarCmd output capture enabled.
func (starcli *StarCmd) RedirectStdout(writer io.Writer) error {
if writer == nil {
return errCommandRedirectNil
}
if err := starcli.ensureConfigurable(); err != nil {
return err
}
starcli.lock.Lock()
starcli.stdoutRedirect = writer
starcli.lock.Unlock()
return nil
}
// RedirectStderr mirrors stderr into writer while keeping StarCmd error capture enabled.
func (starcli *StarCmd) RedirectStderr(writer io.Writer) error {
if writer == nil {
return errCommandRedirectNil
}
if err := starcli.ensureConfigurable(); err != nil {
return err
}
starcli.lock.Lock()
starcli.stderrRedirect = writer
starcli.lock.Unlock()
return nil
}
// RedirectOutput mirrors stdout and stderr into writer while keeping StarCmd capture enabled.
func (starcli *StarCmd) RedirectOutput(writer io.Writer) error {
if writer == nil {
return errCommandRedirectNil
}
if err := starcli.ensureConfigurable(); err != nil {
return err
}
starcli.lock.Lock()
starcli.stdoutRedirect = writer
starcli.stderrRedirect = writer
starcli.lock.Unlock()
return nil
}
// RedirectStdin replaces the managed stdin pipe with reader.
func (starcli *StarCmd) RedirectStdin(reader io.Reader) error {
if reader == nil {
return errCommandRedirectNil
}
if err := starcli.ensureConfigurable(); err != nil {
return err
}
starcli.lock.Lock()
if starcli.infile != nil && !starcli.inclosed {
if err := starcli.infile.Close(); err != nil {
starcli.lock.Unlock()
return err
}
}
starcli.CMD.Stdin = reader
starcli.infile = nil
starcli.inclosed = true
starcli.lock.Unlock()
return nil
}
func (starcli *StarCmd) addCloseAfter(closer io.Closer) {
if starcli == nil || closer == nil {
return
}
starcli.lock.Lock()
starcli.closeAfter = append(starcli.closeAfter, closer)
starcli.lock.Unlock()
}
// RedirectStdoutFile mirrors stdout into path while keeping StarCmd output capture enabled.
func (starcli *StarCmd) RedirectStdoutFile(path string) error {
if err := starcli.ensureConfigurable(); err != nil {
return err
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
if err := starcli.RedirectStdout(file); err != nil {
_ = file.Close()
return err
}
starcli.addCloseAfter(file)
return nil
}
// RedirectStderrFile mirrors stderr into path while keeping StarCmd error capture enabled.
func (starcli *StarCmd) RedirectStderrFile(path string) error {
if err := starcli.ensureConfigurable(); err != nil {
return err
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
if err := starcli.RedirectStderr(file); err != nil {
_ = file.Close()
return err
}
starcli.addCloseAfter(file)
return nil
}
// RedirectOutputFile mirrors stdout and stderr into path while keeping StarCmd capture enabled.
func (starcli *StarCmd) RedirectOutputFile(path string) error {
if err := starcli.ensureConfigurable(); err != nil {
return err
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
if err := starcli.RedirectOutput(file); err != nil {
_ = file.Close()
return err
}
starcli.addCloseAfter(file)
return nil
}
// RedirectStdinFile replaces the managed stdin pipe with path opened for reading.
func (starcli *StarCmd) RedirectStdinFile(path string) error {
if err := starcli.ensureConfigurable(); err != nil {
return err
}
file, err := os.Open(path)
if err != nil {
return err
}
if err := starcli.RedirectStdin(file); err != nil {
_ = file.Close()
return err
}
starcli.addCloseAfter(file)
return nil
}
func (starcli *StarCmd) Exec(cmd string, wait int) (string, error) {
if err := starcli.WriteCmdE(cmd); err != nil {
return "", err
}
time.Sleep(time.Millisecond * time.Duration(wait))
return starcli.NowAllOutput()
}
func (starcli *StarCmd) WriteCmd(cmdstr string) {
_ = starcli.WriteCmdE(cmdstr)
}
// WriteStdinE writes raw bytes to stdin without appending a newline.
func (starcli *StarCmd) WriteStdinE(data []byte) error {
if starcli == nil {
return errNilCommand
}
starcli.lock.Lock()
infile := starcli.infile
inclosed := starcli.inclosed
starcli.lock.Unlock()
if infile == nil {
return errCommandStdinUnavailable
}
if inclosed {
return errCommandStdinClosed
}
_, err := infile.Write(data)
return err
}
// WriteStdinStringE writes raw text to stdin without appending a newline.
func (starcli *StarCmd) WriteStdinStringE(data string) error {
return starcli.WriteStdinE([]byte(data))
}
// WriteStdinLineE writes text to stdin and appends one newline.
func (starcli *StarCmd) WriteStdinLineE(data string) error {
return starcli.WriteStdinStringE(data + "\n")
}
func (starcli *StarCmd) WriteCmdE(cmdstr string) error {
return starcli.WriteStdinLineE(cmdstr)
}
func (starcli *StarCmd) CloseStdin() {
_ = starcli.CloseStdinE()
}
func (starcli *StarCmd) CloseStdinE() error {
if starcli == nil {
return errNilCommand
}
starcli.lock.Lock()
infile := starcli.infile
if infile == nil {
starcli.lock.Unlock()
return errCommandStdinUnavailable
}
if starcli.inclosed {
starcli.lock.Unlock()
return errCommandStdinClosed
}
starcli.inclosed = true
starcli.lock.Unlock()
return infile.Close()
}
func (starcli *StarCmd) PreWrite(cmd ...string) {
if starcli == nil {
return
}
starcli.lock.Lock()
defer starcli.lock.Unlock()
for _, v := range cmd {
starcli.prewrite = append(starcli.prewrite, v)
}
}
func (starcli *StarCmd) PreWriteInterval(dt time.Duration) {
if starcli == nil {
return
}
starcli.lock.Lock()
defer starcli.lock.Unlock()
starcli.prewritetime = dt
}
func (starcli *StarCmd) ExitCode() int {
if starcli == nil {
return starCmdUnknownExitCode
}
starcli.lock.Lock()
defer starcli.lock.Unlock()
return starcli.exitcode
}
func (starcli *StarCmd) Kill() error {
if starcli == nil || starcli.CMD == nil || starcli.CMD.Process == nil {
return errCommandProcessNotStarted
}
err := starcli.CMD.Process.Kill()
if err != nil {
return err
}
return nil
}
func (starcli *StarCmd) GetPid() int {
if starcli == nil || starcli.CMD == nil || starcli.CMD.Process == nil {
return -1
}
return starcli.CMD.Process.Pid
}
func (starcli *StarCmd) Signal(sig os.Signal) error {
if starcli == nil || starcli.CMD == nil || starcli.CMD.Process == nil {
return errCommandProcessNotStarted
}
return starcli.CMD.Process.Signal(sig)
}
+73
View File
@@ -0,0 +1,73 @@
//go:build darwin
// +build darwin
package staros
import "sync/atomic"
func FindProcessByName(name string) (datas []Process, err error) {
return nil, ERR_UNSUPPORTED
}
func FindProcess(compare func(Process) bool) (datas []Process, err error) {
return nil, ERR_UNSUPPORTED
}
func FindProcessByPid(pid int64) (datas Process, err error) {
return datas, ERR_UNSUPPORTED
}
func Daemon(path string, args ...string) (int, error) {
return -1, ERR_UNSUPPORTED
}
func DaemonWithUser(uid, gid uint32, groups []uint32, path string, args ...string) (int, error) {
return -1, ERR_UNSUPPORTED
}
func (starcli *StarCmd) SetRunUser(uid, gid uint32, groups []uint32) {
_ = starcli.SetRunUserE(uid, gid, groups)
}
func (starcli *StarCmd) SetRunUserE(uid, gid uint32, groups []uint32) error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
if atomic.LoadInt32(&starcli.started) != 0 {
return errCommandAlreadyStarted
}
return ERR_UNSUPPORTED
}
func (starcli *StarCmd) Release() error {
return starcli.ReleaseE()
}
func (starcli *StarCmd) Detach() error {
return starcli.DetachE()
}
func (starcli *StarCmd) ReleaseE() error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
return ERR_UNSUPPORTED
}
func (starcli *StarCmd) DetachE() error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
return ERR_UNSUPPORTED
}
func (starcli *StarCmd) SetKeepCaps() error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
return ERR_UNSUPPORTED
}
func SetKeepCaps() error {
return ERR_UNSUPPORTED
}
-147
View File
@@ -1,147 +0,0 @@
// +build linux darwin
package staros
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
)
// FindProcessByName 通过进程名来查询应用信息
func FindProcessByName(pname string) (datas []Process, err error) {
var name, main string
var mainb []byte
paths, errs := ioutil.ReadDir("/proc")
if errs != nil {
err = errs
return
}
for _, v := range paths {
if v.IsDir() && Exists("/proc/"+v.Name()+"/comm") {
name, err = readAsString("/proc/" + v.Name() + "/comm")
if err != nil {
return
}
if strings.TrimSpace(name) == pname {
var tmp Process
main, err = readAsString("/proc/" + v.Name() + "/status")
if err != nil {
return
}
data := splitBy(main, ":")
tmp.Name = pname
tmp.Pid, _ = strconv.ParseInt(data["Pid"], 10, 64)
tmp.PPid, _ = strconv.ParseInt(data["PPid"], 10, 64)
tmp.TPid, _ = strconv.ParseInt(data["TracerPid"], 10, 64)
uids := splitBySpace(data["Uid"])
gids := splitBySpace(data["Gid"])
tmp.RUID, _ = strconv.Atoi(uids[0])
tmp.EUID, _ = strconv.Atoi(uids[1])
tmp.RGID, _ = strconv.Atoi(gids[0])
tmp.EGID, _ = strconv.Atoi(gids[1])
mainb, err = ioutil.ReadFile("/proc/" + v.Name() + "/cmdline")
if err != nil {
return
}
args := bytes.Split(mainb, []byte{0})
for _, v := range args {
tmp.Args = append(tmp.Args, string(v))
}
tmp.LocalPath, err = os.Readlink("/proc/" + v.Name() + "/exe")
if err != nil {
return
}
tmp.LocalPath = filepath.Dir(tmp.LocalPath)
tmp.ExecPath, err = os.Readlink("/proc/" + v.Name() + "/cwd")
if err != nil {
return
}
main, err = readAsString("/proc/" + v.Name() + "/stat")
if err != nil {
return
}
times := splitBySpace(main)
uptime, _ := strconv.ParseInt(strings.TrimSpace(times[21]), 10, 64)
tmp.Uptime = time.Unix(StartTime().Unix()+uptime/100, int64((float64(uptime)/100-float64(uptime/100))*1000000000))
datas = append(datas, tmp)
}
}
}
return
}
// FindProcessByPid 通过Pid来查询应用信息
func FindProcessByPid(pid int64) (datas Process, err error) {
var name, main string
var mainb []byte
if !Exists("/proc/" + fmt.Sprint(pid) + "/comm") {
err = errors.New("Not Found")
return
}
name, err = readAsString("/proc/" + fmt.Sprint(pid) + "/comm")
if err != nil {
return
}
main, err = readAsString("/proc/" + fmt.Sprint(pid) + "/status")
if err != nil {
return
}
data := splitBy(main, ":")
datas.Name = strings.TrimSpace(name)
datas.Pid, _ = strconv.ParseInt(data["Pid"], 10, 64)
datas.PPid, _ = strconv.ParseInt(data["PPid"], 10, 64)
datas.TPid, _ = strconv.ParseInt(data["TracerPid"], 10, 64)
uids := splitBySpace(data["Uid"])
gids := splitBySpace(data["Gid"])
datas.RUID, _ = strconv.Atoi(uids[0])
datas.EUID, _ = strconv.Atoi(uids[1])
datas.RGID, _ = strconv.Atoi(gids[0])
datas.EGID, _ = strconv.Atoi(gids[1])
mainb, err = ioutil.ReadFile("/proc/" + fmt.Sprint(pid) + "/cmdline")
if err != nil {
return
}
args := bytes.Split(mainb, []byte{0})
for _, v := range args {
datas.Args = append(datas.Args, string(v))
}
datas.LocalPath, err = os.Readlink("/proc/" + fmt.Sprint(pid) + "/exe")
if err != nil {
return
}
datas.LocalPath = filepath.Dir(datas.LocalPath)
datas.ExecPath, err = os.Readlink("/proc/" + fmt.Sprint(pid) + "/cwd")
if err != nil {
return
}
main, err = readAsString("/proc/" + fmt.Sprint(pid) + "/stat")
if err != nil {
return
}
times := splitBySpace(main)
uptime, _ := strconv.ParseInt(strings.TrimSpace(times[21]), 10, 64)
datas.Uptime = time.Unix(StartTime().Unix()+uptime/100, int64((float64(uptime)/100-float64(uptime/100))*1000000000))
return
}
func Daemon(path string, args ...string) (int, error) {
cmd := exec.Command(path, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
if err := cmd.Start(); err != nil {
return -1, err
}
pid := cmd.Process.Pid
err := cmd.Process.Release()
return pid, err
}
+110
View File
@@ -0,0 +1,110 @@
//go:build linux
// +build linux
package staros
import (
"errors"
"reflect"
"syscall"
"testing"
)
func TestStarCmdSetKeepCapsConfiguresAmbientCaps(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
original := loadCurrentKeepCaps
loadCurrentKeepCaps = func() ([]uintptr, error) {
return []uintptr{7, 1, 7}, nil
}
t.Cleanup(func() {
loadCurrentKeepCaps = original
})
cmd.CMD.SysProcAttr = &syscall.SysProcAttr{
AmbientCaps: []uintptr{9, 1},
}
if err := cmd.SetKeepCaps(); err != nil {
t.Fatal(err)
}
want := []uintptr{1, 7, 9}
if got := cmd.CMD.SysProcAttr.AmbientCaps; !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected ambient caps: got=%v want=%v", got, want)
}
}
func TestStarCmdSetKeepCapsPropagatesCapabilityReadError(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
wantErr := errors.New("capget failed")
original := loadCurrentKeepCaps
loadCurrentKeepCaps = func() ([]uintptr, error) {
return nil, wantErr
}
t.Cleanup(func() {
loadCurrentKeepCaps = original
})
if err := cmd.SetKeepCaps(); !errors.Is(err, wantErr) {
t.Fatalf("expected keepcaps read error, got %v", err)
}
}
func TestStarCmdSetRunUserPreservesExistingSysProcAttr(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
original := loadCurrentKeepCaps
loadCurrentKeepCaps = func() ([]uintptr, error) {
return []uintptr{7, 1, 7}, nil
}
t.Cleanup(func() {
loadCurrentKeepCaps = original
})
cmd.CMD.SysProcAttr = &syscall.SysProcAttr{
Pdeathsig: syscall.SIGTERM,
AmbientCaps: []uintptr{9},
}
if err := cmd.SetKeepCaps(); err != nil {
t.Fatal(err)
}
groups := []uint32{3, 4}
if err := cmd.SetRunUserE(1, 2, groups); err != nil {
t.Fatal(err)
}
groups[0] = 99
if got, want := cmd.CMD.SysProcAttr.AmbientCaps, []uintptr{1, 7, 9}; !reflect.DeepEqual(got, want) {
t.Fatalf("ambient caps lost after SetRunUserE: got=%v want=%v", got, want)
}
if got := cmd.CMD.SysProcAttr.Pdeathsig; got != syscall.SIGTERM {
t.Fatalf("expected Pdeathsig to be preserved, got %v", got)
}
if !cmd.CMD.SysProcAttr.Setsid {
t.Fatal("expected Setsid to be enabled")
}
cred := cmd.CMD.SysProcAttr.Credential
if cred == nil {
t.Fatal("expected credential to be configured")
}
if cred.Uid != 1 || cred.Gid != 2 {
t.Fatalf("unexpected credential ids: uid=%d gid=%d", cred.Uid, cred.Gid)
}
if got, want := cred.Groups, []uint32{3, 4}; !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected credential groups: got=%v want=%v", got, want)
}
}
+806 -3
View File
@@ -1,10 +1,813 @@
package staros package staros
import ( import (
"fmt" "bytes"
"context"
"encoding/base64"
"encoding/binary"
"errors"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing" "testing"
"time"
"unicode/utf16"
) )
func Test_Process(t *testing.T) { func testCommandArgs(script string) (string, []string) {
fmt.Println(FindProcessByPid(16652)) if runtime.GOOS == "windows" {
return "cmd.exe", []string{"/c", script}
}
return "sh", []string{"-c", script}
}
func testWindowsPowerShellArgs(script string) (string, []string) {
utf16Script := utf16.Encode([]rune(script))
encoded := make([]byte, len(utf16Script)*2)
for i, r := range utf16Script {
binary.LittleEndian.PutUint16(encoded[i*2:], uint16(r))
}
return "powershell.exe", []string{"-NoProfile", "-EncodedCommand", base64.StdEncoding.EncodeToString(encoded)}
}
type closeTrackingWriteCloser struct {
closed bool
}
func (w *closeTrackingWriteCloser) Write(data []byte) (int, error) {
return len(data), nil
}
func (w *closeTrackingWriteCloser) Close() error {
w.closed = true
return nil
}
func TestStarCmdCapturesOutputAndExitCode(t *testing.T) {
script := "printf 'hello'; printf 'err' 1>&2"
command, args := testCommandArgs(script)
if runtime.GOOS == "windows" {
command, args = testWindowsPowerShellArgs("[Console]::Out.Write('hello'); [Console]::Error.Write('err')")
}
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
<-cmd.Stoped()
out, outErr := cmd.AllOutPut()
if out != "hello" {
t.Fatalf("expected stdout %q, got %q", "hello", out)
}
if outErr == nil || outErr.Error() != "err" {
t.Fatalf("expected stderr error %q, got %v", "err", outErr)
}
if got := cmd.ExitCode(); got != 0 {
t.Fatalf("expected exit code 0, got %d", got)
}
}
func TestStarCmdWaitReturnsProcessError(t *testing.T) {
command, args := testCommandArgs("exit 7")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Wait(); !errors.Is(err, errCommandProcessNotStarted) {
t.Fatalf("expected errCommandProcessNotStarted before start, got %v", err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
if err := cmd.Wait(); err == nil {
t.Fatal("expected wait error for non-zero exit")
}
if got := cmd.ExitCode(); got != 7 {
t.Fatalf("expected exit code 7, got %d", got)
}
if err := cmd.Wait(); err == nil {
t.Fatal("expected repeated Wait to keep final process error")
}
}
func TestStarCmdWaitTimeoutAndContext(t *testing.T) {
command, args := testCommandArgs("sleep 1")
if runtime.GOOS == "windows" {
command, args = testCommandArgs("ping -n 2 127.0.0.1 >nul")
}
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
if err := cmd.WaitTimeout(10 * time.Millisecond); !errors.Is(err, ERR_TIMEOUT) {
t.Fatalf("expected ERR_TIMEOUT, got %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
if err := cmd.WaitContext(ctx); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("expected context deadline, got %v", err)
}
if err := cmd.WaitTimeout(3 * time.Second); err != nil {
t.Fatalf("expected command to finish, got %v", err)
}
}
func TestStarCmdWaitReturnsResultAfterProcessDone(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
if err := cmd.Wait(); err != nil {
t.Fatal(err)
}
if err := cmd.WaitTimeout(0); err != nil {
t.Fatalf("expected finished command to beat zero timeout, got %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := cmd.WaitContext(ctx); err != nil {
t.Fatalf("expected finished command to beat canceled context, got %v", err)
}
}
func TestStarCmdWaitContextFinishedCommandWinsOverCanceledContext(t *testing.T) {
t.Run("success", func(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
if err := cmd.Wait(); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := cmd.WaitContext(ctx); err != nil {
t.Fatalf("finished successful command should win over canceled context, got %v", err)
}
})
t.Run("failed", func(t *testing.T) {
command, args := testCommandArgs("exit 7")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
waitErr := cmd.Wait()
if waitErr == nil {
t.Fatal("expected command wait error")
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := cmd.WaitContext(ctx); err == nil || err.Error() != waitErr.Error() {
t.Fatalf("finished failed command should win over canceled context, got %v, want %v", err, waitErr)
}
})
}
func TestStarCmdStoppedAlias(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
select {
case <-cmd.Stopped():
case <-time.After(time.Second):
t.Fatal("Stopped should close after command exits")
}
select {
case <-cmd.Stoped():
case <-time.After(time.Second):
t.Fatal("Stoped compatibility alias should close after command exits")
}
}
func TestStarCmdStopedPublishesFinalExitCode(t *testing.T) {
command, args := testCommandArgs("exit 7")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
<-cmd.Stoped()
if cmd.IsRunning() {
t.Fatal("command should not be running after Stoped closes")
}
if got := cmd.ExitCode(); got != 7 {
t.Fatalf("expected exit code 7, got %d", got)
}
}
func TestStarCmdRejectsRepeatedStart(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
if err := cmd.Start(); !errors.Is(err, errCommandAlreadyStarted) {
t.Fatalf("expected errCommandAlreadyStarted, got %v", err)
}
<-cmd.Stoped()
if err := cmd.Start(); !errors.Is(err, errCommandAlreadyStarted) {
t.Fatalf("expected errCommandAlreadyStarted after exit, got %v", err)
}
}
func TestStarCmdStartFailureClosesStoped(t *testing.T) {
cmd, err := Command("__staros_missing_command__")
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err == nil {
t.Fatal("expected start failure")
}
select {
case <-cmd.Stoped():
case <-time.After(time.Second):
t.Fatal("Stoped should close after start failure")
}
if cmd.IsRunning() {
t.Fatal("command should not be running after start failure")
}
if got := cmd.ExitCode(); got != -1 {
t.Fatalf("expected exit code -1 after start failure, got %d", got)
}
}
func TestStarCmdCapturesLargeOutput(t *testing.T) {
expected := strings.Repeat("x", 256*1024)
script := "awk 'BEGIN{for(i=0;i<262144;i++) printf \"x\"}'"
command, args := testCommandArgs(script)
if runtime.GOOS == "windows" {
command, args = testWindowsPowerShellArgs("[Console]::Out.Write(('x' * 262144))")
}
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
<-cmd.Stoped()
out, err := cmd.AllOutPut()
if err != nil {
t.Fatal(err)
}
if !bytes.Equal([]byte(out), []byte(expected)) {
t.Fatalf("expected %d stdout bytes, got %d", len(expected), len(out))
}
}
func TestStarCmdStreamsOutput(t *testing.T) {
script := "printf 'out'; printf 'err' 1>&2"
command, args := testCommandArgs(script)
if runtime.GOOS == "windows" {
command, args = testWindowsPowerShellArgs("[Console]::Out.Write('out'); [Console]::Error.Write('err')")
}
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
stdout := cmd.StdoutChan()
stderr := cmd.StderrChan()
output := cmd.OutputChan()
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
var stdoutData, stderrData string
var outputData []StarCmdOutput
for stdout != nil || stderr != nil || output != nil {
select {
case data, ok := <-stdout:
if !ok {
stdout = nil
continue
}
stdoutData += string(data)
case data, ok := <-stderr:
if !ok {
stderr = nil
continue
}
stderrData += string(data)
case data, ok := <-output:
if !ok {
output = nil
continue
}
outputData = append(outputData, data)
case <-time.After(3 * time.Second):
t.Fatal("stream output timed out")
}
}
if stdoutData != "out" {
t.Fatalf("expected streamed stdout %q, got %q", "out", stdoutData)
}
if stderrData != "err" {
t.Fatalf("expected streamed stderr %q, got %q", "err", stderrData)
}
var seenStdout, seenStderr bool
for _, item := range outputData {
switch item.Stream {
case StarCmdOutputStdout:
seenStdout = seenStdout || string(item.Data) == "out"
case StarCmdOutputStderr:
seenStderr = seenStderr || string(item.Data) == "err"
default:
t.Fatalf("unknown output stream %v", item.Stream)
}
}
if !seenStdout || !seenStderr {
t.Fatalf("expected combined output stream to include stdout and stderr, got %#v", outputData)
}
}
func TestStarCmdStreamNilReturnsClosedChannels(t *testing.T) {
var cmd *StarCmd
select {
case _, ok := <-cmd.StdoutChan():
if ok {
t.Fatal("nil stdout stream should be closed")
}
case <-time.After(time.Second):
t.Fatal("nil stdout stream should close immediately")
}
select {
case _, ok := <-cmd.StderrChan():
if ok {
t.Fatal("nil stderr stream should be closed")
}
case <-time.After(time.Second):
t.Fatal("nil stderr stream should close immediately")
}
select {
case _, ok := <-cmd.OutputChan():
if ok {
t.Fatal("nil output stream should be closed")
}
case <-time.After(time.Second):
t.Fatal("nil output stream should close immediately")
}
}
func TestStarCmdRedirectOutputWriterKeepsCapture(t *testing.T) {
script := "printf 'out'; printf 'err' 1>&2"
command, args := testCommandArgs(script)
if runtime.GOOS == "windows" {
command, args = testWindowsPowerShellArgs("[Console]::Out.Write('out'); [Console]::Error.Write('err')")
}
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
var redirected bytes.Buffer
if err := cmd.RedirectOutput(&redirected); err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
<-cmd.Stopped()
if got := redirected.String(); got != "outerr" && got != "errout" {
t.Fatalf("expected redirected stdout/stderr bytes, got %q", got)
}
if out := cmd.AllStdOut(); out != "out" {
t.Fatalf("expected captured stdout %q, got %q", "out", out)
}
if err := cmd.AllStdErr(); err == nil || err.Error() != "err" {
t.Fatalf("expected captured stderr %q, got %v", "err", err)
}
}
func TestStarCmdRedirectFiles(t *testing.T) {
dir := t.TempDir()
stdoutFile := filepath.Join(dir, "stdout.txt")
stderrFile := filepath.Join(dir, "stderr.txt")
script := "printf 'out'; printf 'err' 1>&2"
command, args := testCommandArgs(script)
if runtime.GOOS == "windows" {
command, args = testWindowsPowerShellArgs("[Console]::Out.Write('out'); [Console]::Error.Write('err')")
}
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.RedirectStdoutFile(stdoutFile); err != nil {
t.Fatal(err)
}
if err := cmd.RedirectStderrFile(stderrFile); err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
<-cmd.Stopped()
stdoutData, err := ioutil.ReadFile(stdoutFile)
if err != nil {
t.Fatal(err)
}
stderrData, err := ioutil.ReadFile(stderrFile)
if err != nil {
t.Fatal(err)
}
if string(stdoutData) != "out" {
t.Fatalf("expected stdout file %q, got %q", "out", string(stdoutData))
}
if string(stderrData) != "err" {
t.Fatalf("expected stderr file %q, got %q", "err", string(stderrData))
}
}
func TestStarCmdRedirectStdin(t *testing.T) {
command, args := testCommandArgs("cat")
if runtime.GOOS == "windows" {
command, args = testCommandArgs("more")
}
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.RedirectStdin(strings.NewReader("hello\n")); err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
<-cmd.Stopped()
if out := cmd.AllStdOut(); !strings.Contains(out, "hello") {
t.Fatalf("expected redirected stdin in stdout, got %q", out)
}
if err := cmd.AllStdErr(); err != nil {
t.Fatalf("redirected stdin should not create command error, got %v", err)
}
if err := cmd.WriteCmdE("again"); !errors.Is(err, errCommandStdinUnavailable) {
t.Fatalf("expected errCommandStdinUnavailable after stdin redirect, got %v", err)
}
}
func TestStarCmdRedirectStdinClosesManagedPipe(t *testing.T) {
command, args := testCommandArgs("cat")
if runtime.GOOS == "windows" {
command, args = testCommandArgs("more")
}
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
tracker := &closeTrackingWriteCloser{}
cmd.lock.Lock()
cmd.infile = tracker
cmd.inclosed = false
cmd.lock.Unlock()
if err := cmd.RedirectStdin(strings.NewReader("hello\n")); err != nil {
t.Fatal(err)
}
if !tracker.closed {
t.Fatal("RedirectStdin should close the previously managed stdin pipe")
}
if err := cmd.WriteCmdE("again"); !errors.Is(err, errCommandStdinUnavailable) {
t.Fatalf("expected managed stdin to be unavailable after redirect, got %v", err)
}
}
func TestStarCmdDetachClosesManagedPipe(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
tracker := &closeTrackingWriteCloser{}
cmd.lock.Lock()
original := cmd.infile
cmd.infile = tracker
cmd.inclosed = false
cmd.lock.Unlock()
if original != nil {
_ = original.Close()
}
if err := cmd.DetachE(); errors.Is(err, ERR_UNSUPPORTED) {
t.Skip(err)
} else if err != nil {
t.Fatal(err)
}
if !tracker.closed {
t.Fatal("DetachE should close the managed stdin pipe")
}
if err := cmd.WriteCmdE("again"); !errors.Is(err, errCommandStdinClosed) {
t.Fatalf("expected detached stdin to be closed, got %v", err)
}
}
func TestStarCmdRedirectRejectsInvalidState(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.RedirectStdout(nil); !errors.Is(err, errCommandRedirectNil) {
t.Fatalf("expected errCommandRedirectNil, got %v", err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
if err := cmd.RedirectStdout(&bytes.Buffer{}); !errors.Is(err, errCommandAlreadyStarted) {
t.Fatalf("expected errCommandAlreadyStarted, got %v", err)
}
<-cmd.Stopped()
}
func TestStarCmdCloseStdinLetsCommandExit(t *testing.T) {
script := "cat"
if runtime.GOOS == "windows" {
script = "more"
}
command, args := testCommandArgs(script)
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
if err := cmd.WriteCmdE("hello"); err != nil {
t.Fatal(err)
}
if err := cmd.CloseStdinE(); err != nil {
t.Fatal(err)
}
select {
case <-cmd.Stoped():
case <-time.After(3 * time.Second):
t.Fatal("command should exit after stdin closes")
}
if out := cmd.AllStdOut(); !strings.Contains(out, "hello") {
t.Fatalf("expected echoed stdin, got %q", out)
}
if err := cmd.CloseStdinE(); !errors.Is(err, errCommandStdinClosed) {
t.Fatalf("expected errCommandStdinClosed, got %v", err)
}
if err := cmd.WriteCmdE("again"); !errors.Is(err, errCommandStdinClosed) {
t.Fatalf("expected errCommandStdinClosed after close, got %v", err)
}
}
func TestStarCmdWriteStdinRawDoesNotAppendNewline(t *testing.T) {
script := "cat"
if runtime.GOOS == "windows" {
script = "more"
}
command, args := testCommandArgs(script)
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
if err := cmd.WriteStdinStringE("raw"); err != nil {
t.Fatal(err)
}
if err := cmd.WriteStdinE([]byte("-bytes")); err != nil {
t.Fatal(err)
}
if err := cmd.CloseStdinE(); err != nil {
t.Fatal(err)
}
if err := cmd.WaitTimeout(3 * time.Second); err != nil {
t.Fatal(err)
}
if out := cmd.AllStdOut(); !strings.Contains(out, "raw-bytes") {
t.Fatalf("expected raw stdin without inserted newline, got %q", out)
}
}
func TestStarCmdNilGuards(t *testing.T) {
var cmd *StarCmd
if cmd.IsRunning() {
t.Fatal("nil StarCmd should not be running")
}
if got := cmd.GetPid(); got != -1 {
t.Fatalf("expected nil pid -1, got %d", got)
}
if err := cmd.Release(); !errors.Is(err, errNilCommand) {
t.Fatalf("expected errNilCommand, got %v", err)
}
if err := cmd.SetKeepCaps(); !errors.Is(err, errNilCommand) {
t.Fatalf("expected errNilCommand, got %v", err)
}
if err := cmd.ReleaseE(); !errors.Is(err, errNilCommand) {
t.Fatalf("expected errNilCommand, got %v", err)
}
if err := cmd.DetachE(); !errors.Is(err, errNilCommand) {
t.Fatalf("expected errNilCommand, got %v", err)
}
if err := cmd.SetRunUserE(0, 0, nil); !errors.Is(err, errNilCommand) {
t.Fatalf("expected errNilCommand, got %v", err)
}
if err := cmd.WriteCmdE("noop"); !errors.Is(err, errNilCommand) {
t.Fatalf("expected errNilCommand, got %v", err)
}
if err := cmd.WriteStdinE([]byte("noop")); !errors.Is(err, errNilCommand) {
t.Fatalf("expected errNilCommand, got %v", err)
}
if err := cmd.WriteStdinStringE("noop"); !errors.Is(err, errNilCommand) {
t.Fatalf("expected errNilCommand, got %v", err)
}
if err := cmd.WriteStdinLineE("noop"); !errors.Is(err, errNilCommand) {
t.Fatalf("expected errNilCommand, got %v", err)
}
if err := cmd.Wait(); !errors.Is(err, errNilCommand) {
t.Fatalf("expected errNilCommand, got %v", err)
}
if err := cmd.CloseStdinE(); !errors.Is(err, errNilCommand) {
t.Fatalf("expected errNilCommand, got %v", err)
}
}
func TestStarCmdReleaseUsesStartLifecycle(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.ReleaseE(); errors.Is(err, ERR_UNSUPPORTED) {
t.Skip(err)
} else if err != nil {
t.Fatal(err)
}
<-cmd.Stoped()
if got := cmd.ExitCode(); got != 0 {
t.Fatalf("expected exit code 0, got %d", got)
}
if err := cmd.ReleaseE(); !errors.Is(err, errCommandAlreadyReleased) {
t.Fatalf("expected errCommandAlreadyReleased, got %v", err)
}
}
func TestStarCmdReleaseAfterStartKeepsLifecycle(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
if err := cmd.ReleaseE(); errors.Is(err, ERR_UNSUPPORTED) {
t.Skip(err)
} else if err != nil {
t.Fatal(err)
}
<-cmd.Stoped()
if got := cmd.ExitCode(); got != 0 {
t.Fatalf("expected exit code 0, got %d", got)
}
}
func TestStarCmdDetachRejectsRepeatedDetach(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.DetachE(); errors.Is(err, ERR_UNSUPPORTED) {
t.Skip(err)
} else if err != nil {
t.Fatal(err)
}
if err := cmd.DetachE(); !errors.Is(err, errCommandAlreadyDetached) {
t.Fatalf("expected errCommandAlreadyDetached, got %v", err)
}
if err := cmd.Start(); !errors.Is(err, errCommandDetached) {
t.Fatalf("expected errCommandDetached, got %v", err)
}
}
func TestStarCmdDetachPublishesWaitResult(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.DetachE(); errors.Is(err, ERR_UNSUPPORTED) {
t.Skip(err)
} else if err != nil {
t.Fatal(err)
}
if err := cmd.WaitTimeout(0); err != nil {
t.Fatalf("detached command should publish final wait result, got %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := cmd.WaitContext(ctx); err != nil {
t.Fatalf("detached command should beat canceled context, got %v", err)
}
waitErr := make(chan error, 1)
go func() {
waitErr <- cmd.Wait()
}()
select {
case err := <-waitErr:
if err != nil {
t.Fatalf("detached command wait got %v", err)
}
case <-time.After(time.Second):
t.Fatal("detached command wait did not observe final result")
}
}
func TestStarCmdDetachDoesNotCaptureOutput(t *testing.T) {
script := "printf 'detached'; printf 'err' 1>&2"
if runtime.GOOS == "windows" {
script = "<nul set /p =detached & <nul set /p =err 1>&2"
}
command, args := testCommandArgs(script)
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.DetachE(); errors.Is(err, ERR_UNSUPPORTED) {
t.Skip(err)
} else if err != nil {
t.Fatal(err)
}
<-cmd.Stoped()
if out := cmd.AllStdOut(); out != "" {
t.Fatalf("detached command should not be captured, got stdout %q", out)
}
if err := cmd.AllStdErr(); err != nil {
t.Fatalf("detached command should not capture stderr, got %v", err)
}
}
func TestStarCmdDetachRejectsStartedCommand(t *testing.T) {
command, args := testCommandArgs("exit 0")
cmd, err := Command(command, args...)
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
if err := cmd.DetachE(); errors.Is(err, ERR_UNSUPPORTED) {
t.Skip(err)
} else if !errors.Is(err, errCommandAlreadyStarted) {
t.Fatalf("expected errCommandAlreadyStarted, got %v", err)
}
<-cmd.Stoped()
}
func TestFindProcessByPidCurrentProcess(t *testing.T) {
pid := os.Getpid()
process, err := FindProcessByPid(int64(pid))
if errors.Is(err, ERR_UNSUPPORTED) {
t.Skip(err)
}
if err != nil {
t.Fatal(err)
}
if process.Pid != int64(pid) {
t.Fatalf("expected pid %d, got %d", pid, process.Pid)
}
}
func TestStopedNilReturnsClosedChannel(t *testing.T) {
var cmd *StarCmd
select {
case <-cmd.Stoped():
case <-time.After(time.Second):
t.Fatal("nil Stoped channel should already be closed")
}
select {
case <-cmd.Stopped():
case <-time.After(time.Second):
t.Fatal("nil Stopped channel should already be closed")
}
} }
+492
View File
@@ -0,0 +1,492 @@
//go:build linux
// +build linux
package staros
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"sync/atomic"
"syscall"
"time"
"golang.org/x/sys/unix"
)
var loadCurrentKeepCaps = currentKeepCaps
// FindProcessByName 通过进程名来查询应用信息
func FindProcessByName(name string) (datas []Process, err error) {
return FindProcess(func(in Process) bool {
if name == in.Name {
return true
}
return false
})
}
// FindProcess 通过进程信息来查询应用信息
func FindProcess(compare func(Process) bool) (datas []Process, err error) {
var name, main string
var mainb []byte
netSnapshot := loadNetSnapshot(false)
paths, err := ioutil.ReadDir("/proc")
if err != nil {
return
}
for _, v := range paths {
if v.IsDir() && Exists("/proc/"+v.Name()+"/comm") {
name, err = readAsString("/proc/" + v.Name() + "/comm")
if err != nil {
continue
}
var tmp Process
tmp.LocalPath, err = os.Readlink("/proc/" + v.Name() + "/exe")
tmp.Path = tmp.LocalPath
tmp.LocalPath = filepath.Dir(tmp.LocalPath)
tmp.ExecPath, err = os.Readlink("/proc/" + v.Name() + "/cwd")
tmp.Name = strings.TrimSpace(name)
main, err = readAsString("/proc/" + v.Name() + "/status")
if err != nil {
tmp.Err = err
if compare(tmp) {
netSnapshot.appendTo(&tmp)
datas = append(datas, tmp)
continue
}
} else {
data := splitBy(main, ":")
tmp.Pid, _ = strconv.ParseInt(data["Pid"], 10, 64)
tmp.PPid, _ = strconv.ParseInt(data["PPid"], 10, 64)
tmp.TPid, _ = strconv.ParseInt(data["TracerPid"], 10, 64)
uids := splitBySpace(data["Uid"])
gids := splitBySpace(data["Gid"])
tmp.RUID, _ = atoiField(uids, 0)
tmp.EUID, _ = atoiField(uids, 1)
tmp.RGID, _ = atoiField(gids, 0)
tmp.EGID, _ = atoiField(gids, 1)
tmp.VmPeak = parseProcStatusKB(data["VmPeak"])
tmp.VmSize = parseProcStatusKB(data["VmSize"])
tmp.VmHWM = parseProcStatusKB(data["VmHWM"])
tmp.VmRSS = parseProcStatusKB(data["VmRSS"])
tmp.VmLck = parseProcStatusKB(data["VmLck"])
tmp.VmData = parseProcStatusKB(data["VmData"])
}
mainb, err = ioutil.ReadFile("/proc/" + v.Name() + "/cmdline")
if err != nil {
tmp.Err = err
if compare(tmp) {
netSnapshot.appendTo(&tmp)
datas = append(datas, tmp)
continue
}
} else {
args := bytes.Split(mainb, []byte{0})
for _, v := range args {
tmp.Args = append(tmp.Args, string(v))
}
}
mainb, err = ioutil.ReadFile("/proc/" + v.Name() + "/environ")
if err != nil {
tmp.Err = err
if compare(tmp) {
netSnapshot.appendTo(&tmp)
datas = append(datas, tmp)
continue
}
} else {
args := bytes.Split(mainb, []byte{0})
for _, v := range args {
tmp.Env = append(tmp.Env, string(v))
}
}
main, err = readAsString("/proc/" + v.Name() + "/stat")
if err != nil {
tmp.Err = err
if compare(tmp) {
netSnapshot.appendTo(&tmp)
datas = append(datas, tmp)
continue
}
} else if uptime, ok := procStartTimeFromStat([]byte(main)); ok {
tmp.Uptime = uptime
}
if compare(tmp) {
netSnapshot.appendTo(&tmp)
datas = append(datas, tmp)
}
}
}
return
}
// FindProcessByPid 通过Pid来查询应用信息
func FindProcessByPid(pid int64) (datas Process, err error) {
var name, main string
var mainb []byte
if !Exists("/proc/" + fmt.Sprint(pid) + "/comm") {
err = errors.New("Not Found")
return
}
name, err = readAsString("/proc/" + fmt.Sprint(pid) + "/comm")
if err != nil {
return
}
main, err = readAsString("/proc/" + fmt.Sprint(pid) + "/status")
if err != nil {
return
}
data := splitBy(main, ":")
datas.Name = strings.TrimSpace(name)
datas.Pid, _ = strconv.ParseInt(data["Pid"], 10, 64)
datas.PPid, _ = strconv.ParseInt(data["PPid"], 10, 64)
datas.TPid, _ = strconv.ParseInt(data["TracerPid"], 10, 64)
uids := splitBySpace(data["Uid"])
gids := splitBySpace(data["Gid"])
datas.RUID, _ = atoiField(uids, 0)
datas.EUID, _ = atoiField(uids, 1)
datas.RGID, _ = atoiField(gids, 0)
datas.EGID, _ = atoiField(gids, 1)
datas.VmPeak = parseProcStatusKB(data["VmPeak"])
datas.VmSize = parseProcStatusKB(data["VmSize"])
datas.VmHWM = parseProcStatusKB(data["VmHWM"])
datas.VmRSS = parseProcStatusKB(data["VmRSS"])
datas.VmLck = parseProcStatusKB(data["VmLck"])
datas.VmData = parseProcStatusKB(data["VmData"])
loadNetSnapshot(false).appendTo(&datas)
mainb, err = ioutil.ReadFile("/proc/" + fmt.Sprint(pid) + "/cmdline")
if err != nil {
datas.Err = err
err = nil
} else {
args := bytes.Split(mainb, []byte{0})
for _, v := range args {
datas.Args = append(datas.Args, string(v))
}
}
mainb, err = ioutil.ReadFile("/proc/" + fmt.Sprint(pid) + "/environ")
if err != nil {
datas.Err = err
err = nil
} else {
args := bytes.Split(mainb, []byte{0})
for _, v := range args {
datas.Env = append(datas.Env, string(v))
}
}
datas.LocalPath, err = os.Readlink("/proc/" + fmt.Sprint(pid) + "/exe")
datas.Path = datas.LocalPath
datas.LocalPath = filepath.Dir(datas.LocalPath)
datas.ExecPath, err = os.Readlink("/proc/" + fmt.Sprint(pid) + "/cwd")
main, err = readAsString("/proc/" + fmt.Sprint(pid) + "/stat")
if err != nil {
return
}
if uptime, ok := procStartTimeFromStat([]byte(main)); ok {
datas.Uptime = uptime
}
return
}
func procStartTimeFromStat(content []byte) (time.Time, bool) {
fields := splitProcStat(content)
if len(fields) <= 22 {
return time.Time{}, false
}
startTicks, err := strconv.ParseInt(strings.TrimSpace(fields[22]), 10, 64)
if err != nil {
return time.Time{}, false
}
ticks := int64(clockTicks())
seconds := startTicks / ticks
nanos := (startTicks % ticks) * int64(time.Second) / ticks
return time.Unix(StartTime().Unix()+seconds, nanos), true
}
func atoiField(fields []string, index int) (int, error) {
if index < 0 || index >= len(fields) {
return 0, errors.New("field index out of range")
}
return strconv.Atoi(fields[index])
}
func parseProcStatusKB(value string) int64 {
fields := splitBySpace(value)
if len(fields) == 0 || fields[0] == "" {
return 0
}
size, err := strconv.ParseInt(fields[0], 10, 64)
if err != nil {
return 0
}
return size * 1024
}
type netSnapshot struct {
conns []NetConn
err error
}
func loadNetSnapshot(analysePid bool) netSnapshot {
netInfo, err := NetConnections(analysePid, "")
return netSnapshot{conns: netInfo, err: err}
}
func appendNetInfo(p *Process, analysePid bool) {
loadNetSnapshot(analysePid).appendTo(p)
}
func (snapshot netSnapshot) appendTo(p *Process) {
if snapshot.err != nil {
p.netErr = snapshot.err
return
}
fds, err := ioutil.ReadDir("/proc/" + strconv.Itoa(int(p.Pid)) + "/fd")
if err != nil {
if Exists("/proc/" + strconv.Itoa(int(p.Pid)) + "/fd") {
p.netErr = err
}
return
}
for _, fd := range fds {
socket, err := os.Readlink("/proc/" + strconv.Itoa(int(p.Pid)) + "/fd/" + fd.Name())
if err != nil {
continue
}
start := strings.Index(socket, "[")
if start < 0 {
continue
}
sid := socket[start+1 : len(socket)-1]
for _, v := range snapshot.conns {
if v.Inode == sid {
v.Pid = p.Pid
v.Process = p
p.netConn = append(p.netConn, v)
}
}
}
}
func Daemon(path string, args ...string) (int, error) {
cmd := exec.Command(path, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
if err := cmd.Start(); err != nil {
return -1, err
}
pid := cmd.Process.Pid
err := cmd.Process.Release()
return pid, err
}
func DaemonWithUser(uid, gid uint32, groups []uint32, path string, args ...string) (int, error) {
cmd := exec.Command(path, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uid,
Gid: gid,
Groups: groups,
},
Setsid: true,
}
if err := cmd.Start(); err != nil {
return -1, err
}
pid := cmd.Process.Pid
err := cmd.Process.Release()
return pid, err
}
func (starcli *StarCmd) SetRunUser(uid, gid uint32, groups []uint32) {
_ = starcli.SetRunUserE(uid, gid, groups)
}
func (starcli *StarCmd) SetRunUserE(uid, gid uint32, groups []uint32) error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
if atomic.LoadInt32(&starcli.started) != 0 {
return errCommandAlreadyStarted
}
if starcli.CMD.SysProcAttr == nil {
starcli.CMD.SysProcAttr = &syscall.SysProcAttr{}
}
if starcli.CMD.SysProcAttr.Credential == nil {
starcli.CMD.SysProcAttr.Credential = &syscall.Credential{}
}
starcli.CMD.SysProcAttr.Credential.Uid = uid
starcli.CMD.SysProcAttr.Credential.Gid = gid
starcli.CMD.SysProcAttr.Credential.Groups = append([]uint32(nil), groups...)
starcli.CMD.SysProcAttr.Setsid = true
return nil
}
func (starcli *StarCmd) Release() error {
return starcli.ReleaseE()
}
func (starcli *StarCmd) Detach() error {
return starcli.DetachE()
}
func (starcli *StarCmd) ReleaseE() error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
if !atomic.CompareAndSwapInt32(&starcli.released, 0, 1) {
return errCommandAlreadyReleased
}
if atomic.LoadInt32(&starcli.started) != 0 {
if starcli.CMD.Process == nil {
starcli.lock.Lock()
err := starcli.runerr
starcli.lock.Unlock()
if err != nil {
atomic.StoreInt32(&starcli.released, 0)
return err
}
atomic.StoreInt32(&starcli.released, 0)
return errCommandAlreadyStarted
}
return nil
}
if starcli.CMD.SysProcAttr == nil {
starcli.CMD.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
} else {
if !starcli.CMD.SysProcAttr.Setsid {
starcli.CMD.SysProcAttr.Setsid = true
}
}
if err := starcli.Start(); err != nil {
atomic.StoreInt32(&starcli.released, 0)
return err
}
return nil
}
func (starcli *StarCmd) DetachE() error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
if !atomic.CompareAndSwapInt32(&starcli.detached, 0, 1) {
return errCommandAlreadyDetached
}
if atomic.LoadInt32(&starcli.started) != 0 {
atomic.StoreInt32(&starcli.detached, 0)
return errCommandAlreadyStarted
}
cmd := exec.Command(starcli.CMD.Path, starcli.CMD.Args[1:]...)
cmd.Dir = starcli.CMD.Dir
cmd.Env = append([]string(nil), starcli.CMD.Env...)
if starcli.CMD.SysProcAttr != nil {
attr := *starcli.CMD.SysProcAttr
cmd.SysProcAttr = &attr
} else {
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
}
if !cmd.SysProcAttr.Setsid {
cmd.SysProcAttr.Setsid = true
}
devNull, err := os.OpenFile(os.DevNull, os.O_RDWR, 0)
if err != nil {
atomic.StoreInt32(&starcli.detached, 0)
return err
}
defer devNull.Close()
cmd.Stdin = devNull
cmd.Stdout = devNull
cmd.Stderr = devNull
if err := cmd.Start(); err != nil {
atomic.StoreInt32(&starcli.detached, 0)
return err
}
starcli.CMD.Process = cmd.Process
atomic.StoreInt32(&starcli.started, 1)
starcli.setRunning(false)
starcli.finish()
if err := cmd.Process.Release(); err != nil {
atomic.StoreInt32(&starcli.detached, 0)
return err
}
return nil
}
func (starcli *StarCmd) SetKeepCaps() error {
if err := starcli.ensureConfigurable(); err != nil {
return err
}
caps, err := loadCurrentKeepCaps()
if err != nil {
return err
}
starcli.lock.Lock()
defer starcli.lock.Unlock()
if starcli.CMD.SysProcAttr == nil {
starcli.CMD.SysProcAttr = &syscall.SysProcAttr{}
}
starcli.CMD.SysProcAttr.AmbientCaps = mergeAmbientCaps(starcli.CMD.SysProcAttr.AmbientCaps, caps)
return nil
}
func SetKeepCaps() error {
return unix.Prctl(unix.PR_SET_KEEPCAPS, 1, 0, 0, 0)
}
func currentKeepCaps() ([]uintptr, error) {
hdr := unix.CapUserHeader{Version: unix.LINUX_CAPABILITY_VERSION_3}
data := [2]unix.CapUserData{}
if err := unix.Capget(&hdr, &data[0]); err != nil {
return nil, err
}
return capsFromCapData(data), nil
}
func capsFromCapData(data [2]unix.CapUserData) []uintptr {
var caps []uintptr
for index, item := range data {
mask := item.Permitted
for bit := uint(0); bit < 32; bit++ {
if mask&(1<<bit) == 0 {
continue
}
caps = append(caps, uintptr(index*32)+uintptr(bit))
}
}
return caps
}
func mergeAmbientCaps(existing, extra []uintptr) []uintptr {
if len(existing) == 0 && len(extra) == 0 {
return nil
}
merged := append(append([]uintptr(nil), existing...), extra...)
sort.Slice(merged, func(i, j int) bool {
return merged[i] < merged[j]
})
out := merged[:0]
var last uintptr
for idx, cap := range merged {
if idx == 0 || cap != last {
out = append(out, cap)
last = cap
}
}
return out
}
+137
View File
@@ -1,3 +1,4 @@
//go:build windows
// +build windows // +build windows
package staros package staros
@@ -5,8 +6,11 @@ package staros
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"strconv" "strconv"
"sync/atomic"
"time"
"b612.me/wincmd" "b612.me/wincmd"
) )
@@ -30,6 +34,24 @@ func FindProcessByName(pname string) (data []Process, err error) {
return return
} }
func FindProcess(compare func(Process) bool) (data []Process, err error) {
var lists []map[string]string
lists, err = wincmd.GetRunningProcess()
if err != nil {
return
}
for _, v := range lists {
var tmp Process
tmp.Name = v["name"]
tmp.Pid, _ = strconv.ParseInt(v["pid"], 10, 64)
tmp.PPid, _ = strconv.ParseInt(v["ppid"], 10, 64)
if compare(tmp) {
data = append(data, tmp)
}
}
return
}
// FindProcessByPid 通过pid来查询应用信息 // FindProcessByPid 通过pid来查询应用信息
func FindProcessByPid(pid int64) (data Process, err error) { func FindProcessByPid(pid int64) (data Process, err error) {
var lists []map[string]string var lists []map[string]string
@@ -58,3 +80,118 @@ func Daemon(path string, args ...string) (int, error) {
cmd.Process.Release() cmd.Process.Release()
return pid, nil return pid, nil
} }
func DaemonWithUser(uid, gid uint32, groups []uint32, path string, args ...string) (int, error) {
return -1, ERR_UNSUPPORTED
}
func (starcli *StarCmd) SetRunUser(uid, gid uint32, groups []uint32) {
_ = starcli.SetRunUserE(uid, gid, groups)
}
func (starcli *StarCmd) SetRunUserE(uid, gid uint32, groups []uint32) error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
if atomic.LoadInt32(&starcli.started) != 0 {
return errCommandAlreadyStarted
}
return ERR_UNSUPPORTED
}
func (starcli *StarCmd) Release() error {
return starcli.ReleaseE()
}
func (starcli *StarCmd) Detach() error {
return starcli.DetachE()
}
func (starcli *StarCmd) ReleaseE() error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
if !atomic.CompareAndSwapInt32(&starcli.released, 0, 1) {
return errCommandAlreadyReleased
}
if atomic.LoadInt32(&starcli.started) != 0 {
if starcli.CMD.Process == nil {
starcli.lock.Lock()
err := starcli.runerr
starcli.lock.Unlock()
if err != nil {
atomic.StoreInt32(&starcli.released, 0)
return err
}
atomic.StoreInt32(&starcli.released, 0)
return errCommandAlreadyStarted
}
return nil
}
if err := starcli.Start(); err != nil {
atomic.StoreInt32(&starcli.released, 0)
return err
}
return nil
}
func (starcli *StarCmd) DetachE() error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
if !atomic.CompareAndSwapInt32(&starcli.detached, 0, 1) {
return errCommandAlreadyDetached
}
if atomic.LoadInt32(&starcli.started) != 0 {
atomic.StoreInt32(&starcli.detached, 0)
return errCommandAlreadyStarted
}
cmd := exec.Command(starcli.CMD.Path, starcli.CMD.Args[1:]...)
cmd.Dir = starcli.CMD.Dir
cmd.Env = append([]string(nil), starcli.CMD.Env...)
if starcli.CMD.SysProcAttr != nil {
attr := *starcli.CMD.SysProcAttr
cmd.SysProcAttr = &attr
}
devNull, err := os.OpenFile(os.DevNull, os.O_RDWR, 0)
if err != nil {
atomic.StoreInt32(&starcli.detached, 0)
return err
}
defer devNull.Close()
cmd.Stdin = devNull
cmd.Stdout = devNull
cmd.Stderr = devNull
if err := cmd.Start(); err != nil {
atomic.StoreInt32(&starcli.detached, 0)
return err
}
starcli.CMD.Process = cmd.Process
atomic.StoreInt32(&starcli.started, 1)
starcli.setRunning(false)
starcli.finish()
if err := cmd.Process.Release(); err != nil {
atomic.StoreInt32(&starcli.detached, 0)
return err
}
return nil
}
func (starcli *StarCmd) SetKeepCaps() error {
if starcli == nil || starcli.CMD == nil {
return errNilCommand
}
return ERR_UNSUPPORTED
}
func SetKeepCaps() error {
return ERR_UNSUPPORTED
}
func CpuUsageByPid(pid int, sleep time.Duration) float64 {
return 0
}
func Whoami() (uid, gid int, uname, gname, home string, err error) {
return 0, 0, "", "", "", ERR_UNSUPPORTED
}
+1716
View File
File diff suppressed because it is too large Load Diff
+177
View File
@@ -0,0 +1,177 @@
package sysconf
import (
"bytes"
"encoding/csv"
"errors"
"fmt"
"reflect"
"strings"
)
var ErrNilCSVValue = errors.New("nil csv value")
type CSV struct {
header []string
text [][]string
}
type CSVRow struct {
header []string
data []string
}
type CSVValue struct {
key string
value string
}
func ParseCSV(data []byte, hasHeader bool) (csvData CSV, err error) {
if len(data) == 0 {
return CSV{}, fmt.Errorf("cannot parse data,invalid data format")
}
reader := csv.NewReader(bytes.NewReader(data))
records, err := reader.ReadAll()
if err != nil {
return CSV{}, err
}
if len(records) == 0 {
return CSV{}, fmt.Errorf("cannot parse data,invalid data format")
}
start := 0
if hasHeader {
csvData.header = append([]string(nil), records[0]...)
start = 1
} else {
for i := range records[0] {
csvData.header = append(csvData.header, fmt.Sprint(i))
}
}
for _, record := range records[start:] {
if len(record) != len(csvData.header) {
return CSV{}, fmt.Errorf("cannot parse data line,got %d values but need %d", len(record), len(csvData.header))
}
csvData.text = append(csvData.text, append([]string(nil), record...))
}
return csvData, nil
}
func (csvData *CSV) Header() []string { return csvData.header }
func (csvData *CSV) Data() [][]string { return csvData.text }
func (csvData *CSV) Row(row int) *CSVRow {
if csvData == nil || row < 0 || row >= len(csvData.text) {
return nil
}
return &CSVRow{header: csvData.header, data: csvData.text[row]}
}
func (row *CSVRow) Get(key string) *CSVValue {
if row == nil {
return nil
}
for idx, header := range row.header {
if header == key {
return &CSVValue{key: key, value: row.data[idx]}
}
}
return nil
}
func (row *CSVRow) Col(key int) *CSVValue {
if row == nil || key < 0 || key >= len(row.header) {
return nil
}
return &CSVValue{key: row.header[key], value: row.data[key]}
}
func (row *CSVRow) Header() []string { return row.header }
func (csvData *CSV) MapData() []map[string]string {
var result []map[string]string
for _, record := range csvData.text {
item := make(map[string]string, len(csvData.header))
for idx, header := range csvData.header {
item[header] = record[idx]
}
result = append(result, item)
}
return result
}
func CsvAnalyse(data string) []string { return csvAnalyse(data) }
func csvAnalyse(data string) []string {
reader := csv.NewReader(strings.NewReader(data))
record, err := reader.Read()
if err != nil {
return []string{}
}
return record
}
func MarshalCSV(header []string, ins interface{}) ([]byte, error) {
v := reflect.ValueOf(ins)
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil, ErrNilCSVValue
}
v = v.Elem()
}
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
return nil, fmt.Errorf("not a Slice or Array")
}
rows := make([][]string, 0, v.Len())
for i := 0; i < v.Len(); i++ {
item := v.Index(i)
if item.Kind() == reflect.Ptr {
if item.IsNil() {
continue
}
item = item.Elem()
}
switch item.Kind() {
case reflect.Slice, reflect.Array:
row := make([]string, 0, item.Len())
for j := 0; j < item.Len(); j++ {
row = append(row, fmt.Sprint(item.Index(j).Interface()))
}
rows = append(rows, row)
case reflect.Struct:
row := make([]string, 0, item.NumField())
for j := 0; j < item.NumField(); j++ {
field := item.Field(j)
if !field.CanInterface() {
continue
}
row = append(row, fmt.Sprint(field.Interface()))
}
rows = append(rows, row)
}
}
width := 0
if len(header) > 0 {
width = len(header)
} else if len(rows) > 0 {
width = len(rows[0])
}
for idx, row := range rows {
if len(row) != width {
return nil, fmt.Errorf("line %d got length %d ,but need %d", idx, len(row), width)
}
}
var buf bytes.Buffer
writer := csv.NewWriter(&buf)
if len(header) > 0 {
if err := writer.Write(header); err != nil {
return nil, err
}
}
for _, row := range rows {
if err := writer.Write(row); err != nil {
return nil, err
}
}
writer.Flush()
return buf.Bytes(), writer.Error()
}
+1192
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
package sysconf
import (
"fmt"
"strings"
)
func ExampleNewIni_migration() {
ini := NewIni()
_ = ini.Parse([]byte("[app]\nport=8080\nfeature=alpha\nfeature=beta\n"))
app := ini.Section("app")
_ = app.SetInt("port", 9090, "")
_ = app.SetAll("feature", []string{"stable", "audit"}, "")
fmt.Println(ini.Get("app", "port"))
fmt.Println(ini.GetAll("app", "feature"))
fmt.Println(strings.TrimSpace(string(ini.Build())))
// Output:
// 9090
// [stable audit]
// [app]
// port=9090
// feature=stable
// feature=audit
}
+445
View File
@@ -0,0 +1,445 @@
package sysconf
import (
"errors"
"fmt"
"os"
"reflect"
"sort"
)
type Ini struct {
*Document
}
type IniProfile func(*Ini)
func NewIni() *Ini {
return &Ini{Document: NewDocument()}
}
func NewIniWithProfiles(profiles ...IniProfile) *Ini {
ini := NewIni()
for _, profile := range profiles {
if profile != nil {
profile(ini)
}
}
return ini
}
func DefaultINIProfile() IniProfile {
return func(ini *Ini) {
if ini == nil {
return
}
ini.Document = NewDocument()
}
}
func StrictINIProfile() IniProfile {
return func(ini *Ini) {
if ini == nil {
return
}
if ini.Document == nil {
ini.Document = NewDocument()
}
ini.Strict = true
ini.AllowNoValue = false
}
}
func LinuxConfProfile(equal string) IniProfile {
return func(ini *Ini) {
if ini == nil {
return
}
if ini.Document == nil {
ini.Document = NewDocument()
}
ini.SectionOpen = ""
ini.SectionClose = ""
ini.CommentHeads = []string{"#"}
if equal != "" {
ini.Assign = equal
}
ini.AssignDelimiters = []string{ini.Assign}
}
}
func (i *Ini) ApplyProfile(profile IniProfile) *Ini {
if profile != nil {
profile(i)
}
return i
}
func NewSysConf(equal string) *Ini {
ini := NewIni()
if equal != "" {
ini.Assign = equal
}
ini.AssignDelimiters = []string{ini.Assign}
return ini
}
func NewLinuxConf(equal string) *Ini {
return NewIniWithProfiles(LinuxConfProfile(equal))
}
func (i *Ini) Parse(data []byte) error {
if i == nil || i.Document == nil {
return ErrDocumentClosed
}
return i.Document.Parse(data)
}
func (i *Ini) ParseFromFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
return i.Parse(data)
}
func (i *Ini) Build() []byte {
if i == nil || i.Document == nil {
return nil
}
return i.Document.Bytes()
}
func (i *Ini) Save(path string) error {
if i == nil || i.Document == nil {
return ErrDocumentClosed
}
return i.Document.Save(path)
}
func (i *Ini) SaveAtomic(path string) error {
if i == nil || i.Document == nil {
return ErrDocumentClosed
}
return i.Document.SaveAtomic(path)
}
func (i *Ini) Section(name string) *Section {
if i == nil || i.Document == nil {
return nil
}
return i.Document.Section(name)
}
func (i *Ini) Sections(name string) []*Section {
if i == nil || i.Document == nil {
return nil
}
return i.Document.SectionsByName(name)
}
func (i *Ini) AddSection(name string) *Section {
if i == nil || i.Document == nil {
return nil
}
i.Document.mu.Lock()
defer i.Document.mu.Unlock()
return i.Document.appendSection(name, "", "", "\n")
}
func (i *Ini) DeleteSection(name string) bool {
if i == nil || i.Document == nil {
return false
}
i.Document.mu.Lock()
defer i.Document.mu.Unlock()
i.Document.rebuildSectionIndexLocked()
normalized := normalize(name, i.CaseSensitive)
sections := i.Document.sectionIndex[normalized]
if len(sections) == 0 {
return false
}
delete(i.Document.sectionIndex, normalized)
filtered := i.Document.sections[:0]
for _, section := range i.Document.sections {
if normalize(section.Name, i.CaseSensitive) == normalized {
continue
}
filtered = append(filtered, section)
}
i.Document.sections = filtered
return true
}
func (i *Ini) Get(section, key string) string {
for _, s := range i.Sections(section) {
if s != nil && s.Exist(key) {
return s.Get(key)
}
}
return ""
}
func (i *Ini) GetAll(section, key string) []string {
sections := i.Sections(section)
if len(sections) == 0 {
return nil
}
values := make([]string, 0)
for _, s := range sections {
if s == nil {
continue
}
values = append(values, s.GetAll(key)...)
}
if len(values) == 0 {
return nil
}
return values
}
func (i *Ini) Has(section, key string) bool {
for _, s := range i.Sections(section) {
if s != nil && s.Exist(key) {
return true
}
}
return false
}
func (i *Ini) Set(section, key, value string) {
if i == nil || i.Document == nil {
return
}
i.Document.mu.Lock()
s := i.Document.ensureSection(section)
i.Document.mu.Unlock()
if s != nil {
_ = s.Set(key, value, "")
}
}
func (i *Ini) AddValue(section, key, value string) {
if i == nil || i.Document == nil {
return
}
i.Document.mu.Lock()
s := i.Document.ensureSection(section)
i.Document.mu.Unlock()
if s != nil {
_ = s.AddValue(key, value, "")
}
}
func (i *Ini) Delete(section, key string) bool {
if s := i.Section(section); s != nil {
return s.Delete(key) == nil
}
return false
}
func (i *Ini) SectionsMap() map[string][]*Section {
if i == nil || i.Document == nil {
return nil
}
i.Document.mu.Lock()
defer i.Document.mu.Unlock()
i.Document.rebuildSectionIndexLocked()
out := make(map[string][]*Section, len(i.Document.sectionIndex))
for name, sections := range i.Document.sectionIndex {
out[name] = append([]*Section(nil), sections...)
}
return out
}
func (i *Ini) Unmarshal(dst interface{}) error {
return bindINI(i, dst)
}
func (i *Ini) Marshal(src interface{}) ([]byte, error) {
tmp := newIniLike(i)
if err := marshalINI(tmp, src); err != nil {
return nil, err
}
return tmp.Build(), nil
}
func bindINI(i *Ini, dst interface{}) error {
if dst == nil {
return errors.New("destination is nil")
}
v := reflect.ValueOf(dst)
if v.Kind() != reflect.Ptr || v.IsNil() {
return errors.New("destination must be a non-nil pointer")
}
v = v.Elem()
if v.Kind() != reflect.Struct {
return errors.New("destination must point to a struct")
}
return bindStruct(i, v, "")
}
func bindStruct(i *Ini, v reflect.Value, inheritedSection string) error {
t := v.Type()
for idx := 0; idx < t.NumField(); idx++ {
field := t.Field(idx)
value := v.Field(idx)
if !value.CanSet() {
continue
}
section := field.Tag.Get("seg")
key := field.Tag.Get("key")
if section == "" {
section = inheritedSection
}
if key == "-" {
continue
}
if isNestedConfigStruct(value, key) {
nested := value
for nested.Kind() == reflect.Ptr {
if nested.IsNil() {
nested.Set(reflect.New(nested.Type().Elem()))
}
nested = nested.Elem()
}
if err := bindStruct(i, nested, section); err != nil {
return err
}
continue
}
if key == "" {
continue
}
items := configValuesFromSections(i.Sections(section), key)
if len(items) == 0 {
continue
}
if err := setINIField(value, items); err != nil {
return err
}
}
return nil
}
func setINIField(value reflect.Value, items []configValue) error {
return setConfigValueItems(value, items)
}
func marshalINI(dst *Ini, src interface{}) error {
v := reflect.ValueOf(src)
if !v.IsValid() {
return errors.New("nil source")
}
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return errors.New("nil source")
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return errors.New("source must be struct")
}
return marshalStruct(dst, v, "")
}
func marshalStruct(dst *Ini, v reflect.Value, inheritedSection string) error {
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fv := v.Field(i)
if !fv.CanInterface() {
continue
}
section := field.Tag.Get("seg")
if section == "" {
section = inheritedSection
}
key := field.Tag.Get("key")
comment := field.Tag.Get("comment")
if key == "-" {
continue
}
if nested, ok := nestedConfigValueForWrite(fv, key); ok {
if nested.IsValid() {
if err := marshalStruct(dst, nested, section); err != nil {
return err
}
}
continue
}
if key == "" {
continue
}
if err := setINIValue(dst, section, key, fv); err != nil {
return err
}
if comment != "" {
sec := dst.Section(section)
if sec == nil {
sec = dst.AddSection(section)
}
if sec != nil {
_ = sec.SetComment(key, comment)
}
}
}
return nil
}
func marshalSection(dst *Ini, section string, value reflect.Value) error {
return marshalStruct(dst, value, section)
}
func setINIValue(dst *Ini, section, key string, value reflect.Value) error {
for value.Kind() == reflect.Ptr {
if value.IsNil() {
return nil
}
value = value.Elem()
}
switch value.Kind() {
case reflect.Slice, reflect.Array:
if value.Type().Elem().Kind() != reflect.String {
dst.Set(section, key, fmt.Sprint(value.Interface()))
return nil
}
sec := dst.Section(section)
if sec == nil {
sec = dst.AddSection(section)
}
if sec == nil {
return ErrDocumentClosed
}
values := make([]string, 0, value.Len())
for idx := 0; idx < value.Len(); idx++ {
values = append(values, value.Index(idx).String())
}
return sec.SetAll(key, values, "")
case reflect.Map:
if value.Type().Key().Kind() != reflect.String || value.Type().Elem().Kind() != reflect.String {
dst.Set(section, key, fmt.Sprint(value.Interface()))
return nil
}
keys := make([]string, 0, value.Len())
for _, mapKey := range value.MapKeys() {
keys = append(keys, mapKey.String())
}
sort.Strings(keys)
values := make([]string, 0, len(keys))
for _, mapKey := range keys {
values = append(values, mapKey+"="+value.MapIndex(reflect.ValueOf(mapKey)).String())
}
sec := dst.Section(section)
if sec == nil {
sec = dst.AddSection(section)
}
if sec == nil {
return ErrDocumentClosed
}
return sec.SetAll(key, values, "")
default:
dst.Set(section, key, fmt.Sprint(value.Interface()))
return nil
}
}
-840
View File
@@ -1,840 +0,0 @@
package sysconf
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"reflect"
"strconv"
"strings"
"sync"
"b612.me/staros"
)
type SysConf struct {
Data []*SysSegment
segmap map[string]int64
segId int64
HaveSegMent bool //是否有节这个概念
SegStart string
SegEnd string
CommentFlag []string //评论标识符,如#
EqualFlag string //赋值标识符,如=
ValueFlag string //值标识符,如"
EscapeFlag string //转义字符
CommentCR bool //评论是否能与value同一行,true不行,false可以
SpaceStr string //美化符号
lock sync.RWMutex
}
type SysSegment struct {
Name string
//nodeMap
Comment string
NodeData []*SysNode
nodeId int64
nodeMap map[string]int64
lock sync.RWMutex
}
type SysNode struct {
Key string
Value []string
Comment string
NoValue bool
lock sync.RWMutex
}
func NewSysConf(EqualFlag string) *SysConf {
syscnf := new(SysConf)
syscnf.EqualFlag = EqualFlag
return syscnf
}
// NewLinuxConf sysctl.conf like file
func NewLinuxConf(EqualFlag string) *SysConf {
syscnf := new(SysConf)
syscnf.EqualFlag = EqualFlag
syscnf.HaveSegMent = false
syscnf.CommentCR = true
syscnf.CommentFlag = []string{"#"}
return syscnf
}
func (syscfg *SysConf) ParseFromFile(filepath string) error {
if !staros.Exists(filepath) {
return errors.New(filepath + " 不存在")
}
data, err := ioutil.ReadFile(filepath)
if err != nil {
return err
}
syscfg.Parse(data)
return nil
}
// Parse 生成INI文件结构
func (syscfg *SysConf) Parse(data []byte) error {
syscfg.lock.Lock()
defer syscfg.lock.Unlock()
if syscfg.HaveSegMent && (syscfg.SegStart == "" || syscfg.SegEnd == "") {
return errors.New("SegMent Start or End Flag Not Allowed!")
}
if !syscfg.CommentCR {
return syscfg.parseNOCRComment(data)
}
return syscfg.parseCRComment(data)
}
func (syscfg *SysConf) parseNOCRComment(data []byte) error { //允许comment在同一行
data = bytes.TrimSpace(data)
dataLists := bytes.Split(data, []byte("\n"))
seg := new(SysSegment)
seg.nodeMap = make(map[string]int64)
syscfg.segmap = make(map[string]int64)
if syscfg.HaveSegMent {
seg.Name = "unnamed"
}
syscfg.segId = 0
var node *SysNode
for _, v1 := range dataLists {
var (
isSegStart bool = false
isEscape bool = false
isEqual bool = false
isComment bool = false
tsuMo string = ""
)
cowStr := strings.TrimSpace(string(v1))
for i := 0; i < len(cowStr); i++ {
runeStr := cowStr[i : i+1] //当前字符,rune扫描
if runeStr == syscfg.EscapeFlag && (!isEscape) {
isEscape = true
continue
}
if runeStr == syscfg.SegStart && (!isEscape) {
isSegStart = true
continue
}
if runeStr == syscfg.SegEnd && (!isEscape) {
isSegStart = false
//New segment start from here
if seg.Name == "unnamed" && len(seg.NodeData) == 0 {
seg.Name = tsuMo
tsuMo = ""
continue
}
syscfg.segmap[seg.Name] = syscfg.segId
syscfg.segId++
syscfg.Data = append(syscfg.Data, seg)
seg = new(SysSegment)
seg.nodeMap = make(map[string]int64)
seg.Name = tsuMo
tsuMo = ""
continue
}
if isSegStart {
tsuMo += runeStr
if isEscape {
isEscape = false
}
continue
}
if syscfg.EqualFlag == runeStr && (!isEscape) && (!isEqual) {
key := strings.TrimSpace(tsuMo)
if val, ok := seg.nodeMap[key]; ok {
node = seg.NodeData[val]
} else {
node = new(SysNode)
node.Key = key
seg.nodeMap[node.Key] = seg.nodeId
seg.nodeId++
seg.NodeData = append(seg.NodeData, node)
}
tsuMo = ""
isEqual = true
if syscfg.ValueFlag != "" {
nokoriStr := strings.TrimSpace(cowStr[i+1:])
isFound := false
isValue := false
for k4, v4 := range nokoriStr {
if string([]rune{v4}) == syscfg.ValueFlag {
isValue = !isValue
}
if SliceIn(syscfg.CommentFlag, string([]rune{v4})) && !isValue {
val := nokoriStr[:k4]
isFound = true
startFinder := strings.Index(val, syscfg.ValueFlag)
endFinder := strings.LastIndex(val, syscfg.ValueFlag)
if !((startFinder == -1 || endFinder == -1) || (endFinder-startFinder <= 0)) {
node.Value = append(node.Value, strings.TrimSpace(val[startFinder+1:endFinder]))
}
node.Comment = nokoriStr[k4+1:] + "\n"
}
}
if !isFound {
startFinder := strings.Index(nokoriStr, syscfg.ValueFlag)
endFinder := strings.LastIndex(nokoriStr, syscfg.ValueFlag)
if (startFinder == -1 || endFinder == -1) || (endFinder-startFinder <= 0) {
break
}
node.Value = append(node.Value, strings.TrimSpace(nokoriStr[startFinder+1:endFinder]))
}
break
}
continue
}
if SliceIn(syscfg.CommentFlag, runeStr) && (!isEscape) {
isComment = true
if seg.nodeId == 0 {
seg.Comment += strings.TrimSpace(cowStr[i+1:]) + "\n"
break
}
if tsuMo != "" {
node.Value = append(node.Value, strings.TrimSpace(tsuMo))
tsuMo = ""
}
node.Comment += strings.TrimSpace(cowStr[i+1:]) + "\n"
break
}
isEscape = false
tsuMo += runeStr
}
if isEqual && tsuMo != "" {
node.Value = append(node.Value, strings.TrimSpace(tsuMo))
}
if !isEqual && tsuMo != "" && !isComment {
node = new(SysNode)
node.Key = tsuMo
seg.nodeMap[node.Key] = seg.nodeId
seg.nodeId++
seg.NodeData = append(seg.NodeData, node)
node.NoValue = true
}
}
if seg != nil {
syscfg.segmap[seg.Name] = syscfg.segId
syscfg.segId++
syscfg.Data = append(syscfg.Data, seg)
}
return nil
}
func (syscfg *SysConf) parseCRComment(data []byte) error { //不允许comment在同一行
data = bytes.TrimSpace(data)
dataLists := bytes.Split(data, []byte("\n"))
seg := new(SysSegment)
seg.nodeMap = make(map[string]int64)
syscfg.segmap = make(map[string]int64)
if syscfg.HaveSegMent {
seg.Name = "unnamed"
}
syscfg.segId = 0
var node *SysNode
for _, v1 := range dataLists {
var (
isSegStart bool = false
isEscape bool = false
isEqual bool = false
isComment bool = false
tsuMo string = ""
)
cowStr := strings.TrimSpace(string(v1))
for i := 0; i < len(cowStr); i++ {
runeStr := cowStr[i : i+1] //当前字符,rune扫描
if runeStr == syscfg.EscapeFlag && (!isEscape) {
isEscape = true
continue
}
if runeStr == syscfg.SegStart && (!isEscape) {
isSegStart = true
continue
}
if runeStr == syscfg.SegEnd && (!isEscape) {
isSegStart = false
//New segment start from here
if seg.Name == "unnamed" && len(seg.NodeData) == 0 {
seg.Name = tsuMo
tsuMo = ""
break
}
syscfg.segmap[seg.Name] = syscfg.segId
syscfg.segId++
syscfg.Data = append(syscfg.Data, seg)
seg = new(SysSegment)
seg.nodeMap = make(map[string]int64)
seg.Name = tsuMo
tsuMo = ""
break
}
if isSegStart {
tsuMo += runeStr
if isEscape {
isEscape = false
}
continue
}
if syscfg.EqualFlag == runeStr && (!isEscape) && (!isEqual) {
key := strings.TrimSpace(tsuMo)
if val, ok := seg.nodeMap[key]; ok {
node = seg.NodeData[val]
} else {
node = new(SysNode)
node.Key = key
seg.nodeMap[node.Key] = seg.nodeId
seg.nodeId++
seg.NodeData = append(seg.NodeData, node)
}
tsuMo = ""
isEqual = true
if syscfg.ValueFlag == "" {
node.Value = append(node.Value, TrimEscape(strings.TrimSpace(cowStr[i+1:]), syscfg.EscapeFlag))
} else {
nokoriStr := strings.TrimSpace(cowStr[i+1:])
startFinder := strings.Index(nokoriStr, syscfg.ValueFlag)
endFinder := strings.LastIndex(nokoriStr, syscfg.ValueFlag)
if (startFinder == -1 || endFinder == -1) || (endFinder-startFinder <= 0) {
break
}
node.Value = append(node.Value, strings.TrimSpace(nokoriStr[startFinder+1:endFinder]))
}
break
}
if SliceIn(syscfg.CommentFlag, runeStr) && (!isEscape) {
isComment = true
tsuMo = ""
if seg.nodeId == 0 {
seg.Comment += strings.TrimSpace(cowStr[i+1:]) + "\n"
break
}
node.Comment += strings.TrimSpace(cowStr[i+1:]) + "\n"
break
}
isEscape = false
tsuMo += runeStr
}
if !isEqual && tsuMo != "" && !isComment {
node = new(SysNode)
node.Key = tsuMo
seg.nodeMap[node.Key] = seg.nodeId
seg.nodeId++
seg.NodeData = append(seg.NodeData, node)
node.NoValue = true
}
}
if seg != nil {
syscfg.segmap[seg.Name] = syscfg.segId
syscfg.segId++
syscfg.Data = append(syscfg.Data, seg)
}
return nil
}
func (syscfg *SysConf) Build() []byte {
syscfg.lock.Lock()
defer syscfg.lock.Unlock()
var outPut string
for _, v := range syscfg.Data {
if v == nil {
continue
}
if syscfg.HaveSegMent {
outPut += syscfg.SegStart + v.Name + syscfg.SegEnd + "\n"
}
if v.Comment != "" {
v.Comment = v.Comment[:len(v.Comment)-1]
comment := strings.Split(v.Comment, "\n")
for _, vc := range comment {
if vc != "" {
outPut += syscfg.CommentFlag[0] + vc + "\n"
} else {
outPut += "\n"
}
}
}
for _, v2 := range v.NodeData {
if v2 == nil {
continue
}
if v2.NoValue {
outPut += v2.Key + "\n"
} else {
for _, v3 := range v2.Value {
if syscfg.ValueFlag != "" {
outPut += v2.Key + syscfg.SpaceStr + syscfg.EqualFlag + syscfg.SpaceStr + syscfg.ValueFlag + v3 + syscfg.ValueFlag + "\n"
} else {
outPut += v2.Key + syscfg.SpaceStr + syscfg.EqualFlag + syscfg.SpaceStr + syscfg.addEscape(v3) + "\n"
}
}
if len(v2.Value) == 0 {
outPut += v2.Key + syscfg.SpaceStr + syscfg.EqualFlag + "\n"
}
if v2.Comment != "" {
v2.Comment = v2.Comment[:len(v2.Comment)-1]
comment := strings.Split(v2.Comment, "\n")
for _, vc := range comment {
if vc != "" {
outPut += syscfg.CommentFlag[0] + vc + "\n"
} else {
outPut += "\n"
}
}
}
}
}
}
return []byte(outPut)
}
func (syscfg *SysConf) addEscape(str string) string {
str = strings.ReplaceAll(str, syscfg.EscapeFlag, syscfg.EscapeFlag+syscfg.EscapeFlag)
str = strings.ReplaceAll(str, syscfg.EqualFlag, syscfg.EscapeFlag+syscfg.EqualFlag)
str = strings.ReplaceAll(str, syscfg.SegStart, syscfg.EscapeFlag+syscfg.SegStart)
str = strings.ReplaceAll(str, syscfg.SegEnd, syscfg.EscapeFlag+syscfg.SegEnd)
for _, v := range syscfg.CommentFlag {
str = strings.ReplaceAll(str, v, syscfg.EscapeFlag+v)
}
return str
}
func (syscfg *SysConf) Reverse() {
syscfg.lock.Lock()
defer syscfg.lock.Unlock()
for _, v := range syscfg.Data {
if v == nil {
continue
}
var (
NodeData []*SysNode
nodeId int64
nodeMap map[string]int64
)
nodeMap = make(map[string]int64)
for _, v2 := range v.NodeData {
if v2 == nil {
continue
}
for _, v3 := range v2.Value {
var node *SysNode
if val, ok := nodeMap[v3]; ok {
node = NodeData[val]
} else {
node = new(SysNode)
node.Key = v3
NodeData = append(NodeData, node)
nodeMap[v3] = nodeId
nodeId++
}
node.Value = append(node.Value, strings.TrimSpace(v2.Key))
node.Comment += v2.Comment
}
v.NodeData = NodeData
v.nodeId = nodeId
v.nodeMap = nodeMap
}
}
}
func TrimEscape(text, escape string) string {
var isEscape bool = false
var outPut []rune
if escape == "" {
return text
}
text = strings.TrimSpace(text)
for _, v := range text {
if v == []rune(escape)[0] && !isEscape {
isEscape = true
continue
}
outPut = append(outPut, v)
}
return string(outPut)
}
func SliceIn(slice interface{}, data interface{}) bool {
typed := reflect.ValueOf(slice)
if typed.Kind() == reflect.Slice || typed.Kind() == reflect.Array {
for i := 0; i < typed.Len(); i++ {
if typed.Index(i).Interface() == data {
return true
}
}
}
return false
}
// Unmarshal 输出结果到结构体中
func (cfg *SysConf) Unmarshal(ins interface{}) error {
var structSet func(t reflect.Type, v reflect.Value) error
t := reflect.TypeOf(ins)
v := reflect.ValueOf(ins).Elem()
if v.Kind() != reflect.Struct {
return errors.New("Not a Struct")
}
if t.Kind() != reflect.Ptr || !v.CanSet() {
return errors.New("Cannot Write!")
}
t = t.Elem()
structSet = func(t reflect.Type, v reflect.Value) error {
for i := 0; i < t.NumField(); i++ {
tp := t.Field(i)
vl := v.Field(i)
if !vl.CanSet() {
continue
}
if vl.Type().Kind() == reflect.Struct {
structSet(vl.Type(), vl)
continue
}
seg := tp.Tag.Get("seg")
key := tp.Tag.Get("key")
if seg == "" || key == "" {
continue
}
if _, ok := cfg.segmap[seg]; !ok {
continue
}
segs := cfg.Data[cfg.segmap[seg]]
if segs.Get(key) == "" {
continue
}
switch vl.Kind() {
case reflect.String:
vl.SetString(segs.Get(key))
case reflect.Int, reflect.Int32, reflect.Int64:
vl.SetInt(segs.Int64(key))
case reflect.Float32, reflect.Float64:
vl.SetFloat(segs.Float64(key))
case reflect.Bool:
vl.SetBool(segs.Bool(key))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
vl.SetUint(uint64(segs.Int64(key)))
default:
continue
}
}
return nil
}
return structSet(t, v)
}
// Marshal 输出结果到结构体中
func (cfg *SysConf) Marshal(ins interface{}) ([]byte, error) {
var structSet func(t reflect.Type, v reflect.Value)
t := reflect.TypeOf(ins)
v := reflect.ValueOf(ins)
if v.Kind() != reflect.Struct {
return nil, errors.New("Not a Struct")
}
if t.Kind() == reflect.Ptr {
t = t.Elem()
v = v.Elem()
}
structSet = func(t reflect.Type, v reflect.Value) {
for i := 0; i < t.NumField(); i++ {
var seg, key, comment string = "", "", ""
tp := t.Field(i)
vl := v.Field(i)
if vl.Type().Kind() == reflect.Struct {
structSet(vl.Type(), vl)
continue
}
seg = tp.Tag.Get("seg")
key = tp.Tag.Get("key")
comment = tp.Tag.Get("comment")
if seg == "" || key == "" {
continue
}
if _, ok := cfg.segmap[seg]; !ok {
cfg.AddSeg(seg)
}
cfg.Seg(seg).Set(key, fmt.Sprint(vl), comment)
}
}
structSet(t, v)
return cfg.Build(), nil
}
func (syscfg *SysConf) Seg(name string) *SysSegment {
if _, ok := syscfg.segmap[name]; !ok {
return nil
}
seg := syscfg.Data[syscfg.segmap[name]]
return seg
}
func (syscfg *SysConf) AddSeg(name string) *SysSegment {
syscfg.lock.Lock()
defer syscfg.lock.Unlock()
if _, ok := syscfg.segmap[name]; !ok {
newseg := new(SysSegment)
newseg.Name = name
newseg.nodeMap = make(map[string]int64)
syscfg.Data = append(syscfg.Data, newseg)
syscfg.segId++
if syscfg.segmap == nil {
syscfg.segId = 0
syscfg.segmap = make(map[string]int64)
}
syscfg.segmap[newseg.Name] = syscfg.segId
return newseg
}
seg := syscfg.Data[syscfg.segmap[name]]
return seg
}
func (syscfg *SysConf) DeleteSeg(name string) error {
syscfg.lock.Lock()
defer syscfg.lock.Unlock()
if _, ok := syscfg.segmap[name]; !ok {
return errors.New("Seg Not Exists")
}
syscfg.Data[syscfg.segmap[name]] = nil
delete(syscfg.segmap, name)
return nil
}
func (syscfg *SysSegment) GetComment(key string) string {
if v, ok := syscfg.nodeMap[key]; !ok {
return ""
} else {
return syscfg.NodeData[v].Comment
}
}
func (syscfg *SysSegment) SetComment(key, comment string) error {
syscfg.lock.Lock()
defer syscfg.lock.Unlock()
if v, ok := syscfg.nodeMap[key]; !ok {
return errors.New("Key Not Exists")
} else {
syscfg.NodeData[v].Comment = comment
return nil
}
}
func (syscfg *SysSegment) Exist(key string) bool {
if _, ok := syscfg.nodeMap[key]; !ok {
return false
} else {
return true
}
}
func (syscfg *SysSegment) Get(key string) string {
if v, ok := syscfg.nodeMap[key]; !ok {
return ""
} else {
if len(syscfg.NodeData[v].Value) >= 1 {
return syscfg.NodeData[v].Value[0]
}
}
return ""
}
func (syscfg *SysSegment) GetAll(key string) []string {
if v, ok := syscfg.nodeMap[key]; !ok {
return []string{}
} else {
return syscfg.NodeData[v].Value
}
}
func (syscfg *SysSegment) Int(key string) int {
val := syscfg.Get(key)
if val == "" {
return 0
}
res, _ := strconv.Atoi(val)
return res
}
func (syscfg *SysSegment) Int64(key string) int64 {
val := syscfg.Get(key)
if val == "" {
return 0
}
res, _ := strconv.ParseInt(val, 10, 64)
return res
}
func (syscfg *SysSegment) Int32(key string) int32 {
val := syscfg.Get(key)
if val == "" {
return 0
}
res, _ := strconv.ParseInt(val, 10, 32)
return int32(res)
}
func (syscfg *SysSegment) Float64(key string) float64 {
val := syscfg.Get(key)
if val == "" {
return 0
}
res, _ := strconv.ParseFloat(val, 64)
return res
}
func (syscfg *SysSegment) Float32(key string) float32 {
val := syscfg.Get(key)
if val == "" {
return 0
}
res, _ := strconv.ParseFloat(val, 32)
return float32(res)
}
func (syscfg *SysSegment) Bool(key string) bool {
val := syscfg.Get(key)
if val == "" {
return false
}
res, _ := strconv.ParseBool(val)
return res
}
func (syscfg *SysSegment) SetBool(key string, value bool, comment string) error {
res := strconv.FormatBool(value)
return syscfg.Set(key, res, comment)
}
func (syscfg *SysSegment) SetFloat64(key string, prec int, value float64, comment string) error {
res := strconv.FormatFloat(value, 'f', prec, 64)
return syscfg.Set(key, res, comment)
}
func (syscfg *SysSegment) SetFloat32(key string, prec int, value float32, comment string) error {
res := strconv.FormatFloat(float64(value), 'f', prec, 32)
return syscfg.Set(key, res, comment)
}
func (syscfg *SysSegment) SetUint64(key string, value uint64, comment string) error {
res := strconv.FormatUint(value, 10)
return syscfg.Set(key, res, comment)
}
func (syscfg *SysSegment) SetInt64(key string, value int64, comment string) error {
res := strconv.FormatInt(value, 10)
return syscfg.Set(key, res, comment)
}
func (syscfg *SysSegment) SetInt32(key string, value int32, comment string) error {
res := strconv.FormatInt(int64(value), 10)
return syscfg.Set(key, res, comment)
}
func (syscfg *SysSegment) SetInt(key string, value int, comment string) error {
res := strconv.Itoa(value)
return syscfg.Set(key, res, comment)
}
func (syscfg *SysSegment) Set(key, value, comment string) error {
syscfg.lock.Lock()
defer syscfg.lock.Unlock()
if v, ok := syscfg.nodeMap[key]; !ok {
node := new(SysNode)
node.Key = key
node.Value = append(node.Value, value)
node.Comment = comment
syscfg.NodeData = append(syscfg.NodeData, node)
syscfg.nodeMap[key] = syscfg.nodeId
syscfg.nodeId++
return nil
} else {
syscfg.NodeData[v].Value = []string{value}
if comment != "" {
syscfg.NodeData[v].Comment = comment
}
}
return nil
}
func (syscfg *SysSegment) SetAll(key string, value []string, comment string) error {
syscfg.lock.Lock()
defer syscfg.lock.Unlock()
if v, ok := syscfg.nodeMap[key]; !ok {
node := new(SysNode)
node.Key = key
node.Value = value
node.Comment = comment
syscfg.NodeData = append(syscfg.NodeData, node)
syscfg.nodeMap[key] = syscfg.nodeId
syscfg.nodeId++
return nil
} else {
syscfg.NodeData[v].Value = value
if comment != "" {
syscfg.NodeData[v].Comment = comment
}
}
return nil
}
func (syscfg *SysSegment) AddValue(key, value, comment string) error {
syscfg.lock.Lock()
defer syscfg.lock.Unlock()
if v, ok := syscfg.nodeMap[key]; !ok {
node := new(SysNode)
node.Key = key
node.Value = append(node.Value, value)
node.Comment = comment
syscfg.NodeData = append(syscfg.NodeData, node)
syscfg.nodeMap[key] = syscfg.nodeId
syscfg.nodeId++
return nil
} else {
syscfg.NodeData[v].Value = append(syscfg.NodeData[v].Value, value)
if comment != "" {
syscfg.NodeData[v].Comment = comment
}
}
return nil
}
func (syscfg *SysSegment) Delete(key string) error {
syscfg.lock.Lock()
defer syscfg.lock.Unlock()
if v, ok := syscfg.nodeMap[key]; !ok {
return errors.New("Key not exists!")
} else {
if syscfg.NodeData[v].Comment != "" {
cmtSet := false
for j := v - 1; j >= 0; j-- {
if syscfg.NodeData[j] != nil {
syscfg.NodeData[j].Comment += syscfg.NodeData[v].Comment
cmtSet = true
break
}
}
if !cmtSet {
syscfg.Comment += syscfg.NodeData[v].Comment
}
}
syscfg.NodeData[v] = nil
delete(syscfg.nodeMap, key)
}
return nil
}
func (syscfg *SysSegment) DeleteValue(key string, Value string) error {
syscfg.lock.Lock()
defer syscfg.lock.Unlock()
if v, ok := syscfg.nodeMap[key]; !ok {
return errors.New("Key not exists!")
} else {
data := syscfg.NodeData[v].Value
var vals []string
for _, v := range data {
if v != Value {
vals = append(vals, v)
}
}
syscfg.NodeData[v].Value = vals
}
return nil
}
+1037 -29
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -1 +1,28 @@
package sysconf package sysconf
import "strconv"
func (s *Section) Uint64(key string) uint64 {
v, _ := strconv.ParseUint(s.Get(key), 10, 64)
return v
}
func (s *Section) MustInt(key string) int {
return s.Int(key)
}
func (s *Section) MustInt64(key string) int64 {
return s.Int64(key)
}
func (s *Section) MustUint64(key string) uint64 {
return s.Uint64(key)
}
func (s *Section) MustBool(key string) bool {
return s.Bool(key)
}
func (s *Section) MustFloat64(key string) float64 {
return s.Float64(key)
}
+115
View File
@@ -2,7 +2,9 @@ package staros
import ( import (
"bytes" "bytes"
"errors"
"io/ioutil" "io/ioutil"
"strconv"
"strings" "strings"
) )
@@ -42,3 +44,116 @@ func readAsString(path string) (string, error) {
} }
return string(data), nil return string(data), nil
} }
func remainOne(data, old, new string) string {
data = strings.TrimSpace(data)
if !strings.Contains(data, old) {
return data
}
data = strings.ReplaceAll(data, old, new)
return remainOne(data, old, new)
}
func parseHexIpPort(str string) (string, int, error) {
str = strings.TrimSpace(str)
if len(str) != 13 && len(str) != 37 {
return "", 0, errors.New("Not a valid ip:port addr:" + str)
}
ipPort := strings.Split(str, ":")
if len(ipPort) != 2 {
return "", 0, errors.New("Not a valid ip:port addr:" + str)
}
if len(ipPort[0]) == 8 {
ip, err := parseHexIPv4(ipPort[0])
if err != nil {
return "", 0, err
}
port, err := parseHexPort(ipPort[1])
return ip, port, err
}
if len(ipPort[0]) == 32 {
ip, err := parseHexIPv6(ipPort[0])
if err != nil {
return "", 0, err
}
port, err := parseHexPort(ipPort[1])
return ip, port, err
}
return "", 0, errors.New("Invalid ip address:" + str)
}
func parseHexPort(str string) (int, error) {
tmpUint32, err := strconv.ParseUint(str, 16, 32)
return int(tmpUint32), err
}
func parseHexIPv4(str string) (string, error) {
var result string
if len(str) != 8 {
return "", errors.New("Not a vaild ipv4:" + str)
}
tmpUint64, err := strconv.ParseUint(str, 16, 32)
if err != nil {
return "", err
}
numicIp := uint32(tmpUint64)
for i := 0; i < 4; i++ {
result += strconv.FormatUint(uint64(uint8(numicIp>>(8*uint8(i)))), 10) + "."
}
return result[0 : len(result)-1], nil
}
func parseHexIPv6(str string) (string, error) {
var result string
if len(str) != 32 {
return "", errors.New("Not a vaild ipv6:" + str)
}
for i := 0; i < 4; i++ {
part := str[i*8 : (i+1)*8]
tmpUint64, err := strconv.ParseUint(part, 16, 32)
if err != nil {
return "", err
}
tmpUint32 := uint32(tmpUint64)
//07C2022A
for i := 0; i < 4; i++ {
tmp := strconv.FormatUint(uint64(uint8(tmpUint32>>uint8(8*i))), 16)
if len(tmp) == 1 {
tmp = "0" + tmp
}
result += tmp
if (i+1)%2 == 0 {
result += ":"
}
}
}
ipv6 := result[0 : len(result)-1]
ipv6List := strings.Split(ipv6, ":")
prepareZero := false
alreadyZero := false
for k, v := range ipv6List {
if v == "0000" && !alreadyZero {
ipv6List[k] = ""
prepareZero = true
continue
}
if v != "0000" && prepareZero {
alreadyZero = true
}
var nonZero = 0
for i := 0; i < 4; i++ {
sig := v[i : i+1]
if sig != "0" {
nonZero = i
break
}
}
ipv6List[k] = v[nonZero:4]
}
ipv6 = strings.TrimSuffix(remainOne(strings.Join(ipv6List, ":"), ":::", "::"), "::")
if ipv6 == "" {
ipv6 = "::0"
}
return ipv6, nil
}
+74
View File
@@ -4,6 +4,46 @@ import (
"time" "time"
) )
const (
KB = 1024
MB = KB << 10
GB = MB << 10
TB = GB << 10
PB = TB << 10
)
const (
TCP_UNKNOWN = iota
TCP_ESTABLISHED
TCP_SYN_SENT
TCP_SYN_RECV
TCP_FIN_WAIT1
TCP_FIN_WAIT2
TCP_TIME_WAIT
TCP_CLOSE
TCP_CLOSE_WAIT
TCP_LAST_ACK
TCP_LISTEN
TCP_CLOSING
)
const TCP_LAST_ACL = TCP_LAST_ACK
var TCP_STATE = []string{"TCP_UNKNOWN", "TCP_ESTABLISHED", "TCP_SYN_SENT", "TCP_SYN_RECV", "TCP_FIN_WAIT1", "TCP_FIN_WAIT2", "TCP_TIME_WAIT", "TCP_CLOSE", "TCP_CLOSE_WAIT", "TCP_LAST_ACK", "TCP_LISTEN", "TCP_CLOSING"}
type NetAdapter struct {
Name string
RecvBytes uint64
SendBytes uint64
}
type NetSpeed struct {
Name string
RecvSpeeds float64
SendSpeeds float64
RecvBytes uint64
SendBytes uint64
}
// Process 定义一个进程的信息 // Process 定义一个进程的信息
type Process struct { type Process struct {
PPid int64 PPid int64
@@ -11,13 +51,28 @@ type Process struct {
Name string Name string
ExecPath string ExecPath string
LocalPath string LocalPath string
Path string
Args []string Args []string
Env []string
RUID int RUID int
EUID int EUID int
RGID int RGID int
EGID int EGID int
TPid int64 TPid int64
Uptime time.Time Uptime time.Time
VmPeak int64
VmSize int64
VmLck int64
VmHWM int64
VmRSS int64
VmData int64
netConn []NetConn
netErr error
Err error
}
func (p Process) GetNetConns() ([]NetConn, error) {
return p.netConn, p.netErr
} }
type MemStatus struct { type MemStatus struct {
@@ -42,3 +97,22 @@ type DiskStatus struct {
Free uint64 Free uint64
Available uint64 Available uint64
} }
type NetConn struct {
LocalAddr string
LocalPort int
Typed string
RemoteAddr string
RemotePort int
Socket string
Inode string
Status string
TX_Queue int64
RX_Queue int64
TimerActive string
TimerJiffies int64
RtoTimer int64
Pid int64
Uid int64
Process *Process
}
+12
View File
@@ -0,0 +1,12 @@
package staros
import "testing"
func TestTCPStateLastACKSpellingAndCompatibilityAlias(t *testing.T) {
if TCP_STATE[TCP_LAST_ACK] != "TCP_LAST_ACK" {
t.Fatalf("unexpected LAST_ACK state string: %s", TCP_STATE[TCP_LAST_ACK])
}
if TCP_LAST_ACL != TCP_LAST_ACK {
t.Fatalf("TCP_LAST_ACL should remain a compatibility alias")
}
}