diff --git a/.github/workflows/source-check.yml b/.github/workflows/source-check.yml index 1d02ffc..276adf0 100644 --- a/.github/workflows/source-check.yml +++ b/.github/workflows/source-check.yml @@ -17,3 +17,4 @@ jobs: - run: python -m unittest discover -s tests -p 'test_hidden_riot_recovery.py' - run: python -m unittest discover -s tests -p 'test_desktop_run.py' - run: python -m unittest discover -s tests -p 'test_urlsession_runner.py' + - run: python -m unittest discover -s tests -p 'test_system_profiler.py' diff --git a/docs/PLATFORM_IDENTITY.md b/docs/PLATFORM_IDENTITY.md index 9e623db..3d132ea 100644 --- a/docs/PLATFORM_IDENTITY.md +++ b/docs/PLATFORM_IDENTITY.md @@ -134,3 +134,15 @@ a per-prefix lock. Repeated writes preserve identifiers and unchanged files. Previewing a fresh identity generates a sample; only `--write` makes it stable. To restore host-derived defaults after provisioning, remove both identity files while stopped, so a later provisioning run cannot reimport the legacy copy. + +## system_profiler + +The prefix's `/usr/sbin/system_profiler` (installed by +`tools/install-darling-system-profiler.py`) answers `SPHardwareDataType` and +`SPSoftwareDataType` from the running guest: model, serial number, platform UUID +and provisioning UDID from IOKit's `IOPlatformExpertDevice` (via `ioreg`), CPU, +cache and memory from `sysctl`, and the OS version from `sw_vers` and `uname`. It +keeps no identity of its own, so it reports the same values as IOKit, including any +override saved by this tool. A field the guest does not provide is left out. +`python3 -m unittest tests.test_system_profiler` checks the output with stub +commands. diff --git a/tests/test_system_profiler.py b/tests/test_system_profiler.py new file mode 100644 index 0000000..49d022d --- /dev/null +++ b/tests/test_system_profiler.py @@ -0,0 +1,97 @@ +"""system_profiler hardware/software overviews, with stub guest commands; never starts Darling.""" +import os +from pathlib import Path +import plistlib +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / 'tools/darling-system_profiler.sh' + +IOREG = '''+-o MacBookPro16,1 + { + "IOPlatformSerialNumber" = "USER-SUPPLIED" + "model" = <"MacBookPro16,1"> + "manufacturer" = <"Apple Inc."> + "IOPlatformUUID" = "8E1F2A3B-4C5D-4E6F-8A7B-9C0D1E2F3A4B" + "provisioning_UDID" = "8E1F2A3B-4C5D-4E6F-8A7B-9C0D1E2F3A4B" + "ROM" = <0a1b2c3d4e5f> + } +''' +SYSCTL = { + 'hw.physicalcpu': '8', 'hw.packages': '1', 'hw.memsize': str(32 << 30), + 'machdep.cpu.brand_string': 'AMD Ryzen 7 5800X 8-Core Processor & more', + 'hw.l2cachesize': str(512 << 10), 'hw.l3cachesize': str(32 << 20), 'hw.cpufrequency': '3800000000', +} + + +class SystemProfilerTests(unittest.TestCase): + def setUp(self): + self.folder = tempfile.TemporaryDirectory() + self.bin = Path(self.folder.name) + self.stub('sysctl', 'import sys\nvalues = %r\nname = sys.argv[-1]\n' + 'if name not in values: sys.exit(1)\nprint(values[name])' % SYSCTL) + self.stub('sw_vers', 'import sys\nprint({"-productName": "macOS", "-productVersion": "11.7.4", ' + '"-buildVersion": "20G1120"}[sys.argv[1]])') + self.stub('uname', 'print("Darwin 20.6.0")') + self.stub('hostname', 'print("darling")') + self.stub('ioreg', 'import sys\nsys.stdout.write(%r)' % IOREG) + + def tearDown(self): + self.folder.cleanup() + + def stub(self, name, body): + path = self.bin / name + path.write_text('#!%s\n%s\n' % (sys.executable, body)) + path.chmod(0o755) + + def run_profiler(self, *args): + env = {'PATH': '%s:%s' % (self.bin, os.environ['PATH']), 'USER': 'player'} + return subprocess.run(['sh', str(SCRIPT), *args], env=env, check=True, capture_output=True, text=True).stdout + + def xml_items(self, data_type): + report = plistlib.loads(self.run_profiler('-xml', data_type).encode()) + self.assertEqual(report[0]['_dataType'], data_type) + return report[0]['_items'][0] + + def test_hardware_reports_iokit_identity_and_sysctl_values(self): + item = self.xml_items('SPHardwareDataType') + self.assertEqual(item, { + '_name': 'hardware_overview', 'machine_name': 'MacBook Pro', 'machine_model': 'MacBookPro16,1', + 'cpu_type': 'AMD Ryzen 7 5800X 8-Core Processor & more', 'current_processor_speed': '3.8 GHz', + 'packages': 1, 'number_processors': 8, 'l2_cache_core': '512 KB', 'l3_cache': '32 MB', + 'physical_memory': '32 GB', 'serial_number': 'USER-SUPPLIED', + 'platform_UUID': '8E1F2A3B-4C5D-4E6F-8A7B-9C0D1E2F3A4B', + 'provisioning_UDID': '8E1F2A3B-4C5D-4E6F-8A7B-9C0D1E2F3A4B', + }) + text = self.run_profiler('SPHardwareDataType') + self.assertIn(' Serial Number (system): USER-SUPPLIED\n', text) + self.assertIn(' Model Name: MacBook Pro\n', text) + + def test_missing_iokit_and_sysctl_values_are_omitted(self): + self.stub('ioreg', 'import sys\nsys.exit(1)') + self.stub('sysctl', 'import sys\nsys.exit(1)') + self.assertEqual(self.xml_items('SPHardwareDataType'), {'_name': 'hardware_overview'}) + self.assertNotIn('Serial Number', self.run_profiler('SPHardwareDataType')) + + def test_software_overview(self): + item = self.xml_items('SPSoftwareDataType') + self.assertEqual(item, { + '_name': 'os_overview', 'os_version': 'macOS 11.7.4 (20G1120)', 'kernel_version': 'Darwin 20.6.0', + 'boot_volume': 'Macintosh HD', 'boot_mode': 'normal_boot', 'local_host_name': 'darling', + 'user_name': 'player', + }) + self.assertIn(' Boot Mode: Normal\n', self.run_profiler('SPSoftwareDataType')) + + def test_listed_and_default_types(self): + listed = self.run_profiler('-listDataTypes') + self.assertIn('SPHardwareDataType', listed) + self.assertIn('SPSoftwareDataType', listed) + types = [entry['_dataType'] for entry in plistlib.loads(self.run_profiler('-xml').encode())] + self.assertEqual(types[:2], ['SPHardwareDataType', 'SPSoftwareDataType']) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/darling-system_profiler.sh b/tools/darling-system_profiler.sh index 3410740..540b101 100644 --- a/tools/darling-system_profiler.sh +++ b/tools/darling-system_profiler.sh @@ -1,6 +1,8 @@ #!/bin/sh # system_profiler for the Darling prefix: reports the host's real display and -# network hardware for the data types applications query at startup. +# network hardware for the data types applications query at startup. The hardware +# and software overviews come from the guest's own IOKit, sysctl and sw_vers, so they +# match what other APIs report (including a configure-darling-platform.py override). # Installed as /usr/sbin/system_profiler by tools/install-darling-system-profiler.py. host=/Volumes/SystemRoot xml=0 @@ -9,12 +11,12 @@ while [ $# -gt 0 ]; do case "$1" in -xml) xml=1;; -detailLevel|-timeout) shift;; - -listDataTypes) printf 'Available Datatypes:\nSPAirPortDataType\nSPDisplaysDataType\nSPEthernetDataType\n'; exit 0;; + -listDataTypes) printf 'Available Datatypes:\nSPAirPortDataType\nSPDisplaysDataType\nSPEthernetDataType\nSPHardwareDataType\nSPSoftwareDataType\n'; exit 0;; SP*DataType) types="$types $1";; esac shift done -[ -n "$types" ] || types=" SPDisplaysDataType SPEthernetDataType SPAirPortDataType" +[ -n "$types" ] || types=" SPHardwareDataType SPSoftwareDataType SPDisplaysDataType SPEthernetDataType SPAirPortDataType" escape() { sed -e 's/&/\&/g' -e 's//\>/g'; } @@ -46,6 +48,80 @@ displays() { # one line per GPU: vendor|device|vram MB or empty|name done } +load_platform() { # IOPlatformExpertDevice string properties, "key|value" per line + # String values print as "value", NUL-terminated data as <"value">; other data is skipped. + platform_properties=$(ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null | + sed -n 's/^[ |]*"\([^"]*\)" = <\{0,1\}"\(.*\)">\{0,1\}$/\1|\2/p') +} +platform() { + printf '%s\n' "$platform_properties" | awk -v key="$1" 'index($0, key "|") == 1 { print substr($0, length(key) + 2); exit }' +} + +machine_name() { # model identifier -> marketing family, as in About This Mac + case "$1" in + MacBookPro*) echo 'MacBook Pro';; MacBookAir*) echo 'MacBook Air';; MacBook*) echo 'MacBook';; + iMacPro*) echo 'iMac Pro';; iMac*) echo 'iMac';; Macmini*) echo 'Mac mini';; MacPro*) echo 'Mac Pro';; + *) echo 'Mac';; + esac +} + +hardware() { # label|xml key|xml type|value for each known field + load_platform + model=$(platform model) + cores=$(sysctl -n hw.physicalcpu 2>/dev/null) + memory=$(sysctl -n hw.memsize 2>/dev/null | awk '$1 > 0 { printf "%d GB", $1 / 1073741824 + 0.5 }') + speed=$(sysctl -n hw.cpufrequency 2>/dev/null | awk '$1 > 0 { printf "%.1f GHz", $1 / 1e9 }') + l2=$(sysctl -n hw.l2cachesize 2>/dev/null | awk '$1 > 0 { printf "%d KB", $1 / 1024 }') + l3=$(sysctl -n hw.l3cachesize 2>/dev/null | awk '$1 > 0 { printf "%d MB", $1 / 1048576 }') + echo "Model Name|machine_name|string|${model:+$(machine_name "$model")}" + echo "Model Identifier|machine_model|string|$model" + echo "Processor Name|cpu_type|string|$(sysctl -n machdep.cpu.brand_string 2>/dev/null)" + echo "Processor Speed|current_processor_speed|string|$speed" + echo "Number of Processors|packages|integer|$(sysctl -n hw.packages 2>/dev/null)" + echo "Total Number of Cores|number_processors|integer|$cores" + echo "L2 Cache (per Core)|l2_cache_core|string|$l2" + echo "L3 Cache|l3_cache|string|$l3" + echo "Memory|physical_memory|string|$memory" + echo "Serial Number (system)|serial_number|string|$(platform IOPlatformSerialNumber)" + echo "Hardware UUID|platform_UUID|string|$(platform IOPlatformUUID)" + echo "Provisioning UDID|provisioning_UDID|string|$(platform provisioning_UDID)" +} + +software() { + name=$(sw_vers -productName 2>/dev/null); version=$(sw_vers -productVersion 2>/dev/null) + build=$(sw_vers -buildVersion 2>/dev/null) + echo "System Version|os_version|string|${version:+${name:-macOS} $version${build:+ ($build)}}" + echo "Kernel Version|kernel_version|string|$(uname -sr 2>/dev/null)" + echo "Boot Volume|boot_volume|string|Macintosh HD" + echo "Boot Mode|boot_mode|string|normal_boot" + echo "Computer Name|local_host_name|string|$(hostname -s 2>/dev/null)" + echo "User Name|user_name|string|${USER:-$(id -un 2>/dev/null)}" +} + +overview_xml() { # $1 = _name; fields on stdin; empty values are omitted + echo ' ' + echo " _name$1" + while IFS='|' read -r label key kind value; do + [ -n "$value" ] || continue + if [ "$kind" = integer ]; then + case "$value" in *[!0-9]*) continue;; esac + echo " $key$value" + else + echo " $key$(printf '%s' "$value" | escape)" + fi + done + echo ' ' +} + +overview_text() { # $1 = section title, $2 = overview title; fields on stdin + printf '%s:\n\n %s:\n\n' "$1" "$2" + while IFS='|' read -r label key kind value; do + [ "$key" != boot_mode ] || value=Normal + [ -z "$value" ] || printf ' %s: %s\n' "$label" "$value" + done + echo +} + interfaces() { # $1 = wired|wireless; physical host interfaces only for path in "$host"/sys/class/net/*; do [ -e "$path/device" ] || continue @@ -65,6 +141,8 @@ if [ "$xml" = 1 ]; then echo ' _items' echo ' ' case "$type" in + SPHardwareDataType) hardware | overview_xml hardware_overview;; + SPSoftwareDataType) software | overview_xml os_overview;; SPDisplaysDataType) displays | while IFS='|' read -r vendor device vram name; do name=$(printf '%s' "$name" | escape) @@ -108,6 +186,8 @@ fi for type in $types; do case "$type" in + SPHardwareDataType) hardware | overview_text Hardware 'Hardware Overview';; + SPSoftwareDataType) software | overview_text Software 'System Software Overview';; SPDisplaysDataType) printf 'Graphics/Displays:\n\n' displays | while IFS='|' read -r vendor device vram name; do