diff --git a/.changelog/200.txt b/.changelog/200.txt new file mode 100644 index 0000000..51afa52 --- /dev/null +++ b/.changelog/200.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +Adds NativeArchitecture to HostInfo to allow applications to detect whether they are running in emulation. +``` \ No newline at end of file diff --git a/providers/darwin/arch_darwin.go b/providers/darwin/arch_darwin.go index 8b3ed91..92251c3 100644 --- a/providers/darwin/arch_darwin.go +++ b/providers/darwin/arch_darwin.go @@ -21,11 +21,17 @@ package darwin import ( "fmt" + "os" "golang.org/x/sys/unix" ) -const hardwareMIB = "hw.machine" +const ( + hardwareMIB = "hw.machine" + procTranslated = "sysctl.proc_translated" + archIntel = "x86_64" + archApple = "arm64" +) func Architecture() (string, error) { arch, err := unix.Sysctl(hardwareMIB) @@ -35,3 +41,33 @@ func Architecture() (string, error) { return arch, nil } + +func NativeArchitecture() (string, error) { + processArch, err := Architecture() + if err != nil { + return "", err + } + + // https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment + + translated, err := unix.SysctlUint32(procTranslated) + if err != nil { + // macos without Rosetta installed doesn't have sysctl.proc_translated + if os.IsNotExist(err) { + return processArch, nil + } + return "", fmt.Errorf("failed to read sysctl.proc_translated: %w", err) + } + + var nativeArch string + + switch translated { + case 0: + nativeArch = processArch + case 1: + // Rosetta 2 is supported only on Apple silicon + nativeArch = archApple + } + + return nativeArch, nil +} diff --git a/providers/darwin/arch_darwin_test.go b/providers/darwin/arch_darwin_test.go index 01a25a5..340f7e8 100644 --- a/providers/darwin/arch_darwin_test.go +++ b/providers/darwin/arch_darwin_test.go @@ -28,3 +28,9 @@ func TestArchitecture(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, a) } + +func TestNativeArchitecture(t *testing.T) { + a, err := NativeArchitecture() + assert.NoError(t, err) + assert.NotEmpty(t, a) +} diff --git a/providers/darwin/host_darwin.go b/providers/darwin/host_darwin.go index 70862e8..4a6adc1 100644 --- a/providers/darwin/host_darwin.go +++ b/providers/darwin/host_darwin.go @@ -167,6 +167,7 @@ func newHost() (*host, error) { h := &host{} r := &reader{} r.architecture(h) + r.nativeArchitecture(h) r.bootTime(h) r.hostname(h) r.network(h) @@ -206,6 +207,14 @@ func (r *reader) architecture(h *host) { h.info.Architecture = v } +func (r *reader) nativeArchitecture(h *host) { + v, err := NativeArchitecture() + if r.addErr(err) { + return + } + h.info.NativeArchitecture = v +} + func (r *reader) bootTime(h *host) { v, err := BootTime() if r.addErr(err) { diff --git a/providers/linux/arch_linux.go b/providers/linux/arch_linux.go index e1d2893..f1d807f 100644 --- a/providers/linux/arch_linux.go +++ b/providers/linux/arch_linux.go @@ -19,9 +19,19 @@ package linux import ( "fmt" + "os" + "strings" "syscall" ) +const ( + procSysKernelArch = "/proc/sys/kernel/arch" + procVersion = "/proc/version" + archAmd64 = "amd64" + archArm64 = "arm64" + archAarch64 = "aarch64" +) + func Architecture() (string, error) { var uname syscall.Utsname if err := syscall.Uname(&uname); err != nil { @@ -38,3 +48,38 @@ func Architecture() (string, error) { return string(data), nil } + +func NativeArchitecture() (string, error) { + // /proc/sys/kernel/arch was introduced in Kernel 6.1 + // https://www.kernel.org/doc/html/v6.1/admin-guide/sysctl/kernel.html#arch + // It's the same as uname -m, except that for a process running in emulation + // machine returned from syscall reflects the emulated machine, whilst /proc + // filesystem is read as file so its value is not emulated + data, err := os.ReadFile(procSysKernelArch) + if err != nil { + if os.IsNotExist(err) { + // fallback to checking version string for older kernels + version, err := os.ReadFile(procVersion) + if err != nil { + return "", nil + } + + versionStr := string(version) + if strings.Contains(versionStr, archAmd64) { + return archAmd64, nil + } else if strings.Contains(versionStr, archArm64) { + // for parity with Architecture() and /proc/sys/kernel/arch + // as aarch64 and arm64 are used interchangeably + return archAarch64, nil + } + return "", nil + } + + return "", fmt.Errorf("failed to read kernel arch: %w", err) + } + + nativeArch := string(data) + nativeArch = strings.TrimRight(nativeArch, "\n") + + return string(data), nil +} diff --git a/providers/linux/arch_linux_test.go b/providers/linux/arch_linux_test.go new file mode 100644 index 0000000..be46203 --- /dev/null +++ b/providers/linux/arch_linux_test.go @@ -0,0 +1,36 @@ +// 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 linux + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestArchitecture(t *testing.T) { + a, err := Architecture() + assert.NoError(t, err) + assert.NotEmpty(t, a) +} + +func TestNativeArchitecture(t *testing.T) { + a, err := NativeArchitecture() + assert.NoError(t, err) + assert.NotEmpty(t, a) +} diff --git a/providers/linux/host_linux.go b/providers/linux/host_linux.go index c2bdeb4..2e0d604 100644 --- a/providers/linux/host_linux.go +++ b/providers/linux/host_linux.go @@ -156,6 +156,7 @@ func newHost(fs procFS) (*host, error) { h := &host{stat: stat, procFS: fs} r := &reader{} r.architecture(h) + r.nativeArchitecture(h) r.bootTime(h) r.containerized(h) r.hostname(h) @@ -197,6 +198,14 @@ func (r *reader) architecture(h *host) { h.info.Architecture = v } +func (r *reader) nativeArchitecture(h *host) { + v, err := NativeArchitecture() + if r.addErr(err) { + return + } + h.info.NativeArchitecture = v +} + func (r *reader) bootTime(h *host) { v, err := bootTime(h.procFS.FS) if r.addErr(err) { diff --git a/providers/windows/arch_windows.go b/providers/windows/arch_windows.go index 0edfc4d..fc4b9a9 100644 --- a/providers/windows/arch_windows.go +++ b/providers/windows/arch_windows.go @@ -18,14 +18,46 @@ package windows import ( - windows "github.com/elastic/go-windows" + "golang.org/x/sys/windows" + + gowindows "github.com/elastic/go-windows" +) + +const ( + imageFileMachineAmd64 = 0x8664 + imageFileMachineArm64 = 0xAA64 + archIntel = "x86_64" + archArm64 = "arm64" ) func Architecture() (string, error) { - systemInfo, err := windows.GetNativeSystemInfo() + systemInfo, err := gowindows.GetNativeSystemInfo() if err != nil { return "", err } return systemInfo.ProcessorArchitecture.String(), nil } + +func NativeArchitecture() (string, error) { + var processMachine, nativeMachine uint16 + // the pseudo handle doesn't need to be closed + var currentProcessHandle = windows.CurrentProcess() + + err := windows.IsWow64Process2(currentProcessHandle, &processMachine, &nativeMachine) + if err != nil { + return "", err + } + + var nativeArch string + + switch nativeMachine { + case imageFileMachineAmd64: + // for parity with Architecture() as amd64 and x86_64 are used interchangeably + nativeArch = archIntel + case imageFileMachineArm64: + nativeArch = archArm64 + } + + return nativeArch, nil +} diff --git a/providers/windows/arch_windows_test.go b/providers/windows/arch_windows_test.go new file mode 100644 index 0000000..1e60ac9 --- /dev/null +++ b/providers/windows/arch_windows_test.go @@ -0,0 +1,36 @@ +// 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 windows + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestArchitecture(t *testing.T) { + a, err := Architecture() + assert.NoError(t, err) + assert.NotEmpty(t, a) +} + +func TestNativeArchitecture(t *testing.T) { + a, err := NativeArchitecture() + assert.NoError(t, err) + assert.NotEmpty(t, a) +} diff --git a/providers/windows/host_windows.go b/providers/windows/host_windows.go index f7b527b..e476778 100644 --- a/providers/windows/host_windows.go +++ b/providers/windows/host_windows.go @@ -102,6 +102,7 @@ func newHost() (*host, error) { h := &host{} r := &reader{} r.architecture(h) + r.nativeArchitecture(h) r.bootTime(h) r.hostname(h) r.network(h) @@ -141,6 +142,14 @@ func (r *reader) architecture(h *host) { h.info.Architecture = v } +func (r *reader) nativeArchitecture(h *host) { + v, err := NativeArchitecture() + if r.addErr(err) { + return + } + h.info.NativeArchitecture = v +} + func (r *reader) bootTime(h *host) { v, err := BootTime() if r.addErr(err) { diff --git a/types/host.go b/types/host.go index 27ea488..9f3bc3b 100644 --- a/types/host.go +++ b/types/host.go @@ -73,17 +73,18 @@ type VMStat interface { // HostInfo contains basic host information. type HostInfo struct { - Architecture string `json:"architecture"` // Hardware architecture (e.g. x86_64, arm, ppc, mips). - BootTime time.Time `json:"boot_time"` // Host boot time. - Containerized *bool `json:"containerized,omitempty"` // Is the process containerized. - Hostname string `json:"name"` // Hostname, lowercased. - IPs []string `json:"ip,omitempty"` // List of all IPs. - KernelVersion string `json:"kernel_version"` // Kernel version. - MACs []string `json:"mac"` // List of MAC addresses. - OS *OSInfo `json:"os"` // OS information. - Timezone string `json:"timezone"` // System timezone. - TimezoneOffsetSec int `json:"timezone_offset_sec"` // Timezone offset (seconds from UTC). - UniqueID string `json:"id,omitempty"` // Unique ID of the host (optional). + Architecture string `json:"architecture"` // Process hardware architecture (e.g. x86_64, arm, ppc, mips). + NativeArchitecture string `json:"native_architecture"` // Native OS hardware architecture (e.g. x86_64, arm, ppc, mips). + BootTime time.Time `json:"boot_time"` // Host boot time. + Containerized *bool `json:"containerized,omitempty"` // Is the process containerized. + Hostname string `json:"name"` // Hostname, lowercased. + IPs []string `json:"ip,omitempty"` // List of all IPs. + KernelVersion string `json:"kernel_version"` // Kernel version. + MACs []string `json:"mac"` // List of MAC addresses. + OS *OSInfo `json:"os"` // OS information. + Timezone string `json:"timezone"` // System timezone. + TimezoneOffsetSec int `json:"timezone_offset_sec"` // Timezone offset (seconds from UTC). + UniqueID string `json:"id,omitempty"` // Unique ID of the host (optional). } // Uptime returns the system uptime