utils/platform/os.go
/*
* Copyright (C) 2020-2022 Arm Limited or its affiliates and Contributors. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package platform
import (
"errors"
"fmt"
"os"
"regexp"
"runtime"
"strings"
"time"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/mem"
"github.com/ARM-software/golang-utils/utils/commonerrors"
)
var (
errNotSupportedByWindows = errors.New("not supported by windows")
// https://learn.microsoft.com/en-us/previous-versions/troubleshoot/winautomation/product-documentation/best-practices/variables/percentage-character-usage-in-notations
// https://ss64.com/nt/syntax-replace.html
windowsVariableExpansionRegexStr = `%(?P<variable>[^:=]*)(:(?P<StrToFind>.*)=(?P<NewString>.*))?%`
// UnixVariableNameRegexString defines the schema for variable names on Unix.
// See https://www.gnu.org/software/bash/manual/bash.html#index-name and https://mywiki.wooledge.org/BashFAQ/006
UnixVariableNameRegexString = "^[a-zA-Z_][a-zA-Z_0-9]*$"
// WindowsVariableNameRegexString defines the schema for variable names on Windows.
// See https://ss64.com/nt/syntax-variables.html
WindowsVariableNameRegexString = "^[A-Za-z#$'()*+,.?@\\[\\]_`{}~][A-Za-z0-9#$'()*+,.?@\\[\\]_`{}~\\s]*$"
errVariableNameInvalid = validation.NewError("validation_is_variable_name", "must be a valid variable name")
// IsWindowsVariableName defines a validation rule for variable names on Windows for use with github.com/go-ozzo/ozzo-validation
IsWindowsVariableName = validation.NewStringRuleWithError(isWindowsVarName, errVariableNameInvalid)
// IsUnixVariableName defines a validation rule for variable names on Unix for use with github.com/go-ozzo/ozzo-validation
IsUnixVariableName = validation.NewStringRuleWithError(isUnixVarName, errVariableNameInvalid)
// IsVariableName defines a validation rule for variable names for use with github.com/go-ozzo/ozzo-validation
IsVariableName = validation.NewStringRuleWithError(isVarName, errVariableNameInvalid)
)
func isWindowsVarName(value string) bool {
if validation.Required.Validate(value) != nil {
return false
}
regex := regexp.MustCompile(WindowsVariableNameRegexString)
return regex.MatchString(value)
}
func isUnixVarName(value string) bool {
if validation.Required.Validate(value) != nil {
return false
}
regex := regexp.MustCompile(UnixVariableNameRegexString)
return regex.MatchString(value)
}
func isVarName(value string) bool {
if IsWindows() {
return isWindowsVarName(value)
}
return isUnixVarName(value)
}
// ConvertError converts a platform error into a commonerrors
func ConvertError(err error) error {
switch {
case err == nil:
return err
case commonerrors.Any(err, commonerrors.ErrNotImplemented, commonerrors.ErrUnsupported):
return err
case IsWindows() && commonerrors.Any(err, errNotSupportedByWindows):
return fmt.Errorf("%w: %v", commonerrors.ErrUnsupported, err.Error())
case commonerrors.CorrespondTo(err, "not supported"):
return fmt.Errorf("%w: %v", commonerrors.ErrUnsupported, err.Error())
default:
return err
// TODO extend with more platform specific errors
}
}
// IsWindows checks whether we are running on Windows or not.
func IsWindows() bool {
return runtime.GOOS == "windows"
}
// LineSeparator returns the line separator.
func LineSeparator() string {
if IsWindows() {
return "\r\n"
}
return UnixLineSeparator()
}
// UnixLineSeparator returns the line separator on Unix platform.
func UnixLineSeparator() string {
return "\n"
}
// Hostname returns the hostname.
func Hostname() (string, error) {
return os.Hostname()
}
// UpTime returns system uptime.
func UpTime() (uptime time.Duration, err error) {
_uptime, err := host.Uptime()
if err != nil {
return
}
uptime = time.Duration(_uptime) * time.Second
return
}
// BootTime returns system uptime.
func BootTime() (bootime time.Time, err error) {
_bootime, err := host.BootTime()
if err != nil {
return
}
bootime = time.Unix(int64(_bootime), 0)
return
}
// NodeName returns the system node name (equivalent to uname -n).
func NodeName() (nodename string, err error) {
info, err := host.Info()
if err != nil {
return
}
nodename = fmt.Sprintf("%v (%v)", info.Hostname, info.HostID)
return
}
// PlatformInformation returns the platform information (equivalent to uname -s).
func PlatformInformation() (information string, err error) {
platform, family, version, err := host.PlatformInformation()
if err != nil {
return
}
information = fmt.Sprintf("%v (%v/%v)", platform, family, version)
return
}
// SystemInformation returns the system information (equivalent to uname -a)
func SystemInformation() (information string, err error) {
hostname, err := Hostname()
if err != nil {
return
}
nodename, err := NodeName()
if err != nil {
return
}
platform, err := PlatformInformation()
if err != nil {
return
}
uptime, err := UpTime()
if err != nil {
return
}
bootime, err := BootTime()
if err != nil {
return
}
information = fmt.Sprintf("Host: %v, Node: %v, Platform: %v, Up time: %v, Boot time: %v", hostname, nodename, platform, uptime, bootime)
return
}
func Uname() (string, error) {
return SystemInformation()
}
type RAM interface {
// GetTotal returns total amount of RAM on this system
GetTotal() uint64
// GetAvailable returns RAM available for programs to allocate
GetAvailable() uint64
// GetUsed returns RAM used by programs
GetUsed() uint64
// GetUsedPercent returns Percentage of RAM used by programs
GetUsedPercent() float64
// GetFree returns kernel's notion of free memory
GetFree() uint64
}
type VirtualMemory struct {
Total uint64
Available uint64
Used uint64
UsedPercent float64
Free uint64
}
func (m *VirtualMemory) GetTotal() uint64 { return m.Total }
func (m *VirtualMemory) GetAvailable() uint64 { return m.Available }
func (m *VirtualMemory) GetUsed() uint64 { return m.Used }
func (m *VirtualMemory) GetUsedPercent() float64 { return m.UsedPercent }
func (m *VirtualMemory) GetFree() uint64 { return m.Free }
func GetRAM() (ram RAM, err error) {
vm, err := mem.VirtualMemory()
if err != nil {
return
}
ram = &VirtualMemory{
Total: vm.Total,
Available: vm.Available,
Used: vm.Used,
UsedPercent: vm.UsedPercent,
Free: vm.Free,
}
return
}
// SubstituteParameter performs parameter substitution on all platforms.
// - the first element is the parameter to substitute
// - if find and replace is also wanted, pass the pattern and the replacement as following arguments in that order.
func SubstituteParameter(parameter ...string) string {
if IsWindows() {
return SubstituteParameterWindows(parameter...)
}
return SubstituteParameterUnix(parameter...)
}
// SubstituteParameterUnix performs Unix parameter substitution:
// See https://tldp.org/LDP/abs/html/parameter-substitution.html
// - the first element is the parameter to substitute
// - if find and replace is also wanted, pass the pattern and the replacement as following arguments in that order.
func SubstituteParameterUnix(parameter ...string) string {
if len(parameter) < 1 || !isUnixVarName(parameter[0]) {
return "${}"
}
if len(parameter) < 3 || parameter[1] == "" {
return fmt.Sprintf("${%v}", parameter[0])
}
return fmt.Sprintf("${%v//%v/%v}", parameter[0], parameter[1], parameter[2])
}
// SubstituteParameterWindows performs Windows parameter substitution:
// See https://ss64.com/nt/syntax-replace.html
// - the first element is the parameter to substitute
// - if find and replace is also wanted, pass the pattern and the replacement as following arguments in that order.
func SubstituteParameterWindows(parameter ...string) string {
if len(parameter) < 1 || !isWindowsVarName(parameter[0]) {
return "%%"
}
if len(parameter) < 3 || parameter[1] == "" {
return "%" + parameter[0] + "%"
}
return "%" + fmt.Sprintf("%v:%v=%v", parameter[0], parameter[1], parameter[2]) + "%"
}
// ExpandParameter expands a variable expressed in a string `s` with its value returned by the mapping function.
// If the mapping function returns a string with variables, it will expand them too if recursive is set to true.
func ExpandParameter(s string, mappingFunc func(string) (string, bool), recursive bool) string {
if IsWindows() {
return ExpandWindowsParameter(s, mappingFunc, recursive)
}
return ExpandUnixParameter(s, mappingFunc, recursive)
}
func newMappingFunc(recursive bool, mappingFunc func(string) (string, bool), expansionFunc func(s string, mappingFunc func(string) (string, bool)) string) func(string) (string, bool) {
if recursive {
return recursiveMapping(mappingFunc, expansionFunc)
}
return mappingFunc
}
func recursiveMapping(mappingFunc func(string) (string, bool), expansionFunc func(s string, mappingFunc func(string) (string, bool)) string) func(string) (string, bool) {
newMappingFunc := func(entry string) (string, bool) {
mappedEntry, found := mappingFunc(entry)
if !found {
return mappedEntry, found
}
newExpanded := expansionFunc(mappedEntry, mappingFunc)
if mappedEntry == newExpanded {
return newExpanded, true
}
return expansionFunc(newExpanded, mappingFunc), true
}
return newMappingFunc
}
// ExpandUnixParameter expands a ${param} or $param in `s` based on the mapping function
// See https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
// os.Expand is used under the bonnet and so, only basic parameter substitution is performed.
// TODO if os.Expand is not good enough, consider using other libraries such as https://github.com/ganbarodigital/go_shellexpand or https://github.com/mvdan/sh
func ExpandUnixParameter(s string, mappingFunc func(string) (string, bool), recursive bool) string {
mapping := newMappingFunc(recursive, mappingFunc, expandUnixParameter)
return expandUnixParameter(s, mapping)
}
func expandUnixParameter(s string, mappingFunc func(string) (string, bool)) string {
return os.Expand(s, func(variable string) string {
mapped, _ := mappingFunc(variable)
return mapped
})
}
// ExpandWindowsParameter expands a %param% in `s` based on the mapping function
// See https://learn.microsoft.com/en-us/previous-versions/troubleshoot/winautomation/product-documentation/best-practices/variables/percentage-character-usage-in-notations
// https://devblogs.microsoft.com/oldnewthing/20060823-00/?p=29993
// https://github.com/golang/go/issues/24848
// WARNING: currently the function only works with one parameter substitution in `s`.
func ExpandWindowsParameter(s string, mappingFunc func(string) (string, bool), recursive bool) string {
mapping := newMappingFunc(recursive, mappingFunc, expandWindowsParameter)
return expandWindowsParameter(s, mapping)
}
func expandWindowsParameter(s string, mappingFunc func(string) (string, bool)) string {
variableRegex := regexp.MustCompile(windowsVariableExpansionRegexStr)
if !variableRegex.MatchString(s) {
return s
}
allMatches := variableRegex.FindAllStringSubmatch(s, -1)
expandedString := s
for i := range allMatches {
old, newStr := expandedVariableWithEdit(allMatches[i], mappingFunc)
expandedString = strings.ReplaceAll(expandedString, old, newStr)
}
return expandedString
}
func expandedVariableWithoutEdit(match []string, mappingFunc func(string) (string, bool)) (string, string, bool) {
if len(match) < 1 {
return "", "", false
}
if len(match) < 2 {
return match[0], "", false
}
variable := match[1]
if len(strings.TrimSpace(variable)) == 0 {
return match[0], match[0], false
}
expandedVariable, found := mappingFunc(variable)
if found {
return match[0], expandedVariable, true
}
return match[0], match[0], false
}
func expandedVariableWithEdit(match []string, mappingFunc func(string) (string, bool)) (string, string) {
if len(match) != 5 {
s, expandedVariable, _ := expandedVariableWithoutEdit(match, mappingFunc)
return s, expandedVariable
}
strToFind := match[3]
newString := match[4]
s, expandedVariable, expanded := expandedVariableWithoutEdit(match, mappingFunc)
if !expanded {
return s, expandedVariable
}
return s, strings.ReplaceAll(expandedVariable, strToFind, newString)
}
// ExpandFromEnvironment expands a string containing variables with values from the environment.
// On unix, it is equivalent to os.ExpandEnv but differs on Windows due to the following issues:
// - https://learn.microsoft.com/en-gb/windows/win32/api/processenv/nf-processenv-expandenvironmentstringsa?redirectedfrom=MSDN
// - https://github.com/golang/go/issues/43763
// - https://github.com/golang/go/issues/24848
func ExpandFromEnvironment(s string, recursive bool) string {
if IsWindows() {
expanded := expandFromEnvironment(s)
if recursive {
newExpanded := expandFromEnvironment(expanded)
if expanded == newExpanded {
return expanded
}
return ExpandFromEnvironment(newExpanded, recursive)
}
return expanded
}
return ExpandUnixParameter(s, os.LookupEnv, recursive)
}