From 846df7873dfdbf0ac8cff39b77f8873d9b613982 Mon Sep 17 00:00:00 2001 From: David Bolcsfoldi Date: Mon, 11 Feb 2019 19:57:57 -0800 Subject: [PATCH 1/3] FreeBSD support (#1) * Add HostInfo support for FreeBSD Initial implementation of HostInfo for FreeBSD platforms. * Use os.SameFile to compare cwds On some platforms (e.g. FreeBSD) user home directories are symlinks and os.Getcwd will return the symlink path. On FreeBSD CWD for processes that's not the running process will not give the symlink but the resolved path causing the test to fail at this point. Fix is to use the os.SameFile call to resolve both Os.Getcwd and CWD. * Add basic support for ProcessInfo on FreeBSD. Implement Process(), Self() and Processes() to the bare minimum. That is passing all basic tests. * Add support for OpenHandleEnumerator and OpenHandleCounter. * Add support for Environment * Review fixes. - Fix issue on FreeBSD 12. - Minor cosmetic issues. * Clean up build process. - Remove cgo build directives from things not needing it. - Add linker flags into .go files. --- providers/freebsd/arch.go | 35 ++ providers/freebsd/boottime_freebsd.go | 37 ++ providers/freebsd/defs_freebsd.go | 34 ++ providers/freebsd/doc.go | 20 ++ providers/freebsd/host_freebsd.go | 229 ++++++++++++ providers/freebsd/kernel.go | 35 ++ providers/freebsd/machineid.go | 35 ++ providers/freebsd/memory_freebsd.go | 111 ++++++ providers/freebsd/os_freebsd.go | 66 ++++ providers/freebsd/process_freebsd.go | 401 ++++++++++++++++++++++ providers/freebsd/process_freebsd_test.go | 25 ++ providers/freebsd/syscall_freebsd.go | 84 +++++ providers/freebsd/ztypes_freebsd.go | 40 +++ system.go | 3 +- system_test.go | 27 +- 15 files changed, 1180 insertions(+), 2 deletions(-) create mode 100644 providers/freebsd/arch.go create mode 100644 providers/freebsd/boottime_freebsd.go create mode 100644 providers/freebsd/defs_freebsd.go create mode 100644 providers/freebsd/doc.go create mode 100644 providers/freebsd/host_freebsd.go create mode 100644 providers/freebsd/kernel.go create mode 100644 providers/freebsd/machineid.go create mode 100644 providers/freebsd/memory_freebsd.go create mode 100644 providers/freebsd/os_freebsd.go create mode 100644 providers/freebsd/process_freebsd.go create mode 100644 providers/freebsd/process_freebsd_test.go create mode 100644 providers/freebsd/syscall_freebsd.go create mode 100644 providers/freebsd/ztypes_freebsd.go diff --git a/providers/freebsd/arch.go b/providers/freebsd/arch.go new file mode 100644 index 00000000..844c331a --- /dev/null +++ b/providers/freebsd/arch.go @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package freebsd + +import ( + "syscall" + + "github.com/pkg/errors" +) + +const hardwareMIB = "hw.machine" + +func Architecture() (string, error) { + arch, err := syscall.Sysctl(hardwareMIB) + if err != nil { + return "", errors.Wrap(err, "failed to get architecture") + } + + return arch, nil +} diff --git a/providers/freebsd/boottime_freebsd.go b/providers/freebsd/boottime_freebsd.go new file mode 100644 index 00000000..f5364e27 --- /dev/null +++ b/providers/freebsd/boottime_freebsd.go @@ -0,0 +1,37 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package freebsd + +import ( + "syscall" + "time" + + "github.com/pkg/errors" +) + +const kernBoottimeMIB = "kern.boottime" + +func BootTime() (time.Time, error) { + var tv syscall.Timeval + if err := sysctlByName(kernBoottimeMIB, &tv); err != nil { + return time.Time{}, errors.Wrap(err, "failed to get host uptime") + } + + bootTime := time.Unix(int64(tv.Sec), int64(tv.Usec)*int64(time.Microsecond)) + return bootTime, nil +} diff --git a/providers/freebsd/defs_freebsd.go b/providers/freebsd/defs_freebsd.go new file mode 100644 index 00000000..553cd371 --- /dev/null +++ b/providers/freebsd/defs_freebsd.go @@ -0,0 +1,34 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build ignore + +package freebsd + +/* +#include +#include +#include +#include +*/ +import "C" + +type vmTotal C.struct_vmtotal + +type kvmSwap C.struct_kvm_swap + +type clockInfo C.struct_clockinfo diff --git a/providers/freebsd/doc.go b/providers/freebsd/doc.go new file mode 100644 index 00000000..336d45aa --- /dev/null +++ b/providers/freebsd/doc.go @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Package freebsd implements the HostProvider and ProcessProvider interfaces +// for providing information about FreeBSD +package freebsd diff --git a/providers/freebsd/host_freebsd.go b/providers/freebsd/host_freebsd.go new file mode 100644 index 00000000..a6fe2bd6 --- /dev/null +++ b/providers/freebsd/host_freebsd.go @@ -0,0 +1,229 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build,freebsd,cgo + +package freebsd + +// #cgo LDFLAGS: -lkvm +//#include +//#include +import "C" + +import ( + "os" + "time" + + "github.com/joeshaw/multierror" + "github.com/pkg/errors" + + "github.com/dbolcsfoldi/go-sysinfo/internal/registry" + "github.com/elastic/go-sysinfo/providers/shared" + "github.com/elastic/go-sysinfo/types" +) + +func init() { + registry.Register(freebsdSystem{}) +} + +type freebsdSystem struct{} + +func (s freebsdSystem) Host() (types.Host, error) { + return newHost() +} + +type host struct { + info types.HostInfo +} + +func (h *host) Info() types.HostInfo { + return h.info +} + +func (h *host) CPUTime() (types.CPUTimes, error) { + cpu := types.CPUTimes{} + r := &reader{} + r.cpuTime(&cpu) + + return cpu, nil +} + +func (h *host) Memory() (*types.HostMemoryInfo, error) { + m := &types.HostMemoryInfo{} + r := &reader{} + r.memInfo(m) + return m, r.Err() +} + +func newHost() (*host, error) { + h := &host{} + r := &reader{} + r.architecture(h) + r.bootTime(h) + r.hostname(h) + r.network(h) + r.kernelVersion(h) + r.os(h) + r.time(h) + r.uniqueID(h) + return h, r.Err() +} + +type reader struct { + errs []error +} + +func (r *reader) addErr(err error) bool { + if err != nil { + if errors.Cause(err) != types.ErrNotImplemented { + r.errs = append(r.errs, err) + } + return true + } + return false +} + +func (r *reader) Err() error { + if len(r.errs) > 0 { + return &multierror.MultiError{Errors: r.errs} + } + return nil +} + +func (r *reader) cpuTime(cpu *types.CPUTimes) { + cptime, err := Cptime() + + if r.addErr(err) { + return + } + + cpu.User = time.Duration(cptime["User"]) + cpu.System = time.Duration(cptime["System"]) + cpu.Idle = time.Duration(cptime["Idle"]) + cpu.Nice = time.Duration(cptime["Nice"]) + cpu.IRQ = time.Duration(cptime["IRQ"]) +} + +func (r *reader) memInfo(m *types.HostMemoryInfo) { + pageSize, err := PageSize() + + if r.addErr(err) { + return + } + + totalMemory, err := TotalMemory() + if r.addErr(err) { + return + } + + m.Total = totalMemory + + vm, err := VmTotal() + if r.addErr(err) { + return + } + + m.Free = uint64(vm.Free) * uint64(pageSize) + m.Used = m.Total - m.Free + + numFreeBuffers, err := NumFreeBuffers() + if r.addErr(err) { + return + } + + m.Available = m.Free + (uint64(numFreeBuffers) * uint64(pageSize)) + + swap, err := KvmGetSwapInfo() + if r.addErr(err) { + return + } + + swapMaxPages, err := SwapMaxPages() + if r.addErr(err) { + return + } + + if swap.Total > swapMaxPages { + swap.Total = swapMaxPages + } + + m.VirtualTotal = uint64(swap.Total) * uint64(pageSize) + m.VirtualUsed = uint64(swap.Used) * uint64(pageSize) + m.VirtualFree = m.VirtualTotal - m.VirtualUsed +} + +func (r *reader) architecture(h *host) { + v, err := Architecture() + if r.addErr(err) { + return + } + h.info.Architecture = v +} + +func (r *reader) bootTime(h *host) { + v, err := BootTime() + if r.addErr(err) { + return + } + + h.info.BootTime = v +} + +func (r *reader) hostname(h *host) { + v, err := os.Hostname() + if r.addErr(err) { + return + } + h.info.Hostname = v +} + +func (r *reader) network(h *host) { + ips, macs, err := shared.Network() + if r.addErr(err) { + return + } + h.info.IPs = ips + h.info.MACs = macs +} + +func (r *reader) kernelVersion(h *host) { + v, err := KernelVersion() + if r.addErr(err) { + return + } + h.info.KernelVersion = v +} + +func (r *reader) os(h *host) { + v, err := OperatingSystem() + if r.addErr(err) { + return + } + h.info.OS = v +} + +func (r *reader) time(h *host) { + h.info.Timezone, h.info.TimezoneOffsetSec = time.Now().Zone() +} + +func (r *reader) uniqueID(h *host) { + v, err := MachineID() + if r.addErr(err) { + return + } + h.info.UniqueID = v +} diff --git a/providers/freebsd/kernel.go b/providers/freebsd/kernel.go new file mode 100644 index 00000000..3ade2725 --- /dev/null +++ b/providers/freebsd/kernel.go @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package freebsd + +import ( + "syscall" + + "github.com/pkg/errors" +) + +const kernelReleaseMIB = "kern.osrelease" + +func KernelVersion() (string, error) { + version, err := syscall.Sysctl(kernelReleaseMIB) + if err != nil { + return "", errors.Wrap(err, "failed to get kernel version") + } + + return version, nil +} diff --git a/providers/freebsd/machineid.go b/providers/freebsd/machineid.go new file mode 100644 index 00000000..2857ee1e --- /dev/null +++ b/providers/freebsd/machineid.go @@ -0,0 +1,35 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package freebsd + +import ( + "syscall" + + "github.com/pkg/errors" +) + +const kernelHostUUIDMIB = "kern.hostuuid" + +func MachineID() (string, error) { + uuid, err := syscall.Sysctl(kernelHostUUIDMIB) + if err != nil { + return "", errors.Wrap(err, "failed to get machine id") + } + + return uuid, nil +} diff --git a/providers/freebsd/memory_freebsd.go b/providers/freebsd/memory_freebsd.go new file mode 100644 index 00000000..e824db7d --- /dev/null +++ b/providers/freebsd/memory_freebsd.go @@ -0,0 +1,111 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build freebsd,cgo + +package freebsd + +// #cgo LDFLAGS: -lkvm +//#include +//#include +//#include + +//#include +//#include +//#include +import "C" + +import ( + "github.com/pkg/errors" + "syscall" + "unsafe" +) + +const hwPhysmemMIB = "hw.physmem" +const hwPagesizeMIB = "hw.pagesize" +const vmVmtotalMIB = "vm.vmtotal" +const vmSwapmaxpagesMIB = "vm.swap_maxpages" +const vfsNumfreebuffersMIB = "vfs.numfreebuffers" +const devNull = "/dev/null" +const kvmOpen = "kvm_open" + +func PageSize() (uint32, error) { + var pageSize uint32 + if err := sysctlByName(hwPagesizeMIB, &pageSize); err != nil { + return 0, errors.Wrap(err, "failed to get hw.pagesize") + } + + return pageSize, nil +} + +func SwapMaxPages() (uint32, error) { + var maxPages uint32 + if err := sysctlByName(hwPhysmemMIB, &maxPages); err != nil { + return 0, errors.Wrap(err, "failed to get vm.swap_maxpages") + } + + return maxPages, nil +} + +func TotalMemory() (uint64, error) { + var size uint64 + if err := sysctlByName(hwPhysmemMIB, &size); err != nil { + return 0, errors.Wrap(err, "failed to get hw.physmem") + } + + return size, nil +} + +func VmTotal() (vmTotal, error) { + var vm vmTotal + if err := sysctlByName(vmVmtotalMIB, &vm); err != nil { + return vmTotal{}, errors.Wrap(err, "failed to get vm.vmtotal") + } + + return vm, nil +} + +func NumFreeBuffers() (uint32, error) { + var numfreebuffers uint32 + if err := sysctlByName(vfsNumfreebuffersMIB, &numfreebuffers); err != nil { + return 0, errors.Wrap(err, "failed to get vfs.numfreebuffers") + } + + return numfreebuffers, nil +} + +func KvmGetSwapInfo() (kvmSwap, error) { + var kdC *C.struct_kvm_t + + devNullC := C.CString(devNull) + defer C.free(unsafe.Pointer(devNullC)) + kvmOpenC := C.CString(kvmOpen) + defer C.free(unsafe.Pointer(kvmOpenC)) + + if kdC, err := C.kvm_open(nil, devNullC, nil, syscall.O_RDONLY, kvmOpenC); kdC == nil { + return kvmSwap{}, errors.Wrap(err, "failed to open kvm") + } + + defer C.kvm_close((*C.struct___kvm)(unsafe.Pointer(kdC))) + + var swap kvmSwap + if n, err := C.kvm_getswapinfo((*C.struct___kvm)(unsafe.Pointer(kdC)), (*C.struct_kvm_swap)(unsafe.Pointer(&swap)), 1, 0); n != 0 { + return kvmSwap{}, errors.Wrap(err, "failed to get kvm_getswapinfo") + } + + return swap, nil +} diff --git a/providers/freebsd/os_freebsd.go b/providers/freebsd/os_freebsd.go new file mode 100644 index 00000000..1c1bab48 --- /dev/null +++ b/providers/freebsd/os_freebsd.go @@ -0,0 +1,66 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package freebsd + +import ( + "strconv" + "strings" + "syscall" + + "github.com/elastic/go-sysinfo/types" +) + +const ostypeMIB = "kern.ostype" +const osreleaseMIB = "kern.osrelease" +const osrevisionMIB = "kern.osrevision" + +func OperatingSystem() (*types.OSInfo, error) { + info := &types.OSInfo{ + Family: "freebsd", + Platform: "freebsd", + } + + ostype, err := syscall.Sysctl(ostypeMIB) + if err != nil { + return info, err + } + info.Name = ostype + + osrelease, err := syscall.Sysctl(osreleaseMIB) + if err != nil { + return info, err + } + info.Version = osrelease + + elems := strings.Split(osrelease, "-") + majorminor := strings.Split(elems[0], ".") + + if len(majorminor) > 0 { + info.Major, _ = strconv.Atoi(majorminor[0]) + } + + if len(majorminor) > 1 { + info.Minor, _ = strconv.Atoi(majorminor[1]) + } + + if len(elems) > 1 { + info.Patch, _ = strconv.Atoi(strings.TrimPrefix(elems[2], "p")) + } + + return info, nil +} diff --git a/providers/freebsd/process_freebsd.go b/providers/freebsd/process_freebsd.go new file mode 100644 index 00000000..a2dd00ff --- /dev/null +++ b/providers/freebsd/process_freebsd.go @@ -0,0 +1,401 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//+build,freebsd,cgo + +package freebsd + +// #cgo LDFLAGS: -lkvm -lprocstat +//#include +//#include +//#include +//#include +//#include +//#include +//#include +// +//#include +//#include +//struct kinfo_proc getProcInfoAt(struct kinfo_proc *procs, unsigned int index) { +// return procs[index]; +//} +//unsigned int countArrayItems(char **items) { +// unsigned int i = 0; +// for (i = 0; items[i] != NULL; ++i); +// return i; +//} +//char * itemAtIndex(char **items, unsigned int index) { +// return items[index]; +//} +//unsigned int countFileStats(struct filestat_list *head) { +// unsigned int count = 0; +// struct filestat *fst; +// STAILQ_FOREACH(fst, head, next) { +// ++count; +// } +// +// return count; +//} +//void copyFileStats(struct filestat_list *head, struct filestat *out, unsigned int size) { +// unsigned int index = 0; +// struct filestat *fst; +// STAILQ_FOREACH(fst, head, next) { +// if (!size) { +// break; +// } +// memcpy(out, fst, sizeof(*fst)); +// ++out; +// --size; +// } +//} +// +import "C" + +import ( + "os" + "strconv" + "strings" + "syscall" + "time" + + "github.com/pkg/errors" + + "github.com/elastic/go-sysinfo/types" +) + +func getProcInfo(op int, arg int) ([]process, error) { + procstat, err := C.procstat_open_sysctl() + + if procstat == nil { + return nil, errors.Wrap(err, "failed to open procstat sysctl") + } + defer C.procstat_close(procstat) + + var count C.uint = 0 + kprocs, err := C.procstat_getprocs(procstat, C.int(op), C.int(arg), &count) + if kprocs == nil { + return nil, errors.Wrap(err, "getprocs failed") + } + defer C.procstat_freeprocs(procstat, kprocs) + + procs := make([]process, count) + var index C.uint + for index = 0; index < count; index++ { + proc := C.getProcInfoAt(kprocs, index) + procs[index].kinfo = proc + procs[index].pid = int(proc.ki_pid) + } + + return procs, nil +} + +func copyArray(from **C.char) []string { + if from == nil { + return nil + } + + count := C.countArrayItems(from) + out := make([]string, count) + + for index := C.uint(0); index < count; index++ { + out[index] = C.GoString(C.itemAtIndex(from, index)) + } + + return out +} + +func makeMap(from []string) map[string]string { + out := make(map[string]string, len(from)) + + for _, env := range from { + parts := strings.Split(env, "=") + if len(parts) > 1 { + out[parts[0]] = parts[1] + } + } + + return out +} + +func getProcEnv(p *process) (map[string]string, error) { + procstat, err := C.procstat_open_sysctl() + + if procstat == nil { + return nil, errors.Wrap(err, "failed to open procstat sysctl") + } + defer C.procstat_close(procstat) + + env, err := C.procstat_getenvv(procstat, &p.kinfo, 0) + defer C.procstat_freeenvv(procstat) + + return makeMap(copyArray(env)), err +} + +func getProcArgs(p *process) ([]string, error) { + procstat, err := C.procstat_open_sysctl() + + if procstat == nil { + return nil, errors.Wrap(err, "failed to open procstat sysctl") + } + defer C.procstat_close(procstat) + + args, err := C.procstat_getargv(procstat, &p.kinfo, 0) + defer C.procstat_freeargv(procstat) + + return copyArray(args), err +} + +func getProcPathname(p *process) (string, error) { + procstat, err := C.procstat_open_sysctl() + + if procstat == nil { + return "", errors.Wrap(err, "failed to open procstat sysctl") + } + defer C.procstat_close(procstat) + + const maxlen = uint(1024) + out := make([]C.char, maxlen) + if res, err := C.procstat_getpathname(procstat, &p.kinfo, &out[0], C.ulong(maxlen)); res != 0 { + return "", err + } + return C.GoString(&out[0]), nil +} + +func getFileStats(fileStats *C.struct_filestat_list) []C.struct_filestat { + count := C.countFileStats(fileStats) + + if count < 1 { + return nil + } + + out := make([]C.struct_filestat, count) + + C.copyFileStats(fileStats, &out[0], count) + return out +} + +func getProcCWD(p *process) (string, error) { + procstat, err := C.procstat_open_sysctl() + + if procstat == nil { + return "", errors.Wrap(err, "failed to open procstat sysctl") + } + defer C.procstat_close(procstat) + + fs, err := C.procstat_getfiles(procstat, &p.kinfo, 0) + if fs == nil { + return "", errors.Wrap(err, "failed to get files") + } + + defer C.procstat_freefiles(procstat, fs) + + files := getFileStats(fs) + for _, f := range files { + if f.fs_uflags == C.PS_FST_UFLAG_CDIR { + return C.GoString(f.fs_path), nil + } + } + + return "", nil +} + +type process struct { + pid int + kinfo C.struct_kinfo_proc +} + +func timevalToDuration(tm C.struct_timeval) time.Duration { + return (time.Duration(tm.tv_sec) * 1000000) + (time.Duration(tm.tv_usec) * 1000) +} + +func (p *process) CPUTime() (types.CPUTimes, error) { + procs, err := getProcInfo(C.KERN_PROC_PID, p.PID()) + + if err != nil { + return types.CPUTimes{}, err + } + p.kinfo = procs[0].kinfo + + return types.CPUTimes{ + User: timevalToDuration(p.kinfo.ki_rusage.ru_utime), + System: timevalToDuration(p.kinfo.ki_rusage.ru_stime), + }, nil +} + +func (p *process) Info() (types.ProcessInfo, error) { + procs, err := getProcInfo(C.KERN_PROC_PID, p.PID()) + + if err != nil { + return types.ProcessInfo{}, err + } + p.kinfo = procs[0].kinfo + + cwd, err := getProcCWD(p) + if err != nil { + return types.ProcessInfo{}, err + } + + args, err := getProcArgs(p) + if err != nil { + return types.ProcessInfo{}, err + } + + exe, _ := getProcPathname(p) + + return types.ProcessInfo{ + Name: C.GoString(&p.kinfo.ki_comm[0]), + PID: int(p.kinfo.ki_pid), + PPID: int(p.kinfo.ki_ppid), + CWD: cwd, + Exe: exe, + Args: args, + StartTime: time.Unix(int64(p.kinfo.ki_start.tv_sec), int64(p.kinfo.ki_start.tv_usec)*1000), + }, nil +} + +func (p *process) Memory() (types.MemoryInfo, error) { + procs, err := getProcInfo(C.KERN_PROC_PID, p.PID()) + + if err != nil { + return types.MemoryInfo{}, err + } + p.kinfo = procs[0].kinfo + + return types.MemoryInfo{ + Resident: uint64(p.kinfo.ki_rssize), + Virtual: uint64(p.kinfo.ki_size), + }, nil +} + +func (p *process) User() (types.UserInfo, error) { + procs, err := getProcInfo(C.KERN_PROC_PID, p.PID()) + + if err != nil { + return types.UserInfo{}, err + } + + p.kinfo = procs[0].kinfo + + return types.UserInfo{ + UID: strconv.FormatUint(uint64(p.kinfo.ki_ruid), 10), + EUID: strconv.FormatUint(uint64(p.kinfo.ki_uid), 10), + SUID: strconv.FormatUint(uint64(p.kinfo.ki_svuid), 10), + GID: strconv.FormatUint(uint64(p.kinfo.ki_rgid), 10), + EGID: strconv.FormatUint(uint64(p.kinfo.ki_groups[0]), 10), + SGID: strconv.FormatUint(uint64(p.kinfo.ki_svgid), 10), + }, nil +} + +func (p *process) PID() int { + return p.pid +} + +func (p *process) OpenHandles() ([]string, error) { + procstat := C.procstat_open_sysctl() + + if procstat == nil { + return nil, errors.New("failed to open procstat sysctl") + } + defer C.procstat_close(procstat) + + fs := C.procstat_getfiles(procstat, &p.kinfo, 0) + defer C.procstat_freefiles(procstat, fs) + + files := getFileStats(fs) + names := make([]string, 0, len(files)) + + for _, file := range files { + if file.fs_uflags == 0 { + names = append(names, C.GoString(file.fs_path)) + } + } + + return names, nil +} + +func (p *process) OpenHandleCount() (int, error) { + procstat := C.procstat_open_sysctl() + + if procstat == nil { + return 0, errors.New("failed to open procstat sysctl") + } + defer C.procstat_close(procstat) + + fs := C.procstat_getfiles(procstat, &p.kinfo, 0) + defer C.procstat_freefiles(procstat, fs) + return int(C.countFileStats(fs)), nil +} + +func (p *process) Environment() (map[string]string, error) { + return getProcEnv(p) +} + +func (s freebsdSystem) Processes() ([]types.Process, error) { + procs, err := getProcInfo(C.KERN_PROC_PROC, 0) + out := make([]types.Process, 0, len(procs)) + + for _, proc := range procs { + out = append(out, &process{ + pid: proc.pid, + kinfo: proc.kinfo, + }) + } + + return out, err +} + +func (s freebsdSystem) Process(pid int) (types.Process, error) { + p := process{pid: pid} + return &p, nil +} + +func (s freebsdSystem) Self() (types.Process, error) { + return s.Process(os.Getpid()) +} + +const kernCptimeMIB = "kern.cp_time" +const kernClockrateMIB = "kern.clockrate" + +func Cptime() (map[string]uint64, error) { + var clock clockInfo + + if err := sysctlByName(kernClockrateMIB, &clock); err != nil { + return make(map[string]uint64), errors.Wrap(err, "failed to get kern.clockrate") + } + + cptime, err := syscall.Sysctl(kernCptimeMIB) + if err != nil { + return make(map[string]uint64), errors.Wrap(err, "failed to get kern.cp_time") + } + + cpMap := make(map[string]uint64) + + times := strings.Split(cptime, " ") + names := [5]string{"User", "Nice", "System", "IRQ", "Idle"} + + for index, time := range times { + i, err := strconv.ParseUint(time, 10, 64) + + if err != nil { + return cpMap, errors.Wrap(err, "error parsing kern.cp_time") + } + + cpMap[names[index]] = i * uint64(clock.Tick) * 1000 + } + + return cpMap, nil +} diff --git a/providers/freebsd/process_freebsd_test.go b/providers/freebsd/process_freebsd_test.go new file mode 100644 index 00000000..566fa001 --- /dev/null +++ b/providers/freebsd/process_freebsd_test.go @@ -0,0 +1,25 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package freebsd + +import ( + "github.com/elastic/go-sysinfo/internal/registry" +) + +var _ registry.HostProvider = freebsdSystem{} +var _ registry.ProcessProvider = freebsdSystem{} diff --git a/providers/freebsd/syscall_freebsd.go b/providers/freebsd/syscall_freebsd.go new file mode 100644 index 00000000..5a6ddb1f --- /dev/null +++ b/providers/freebsd/syscall_freebsd.go @@ -0,0 +1,84 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build freebsd,cgo + +package freebsd + +//#include +//#include +import "C" + +import ( + "bytes" + "encoding/binary" + "sync" + "unsafe" +) + +// Buffer Pool + +var bufferPool = sync.Pool{ + New: func() interface{} { + return &poolMem{ + buf: make([]byte, 512), + } + }, +} + +type poolMem struct { + buf []byte + pool *sync.Pool +} + +func getPoolMem() *poolMem { + pm := bufferPool.Get().(*poolMem) + pm.buf = pm.buf[0:cap(pm.buf)] + pm.pool = &bufferPool + return pm +} + +func (m *poolMem) Release() { m.pool.Put(m) } + +func sysctlbyname(name string, value interface{}) error { + mem := getPoolMem() + defer mem.Release() + + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + + size := C.ulong(len(mem.buf)) + if n, err := C.sysctlbyname(cname, unsafe.Pointer(&mem.buf[0]), &size, nil, C.ulong(0)); n != 0 { + return err + } + + data := mem.buf[0:size] + + switch v := value.(type) { + case *[]byte: + out := make([]byte, len(data)) + copy(out, data) + *v = out + return nil + default: + return binary.Read(bytes.NewReader(data), binary.LittleEndian, v) + } +} + +func sysctlByName(name string, out interface{}) error { + return sysctlbyname(name, out) +} diff --git a/providers/freebsd/ztypes_freebsd.go b/providers/freebsd/ztypes_freebsd.go new file mode 100644 index 00000000..230b857e --- /dev/null +++ b/providers/freebsd/ztypes_freebsd.go @@ -0,0 +1,40 @@ +// Code generated by cmd/cgo -godefs; and then patched up to fix +// an alignment issue +// cgo -godefs defs_freebsd.go + +package freebsd + +type vmTotal struct { + Rq int16 + Dw int16 + Pw int16 + Sl int16 + _ int16 // cgo doesn't generate the same alignment as C does + Sw int16 + Vm int32 + Avm int32 + Rm int32 + Arm int32 + Vmshr int32 + Avmshr int32 + Rmshr int32 + Armshr int32 + Free int32 +} + +type kvmSwap struct { + Devname [32]int8 + Used uint32 + Total uint32 + Flags int32 + Reserved1 uint32 + Reserved2 uint32 +} + +type clockInfo struct { + Hz int32 + Tick int32 + Spare int32 + Stathz int32 + Profhz int32 +} diff --git a/system.go b/system.go index 90f81691..dcecd965 100644 --- a/system.go +++ b/system.go @@ -20,13 +20,14 @@ package sysinfo import ( "runtime" - "github.com/elastic/go-sysinfo/internal/registry" + "github.com/dbolcsfoldi/go-sysinfo/internal/registry" "github.com/elastic/go-sysinfo/types" // Register host and process providers. _ "github.com/elastic/go-sysinfo/providers/darwin" _ "github.com/elastic/go-sysinfo/providers/linux" _ "github.com/elastic/go-sysinfo/providers/windows" + _ "github.com/dbolcsfoldi/go-sysinfo/providers/freebsd" ) // Go returns information about the Go runtime. diff --git a/system_test.go b/system_test.go index 97a2de5d..a0b6f4f7 100644 --- a/system_test.go +++ b/system_test.go @@ -19,6 +19,7 @@ package sysinfo import ( "encoding/json" + "fmt" "os" osUser "os/user" "runtime" @@ -62,6 +63,12 @@ var expectedProcessFeatures = map[string]*ProcessFeatures{ OpenHandleEnumerator: false, OpenHandleCounter: true, }, + "freebsd": &ProcessFeatures{ + ProcessInfo: true, + Environment: true, + OpenHandleEnumerator: true, + OpenHandleCounter: true, + }, } func TestProcessFeaturesMatrix(t *testing.T) { @@ -90,6 +97,7 @@ func TestProcessFeaturesMatrix(t *testing.T) { } func TestSelf(t *testing.T) { + fmt.Printf("Getting Self()...\n") process, err := Self() if err == types.ErrNotImplemented { t.Skip("process provider not implemented on", runtime.GOOS) @@ -107,6 +115,7 @@ func TestSelf(t *testing.T) { } output := map[string]interface{}{} + fmt.Printf("Getting ProcessInfo...\n") info, err := process.Info() if err != nil { t.Fatal(err) @@ -120,7 +129,18 @@ func TestSelf(t *testing.T) { if err != nil { t.Fatal(err) } - assert.EqualValues(t, wd, info.CWD) + + wdStat, err := os.Stat(wd) + if err != nil { + t.Fatal(err) + } + + cwdStat, err := os.Stat(info.CWD) + if err != nil { + t.Fatal(err) + } + + assert.EqualValues(t, os.SameFile(wdStat, cwdStat), true) exe, err := os.Executable() if err != nil { @@ -133,6 +153,7 @@ func TestSelf(t *testing.T) { } assert.WithinDuration(t, info.StartTime, time.Now(), 10*time.Second) + fmt.Printf("Getting UserInfo...\n") user, err := process.User() if err != nil { t.Fatal(err) @@ -151,6 +172,7 @@ func TestSelf(t *testing.T) { assert.EqualValues(t, strconv.Itoa(os.Getegid()), user.EGID) } + fmt.Printf("Getting Environment...\n") if v, ok := process.(types.Environment); ok { expectedEnv := map[string]string{} for _, keyValue := range os.Environ() { @@ -168,6 +190,7 @@ func TestSelf(t *testing.T) { output["process.env"] = actualEnv } + fmt.Printf("Getting MemoryInfo...\n") memInfo, err := process.Memory() require.NoError(t, err) if runtime.GOOS != "windows" { @@ -178,6 +201,7 @@ func TestSelf(t *testing.T) { assert.NotZero(t, memInfo.Resident) output["process.mem"] = memInfo + fmt.Printf("Getting CPUTimes...\n") for { cpuTimes, err := process.CPUTime() require.NoError(t, err) @@ -191,6 +215,7 @@ func TestSelf(t *testing.T) { // measurement. } + fmt.Printf("Getting OpenHandleEnumerator...\n") if v, ok := process.(types.OpenHandleEnumerator); ok { fds, err := v.OpenHandles() if assert.NoError(t, err) { From 99bb2c6f5210f04ffb4a7d1ed98bc6c22f95b85c Mon Sep 17 00:00:00 2001 From: David Bolcsfoldi Date: Tue, 12 Feb 2019 03:31:32 -0800 Subject: [PATCH 2/3] Switch from github.com/dbolcsfoldi to github.com/elastic --- providers/freebsd/host_freebsd.go | 2 +- system.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/providers/freebsd/host_freebsd.go b/providers/freebsd/host_freebsd.go index a6fe2bd6..ef3ee0bd 100644 --- a/providers/freebsd/host_freebsd.go +++ b/providers/freebsd/host_freebsd.go @@ -31,7 +31,7 @@ import ( "github.com/joeshaw/multierror" "github.com/pkg/errors" - "github.com/dbolcsfoldi/go-sysinfo/internal/registry" + "github.com/elastic/go-sysinfo/internal/registry" "github.com/elastic/go-sysinfo/providers/shared" "github.com/elastic/go-sysinfo/types" ) diff --git a/system.go b/system.go index dcecd965..f7e8f422 100644 --- a/system.go +++ b/system.go @@ -20,14 +20,14 @@ package sysinfo import ( "runtime" - "github.com/dbolcsfoldi/go-sysinfo/internal/registry" + "github.com/elastic/go-sysinfo/internal/registry" "github.com/elastic/go-sysinfo/types" // Register host and process providers. _ "github.com/elastic/go-sysinfo/providers/darwin" + _ "github.com/elastic/go-sysinfo/providers/freebsd" _ "github.com/elastic/go-sysinfo/providers/linux" _ "github.com/elastic/go-sysinfo/providers/windows" - _ "github.com/dbolcsfoldi/go-sysinfo/providers/freebsd" ) // Go returns information about the Go runtime. From 58d4dfbdcc450b4d466e1f5cc9d66ea523d7d50c Mon Sep 17 00:00:00 2001 From: David Bolcsfoldi Date: Tue, 12 Feb 2019 03:52:00 -0800 Subject: [PATCH 3/3] Remove fmt.Printf used when debugging system test. --- system_test.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/system_test.go b/system_test.go index a0b6f4f7..a9970c10 100644 --- a/system_test.go +++ b/system_test.go @@ -19,7 +19,6 @@ package sysinfo import ( "encoding/json" - "fmt" "os" osUser "os/user" "runtime" @@ -97,7 +96,6 @@ func TestProcessFeaturesMatrix(t *testing.T) { } func TestSelf(t *testing.T) { - fmt.Printf("Getting Self()...\n") process, err := Self() if err == types.ErrNotImplemented { t.Skip("process provider not implemented on", runtime.GOOS) @@ -115,7 +113,6 @@ func TestSelf(t *testing.T) { } output := map[string]interface{}{} - fmt.Printf("Getting ProcessInfo...\n") info, err := process.Info() if err != nil { t.Fatal(err) @@ -153,7 +150,6 @@ func TestSelf(t *testing.T) { } assert.WithinDuration(t, info.StartTime, time.Now(), 10*time.Second) - fmt.Printf("Getting UserInfo...\n") user, err := process.User() if err != nil { t.Fatal(err) @@ -172,7 +168,6 @@ func TestSelf(t *testing.T) { assert.EqualValues(t, strconv.Itoa(os.Getegid()), user.EGID) } - fmt.Printf("Getting Environment...\n") if v, ok := process.(types.Environment); ok { expectedEnv := map[string]string{} for _, keyValue := range os.Environ() { @@ -190,7 +185,6 @@ func TestSelf(t *testing.T) { output["process.env"] = actualEnv } - fmt.Printf("Getting MemoryInfo...\n") memInfo, err := process.Memory() require.NoError(t, err) if runtime.GOOS != "windows" { @@ -201,7 +195,6 @@ func TestSelf(t *testing.T) { assert.NotZero(t, memInfo.Resident) output["process.mem"] = memInfo - fmt.Printf("Getting CPUTimes...\n") for { cpuTimes, err := process.CPUTime() require.NoError(t, err) @@ -215,7 +208,6 @@ func TestSelf(t *testing.T) { // measurement. } - fmt.Printf("Getting OpenHandleEnumerator...\n") if v, ok := process.(types.OpenHandleEnumerator); ok { fds, err := v.OpenHandles() if assert.NoError(t, err) {