diff options
Diffstat (limited to 'src/detection')
444 files changed, 50053 insertions, 0 deletions
diff --git a/src/detection/battery/battery.h b/src/detection/battery/battery.h new file mode 100644 index 0000000..1a3b4e9 --- /dev/null +++ b/src/detection/battery/battery.h @@ -0,0 +1,32 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/battery/option.h" + +#define FF_BATTERY_TEMP_UNSET (-DBL_MAX) + +typedef enum FFBatteryStatus { + FF_BATTERY_STATUS_NONE = 0, + FF_BATTERY_STATUS_UNKNOWN = 1 << 0, + FF_BATTERY_STATUS_AC_CONNECTED = 1 << 1, + FF_BATTERY_STATUS_USB_CONNECTED = 1 << 2, + FF_BATTERY_STATUS_WIRELESS_CONNECTED = 1 << 3, + FF_BATTERY_STATUS_CHARGING = 1 << 4, + FF_BATTERY_STATUS_DISCHARGING = 1 << 5, + FF_BATTERY_STATUS_CRITICAL = 1 << 6, +} FFBatteryStatus; + +typedef struct FFBatteryResult { + FFstrbuf manufacturer; + FFstrbuf manufactureDate; + FFstrbuf modelName; + FFstrbuf technology; + FFstrbuf serial; + FFBatteryStatus status; + double capacity; + double temperature; + uint32_t cycleCount; + int32_t timeRemaining; // in seconds, -1 if unknown +} FFBatteryResult; + +const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results); diff --git a/src/detection/battery/battery_android.c b/src/detection/battery/battery_android.c new file mode 100644 index 0000000..d76c4ad --- /dev/null +++ b/src/detection/battery/battery_android.c @@ -0,0 +1,160 @@ +#include "fastfetch.h" +#include "battery.h" +#include "common/strutil.h" +#include "common/processing.h" +#include "common/properties.h" + +#define FF_TERMUX_API_PATH FASTFETCH_TARGET_DIR_ROOT "/libexec/termux-api" +#define FF_TERMUX_API_PARAM "BatteryStatus" + +static inline void wrapYyjsonFree(yyjson_doc** doc) { + assert(doc); + if (*doc) { + yyjson_doc_free(*doc); + } +} + +static const char* parseTermuxApi(FFBatteryOptions* options, FFlist* results) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + if (ffProcessAppendStdOut(&buffer, (char* const[]) { FF_TERMUX_API_PATH, FF_TERMUX_API_PARAM, NULL })) { + return "Starting `" FF_TERMUX_API_PATH " " FF_TERMUX_API_PARAM "` failed"; + } + + yyjson_doc* FF_A_CLEANUP(wrapYyjsonFree) doc = yyjson_read_opts(buffer.chars, buffer.length, 0, NULL, NULL); + if (!doc) { + return "Failed to parse battery info"; + } + + yyjson_val* root = yyjson_doc_get_root(doc); + if (!yyjson_is_obj(root)) { + return "Battery info result is not a JSON object"; + } + + FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); + battery->temperature = FF_BATTERY_TEMP_UNSET; + battery->cycleCount = 0; + battery->timeRemaining = -1; + battery->status = FF_BATTERY_STATUS_NONE; + ffStrbufInit(&battery->manufacturer); + ffStrbufInit(&battery->modelName); + ffStrbufInit(&battery->technology); + ffStrbufInit(&battery->serial); + ffStrbufInit(&battery->manufactureDate); + + battery->capacity = yyjson_get_num(yyjson_obj_get(root, "percentage")); + const char* acStatus = yyjson_get_str(yyjson_obj_get(root, "plugged")); + if (acStatus) { + if (ffStrEquals(acStatus, "PLUGGED_AC")) { + battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; + } else if (ffStrEquals(acStatus, "PLUGGED_USB")) { + battery->status |= FF_BATTERY_STATUS_USB_CONNECTED; + } else if (ffStrEquals(acStatus, "PLUGGED_WIRELESS")) { + battery->status |= FF_BATTERY_STATUS_WIRELESS_CONNECTED; + } + } + const char* status = yyjson_get_str(yyjson_obj_get(root, "status")); + if (status) { + if (ffStrEquals(status, "CHARGING")) { + battery->status |= FF_BATTERY_STATUS_CHARGING; + } else if (ffStrEquals(status, "DISCHARGING")) { + battery->status |= FF_BATTERY_STATUS_DISCHARGING; + } + } + + if (options->temp) { + battery->temperature = yyjson_get_num(yyjson_obj_get(root, "temperature")); + } + + return NULL; +} + +static const char* parseDumpsys(FFBatteryOptions* options, FFlist* results) { + FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); + if (ffProcessAppendStdOut(&buf, (char*[]) { + "/system/bin/dumpsys", + "battery", + NULL, + }) != NULL || + buf.length == 0) { + return "Executing `/system/bin/dumpsys battery` failed"; // Only works in `adb shell`, or when rooted + } + + if (!ffStrbufStartsWithS(&buf, "Current Battery Service state:\n")) { + return "Invalid `/system/bin/dumpsys battery` result"; + } + + const char* start = buf.chars + strlen("Current Battery Service state:\n"); + + FF_STRBUF_AUTO_DESTROY temp = ffStrbufCreate(); + if (!ffParsePropLines(start, "present: ", &temp) || !ffStrbufEqualS(&temp, "true")) { + return NULL; + } + ffStrbufClear(&temp); + + FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); + battery->temperature = FF_BATTERY_TEMP_UNSET; + battery->cycleCount = 0; + battery->timeRemaining = -1; + battery->capacity = 0; + battery->status = FF_BATTERY_STATUS_NONE; + ffStrbufInit(&battery->manufacturer); + ffStrbufInit(&battery->modelName); + ffStrbufInit(&battery->technology); + ffStrbufInit(&battery->serial); + ffStrbufInit(&battery->manufactureDate); + + if (ffParsePropLines(start, "AC powered: ", &temp) && ffStrbufEqualS(&temp, "true")) { + battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; + } + ffStrbufClear(&temp); + + if (ffParsePropLines(start, "USB powered: ", &temp) && ffStrbufEqualS(&temp, "true")) { + battery->status |= FF_BATTERY_STATUS_USB_CONNECTED; + } + ffStrbufClear(&temp); + + if (ffParsePropLines(start, "Wireless powered: ", &temp) && ffStrbufEqualS(&temp, "true")) { + battery->status |= FF_BATTERY_STATUS_WIRELESS_CONNECTED; + } + ffStrbufClear(&temp); + + { + double level = 0, scale = 0; + if (ffParsePropLines(start, "level: ", &temp)) { + level = ffStrbufToDouble(&temp, -DBL_MAX); + } + ffStrbufClear(&temp); + + if (ffParsePropLines(start, "scale: ", &temp)) { + scale = ffStrbufToDouble(&temp, -DBL_MAX); + } + ffStrbufClear(&temp); + + if (level > 0 && scale > 0) { + battery->capacity = level * 100 / scale; + } + } + + if (options->temp) { + if (ffParsePropLines(start, "temperature: ", &temp)) { + battery->temperature = ffStrbufToDouble(&temp, FF_BATTERY_TEMP_UNSET); + if (battery->temperature != FF_BATTERY_TEMP_UNSET) { + battery->temperature /= 10.0; // Android returns temperature in tenths of a degree + } + } + ffStrbufClear(&temp); + } + + ffParsePropLines(start, "technology: ", &battery->technology); + + return NULL; +} + +const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) { + const char* error = parseTermuxApi(options, results); + if (error && parseDumpsys(options, results) == NULL) { + return NULL; + } + return error; +} diff --git a/src/detection/battery/battery_apple.c b/src/detection/battery/battery_apple.c new file mode 100644 index 0000000..c22aa6d --- /dev/null +++ b/src/detection/battery/battery_apple.c @@ -0,0 +1,117 @@ +#include "fastfetch.h" +#include "battery.h" +#include "common/apple/cf_helpers.h" +#include "common/apple/smc_temps.h" + +#include <IOKit/IOKitLib.h> +#include <IOKit/pwr_mgt/IOPM.h> + +const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) { + FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL; + if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching("AppleSmartBattery"), &iterator) != kIOReturnSuccess) { + return "IOServiceGetMatchingServices() failed"; + } + + io_registry_entry_t registryEntry; + while ((registryEntry = IOIteratorNext(iterator)) != IO_OBJECT_NULL) { + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryBattery = registryEntry; + FF_CFTYPE_AUTO_RELEASE CFMutableDictionaryRef properties = NULL; + if (IORegistryEntryCreateCFProperties(entryBattery, &properties, kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess) { + continue; + } + + int currentCapacity, maxCapacity; + + if (ffCfDictGetInt(properties, CFSTR(kIOPMPSMaxCapacityKey), &maxCapacity) != NULL || maxCapacity <= 0) { + continue; + } + + if (ffCfDictGetInt(properties, CFSTR(kIOPMPSCurrentCapacityKey), ¤tCapacity) != NULL || currentCapacity <= 0) { + continue; + } + + bool boolValue; + + FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); + battery->temperature = FF_BATTERY_TEMP_UNSET; + ffStrbufInit(&battery->manufacturer); + ffStrbufInit(&battery->modelName); + ffStrbufInit(&battery->serial); + ffStrbufInit(&battery->technology); + ffStrbufInit(&battery->manufactureDate); + battery->status = FF_BATTERY_STATUS_NONE; + battery->capacity = currentCapacity * 100.0 / maxCapacity; + battery->cycleCount = 0; + battery->timeRemaining = -1; + + ffCfDictGetString(properties, CFSTR(kIOPMDeviceNameKey), &battery->modelName); + ffCfDictGetString(properties, CFSTR(kIOPMPSSerialKey), &battery->serial); + ffCfDictGetString(properties, CFSTR(kIOPMPSManufacturerKey), &battery->manufacturer); + + if (!ffCfDictGetBool(properties, CFSTR("built-in"), &boolValue) && boolValue) { + if (!battery->manufacturer.length) { + ffStrbufAppendS(&battery->manufacturer, "Apple Inc."); + } + ffStrbufAppendS(&battery->technology, "Lithium"); + if (!battery->modelName.length) { + ffStrbufAppendS(&battery->modelName, "Built-in"); + } + } + + int32_t cycleCount = 0; + ffCfDictGetInt(properties, CFSTR(kIOPMPSCycleCountKey), &cycleCount); + battery->cycleCount = cycleCount < 0 ? 0 : (uint32_t) cycleCount; + + battery->timeRemaining = -1; + if (ffCfDictGetBool(properties, CFSTR(kIOPMPSExternalConnectedKey), &boolValue) == NULL) { + if (boolValue) { + battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; + } else { + battery->status |= FF_BATTERY_STATUS_DISCHARGING; + ffCfDictGetInt(properties, CFSTR("AvgTimeToEmpty"), &battery->timeRemaining); // in minutes + if (battery->timeRemaining < 0 || battery->timeRemaining >= 0xFFFF) { + battery->timeRemaining = -1; + } else { + battery->timeRemaining *= 60; + } + } + } + if (ffCfDictGetBool(properties, CFSTR(kIOPMPSIsChargingKey), &boolValue) == NULL && boolValue) { + battery->status |= FF_BATTERY_STATUS_CHARGING; + } + if (ffCfDictGetBool(properties, CFSTR(kIOPMPSAtCriticalLevelKey), &boolValue) == NULL && boolValue) { + battery->status |= FF_BATTERY_STATUS_CRITICAL; + } + + int sbdsManufactureDate = 0; + if (ffCfDictGetInt(properties, CFSTR(kIOPMPSManufactureDateKey), &sbdsManufactureDate) == NULL) { + int day = sbdsManufactureDate & 0b11111; + int month = (sbdsManufactureDate >> 5) & 0b1111; + int year = (sbdsManufactureDate >> 9) + 1800; + ffStrbufSetF(&battery->manufactureDate, "%.4d-%.2d-%.2d", year, month, day); + } else { + CFDictionaryRef batteryData; + if (ffCfDictGetDict(properties, CFSTR("BatteryData"), &batteryData) == NULL) { + char manufactureDate[sizeof(uint64_t)]; + if (ffCfDictGetInt64(batteryData, CFSTR(kIOPMPSManufactureDateKey), (int64_t*) manufactureDate) == NULL) { + // https://github.com/AsahiLinux/linux/blob/b5c05cbffb0488c7618106926d522cc3b43d93d5/drivers/power/supply/macsmc_power.c#L410-L419 + int year = (manufactureDate[0] - '0') * 10 + (manufactureDate[1] - '0') + 2000 - 8; + int month = (manufactureDate[2] - '0') * 10 + (manufactureDate[3] - '0'); + int day = (manufactureDate[4] - '0') * 10 + (manufactureDate[3] - '0'); + ffStrbufSetF(&battery->manufactureDate, "%.4d-%.2d-%.2d", year, month, day); + } + } + } + + if (options->temp) { + int64_t temp; + if (!ffCfDictGetInt64(properties, CFSTR(kIOPMPSBatteryTemperatureKey), &temp)) { + battery->temperature = (double) temp / 10 - 273.15; + } else { + ffDetectSmcTemps(FF_TEMP_BATTERY, &battery->temperature); + } + } + } + + return NULL; +} diff --git a/src/detection/battery/battery_bsd.c b/src/detection/battery/battery_bsd.c new file mode 100644 index 0000000..336a17e --- /dev/null +++ b/src/detection/battery/battery_bsd.c @@ -0,0 +1,90 @@ +#include "fastfetch.h" +#include "common/sysctl.h" +#include "common/io.h" +#include "battery.h" + +#include <dev/acpica/acpiio.h> +#include <sys/ioctl.h> +#include <sys/fcntl.h> +#include <unistd.h> + +const char* ffDetectBattery(FF_A_UNUSED FFBatteryOptions* options, FFlist* results) { + // https://www.freebsd.org/cgi/man.cgi?acpi_battery(4) + // https://gitlab.xfce.org/panel-plugins/xfce4-battery-plugin/-/blob/master/panel-plugin/libacpi.c + + int units = ffSysctlGetInt("hw.acpi.battery.units", -100); + if (units < 0) { + return "sysctlbyname(\"hw.acpi.battery.units\") failed"; + } + + if (units == 0) { + return NULL; + } + + FF_AUTO_CLOSE_FD int acpifd = open("/dev/acpi", O_RDONLY | O_CLOEXEC); + if (acpifd < 0) { + return "open(\"/dev/acpi\", O_RDONLY | O_CLOEXEC) failed"; + } + + for (int i = 0; i < units; ++i) { + union acpi_battery_ioctl_arg battio; + battio.unit = i; + + if (ioctl(acpifd, ACPIIO_BATT_GET_BATTINFO, &battio) < 0 || (battio.battinfo.state == ACPI_BATT_STAT_NOT_PRESENT)) { + continue; + } + + FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); + battery->temperature = FF_BATTERY_TEMP_UNSET; + battery->cycleCount = 0; + ffStrbufInit(&battery->manufacturer); + ffStrbufInit(&battery->modelName); + ffStrbufInit(&battery->technology); + ffStrbufInit(&battery->serial); + ffStrbufInit(&battery->manufactureDate); + battery->status = FF_BATTERY_STATUS_NONE; + battery->timeRemaining = -1; + if (battio.battinfo.min > 0) { + battery->timeRemaining = battio.battinfo.min * 60; + } + battery->capacity = battio.battinfo.cap; + if (battio.battinfo.state == ACPI_BATT_STAT_INVALID) { + battery->status |= FF_BATTERY_STATUS_UNKNOWN; + } else { + if (battio.battinfo.state & ACPI_BATT_STAT_DISCHARG) { + battery->status |= FF_BATTERY_STATUS_DISCHARGING; + } + if (battio.battinfo.state & ACPI_BATT_STAT_CHARGING) { + battery->status |= FF_BATTERY_STATUS_CHARGING; + } + if (battio.battinfo.state & ACPI_BATT_STAT_CRITICAL) { + battery->status |= FF_BATTERY_STATUS_CRITICAL; + } + } + + int acadStatus; + if (ioctl(acpifd, ACPIIO_ACAD_GET_STATUS, &acadStatus) >= 0 && acadStatus) { + battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; + } + +#ifdef ACPIIO_BATT_GET_BIX + battio.unit = i; + if (ioctl(acpifd, ACPIIO_BATT_GET_BIX, &battio) >= 0) { + ffStrbufAppendS(&battery->manufacturer, battio.bix.oeminfo); + ffStrbufAppendS(&battery->modelName, battio.bix.model); + ffStrbufAppendS(&battery->technology, battio.bix.type); + ffStrbufAppendS(&battery->serial, battio.bix.serial); + battery->cycleCount = battio.bix.cycles; + } +#elif defined(ACPIIO_BATT_GET_BIF) + battio.unit = i; + if (ioctl(acpifd, ACPIIO_BATT_GET_BIF, &battio) >= 0) { + ffStrbufAppendS(&battery->manufacturer, battio.bif.oeminfo); + ffStrbufAppendS(&battery->modelName, battio.bif.model); + ffStrbufAppendS(&battery->technology, battio.bif.type); + ffStrbufAppendS(&battery->serial, battio.bif.serial); + } +#endif + } + return NULL; +} diff --git a/src/detection/battery/battery_haiku.c b/src/detection/battery/battery_haiku.c new file mode 100644 index 0000000..aa620e6 --- /dev/null +++ b/src/detection/battery/battery_haiku.c @@ -0,0 +1,71 @@ +#include "fastfetch.h" +#include "battery.h" +#include "common/io.h" + +#include <private/device/power_managment.h> +#include <sys/ioctl.h> +#include <fcntl.h> + +const char* parseBattery(int dfd, const char* battId, FFlist* results) { + FF_AUTO_CLOSE_FD int fd = openat(dfd, battId, O_RDWR); + if (fd < 0) { + return "openat() failed"; + } + + acpi_battery_info basic = {}; + if (ioctl(fd, GET_BATTERY_INFO, &basic, sizeof(basic)) != 0) { + return "ioctl(GET_BATTERY_INFO) failed"; + } + acpi_extended_battery_info extended = {}; + if (ioctl(fd, GET_EXTENDED_BATTERY_INFO, &extended, sizeof(extended)) != 0) { + return "ioctl(GET_EXTENDED_BATTERY_INFO) failed"; + } + + if (extended.last_full_charge == (uint32) -1) { + return "Skipped"; + } + + FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); + ffStrbufInitS(&battery->modelName, extended.model_number); + ffStrbufInitS(&battery->manufacturer, extended.oem_info); + ffStrbufInit(&battery->manufactureDate); + ffStrbufInitS(&battery->technology, extended.type); // extended.technology? + ffStrbufInitS(&battery->serial, extended.serial_number); + battery->status = FF_BATTERY_STATUS_NONE; + battery->temperature = FF_BATTERY_TEMP_UNSET; + battery->cycleCount = extended.cycles; + battery->timeRemaining = -1; + battery->capacity = (double) basic.capacity * 100. / (double) extended.last_full_charge; + + if (basic.state & BATTERY_DISCHARGING) { + battery->status |= FF_BATTERY_STATUS_DISCHARGING; + } + if (basic.state & BATTERY_CHARGING) { + battery->status |= FF_BATTERY_STATUS_CHARGING; + } + if (basic.state & BATTERY_CRITICAL_STATE) { + battery->status |= FF_BATTERY_STATUS_CRITICAL; + } + if (basic.state & BATTERY_NOT_CHARGING || basic.state & BATTERY_CHARGING) { + battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; + } + + return NULL; +} + +const char* ffDetectBattery(FF_A_UNUSED FFBatteryOptions* options, FFlist* results) { + FF_AUTO_CLOSE_DIR DIR* dir = opendir("/dev/power/acpi_battery/"); + if (!dir) { + return "opendir(/dev/power/acpi_battery) failed"; + } + + struct dirent* entry; + while ((entry = readdir(dir))) { + if (entry->d_name[0] == '.') { + continue; + } + parseBattery(dirfd(dir), entry->d_name, results); + } + + return NULL; +} diff --git a/src/detection/battery/battery_linux.c b/src/detection/battery/battery_linux.c new file mode 100644 index 0000000..6c1f711 --- /dev/null +++ b/src/detection/battery/battery_linux.c @@ -0,0 +1,203 @@ +#include "battery.h" +#include "common/io.h" +#include "common/strutil.h" +#include "common/debug.h" + +#include <dirent.h> +#include <unistd.h> +#include <fcntl.h> + +// https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-power + +static bool parseBattery(int dfd, const char* id, FFBatteryOptions* options, FFlist* results, bool* acConnected) { + FF_STRBUF_AUTO_DESTROY tmpBuffer = ffStrbufCreate(); + + { + char present = '\0'; + if (ffReadFileDataRelative(dfd, "present", 1, &present) && present == '0') { + FF_DEBUG("Battery \"%s\": Not present", id); + return false; + } + } + + // type must exist + if (!ffReadFileBufferRelative(dfd, "type", &tmpBuffer)) { + FF_DEBUG("Battery \"%s\": No type file", id); + return false; + } + ffStrbufTrimRightSpace(&tmpBuffer); + if (ffStrbufEqualS(&tmpBuffer, "Mains")) { + if (*acConnected) { + FF_DEBUG("Battery \"%s\": Type is Mains, but AC is already connected", id); + return false; + } + + char online = '\0'; + if (ffReadFileDataRelative(dfd, "online", 1, &online) == 1 && online == '1') { + *acConnected = true; + } + FF_DEBUG("Battery \"%s\": Type is Mains, AC Connected: %s", id, *acConnected ? "Yes" : "No"); + return false; + } else if (!ffStrbufEqualS(&tmpBuffer, "Battery")) { + FF_DEBUG("Battery \"%s\": Type is not Battery or Mains, but \"%s\"", id, tmpBuffer.chars); + return false; + } + + // scope may not exist or must not be "Device" + if (ffReadFileBufferRelative(dfd, "scope", &tmpBuffer)) { + ffStrbufTrimRightSpace(&tmpBuffer); + + FF_DEBUG("Battery \"%s\": Scope is \"%s\"", id, tmpBuffer.chars); + if (ffStrbufEqualS(&tmpBuffer, "Device")) { + return false; + } + } + + // `capacity` must exist + // This is expensive in my laptop + if (!ffReadFileBufferRelative(dfd, "capacity", &tmpBuffer)) { + FF_DEBUG("Battery \"%s\": No capacity file", id); + return false; + } + + FFBatteryResult* result = FF_LIST_ADD(FFBatteryResult, *results); + ffStrbufInit(&result->manufacturer); + ffStrbufInit(&result->modelName); + ffStrbufInit(&result->technology); + ffStrbufInit(&result->serial); + ffStrbufInit(&result->manufactureDate); + result->status = FF_BATTERY_STATUS_NONE; + result->capacity = ffStrbufToDouble(&tmpBuffer, 0); + result->cycleCount = 0; + result->temperature = FF_BATTERY_TEMP_UNSET; + result->timeRemaining = -1; + + // At this point, we have a battery. Try to get as much values as possible. + + if (ffReadFileBufferRelative(dfd, "manufacturer", &result->manufacturer)) { + ffStrbufTrimRightSpace(&result->manufacturer); + } else if (ffStrEquals(id, "macsmc-battery")) { // asahi + ffStrbufSetStatic(&result->manufacturer, "Apple Inc."); + } + + if (ffReadFileBufferRelative(dfd, "model_name", &result->modelName)) { + ffStrbufTrimRightSpace(&result->modelName); + } + + if (ffReadFileBufferRelative(dfd, "technology", &result->technology)) { + ffStrbufTrimRightSpace(&result->technology); + } + + if (ffReadFileBufferRelative(dfd, "status", &tmpBuffer)) { + ffStrbufTrimRightSpace(&tmpBuffer); + } + + // Unknown, Charging, Discharging, Not charging, Full + + if (ffStrbufEqualS(&tmpBuffer, "Discharging")) { + result->status |= FF_BATTERY_STATUS_DISCHARGING; + FF_STRBUF_AUTO_DESTROY now = ffStrbufCreate(); + if (ffReadFileBufferRelative(dfd, "time_to_empty_now", &now)) { + result->timeRemaining = (int32_t) ffStrbufToSInt(&now, 0); + } else { + if (ffReadFileBufferRelative(dfd, "charge_now", &now)) { + int64_t chargeNow = ffStrbufToSInt(&now, 0); + if (chargeNow > 0) { + if (ffReadFileBufferRelative(dfd, "current_now", &now)) { + int64_t currentNow = ffStrbufToSInt(&now, INT64_MIN); + if (currentNow < 0) { + currentNow = -currentNow; + } + if (currentNow > 0) { + result->timeRemaining = (int32_t) ((chargeNow * 3600) / currentNow); + } + } + } + } + } + } else if (ffStrbufEqualS(&tmpBuffer, "Charging")) { + result->status |= FF_BATTERY_STATUS_CHARGING; + } else if (ffStrbufEqualS(&tmpBuffer, "Unknown")) { + result->status |= FF_BATTERY_STATUS_UNKNOWN; + } + + if (ffReadFileBufferRelative(dfd, "capacity_level", &tmpBuffer)) { + ffStrbufTrimRightSpace(&tmpBuffer); + if (ffStrbufEqualS(&tmpBuffer, "Critical")) { + result->status |= FF_BATTERY_STATUS_CRITICAL; + } + } + + if (ffReadFileBufferRelative(dfd, "serial_number", &result->serial)) { + ffStrbufTrimRightSpace(&result->serial); + } + + if (ffReadFileBufferRelative(dfd, "cycle_count", &tmpBuffer)) { + int64_t cycleCount = ffStrbufToSInt(&tmpBuffer, 0); + result->cycleCount = cycleCount < 0 || cycleCount > UINT32_MAX ? 0 : (uint32_t) cycleCount; + } + + if (ffReadFileBufferRelative(dfd, "manufacture_year", &tmpBuffer)) { + int year = (int) ffStrbufToSInt(&tmpBuffer, 0); + if (year > 0) { + if (ffReadFileBufferRelative(dfd, "manufacture_month", &tmpBuffer)) { + int month = (int) ffStrbufToSInt(&tmpBuffer, 0); + if (month > 0) { + if (ffReadFileBufferRelative(dfd, "manufacture_day", &tmpBuffer)) { + int day = (int) ffStrbufToSInt(&tmpBuffer, 0); + if (day > 0) { + ffStrbufSetF(&result->manufactureDate, "%.4d-%.2d-%.2d", year, month, day); + } + } + } + } + } + } + + if (options->temp) { + if (ffReadFileBufferRelative(dfd, "temp", &tmpBuffer)) { + result->temperature = ffStrbufToDouble(&tmpBuffer, FF_BATTERY_TEMP_UNSET); + if (result->temperature != FF_BATTERY_TEMP_UNSET) { + result->temperature /= 10; + } + } + } + + FF_DEBUG("Battery \"%s\": Capacity: %.2f%%, Status: \"%x\", Time Remaining: %d seconds, Temperature: %.1f°C, Cycle Count: %u", + id, + result->capacity, + result->status, + result->timeRemaining, + result->temperature, + result->cycleCount); + return true; +} + +const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/power_supply/"); + if (dirp == NULL) { + return "opendir(\"/sys/class/power_supply/\") == NULL"; + } + + bool acConnected = false; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + FF_AUTO_CLOSE_FD int dfd = openat(dirfd(dirp), entry->d_name, O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY); + if (dfd >= 0) { + parseBattery(dfd, entry->d_name, options, results, &acConnected); + } + } + + if (acConnected) { + FF_LIST_FOR_EACH (FFBatteryResult, batt, *results) { + batt->status |= FF_BATTERY_STATUS_AC_CONNECTED; + } + } + + return NULL; +} diff --git a/src/detection/battery/battery_nbsd.c b/src/detection/battery/battery_nbsd.c new file mode 100644 index 0000000..2e8b426 --- /dev/null +++ b/src/detection/battery/battery_nbsd.c @@ -0,0 +1,109 @@ +#include "battery.h" +#include "common/io.h" +#include "common/FFstrbuf.h" +#include "common/strutil.h" + +#include <prop/prop_array.h> +#include <prop/prop_bool.h> +#include <prop/prop_dictionary.h> +#include <prop/prop_object.h> +#include <sys/envsys.h> +#include <prop/proplib.h> +#include <paths.h> +#include <time.h> +#include <unistd.h> +#include <fcntl.h> + +const char* ffDetectBattery(FF_A_UNUSED FFBatteryOptions* options, FFlist* results) { + FF_AUTO_CLOSE_FD int fd = open(_PATH_SYSMON, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return "open(_PATH_SYSMON, O_RDONLY | O_CLOEXEC) failed"; + } + + prop_dictionary_t root = NULL; + if (prop_dictionary_recv_ioctl(fd, ENVSYS_GETDICTIONARY, &root) < 0) { + return "prop_dictionary_recv_ioctl(ENVSYS_GETDICTIONARY) failed"; + } + + bool acConnected = false; + { + prop_array_t acad = prop_dictionary_get(root, "acpiacad0"); + if (acad) { + prop_dictionary_t dict = prop_array_get(acad, 0); + prop_dictionary_get_uint8(dict, "cur-value", (uint8_t*) &acConnected); + } + } + + prop_object_iterator_t itKey = prop_dictionary_iterator(root); + for (prop_dictionary_keysym_t key; (key = prop_object_iterator_next(itKey)) != NULL;) { + if (!ffStrStartsWith(prop_dictionary_keysym_value(key), "acpibat")) { + continue; + } + + prop_array_t bat = prop_dictionary_get_keysym(root, key); + uint32_t max = 0, curr = 0, dischargeRate = 0; + bool charging = false, critical = false; + prop_object_iterator_t iter = prop_array_iterator(bat); + for (prop_dictionary_t dict; (dict = prop_object_iterator_next(iter)) != NULL;) { + if (prop_object_type(dict) != PROP_TYPE_DICTIONARY) { + continue; + } + + const char* desc = NULL; + if (!prop_dictionary_get_string(dict, "description", &desc)) { + continue; + } + + if (ffStrEquals(desc, "present")) { + int value = 0; + if (prop_dictionary_get_int(dict, "cur-value", &value) && value == 0) { + continue; + } + } else if (ffStrEquals(desc, "charging")) { + prop_dictionary_get_uint8(dict, "cur-value", (uint8_t*) &charging); + } else if (ffStrEquals(desc, "charge")) { + prop_dictionary_get_uint32(dict, "max-value", &max); + prop_dictionary_get_uint32(dict, "cur-value", &curr); + const char* state = NULL; + if (prop_dictionary_get_string(dict, "state", &state) && ffStrEquals(state, "critical")) { + critical = true; + } + } else if (ffStrEquals(desc, "discharge rate")) { + prop_dictionary_get_uint(dict, "cur-value", &dischargeRate); + } + } + + if (max > 0) { + FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); + battery->temperature = FF_BATTERY_TEMP_UNSET; + battery->cycleCount = 0; + ffStrbufInit(&battery->manufacturer); + ffStrbufInit(&battery->modelName); + ffStrbufInit(&battery->technology); + ffStrbufInit(&battery->serial); + ffStrbufInit(&battery->manufactureDate); + battery->status = FF_BATTERY_STATUS_NONE; + battery->timeRemaining = -1; + + battery->capacity = (double) curr / (double) max * 100.; + if (charging) { + battery->status |= FF_BATTERY_STATUS_CHARGING; + } else if (dischargeRate) { + battery->status |= FF_BATTERY_STATUS_DISCHARGING; + battery->timeRemaining = (int32_t) ((double) curr / dischargeRate * 3600); + } + if (critical) { + battery->status |= FF_BATTERY_STATUS_CRITICAL; + } + if (acConnected) { + battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; + } + } + + prop_object_iterator_release(iter); + } + prop_object_iterator_release(itKey); + prop_object_release(root); + + return NULL; +} diff --git a/src/detection/battery/battery_nosupport.c b/src/detection/battery/battery_nosupport.c new file mode 100644 index 0000000..efd0f73 --- /dev/null +++ b/src/detection/battery/battery_nosupport.c @@ -0,0 +1,7 @@ +#include "fastfetch.h" +#include "battery.h" + +const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) { + FF_UNUSED(options, results) + return "Not supported on this platform"; +} diff --git a/src/detection/battery/battery_obsd.c b/src/detection/battery/battery_obsd.c new file mode 100644 index 0000000..ee664ce --- /dev/null +++ b/src/detection/battery/battery_obsd.c @@ -0,0 +1,60 @@ +#include "battery.h" +#include "common/io.h" + +#include <machine/apmvar.h> +#include <sys/ioctl.h> +#include <unistd.h> +#include <fcntl.h> + +const char* ffDetectBattery(FF_A_UNUSED FFBatteryOptions* options, FFlist* result) { + FF_AUTO_CLOSE_FD int devfd = open("/dev/apm", O_RDONLY | O_CLOEXEC); + + if (devfd < 0) { + return "open(dev/apm, O_RDONLY | O_CLOEXEC) failed"; + } + + struct apm_power_info info = {}; + + if (ioctl(devfd, APM_IOC_GETPOWER, &info) < 0) { + return "ioctl(APM_IOC_GETPOWER) failed"; + } + + if (info.battery_state == APM_BATTERY_ABSENT) { + return NULL; + } + + FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *result); + battery->temperature = FF_BATTERY_TEMP_UNSET; + battery->cycleCount = 0; + battery->timeRemaining = -1; + battery->capacity = info.battery_life; + ffStrbufInit(&battery->manufacturer); + ffStrbufInit(&battery->modelName); + ffStrbufInit(&battery->technology); + ffStrbufInit(&battery->serial); + ffStrbufInit(&battery->manufactureDate); + battery->status = FF_BATTERY_STATUS_NONE; + + if (info.ac_state == APM_AC_ON || info.ac_state == APM_AC_BACKUP) { + battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; + } else if (info.ac_state == APM_AC_OFF) { + battery->timeRemaining = (int) info.minutes_left * 60; + battery->status |= FF_BATTERY_STATUS_DISCHARGING; + } + + if (info.battery_state == APM_BATT_CRITICAL || info.battery_state == APM_BATT_CHARGING || info.battery_state == APM_BATT_UNKNOWN) { + switch (info.battery_state) { + case APM_BATT_UNKNOWN: + battery->status |= FF_BATTERY_STATUS_UNKNOWN; + break; + case APM_BATT_CHARGING: + battery->status |= FF_BATTERY_STATUS_CHARGING; + break; + case APM_BATT_CRITICAL: + battery->status |= FF_BATTERY_STATUS_CRITICAL; + break; + } + } + + return NULL; +} diff --git a/src/detection/battery/battery_windows.c b/src/detection/battery/battery_windows.c new file mode 100644 index 0000000..84f2a90 --- /dev/null +++ b/src/detection/battery/battery_windows.c @@ -0,0 +1,384 @@ +#define INITGUID + +#include "battery.h" + +#include "common/debug.h" +#include "common/mallocHelper.h" +#include "common/windows/unicode.h" +#include "common/windows/wmi.h" + +#include <winerror.h> + +typedef void(WINAPI* PINTERFACE_REFERENCE)(PVOID Context); +typedef void(WINAPI* PINTERFACE_DEREFERENCE)(PVOID Context); +typedef struct _DEVICE_OBJECT* PDEVICE_OBJECT; +typedef struct _IRP* PIRP; +#ifdef _WINDOWS_ + #undef _WINDOWS_ +#endif + +#include <batclass.h> + +#pragma GCC diagnostic ignored "-Wmultichar" + +typedef struct FFBatteryWmiEntry { + ULONG tag; + FFBatteryResult* result; +} FFBatteryWmiEntry; + +static FFBatteryWmiEntry* getBatteryEntry(FFlist* entries, FFlist* results, ULONG tag) { + FF_LIST_FOR_EACH (FFBatteryWmiEntry, entry, *entries) { + if (entry->tag == tag) { + return entry; + } + } + + FFBatteryWmiEntry* entry = FF_LIST_ADD(FFBatteryWmiEntry, *entries); + entry->tag = tag; + FFBatteryResult* battery = FF_LIST_ADD(FFBatteryResult, *results); + entry->result = battery; + ffStrbufInit(&battery->manufacturer); + ffStrbufInit(&battery->manufactureDate); + ffStrbufInit(&battery->modelName); + ffStrbufInit(&battery->technology); + ffStrbufInit(&battery->serial); + battery->status = FF_BATTERY_STATUS_NONE; + battery->capacity = -1; + battery->temperature = FF_BATTERY_TEMP_UNSET; + battery->cycleCount = 0; + battery->timeRemaining = -1; + return entry; +} + +static const char* queryWmiAllData(const GUID* guid, const char* guidStr, PWNODE_ALL_DATA* pAllData, ULONG* pBufferSize) { + FF_AUTO_CLOSE_WMI_BLOCK HANDLE hBlock = NULL; + ULONG status = WmiOpenBlock(guid, WMIGUID_QUERY, &hBlock); + if (status != ERROR_SUCCESS) { + FF_DEBUG("WMI: WmiOpenBlock() failed for %s: %s", guidStr, ffDebugWin32Error(status)); + return "WmiOpenBlock() failed"; + } + + status = WmiQueryAllDataW(hBlock, pBufferSize, NULL); + if (status != ERROR_SUCCESS && status != ERROR_INSUFFICIENT_BUFFER) { + FF_DEBUG("WMI: first WmiQueryAllDataW() failed: %s", ffDebugWin32Error(status)); + return "WmiQueryAllDataW(NULL) failed"; + } + + if (*pBufferSize == 0) { + return "WmiQueryAllDataW(NULL) returned no data"; + } + + if (*pBufferSize < sizeof(WNODE_ALL_DATA)) { + FF_DEBUG("WMI: WmiQueryAllDataW() returned insufficient buffer size: %lu", *pBufferSize); + return "WmiQueryAllDataW() returned insufficient data for WNODE_ALL_DATA"; + } + + *pAllData = (PWNODE_ALL_DATA) malloc(*pBufferSize); + + status = WmiQueryAllDataW(hBlock, pBufferSize, *pAllData); + if (status != ERROR_SUCCESS) { + FF_DEBUG("WMI: second WmiQueryAllDataW failed: %s", ffDebugWin32Error(status)); + free(*pAllData); + *pAllData = NULL; + return "WmiQueryAllDataW(*pAllData) failed"; + } + + return NULL; +} + +static bool getInstanceData(const PWNODE_ALL_DATA allData, ULONG bufferSize, ULONG index, const uint8_t** instanceData, ULONG* instanceLength) { + ULONG dataOffset = 0; + ULONG dataLength = 0; + + if (allData->WnodeHeader.Flags & WNODE_FLAG_FIXED_INSTANCE_SIZE) { + dataLength = allData->FixedInstanceSize; + dataOffset = allData->DataBlockOffset + index * dataLength; + } else { + dataOffset = allData->OffsetInstanceDataAndLength[index].OffsetInstanceData; + dataLength = allData->OffsetInstanceDataAndLength[index].LengthInstanceData; + } + + if (dataLength == 0 || dataOffset >= bufferSize || dataLength > bufferSize - dataOffset) { + return false; + } + + *instanceData = (const uint8_t*) allData + dataOffset; + *instanceLength = dataLength; + return true; +} + +static void detectStaticData(FFlist* entries, FFlist* results) { + FF_DEBUG("detectStaticData"); + FF_AUTO_FREE PWNODE_ALL_DATA allData = NULL; + ULONG bufferSize = 0; + const char* error = queryWmiAllData(&BATTERY_STATIC_DATA_WMI_GUID, "BATTERY_STATIC_DATA_WMI_GUID", &allData, &bufferSize); + if (error) { + return; + } + + for (ULONG i = 0; i < allData->InstanceCount; ++i) { + const uint8_t* instanceData = NULL; + ULONG instanceLength = 0; + if (!getInstanceData(allData, bufferSize, i, &instanceData, &instanceLength) || instanceLength < offsetof(BATTERY_WMI_STATIC_DATA, Strings)) { + continue; + } + + const BATTERY_WMI_STATIC_DATA* data = (const BATTERY_WMI_STATIC_DATA*) instanceData; + FFBatteryWmiEntry* entry = getBatteryEntry(entries, results, data->Tag); + + FF_DEBUG("chemistry: %.4s", (const char*) &data->Chemistry); +#if __BIG_ENDIAN__ + #define htobe32(x) (x) // Should not happen on Windows, but just in case +#else + #define htobe32(x) __builtin_bswap32(x) +#endif + switch (data->Chemistry) { + case htobe32('PbAc'): + ffStrbufSetStatic(&entry->result->technology, "Lead Acid"); + break; + case htobe32('LION'): + case htobe32('Li-I'): + ffStrbufSetStatic(&entry->result->technology, "Lithium Ion"); + break; + case htobe32('NiCd'): + ffStrbufSetStatic(&entry->result->technology, "Nickel Cadmium"); + break; + case htobe32('NiMH'): + ffStrbufSetStatic(&entry->result->technology, "Nickel Metal Hydride"); + break; + case htobe32('NiZn'): + ffStrbufSetStatic(&entry->result->technology, "Nickel Zinc"); + break; + case htobe32('RAM\0'): + ffStrbufSetStatic(&entry->result->technology, "Rechargeable Alkaline-Manganese"); + break; + default: + ffStrbufSetStatic(&entry->result->technology, data->Technology ? "Rechargeable" : "Non Rechargeable"); + break; + } +#undef htobe32 + + const BATTERY_MANUFACTURE_DATE* manufactureDate = (const BATTERY_MANUFACTURE_DATE*) data->ManufactureDate; + if (manufactureDate->Year > 0 && manufactureDate->Month >= 1 && manufactureDate->Month <= 12 && manufactureDate->Day >= 1 && manufactureDate->Day <= 31) { + uint16_t year = manufactureDate->Year; + ffStrbufSetF(&entry->result->manufactureDate, "%.4u-%.2u-%.2u", (unsigned) (year < 1000 ? (year + 1900) : year), (unsigned) manufactureDate->Month, (unsigned) manufactureDate->Day); + } + + // Device Name, Manufacture Name, Serial Number, UniqueID + const struct { + uint16_t size; // in bytes, including the null terminator + wchar_t value[]; + }* cursor = (const void*) data->Strings; + + FFstrbuf* strings[] = { + &entry->result->modelName, + &entry->result->manufacturer, + &entry->result->serial, + }; + + for (size_t i = 0; i < ARRAY_SIZE(strings); ++i) { + if (cursor->size > sizeof(wchar_t)) { + ffStrbufSetNWS(strings[i], cursor->size / sizeof(wchar_t) - 1, cursor->value); + } + cursor = (const void*) ((const uint8_t*) cursor + sizeof(uint16_t) + cursor->size); + } + } +} + +static void detectStatus(FFlist* entries, FFlist* results) { + FF_DEBUG("detectStatus"); + FF_AUTO_FREE PWNODE_ALL_DATA allData = NULL; + ULONG bufferSize = 0; + const char* error = queryWmiAllData(&BATTERY_STATUS_WMI_GUID, "BATTERY_STATUS_WMI_GUID", &allData, &bufferSize); + if (error) { + return; + } + + for (ULONG i = 0; i < allData->InstanceCount; ++i) { + const uint8_t* instanceData = NULL; + ULONG instanceLength = 0; + if (!getInstanceData(allData, bufferSize, i, &instanceData, &instanceLength) || instanceLength < sizeof(BATTERY_WMI_STATUS)) { + continue; + } + + const BATTERY_WMI_STATUS* data = (const BATTERY_WMI_STATUS*) instanceData; + FFBatteryWmiEntry* entry = getBatteryEntry(entries, results, data->Tag); + if (data->RemainingCapacity != BATTERY_UNKNOWN_CAPACITY) { + entry->result->capacity = data->RemainingCapacity; + } + + entry->result->status = FF_BATTERY_STATUS_NONE; + if (data->PowerOnline) { + entry->result->status |= FF_BATTERY_STATUS_AC_CONNECTED; + } + if (data->Charging) { + entry->result->status |= FF_BATTERY_STATUS_CHARGING; + } + if (data->Discharging) { + entry->result->status |= FF_BATTERY_STATUS_DISCHARGING; + } + if (data->Critical) { + entry->result->status |= FF_BATTERY_STATUS_CRITICAL; + } + } +} + +static void detectRuntime(FFlist* entries, FFlist* results) { + FF_DEBUG("detectRuntime"); + FF_AUTO_FREE PWNODE_ALL_DATA allData = NULL; + ULONG bufferSize = 0; + const char* error = queryWmiAllData(&BATTERY_RUNTIME_WMI_GUID, "BATTERY_RUNTIME_WMI_GUID", &allData, &bufferSize); + if (error) { + return; + } + + for (ULONG i = 0; i < allData->InstanceCount; ++i) { + const uint8_t* instanceData = NULL; + ULONG instanceLength = 0; + if (!getInstanceData(allData, bufferSize, i, &instanceData, &instanceLength) || instanceLength < sizeof(BATTERY_WMI_RUNTIME)) { + continue; + } + + const BATTERY_WMI_RUNTIME* data = (const BATTERY_WMI_RUNTIME*) instanceData; + FFBatteryWmiEntry* entry = getBatteryEntry(entries, results, data->Tag); + if (data->EstimatedRuntime != BATTERY_UNKNOWN_TIME) { + entry->result->timeRemaining = (int32_t) data->EstimatedRuntime; + } + } +} + +static void detectFullChargedCapacity(FFlist* entries, FFlist* results) { + FF_DEBUG("detectFullChargedCapacity"); + FF_AUTO_FREE PWNODE_ALL_DATA allData = NULL; + ULONG bufferSize = 0; + const char* error = queryWmiAllData(&BATTERY_FULL_CHARGED_CAPACITY_WMI_GUID, "BATTERY_FULL_CHARGED_CAPACITY_WMI_GUID", &allData, &bufferSize); + if (error) { + return; + } + + for (ULONG i = 0; i < allData->InstanceCount; ++i) { + const uint8_t* instanceData = NULL; + ULONG instanceLength = 0; + if (!getInstanceData(allData, bufferSize, i, &instanceData, &instanceLength) || instanceLength < sizeof(BATTERY_WMI_FULL_CHARGED_CAPACITY)) { + continue; + } + + const BATTERY_WMI_FULL_CHARGED_CAPACITY* data = (const BATTERY_WMI_FULL_CHARGED_CAPACITY*) instanceData; + FFBatteryWmiEntry* entry = getBatteryEntry(entries, results, data->Tag); + + if (data->FullChargedCapacity != BATTERY_UNKNOWN_CAPACITY && entry->result->capacity >= 0) { + entry->result->capacity *= 100; + entry->result->capacity /= data->FullChargedCapacity; + } + } +} + +static void detectCycleCount(FFlist* entries, FFlist* results) { + FF_DEBUG("detectCycleCount"); + FF_AUTO_FREE PWNODE_ALL_DATA allData = NULL; + ULONG bufferSize = 0; + const char* error = queryWmiAllData(&BATTERY_CYCLE_COUNT_WMI_GUID, "BATTERY_CYCLE_COUNT_WMI_GUID", &allData, &bufferSize); + if (error) { + return; + } + + for (ULONG i = 0; i < allData->InstanceCount; ++i) { + const uint8_t* instanceData = NULL; + ULONG instanceLength = 0; + if (!getInstanceData(allData, bufferSize, i, &instanceData, &instanceLength) || instanceLength < sizeof(BATTERY_WMI_CYCLE_COUNT)) { + continue; + } + + const BATTERY_WMI_CYCLE_COUNT* data = (const BATTERY_WMI_CYCLE_COUNT*) instanceData; + getBatteryEntry(entries, results, data->Tag)->result->cycleCount = data->CycleCount; + } +} + +static void detectTemperature(FFlist* entries, FFlist* results) { + FF_DEBUG("detectTemperature"); + FF_AUTO_FREE PWNODE_ALL_DATA allData = NULL; + ULONG bufferSize = 0; + const char* error = queryWmiAllData(&BATTERY_TEMPERATURE_WMI_GUID, "BATTERY_TEMPERATURE_WMI_GUID", &allData, &bufferSize); + if (error) { + return; + } + + for (ULONG i = 0; i < allData->InstanceCount; ++i) { + const uint8_t* instanceData = NULL; + ULONG instanceLength = 0; + if (!getInstanceData(allData, bufferSize, i, &instanceData, &instanceLength) || instanceLength < sizeof(BATTERY_WMI_TEMPERATURE)) { + continue; + } + + const BATTERY_WMI_TEMPERATURE* data = (const BATTERY_WMI_TEMPERATURE*) instanceData; + getBatteryEntry(entries, results, data->Tag)->result->temperature = data->Temperature / 10.0 - 273.15; + } +} + +static const char* detectWithNtApi(FFBatteryResult* battery) { + // Reports summary battery information, not per battery + FF_DEBUG("NtApi: start detection"); + SYSTEM_BATTERY_STATE info; + NTSTATUS status = NtPowerInformation(SystemBatteryState, NULL, 0, &info, sizeof(info)); + if (!NT_SUCCESS(status)) { + FF_DEBUG("NtApi: NtPowerInformation(SystemBatteryState) failed: %s", ffDebugNtStatus(status)); + return "NtPowerInformation(SystemBatteryState) failed"; + } + if (!info.BatteryPresent) { + FF_DEBUG("NtApi reports no battery present"); + return "No battery present"; + } + + if (info.MaxCapacity != BATTERY_UNKNOWN_CAPACITY && info.RemainingCapacity != BATTERY_UNKNOWN_CAPACITY) { + battery->capacity = info.RemainingCapacity * 100.0 / info.MaxCapacity; + } + battery->status = FF_BATTERY_STATUS_NONE; + if (info.AcOnLine) { + battery->status |= FF_BATTERY_STATUS_AC_CONNECTED; + } + if (info.Charging) { + battery->status |= FF_BATTERY_STATUS_CHARGING; + } + if (info.Discharging) { + battery->status |= FF_BATTERY_STATUS_DISCHARGING; + } + if (info.DefaultAlert1 > 0 && info.RemainingCapacity <= info.DefaultAlert1) { + battery->status |= FF_BATTERY_STATUS_CRITICAL; + } + battery->timeRemaining = info.EstimatedTime == BATTERY_UNKNOWN_TIME ? -1 : (int32_t) info.EstimatedTime; + return NULL; +} + +const char* ffDetectBattery(FFBatteryOptions* options, FFlist* results) { + FF_DEBUG("WMI: start detection"); + + FF_LIST_AUTO_DESTROY entries = ffListCreate(); + detectStaticData(&entries, results); + if (results->length == 0) { + return NULL; + } else if (results->length == 1) { + // Fast path for single battery + detectWithNtApi(FF_LIST_FIRST(FFBatteryWmiEntry, entries)->result); + } else { + detectStatus(&entries, results); + detectFullChargedCapacity(&entries, results); + detectRuntime(&entries, results); + } + detectCycleCount(&entries, results); + if (options->temp) { + detectTemperature(&entries, results); + } + + FF_LIST_FOR_EACH (FFBatteryWmiEntry, entry, entries) { + FF_DEBUG( + "WMI: detected battery tag=%lu, name='%s', charge=%.2f%%, status=0x%x, runtime=%d seconds", + entry->tag, + entry->result->modelName.length ? entry->result->modelName.chars : "<unknown>", + entry->result->capacity, + entry->result->status, + entry->result->timeRemaining); + } + + FF_DEBUG("WMI: finished detection, total results=%u", results->length); + return NULL; +} diff --git a/src/detection/bios/bios.h b/src/detection/bios/bios.h new file mode 100644 index 0000000..cdb48c2 --- /dev/null +++ b/src/detection/bios/bios.h @@ -0,0 +1,14 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/bios/option.h" + +typedef struct FFBiosResult { + FFstrbuf date; + FFstrbuf release; + FFstrbuf vendor; + FFstrbuf version; + FFstrbuf type; +} FFBiosResult; + +const char* ffDetectBios(FFBiosResult* bios); diff --git a/src/detection/bios/bios_android.c b/src/detection/bios/bios_android.c new file mode 100644 index 0000000..a53db0d --- /dev/null +++ b/src/detection/bios/bios_android.c @@ -0,0 +1,16 @@ +#include "bios.h" +#include "common/settings.h" + +const char* ffDetectBios(FFBiosResult* bios) { + if (!ffSettingsGetAndroidProperty("ro.bootloader", &bios->version)) { + ffSettingsGetAndroidProperty("ro.boot.bootloader", &bios->version); + } + + if (ffStrbufIgnCaseEqualS(&bios->version, "unknown")) { + ffStrbufClear(&bios->version); + } + + ffStrbufSetStatic(&bios->type, "Bootloader"); + + return NULL; +} diff --git a/src/detection/bios/bios_apple.c b/src/detection/bios/bios_apple.c new file mode 100644 index 0000000..6b046d2 --- /dev/null +++ b/src/detection/bios/bios_apple.c @@ -0,0 +1,58 @@ +#include "bios.h" +#include "common/apple/cf_helpers.h" + +#include <IOKit/IOKitLib.h> + +const char* ffDetectBios(FFBiosResult* bios) { +#ifndef __aarch64__ + + // https://github.com/osquery/osquery/blob/master/osquery/tables/system/darwin/smbios_tables.cpp + // For Intel + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t deviceRom = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/rom"); + if (!deviceRom) { + return "IODeviceTree:/rom not found"; + } + + FF_CFTYPE_AUTO_RELEASE CFMutableDictionaryRef deviceRomProps = NULL; + if (IORegistryEntryCreateCFProperties(deviceRom, &deviceRomProps, kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess) { + return "IORegistryEntryCreateCFProperties(deviceRom) failed"; + } + + ffCfDictGetString(deviceRomProps, CFSTR("vendor"), &bios->vendor); + ffCfDictGetString(deviceRomProps, CFSTR("version"), &bios->version); + ffCfDictGetString(deviceRomProps, CFSTR("release-date"), &bios->date); + ffStrbufSetStatic(&bios->type, "UEFI"); + +#else + + // For arm64 + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t device = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/"); + if (!device) { + return "IODeviceTree:/ not found"; + } + + FF_CFTYPE_AUTO_RELEASE CFDataRef manufacturer = IORegistryEntryCreateCFProperty(device, CFSTR("manufacturer"), kCFAllocatorDefault, kNilOptions); + ffCfStrGetString(manufacturer, &bios->vendor); + FF_CFTYPE_AUTO_RELEASE CFDataRef timeStamp = IORegistryEntryCreateCFProperty(device, CFSTR("time-stamp"), kCFAllocatorDefault, kNilOptions); + ffCfStrGetString(timeStamp, &bios->date); + + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t deviceChosen = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/chosen"); + if (deviceChosen) { + FF_CFTYPE_AUTO_RELEASE CFStringRef tag = IORegistryEntryCreateCFProperty(deviceChosen, CFSTR("iboot-stage-one-tag"), kCFAllocatorDefault, kNilOptions) + ?: IORegistryEntryCreateCFProperty(deviceChosen, CFSTR("system-firmware-version"), kCFAllocatorDefault, kNilOptions); + if (tag) { + ffCfStrGetString(tag, &bios->version); + uint32_t index = ffStrbufFirstIndexC(&bios->version, '-'); + if (index != bios->version.length) { + ffStrbufAppendNS(&bios->type, index, bios->version.chars); + ffStrbufRemoveSubstr(&bios->version, 0, index + 1); + } + } + } + if (!bios->type.length) { + ffStrbufSetStatic(&bios->type, "iBoot"); + } +#endif + + return NULL; +} diff --git a/src/detection/bios/bios_bsd.c b/src/detection/bios/bios_bsd.c new file mode 100644 index 0000000..66e50ea --- /dev/null +++ b/src/detection/bios/bios_bsd.c @@ -0,0 +1,31 @@ +#include "bios.h" + +#include "common/settings.h" +#include "common/sysctl.h" +#include "common/io.h" +#include "common/smbios.h" + +const char* ffDetectBios(FFBiosResult* result) { + ffSettingsGetFreeBSDKenv("smbios.bios.reldate", &result->date); + ffCleanUpSmbiosValue(&result->date); + ffSettingsGetFreeBSDKenv("smbios.bios.revision", &result->release); + ffCleanUpSmbiosValue(&result->release); + ffSettingsGetFreeBSDKenv("smbios.bios.vendor", &result->vendor); + ffCleanUpSmbiosValue(&result->vendor); + ffSettingsGetFreeBSDKenv("smbios.bios.version", &result->version); + ffCleanUpSmbiosValue(&result->version); + ffSysctlGetString("machdep.bootmethod", &result->type); + + if (result->type.length == 0) { + if (ffSettingsGetFreeBSDKenv("loader.efi", &result->type)) { + ffStrbufSetStatic(&result->type, ffStrbufEqualS(&result->type, "1") ? "UEFI" : "BIOS"); + } else { + ffStrbufSetStatic(&result->type, + ffPathExists("/dev/efi" /*efidev*/, FF_PATHTYPE_FILE) || + ffPathExists("/boot/efi/efi/" /*efi partition. Note /boot/efi exists on BIOS system*/, FF_PATHTYPE_DIRECTORY) + ? "UEFI" + : "BIOS"); + } + } + return NULL; +} diff --git a/src/detection/bios/bios_linux.c b/src/detection/bios/bios_linux.c new file mode 100644 index 0000000..8776631 --- /dev/null +++ b/src/detection/bios/bios_linux.c @@ -0,0 +1,21 @@ +#include "bios.h" +#include "common/io.h" +#include "common/smbios.h" + +const char* ffDetectBios(FFBiosResult* bios) { + if (ffGetSmbiosValue("/sys/devices/virtual/dmi/id/bios_date", "/sys/class/dmi/id/bios_date", &bios->date)) { + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/bios_release", "/sys/class/dmi/id/bios_release", &bios->release); + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/bios_vendor", "/sys/class/dmi/id/bios_vendor", &bios->vendor); + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/bios_version", "/sys/class/dmi/id/bios_version", &bios->version); + } else if (ffReadFileBuffer("/proc/device-tree/chosen/u-boot,version", &bios->version)) { + ffStrbufTrimRight(&bios->version, '\0'); + ffStrbufSetStatic(&bios->vendor, "U-Boot"); + } + + if (ffPathExists("/sys/firmware/efi/", FF_PATHTYPE_DIRECTORY) || ffPathExists("/sys/firmware/acpi/tables/UEFI", FF_PATHTYPE_FILE)) { + ffStrbufSetStatic(&bios->type, "UEFI"); + } else { + ffStrbufSetStatic(&bios->type, "BIOS"); + } + return NULL; +} diff --git a/src/detection/bios/bios_nbsd.c b/src/detection/bios/bios_nbsd.c new file mode 100644 index 0000000..3ab43ed --- /dev/null +++ b/src/detection/bios/bios_nbsd.c @@ -0,0 +1,20 @@ +#include "bios.h" +#include "common/sysctl.h" +#include "common/smbios.h" +#include "common/io.h" + +const char* ffDetectBios(FFBiosResult* bios) { + if (ffSysctlGetString("machdep.dmi.bios-date", &bios->date) == NULL) { + ffCleanUpSmbiosValue(&bios->date); + } + if (ffSysctlGetString("machdep.dmi.bios-version", &bios->version) == NULL) { + ffCleanUpSmbiosValue(&bios->version); + } + if (ffSysctlGetString("machdep.dmi.bios-vendor", &bios->vendor) == NULL) { + ffCleanUpSmbiosValue(&bios->vendor); + } + if (ffSysctlGetString("machdep.bootmethod", &bios->type) != NULL) { + ffStrbufSetStatic(&bios->type, ffPathExists("/dev/efi", FF_PATHTYPE_FILE) ? "UEFI" : "BIOS"); + } + return NULL; +} diff --git a/src/detection/bios/bios_nosupport.c b/src/detection/bios/bios_nosupport.c new file mode 100644 index 0000000..e044835 --- /dev/null +++ b/src/detection/bios/bios_nosupport.c @@ -0,0 +1,5 @@ +#include "bios.h" + +const char* ffDetectBios(FF_A_UNUSED FFBiosResult* bios) { + return "Not supported on this platform"; +} diff --git a/src/detection/bios/bios_windows.c b/src/detection/bios/bios_windows.c new file mode 100644 index 0000000..42b7674 --- /dev/null +++ b/src/detection/bios/bios_windows.c @@ -0,0 +1,107 @@ +#include "bios.h" +#include "common/smbios.h" + +#ifdef _WIN32 + #include "common/windows/registry.h" + + #include <ntstatus.h> + #include "common/windows/nt.h" +#elif __OpenBSD__ + #include "common/io.h" + + #include <fcntl.h> + #include <unistd.h> +#elif __sun + #include <libdevinfo.h> + #include <sys/sunddi.h> +#elif __APPLE__ + #include "common/apple/cf_helpers.h" + #include <IOKit/IOKitLib.h> +#endif + +typedef struct FFSmbiosBios { + FFSmbiosHeader Header; + + uint8_t Vendor; // string + uint8_t BiosVersion; // string + uint16_t BiosStartingAddressSegment; // varies + uint8_t BiosReleaseDate; // string + uint8_t BiosRomSize; // string + uint64_t BiosCharacteristics; // bit field + + // 2.4+ + uint8_t BiosCharacteristicsExtensionBytes[2]; // bit field + uint8_t SystemBiosMajorRelease; // varies + uint8_t SystemBiosMinorRelease; // varies + uint8_t EmbeddedControllerFirmwareMajorRelease; // varies + uint8_t EmbeddedControllerFirmwareMinorRelease; // varies + + // 3.1+ + uint16_t ExtendedBiosRomSize; // bit field +} FF_A_PACKED FFSmbiosBios; + +static_assert(offsetof(FFSmbiosBios, ExtendedBiosRomSize) == 0x18, + "FFSmbiosBios: Wrong struct alignment"); + +const char* ffDetectBios(FFBiosResult* bios) { + const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable(); + if (!smbiosTable) { + return "Failed to get SMBIOS data"; + } + + const FFSmbiosBios* data = (const FFSmbiosBios*) (*smbiosTable)[FF_SMBIOS_TYPE_BIOS]; + if (!data) { + return "BIOS section is not found in SMBIOS data"; + } + + const char* strings = (const char*) data + data->Header.Length; + + ffStrbufSetStatic(&bios->version, ffSmbiosLocateString(strings, data->BiosVersion)); + ffCleanUpSmbiosValue(&bios->version); + ffStrbufSetStatic(&bios->vendor, ffSmbiosLocateString(strings, data->Vendor)); + ffCleanUpSmbiosValue(&bios->vendor); + ffStrbufSetStatic(&bios->date, ffSmbiosLocateString(strings, data->BiosReleaseDate)); + ffCleanUpSmbiosValue(&bios->date); + + if (data->Header.Length > offsetof(FFSmbiosBios, SystemBiosMajorRelease)) { + ffStrbufSetF(&bios->release, "%u.%u", data->SystemBiosMajorRelease, data->SystemBiosMinorRelease); + } + +#ifdef _WIN32 + // Same as GetFirmwareType, but support (?) Windows 7 + // https://ntdoc.m417z.com/system_information_class + SYSTEM_BOOT_ENVIRONMENT_INFORMATION sbei; + if (NT_SUCCESS(NtQuerySystemInformation(SystemBootEnvironmentInformation, &sbei, sizeof(sbei), NULL))) { + switch (sbei.FirmwareType) { + case FirmwareTypeBios: + ffStrbufSetStatic(&bios->type, "BIOS"); + break; + case FirmwareTypeUefi: + ffStrbufSetStatic(&bios->type, "UEFI"); + break; + default: + break; + } + } +#elif __sun + di_node_t rootNode = di_init("/", DINFOPROP); + if (rootNode != DI_NODE_NIL) { + char* efiVersion = NULL; + if (di_prop_lookup_strings(DDI_DEV_T_ANY, rootNode, "efi-version", &efiVersion) > 0) { + ffStrbufSetStatic(&bios->type, "UEFI"); + } else { + ffStrbufSetStatic(&bios->type, "BIOS"); + } + } + di_fini(rootNode); +#elif __HAIKU__ || __OpenBSD__ + // Currently SMBIOS detection is supported in legacy BIOS only + ffStrbufSetStatic(&bios->type, "BIOS"); +#elif __APPLE__ + // Intel Macs use UEFI from day one + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t deviceEfi = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/efi"); + ffStrbufSetStatic(&bios->type, deviceEfi ? "UEFI" : "BIOS"); +#endif + + return NULL; +} diff --git a/src/detection/bluetooth/bluetooth.h b/src/detection/bluetooth/bluetooth.h new file mode 100644 index 0000000..dde8085 --- /dev/null +++ b/src/detection/bluetooth/bluetooth.h @@ -0,0 +1,14 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/bluetooth/option.h" + +typedef struct FFBluetoothResult { + FFstrbuf name; + FFstrbuf address; + FFstrbuf type; + uint8_t battery; // 0-100% + bool connected; +} FFBluetoothResult; + +const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */); diff --git a/src/detection/bluetooth/bluetooth_apple.m b/src/detection/bluetooth/bluetooth_apple.m new file mode 100644 index 0000000..9cbf732 --- /dev/null +++ b/src/detection/bluetooth/bluetooth_apple.m @@ -0,0 +1,112 @@ +#include "bluetooth.h" + +#import <IOBluetooth/IOBluetooth.h> + +@interface IOBluetoothDevice() + @property (nonatomic) uint8_t batteryPercentCase; + @property (nonatomic) uint8_t batteryPercentCombined; + @property (nonatomic) uint8_t batteryPercentLeft; + @property (nonatomic) uint8_t batteryPercentRight; + @property (nonatomic) uint8_t batteryPercentSingle; +@end + +const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) +{ + NSArray<IOBluetoothDevice*>* ioDevices = IOBluetoothDevice.pairedDevices; + if(!ioDevices) + return "IOBluetoothDevice.pairedDevices failed"; + + for(IOBluetoothDevice* ioDevice in ioDevices) + { + if (!options->showDisconnected && !ioDevice.isConnected) + continue; + + FFBluetoothResult* device = FF_LIST_ADD(FFBluetoothResult, *devices); + ffStrbufInitS(&device->name, ioDevice.name.UTF8String); + ffStrbufInitS(&device->address, ioDevice.addressString.UTF8String); + ffStrbufReplaceAllC(&device->address, '-', ':'); + ffStrbufUpperCase(&device->address); + ffStrbufInit(&device->type); + + if (ioDevice.batteryPercentSingle) + device->battery = ioDevice.batteryPercentSingle; + else if (ioDevice.batteryPercentCombined) + device->battery = ioDevice.batteryPercentCombined; + else if (ioDevice.batteryPercentCase) + device->battery = ioDevice.batteryPercentCase; + + device->connected = !!ioDevice.isConnected; + if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorLimitedDiscoverableMode) + ffStrbufAppendS(&device->type, "Limited Discoverable Mode, "); + if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorReserved1) + ffStrbufAppendS(&device->type, "LE audio, "); + if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorReserved2) + ffStrbufAppendS(&device->type, "Reserved for future use, "); + if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorPositioning) + ffStrbufAppendS(&device->type, "Positioning, "); + if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorNetworking) + ffStrbufAppendS(&device->type, "Networking, "); + if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorRendering) + ffStrbufAppendS(&device->type, "Rendering, "); + if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorCapturing) + ffStrbufAppendS(&device->type, "Capturing, "); + if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorObjectTransfer) + ffStrbufAppendS(&device->type, "Object Transfer, "); + if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorAudio) + ffStrbufAppendS(&device->type, "Audio, "); + if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorTelephony) + ffStrbufAppendS(&device->type, "Telephony, "); + if(ioDevice.serviceClassMajor & kBluetoothServiceClassMajorInformation) + ffStrbufAppendS(&device->type, "Information, "); + + if(device->type.length == 0) + { + switch(ioDevice.deviceClassMajor) + { + case kBluetoothDeviceClassMajorMiscellaneous: + ffStrbufAppendS(&device->type, "Miscellaneous"); + break; + case kBluetoothDeviceClassMajorComputer: + ffStrbufAppendS(&device->type, "Computer"); + break; + case kBluetoothDeviceClassMajorPhone: + ffStrbufAppendS(&device->type, "Phone"); + break; + case kBluetoothDeviceClassMajorLANAccessPoint: + ffStrbufAppendS(&device->type, "LAN/Network Access point"); + break; + case kBluetoothDeviceClassMajorAudio: + ffStrbufAppendS(&device->type, "Audio/Video"); + break; + case kBluetoothDeviceClassMajorPeripheral: + ffStrbufAppendS(&device->type, "Peripheral"); + break; + case kBluetoothDeviceClassMajorImaging: + ffStrbufAppendS(&device->type, "Imaging"); + break; + case kBluetoothDeviceClassMajorWearable: + ffStrbufAppendS(&device->type, "Wearable"); + break; + case kBluetoothDeviceClassMajorToy: + ffStrbufAppendS(&device->type, "Toy"); + break; + case kBluetoothDeviceClassMajorHealth: + ffStrbufAppendS(&device->type, "Health"); + break; + case kBluetoothDeviceClassMajorUnclassified: + ffStrbufAppendS(&device->type, "Uncategorized"); + break; + default: + ffStrbufAppendS(&device->type, "Unknown"); + break; + } + } + else + { + ffStrbufTrimRight(&device->type, ' '); + ffStrbufTrimRight(&device->type, ','); + } + } + + return NULL; +} diff --git a/src/detection/bluetooth/bluetooth_bsd.c b/src/detection/bluetooth/bluetooth_bsd.c new file mode 100644 index 0000000..db9f23c --- /dev/null +++ b/src/detection/bluetooth/bluetooth_bsd.c @@ -0,0 +1,30 @@ +#include "bluetooth.h" + +#define L2CAP_SOCKET_CHECKED +#include <bluetooth.h> + +static int enumDev(FF_A_UNUSED int sockfd, struct bt_devinfo const* dev, FFlist* devices) { + FFBluetoothResult* device = FF_LIST_ADD(FFBluetoothResult, *devices); + ffStrbufInitS(&device->name, +#if __FreeBSD__ + bt_devremote_name_gen(dev->devname, &dev->bdaddr) +#else + dev->devname +#endif + ); + ffStrbufInitS(&device->address, bt_ntoa(&dev->bdaddr, NULL)); + ffStrbufUpperCase(&device->address); + ffStrbufInit(&device->type); + device->battery = 0; + device->connected = true; + return 0; +} + +const char* ffDetectBluetooth(FF_A_UNUSED FFBluetoothOptions* options, FF_A_UNUSED FFlist* devices /* FFBluetoothResult */) { + // struct hostent* ent = bt_gethostent(); + if (bt_devenum((void*) enumDev, devices) < 0) { + return "bt_devenum() failed"; + } + + return NULL; +} diff --git a/src/detection/bluetooth/bluetooth_haiku.cpp b/src/detection/bluetooth/bluetooth_haiku.cpp new file mode 100644 index 0000000..8ed52c5 --- /dev/null +++ b/src/detection/bluetooth/bluetooth_haiku.cpp @@ -0,0 +1,30 @@ +extern "C" { +#include "bluetooth.h" +#include "common/io.h" +} + +#include <bluetooth/LocalDevice.h> + +const char* ffDetectBluetooth(FF_A_UNUSED FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) { + using namespace Bluetooth; + FF_SUPPRESS_IO(); + + LocalDevice* dev = LocalDevice::GetLocalDevice(); + if (!dev) { + return NULL; + } + + BString devClass; + dev->GetDeviceClass().DumpDeviceClass(devClass); + + FFBluetoothResult* device = FF_LIST_ADD(FFBluetoothResult, *devices); + ffStrbufInitS(&device->name, dev->GetFriendlyName()); + ffStrbufInitS(&device->address, bdaddrUtils::ToString(dev->GetBluetoothAddress()).String()); + ffStrbufInitS(&device->type, devClass.String()); + device->battery = 0; + device->connected = true; + + // TODO: more devices? + + return NULL; +} diff --git a/src/detection/bluetooth/bluetooth_linux.c b/src/detection/bluetooth/bluetooth_linux.c new file mode 100644 index 0000000..fb4e422 --- /dev/null +++ b/src/detection/bluetooth/bluetooth_linux.c @@ -0,0 +1,256 @@ +#include "bluetooth.h" +#include "common/strutil.h" + +#ifdef FF_HAVE_DBUS + #include "common/dbus.h" + #include "common/io.h" + +/* Example dbus reply, striped to only the relevant parts: +array [ //root + dict entry( //object + object path "/org/bluez/hci0/dev_03_21_8B_91_16_4D" + array [ + dict entry( //property + string "org.bluez.Device1" + array [ + dict entry( //value + string "Address" + variant string "03:21:8B:91:16:4D" + ) + dict entry( //value + string "Name" + variant string "JBL TUNE160BT" + ) + dict entry( //value + string "Icon" + variant string "audio-headset" + ) + dict entry( //value + string "Connected" + variant boolean true + ) + ] + ) + dict entry( //property + string "org.bluez.Battery1" + array [ + dict entry( //value + string "Percentage" + variant byte 100 + ) + ] + ) + ] + ) +] +*/ + +static bool detectBluetoothValue(FFDBusData* dbus, DBusMessageIter* iter, FFBluetoothResult* device) { + if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) { + return true; + } + + DBusMessageIter dictIter; + dbus->lib->ffdbus_message_iter_recurse(iter, &dictIter); + + if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_STRING) { + return true; + } + + const char* deviceProperty; + dbus->lib->ffdbus_message_iter_get_basic(&dictIter, &deviceProperty); + + dbus->lib->ffdbus_message_iter_next(&dictIter); + + if (ffStrEquals(deviceProperty, "Address")) { + ffDBusGetString(dbus, &dictIter, &device->address); + } else if (ffStrEquals(deviceProperty, "Name")) { + ffDBusGetString(dbus, &dictIter, &device->name); + } else if (ffStrEquals(deviceProperty, "Icon")) { + ffDBusGetString(dbus, &dictIter, &device->type); + } else if (ffStrEquals(deviceProperty, "Percentage")) { + uint64_t percentage; + if (ffDBusGetUint(dbus, &dictIter, &percentage)) { + device->battery = (uint8_t) percentage; + } + } else if (ffStrEquals(deviceProperty, "Connected")) { + ffDBusGetBool(dbus, &dictIter, &device->connected); + } else if (ffStrEquals(deviceProperty, "Paired")) { + bool paired = true; + ffDBusGetBool(dbus, &dictIter, &paired); + if (!paired) { + return false; + } + } + return true; +} + +static void detectBluetoothProperty(FFDBusData* dbus, DBusMessageIter* iter, FFBluetoothResult* device) { + if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) { + return; + } + + DBusMessageIter dictIter; + dbus->lib->ffdbus_message_iter_recurse(iter, &dictIter); + + if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_STRING) { + return; + } + + const char* propertyType; + dbus->lib->ffdbus_message_iter_get_basic(&dictIter, &propertyType); + + if (!ffStrContains(propertyType, ".Device") && !ffStrContains(propertyType, ".Battery")) { + return; // We don't care about other properties + } + + dbus->lib->ffdbus_message_iter_next(&dictIter); + + if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_ARRAY) { + return; + } + + DBusMessageIter arrayIter; + dbus->lib->ffdbus_message_iter_recurse(&dictIter, &arrayIter); + + do { + bool shouldContinue = detectBluetoothValue(dbus, &arrayIter, device); + if (!shouldContinue) { + ffStrbufClear(&device->name); + break; + } + } while (dbus->lib->ffdbus_message_iter_next(&arrayIter)); +} + +static FFBluetoothResult* detectBluetoothObject(FFlist* devices, FFDBusData* dbus, DBusMessageIter* iter) { + if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) { + return NULL; + } + + DBusMessageIter dictIter; + dbus->lib->ffdbus_message_iter_recurse(iter, &dictIter); + + if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_OBJECT_PATH) { + return NULL; + } + + const char* objectPath; + dbus->lib->ffdbus_message_iter_get_basic(&dictIter, &objectPath); + + // We don't want adapter objects + if (!ffStrContains(objectPath, "/dev_")) { + return NULL; + } + + dbus->lib->ffdbus_message_iter_next(&dictIter); + + if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_ARRAY) { + return NULL; + } + + DBusMessageIter arrayIter; + dbus->lib->ffdbus_message_iter_recurse(&dictIter, &arrayIter); + + FFBluetoothResult* device = FF_LIST_ADD(FFBluetoothResult, *devices); + ffStrbufInit(&device->name); + ffStrbufInit(&device->address); + ffStrbufInit(&device->type); + device->battery = 0; + device->connected = false; + + do { + detectBluetoothProperty(dbus, &arrayIter, device); + } while (dbus->lib->ffdbus_message_iter_next(&arrayIter)); + + return device; +} + +static void detectBluetoothRoot(FFBluetoothOptions* options, FFlist* devices, FFDBusData* dbus, DBusMessageIter* iter, int32_t connectedCount) { + if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_ARRAY) { + return; + } + + DBusMessageIter arrayIter; + dbus->lib->ffdbus_message_iter_recurse(iter, &arrayIter); + + do { + FFBluetoothResult* device = detectBluetoothObject(devices, dbus, &arrayIter); + + if (device) { + if (!options->showDisconnected && !device->connected) { + ffStrbufDestroy(&device->name); + ffStrbufDestroy(&device->address); + ffStrbufDestroy(&device->type); + --devices->length; + } + + if (device->name.length == 0) { + ffStrbufSetStatic(&device->name, "Unknown Device"); + } + + if (device->connected && --connectedCount == 0) { + break; + } + } + } while (dbus->lib->ffdbus_message_iter_next(&arrayIter)); +} + +static const char* detectBluetooth(FFBluetoothOptions* options, FFlist* devices, int32_t connectedCount) { + FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {}; + const char* error = ffDBusLoadData(DBUS_BUS_SYSTEM, &dbus); + if (error) { + return error; + } + + DBusMessage* managedObjects = ffDBusGetMethodReply(&dbus, "org.bluez", "/", "org.freedesktop.DBus.ObjectManager", "GetManagedObjects", NULL, NULL); + if (!managedObjects) { + return "Failed to call GetManagedObjects"; + } + + DBusMessageIter rootIter; + if (!dbus.lib->ffdbus_message_iter_init(managedObjects, &rootIter)) { + dbus.lib->ffdbus_message_unref(managedObjects); + return "Failed to get root iterator of GetManagedObjects"; + } + + detectBluetoothRoot(options, devices, &dbus, &rootIter, connectedCount); + + dbus.lib->ffdbus_message_unref(managedObjects); + return NULL; +} + +static uint32_t connectedDevices(void) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/bluetooth"); + if (dirp == NULL) { + return 0; + } + + uint32_t result = 0; + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (strchr(entry->d_name, ':') != NULL) { + ++result; + } + } + + return result; +} + +#endif + +const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) { +#ifdef FF_HAVE_DBUS + int32_t connectedCount = -1; + if (!options->showDisconnected) { + connectedCount = (int32_t) connectedDevices(); + if (connectedCount == 0) { + return NULL; + } + } + + return detectBluetooth(options, devices, connectedCount); +#else + FF_UNUSED(options, devices); + return "Fastfetch was compiled without DBus support"; +#endif +} diff --git a/src/detection/bluetooth/bluetooth_nosupport.c b/src/detection/bluetooth/bluetooth_nosupport.c new file mode 100644 index 0000000..ac93cee --- /dev/null +++ b/src/detection/bluetooth/bluetooth_nosupport.c @@ -0,0 +1,5 @@ +#include "bluetooth.h" + +const char* ffDetectBluetooth(FF_A_UNUSED FFBluetoothOptions* options, FF_A_UNUSED FFlist* devices /* FFBluetoothResult */) { + return "Not supported on this platform"; +} diff --git a/src/detection/bluetooth/bluetooth_windows.c b/src/detection/bluetooth/bluetooth_windows.c new file mode 100644 index 0000000..8932aef --- /dev/null +++ b/src/detection/bluetooth/bluetooth_windows.c @@ -0,0 +1,223 @@ +#include "bluetooth.h" +#include "common/library.h" +#include "common/mallocHelper.h" +#include "common/windows/unicode.h" + +#define INITGUID +#include <windows.h> +#include <bluetoothapis.h> +#include <cfgmgr32.h> +#include <devpkey.h> + +#pragma GCC diagnostic ignored "-Wpointer-sign" + +// https://github.com/wine-mirror/wine/blob/ab6f4584b89f28504b0b277c0b4c723a86b4d6b7/include/ddk/bthguid.h#L4 +/* DEVPROP_TYPE_STRING */ +DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_DeviceAddress, 0x2bd67d8b, 0x8beb, 0x48d5, 0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a, 1); +/* DEVPROP_TYPE_UINT32 */ +DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_ClassOfDevice, 0x2bd67d8b, 0x8beb, 0x48d5, 0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a, 10); +/* DEVPROP_TYPE_FILETIME */ +DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_LastConnectedTime, 0x2bd67d8b, 0x8beb, 0x48d5, 0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a, 11); +/* DEVPROP_TYPE_GUID */ +DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_ServiceGUID, 0x2bd67d8b, 0x8beb, 0x48d5, 0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, 0x04, 0x0a, 2); +/* DEVPROP_TYPE_UINT8 */ +DEFINE_DEVPROPKEY(DEVPKEY_Bluetooth_BatteryLevel, 0x104ea319, 0x6ee2, 0x4701, 0xbd, 0x47, 0x8d, 0xdb, 0xf4, 0x25, 0xbb, 0xe5, 2); + +// TODO: use CM API to fetch bluetooth devices instead of BluetoothFindFirstDevice, if we find DEVPKEY_Bluetooth_IsConnected or similar +#define GUID_DEVCLASS_BLUETOOTH_STRING L"{e0cbf06c-cd8b-4647-bb8a-263b43f0f974}" // Found in <devguid.h> +#define GUID_DEVCLASS_MEDIA_STRING L"{4d36e96c-e325-11ce-bfc1-08002be10318}" // Found in <devguid.h> + +static const char* ffBluetoothDetectBattery(FFlist* devices) { + ULONG idListLength = 0; + CONFIGRET status = CM_Get_Device_ID_List_SizeW(&idListLength, GUID_DEVCLASS_MEDIA_STRING, CM_GETIDLIST_FILTER_PRESENT); + if (status != CR_SUCCESS) { + return "CM_Get_Device_ID_List_SizeW failed"; + } + + if (idListLength == 0) { + return NULL; + } + + wchar_t* FF_AUTO_FREE idList = (wchar_t*) malloc((size_t) idListLength * sizeof(wchar_t)); + if (!idList) { + return "malloc() failed"; + } + + status = CM_Get_Device_ID_ListW(GUID_DEVCLASS_MEDIA_STRING, idList, idListLength, CM_GETIDLIST_FILTER_PRESENT); + if (status != CR_SUCCESS) { + return "CM_Get_Device_ID_ListW failed"; + } + + for (const wchar_t* deviceId = idList; *deviceId; deviceId += wcslen(deviceId) + 1) { + DEVINST devInst = 0; + + // Hands-Free profile service; headsets often expose battery level through this media device node rather than the Bluetooth device node + // BthHFEnum + if (CM_Locate_DevNodeW(&devInst, (DEVINSTID_W) deviceId, CM_LOCATE_DEVNODE_NORMAL) != CR_SUCCESS) { + continue; + } + + uint8_t battery = 0; + { + DEVPROPTYPE devPropertyType = DEVPROP_TYPE_EMPTY; + ULONG propertySize = sizeof(battery); + if (CM_Get_DevNode_PropertyW(devInst, &DEVPKEY_Bluetooth_BatteryLevel, &devPropertyType, (PBYTE) &battery, &propertySize, 0) != CR_SUCCESS || devPropertyType != DEVPROP_TYPE_BYTE || propertySize != sizeof(battery)) { + continue; + } + } + + WCHAR deviceAddress[13]; // 6 bytes in hex + null terminator + { + DEVPROPTYPE devPropertyType = DEVPROP_TYPE_EMPTY; + ULONG propertySize = sizeof(deviceAddress); + if (CM_Get_DevNode_PropertyW(devInst, &DEVPKEY_Bluetooth_DeviceAddress, &devPropertyType, (PBYTE) deviceAddress, &propertySize, 0) != CR_SUCCESS || devPropertyType != DEVPROP_TYPE_STRING || propertySize != sizeof(deviceAddress)) { + continue; + } + } + + FF_LIST_FOR_EACH (FFBluetoothResult, bt, *devices) { + if (deviceAddress[0] == bt->address.chars[0] && + deviceAddress[1] == bt->address.chars[1] && + deviceAddress[2] == bt->address.chars[3] && + deviceAddress[3] == bt->address.chars[4] && + deviceAddress[4] == bt->address.chars[6] && + deviceAddress[5] == bt->address.chars[7] && + deviceAddress[6] == bt->address.chars[9] && + deviceAddress[7] == bt->address.chars[10] && + deviceAddress[8] == bt->address.chars[12] && + deviceAddress[9] == bt->address.chars[13] && + deviceAddress[10] == bt->address.chars[15] && + deviceAddress[11] == bt->address.chars[16]) { + bt->battery = battery; + break; + } + } + } + + return NULL; +} + +const char* ffDetectBluetooth(FFBluetoothOptions* options, FFlist* devices /* FFBluetoothResult */) { + FF_LIBRARY_LOAD_MESSAGE(bluetoothapis, "bluetoothapis.dll", 1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindFirstDevice) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindNextDevice) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindDeviceClose) + + BLUETOOTH_DEVICE_INFO btdi = { + .dwSize = sizeof(btdi) + }; + HBLUETOOTH_DEVICE_FIND hFind = ffBluetoothFindFirstDevice(&(BLUETOOTH_DEVICE_SEARCH_PARAMS) { + .fReturnConnected = TRUE, + .fReturnRemembered = options->showDisconnected, + .fReturnAuthenticated = options->showDisconnected, + .dwSize = sizeof(BLUETOOTH_DEVICE_SEARCH_PARAMS) }, + &btdi); + if (!hFind) { + if (GetLastError() == ERROR_NO_MORE_ITEMS) { + return NULL; + } + return "BluetoothFindFirstDevice() failed"; + } + + do { + if (!options->showDisconnected && !btdi.fConnected) { + continue; + } + + FFBluetoothResult* device = FF_LIST_ADD(FFBluetoothResult, *devices); + ffStrbufInitWS(&device->name, btdi.szName); + ffStrbufInitF(&device->address, "%02X:%02X:%02X:%02X:%02X:%02X", btdi.Address.rgBytes[5], btdi.Address.rgBytes[4], btdi.Address.rgBytes[3], btdi.Address.rgBytes[2], btdi.Address.rgBytes[1], btdi.Address.rgBytes[0]); + ffStrbufInit(&device->type); + device->battery = 0; + device->connected = !!btdi.fConnected; + + // https://btprodspecificationrefs.blob.core.windows.net/assigned-numbers/Assigned%20Number%20Types/Assigned%20Numbers.pdf + + if (BitTest(&btdi.ulClassofDevice, 13)) { + ffStrbufAppendS(&device->type, "Limited Discoverable Mode, "); + } + if (BitTest(&btdi.ulClassofDevice, 14)) { + ffStrbufAppendS(&device->type, "LE audio, "); + } + if (BitTest(&btdi.ulClassofDevice, 15)) { + ffStrbufAppendS(&device->type, "Reserved for future use, "); + } + if (BitTest(&btdi.ulClassofDevice, 16)) { + ffStrbufAppendS(&device->type, "Positioning, "); + } + if (BitTest(&btdi.ulClassofDevice, 17)) { + ffStrbufAppendS(&device->type, "Networking, "); + } + if (BitTest(&btdi.ulClassofDevice, 18)) { + ffStrbufAppendS(&device->type, "Rendering, "); + } + if (BitTest(&btdi.ulClassofDevice, 19)) { + ffStrbufAppendS(&device->type, "Capturing, "); + } + if (BitTest(&btdi.ulClassofDevice, 20)) { + ffStrbufAppendS(&device->type, "Object Transfer, "); + } + if (BitTest(&btdi.ulClassofDevice, 21)) { + ffStrbufAppendS(&device->type, "Audio, "); + } + if (BitTest(&btdi.ulClassofDevice, 22)) { + ffStrbufAppendS(&device->type, "Telephony, "); + } + if (BitTest(&btdi.ulClassofDevice, 23)) { + ffStrbufAppendS(&device->type, "Information, "); + } + + if (device->type.length == 0) { + uint32_t majorDeviceClasses = (btdi.ulClassofDevice >> 8) & ~(UINT32_MAX << 5); + switch (majorDeviceClasses) { + case 0b00000: + ffStrbufAppendS(&device->type, "Miscellaneous"); + break; + case 0b00001: + ffStrbufAppendS(&device->type, "Computer"); + break; + case 0b00010: + ffStrbufAppendS(&device->type, "Phone"); + break; + case 0b00011: + ffStrbufAppendS(&device->type, "LAN/Network Access point"); + break; + case 0b00100: + ffStrbufAppendS(&device->type, "Audio/Video"); + break; + case 0b00101: + ffStrbufAppendS(&device->type, "Peripheral"); + break; + case 0b00110: + ffStrbufAppendS(&device->type, "Imaging"); + break; + case 0b00111: + ffStrbufAppendS(&device->type, "Wearable"); + break; + case 0b01000: + ffStrbufAppendS(&device->type, "Toy"); + break; + case 0b01001: + ffStrbufAppendS(&device->type, "Health"); + break; + case 0b11111: + ffStrbufAppendS(&device->type, "Uncategorized"); + break; + default: + ffStrbufAppendS(&device->type, "Unknown"); + break; + } + } else { + ffStrbufTrimRight(&device->type, ' '); + ffStrbufTrimRight(&device->type, ','); + } + } while (ffBluetoothFindNextDevice(hFind, &btdi)); + + ffBluetoothFindDeviceClose(hFind); + + if (devices->length > 0) { + ffBluetoothDetectBattery(devices); + } + + return NULL; +} diff --git a/src/detection/bluetoothradio/bluetoothradio.c b/src/detection/bluetoothradio/bluetoothradio.c new file mode 100644 index 0000000..117e225 --- /dev/null +++ b/src/detection/bluetoothradio/bluetoothradio.c @@ -0,0 +1,161 @@ +#include "bluetoothradio.h" + +// https://github.com/ziglang/zig/blob/a84951465b409495095a9598db0cae745f34fa7b/lib/libc/include/any-windows-any/bthdef.h#L187-L236 + +#define BTH_MFG_ERICSSON 0 +#define BTH_MFG_NOKIA 1 +#define BTH_MFG_INTEL 2 +#define BTH_MFG_IBM 3 +#define BTH_MFG_TOSHIBA 4 +#define BTH_MFG_3COM 5 +#define BTH_MFG_MICROSOFT 6 +#define BTH_MFG_LUCENT 7 +#define BTH_MFG_MOTOROLA 8 +#define BTH_MFG_INFINEON 9 +#define BTH_MFG_CSR 10 +#define BTH_MFG_SILICONWAVE 11 +#define BTH_MFG_DIGIANSWER 12 +#define BTH_MFG_TI 13 +#define BTH_MFG_PARTHUS 14 +#define BTH_MFG_BROADCOM 15 +#define BTH_MFG_MITEL 16 +#define BTH_MFG_WIDCOMM 17 +#define BTH_MFG_ZEEVO 18 +#define BTH_MFG_ATMEL 19 +#define BTH_MFG_MITSIBUSHI 20 +#define BTH_MFG_RTX_TELECOM 21 +#define BTH_MFG_KC_TECHNOLOGY 22 +#define BTH_MFG_NEWLOGIC 23 +#define BTH_MFG_TRANSILICA 24 +#define BTH_MFG_ROHDE_SCHWARZ 25 +#define BTH_MFG_TTPCOM 26 +#define BTH_MFG_SIGNIA 27 +#define BTH_MFG_CONEXANT 28 +#define BTH_MFG_QUALCOMM 29 +#define BTH_MFG_INVENTEL 30 +#define BTH_MFG_AVM_BERLIN 31 +#define BTH_MFG_BANDSPEED 32 +#define BTH_MFG_MANSELLA 33 +#define BTH_MFG_NEC 34 +#define BTH_MFG_WAVEPLUS_TECHNOLOGY_CO 35 +#define BTH_MFG_ALCATEL 36 +#define BTH_MFG_PHILIPS_SEMICONDUCTOR 37 +#define BTH_MFG_C_TECHNOLOGIES 38 +#define BTH_MFG_OPEN_INTERFACE 39 +#define BTH_MFG_RF_MICRO_DEVICES 40 +#define BTH_MFG_HITACHI 41 +#define BTH_MFG_SYMBOL_TECHNOLOGIES 42 +#define BTH_MFG_TENOVIS 43 +#define BTH_MFG_MACRONIX_INTERNATIONAL 44 +#define BTH_MFG_MARVELL 72 +#define BTH_MFG_APPLE 76 +#define BTH_MFG_NORDIC_SEMICONDUCTORS_ASA 89 +#define BTH_MFG_ARUBA_NETWORKS 283 +#define BTH_MFG_INTERNAL_USE 65535 + +const char* ffBluetoothRadioGetVendor(uint32_t manufacturerId) { + switch (manufacturerId) { + case BTH_MFG_ERICSSON: + return "Ericsson"; + case BTH_MFG_NOKIA: + return "Nokia"; + case BTH_MFG_INTEL: + return "Intel"; + case BTH_MFG_IBM: + return "IBM"; + case BTH_MFG_TOSHIBA: + return "Toshiba"; + case BTH_MFG_3COM: + return "3Com"; + case BTH_MFG_MICROSOFT: + return "Microsoft"; + case BTH_MFG_LUCENT: + return "Lucent"; + case BTH_MFG_MOTOROLA: + return "Motorola"; + case BTH_MFG_INFINEON: + return "Infineon"; + case BTH_MFG_CSR: + return "CSR"; + case BTH_MFG_SILICONWAVE: + return "Silicon-Wave"; + case BTH_MFG_DIGIANSWER: + return "Digi-Answer"; + case BTH_MFG_TI: + return "Ti"; + case BTH_MFG_PARTHUS: + return "Parthus"; + case BTH_MFG_BROADCOM: + return "Broadcom"; + case BTH_MFG_MITEL: + return "Mitel"; + case BTH_MFG_WIDCOMM: + return "Widcomm"; + case BTH_MFG_ZEEVO: + return "Zeevo"; + case BTH_MFG_ATMEL: + return "Atmel"; + case BTH_MFG_MITSIBUSHI: + return "Mitsubishi"; + case BTH_MFG_RTX_TELECOM: + return "RTX Telecom"; + case BTH_MFG_KC_TECHNOLOGY: + return "KC Technology"; + case BTH_MFG_NEWLOGIC: + return "Newlogic"; + case BTH_MFG_TRANSILICA: + return "Transilica"; + case BTH_MFG_ROHDE_SCHWARZ: + return "Rohde-Schwarz"; + case BTH_MFG_TTPCOM: + return "TTPCom"; + case BTH_MFG_SIGNIA: + return "Signia"; + case BTH_MFG_CONEXANT: + return "Conexant"; + case BTH_MFG_QUALCOMM: + return "Qualcomm"; + case BTH_MFG_INVENTEL: + return "Inventel"; + case BTH_MFG_AVM_BERLIN: + return "AVM Berlin"; + case BTH_MFG_BANDSPEED: + return "Bandspeed"; + case BTH_MFG_MANSELLA: + return "Mansella"; + case BTH_MFG_NEC: + return "NEC"; + case BTH_MFG_WAVEPLUS_TECHNOLOGY_CO: + return "Waveplus"; + case BTH_MFG_ALCATEL: + return "Alcatel"; + case BTH_MFG_PHILIPS_SEMICONDUCTOR: + return "Philips Semiconductors"; + case BTH_MFG_C_TECHNOLOGIES: + return "C Technologies"; + case BTH_MFG_OPEN_INTERFACE: + return "Open Interface"; + case BTH_MFG_RF_MICRO_DEVICES: + return "RF Micro Devices"; + case BTH_MFG_HITACHI: + return "Hitachi"; + case BTH_MFG_SYMBOL_TECHNOLOGIES: + return "Symbol Technologies"; + case BTH_MFG_TENOVIS: + return "Tenovis"; + case BTH_MFG_MACRONIX_INTERNATIONAL: + return "Macronix International"; + case BTH_MFG_MARVELL: + return "Marvell"; + case BTH_MFG_APPLE: + return "Apple"; + case BTH_MFG_NORDIC_SEMICONDUCTORS_ASA: + return "Nordic Semiconductor ASA"; + case BTH_MFG_ARUBA_NETWORKS: + return "Aruba Networks"; + case BTH_MFG_INTERNAL_USE: + return "Internal Use"; + default: + return "Unknown"; + } +} diff --git a/src/detection/bluetoothradio/bluetoothradio.h b/src/detection/bluetoothradio/bluetoothradio.h new file mode 100644 index 0000000..a3c1208 --- /dev/null +++ b/src/detection/bluetoothradio/bluetoothradio.h @@ -0,0 +1,18 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/bluetoothradio/option.h" + +typedef struct FFBluetoothRadioResult { + FFstrbuf name; + FFstrbuf address; + FFstrbuf vendor; + int32_t lmpVersion; + int32_t lmpSubversion; + bool enabled; + bool discoverable; + bool connectable; +} FFBluetoothRadioResult; + +const char* ffDetectBluetoothRadio(FFlist* devices /* FFBluetoothRadioResult */); +const char* ffBluetoothRadioGetVendor(uint32_t manufacturerId); diff --git a/src/detection/bluetoothradio/bluetoothradio_apple.m b/src/detection/bluetoothradio/bluetoothradio_apple.m new file mode 100644 index 0000000..c5154ab --- /dev/null +++ b/src/detection/bluetoothradio/bluetoothradio_apple.m @@ -0,0 +1,69 @@ +#include "bluetoothradio.h" +#include "common/processing.h" + +#import <IOBluetooth/IOBluetooth.h> + +// For some reason the official declaration of IOBluetoothHostController doesn't include property `controllers` +@interface IOBluetoothHostController() ++ (id)controllers; +@end + +const char* ffDetectBluetoothRadio(FFlist* devices /* FFBluetoothRadioResult */) +{ + NSArray<IOBluetoothHostController*>* ctrls = IOBluetoothHostController.controllers; + if(!ctrls) + return "IOBluetoothHostController.controllers returns nil"; + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + if (ffProcessAppendStdOut(&buffer, (char* const[]) { + "system_profiler", + "SPBluetoothDataType", + "-xml", + "-detailLevel", + "basic", + NULL + }) != NULL) + return "Starting `system_profiler SPBluetoothDataType -xml -detailLevel basic` failed"; + + NSArray* arr = [NSPropertyListSerialization propertyListWithData:[NSData dataWithBytes:buffer.chars length:buffer.length] + options:NSPropertyListImmutable + format:nil + error:nil]; + if (!arr || !arr.count) + return "system_profiler SPBluetoothDataType returned an empty array"; + + for (IOBluetoothHostController* ctrl in ctrls) + { + FFBluetoothRadioResult* device = FF_LIST_ADD(FFBluetoothRadioResult, *devices); + ffStrbufInitS(&device->name, ctrl.nameAsString.UTF8String); + ffStrbufInitS(&device->address, ctrl.addressAsString.UTF8String); + ffStrbufInitStatic(&device->vendor, "Apple"); + device->lmpVersion = INT_MIN; + device->lmpSubversion = INT_MIN; + device->enabled = ctrl.powerState == kBluetoothHCIPowerStateON; + device->discoverable = false; + device->connectable = true; + + for (NSDictionary* itemDict in arr[0][@"_items"]) + { + NSDictionary* props = itemDict[@"controller_properties"]; + if (!props) continue; + + if (![ctrl.addressAsString isEqualToString:props[@"controller_address"]]) continue; + + NSString* services = props[@"controller_supportedServices"]; + if ([services containsString:@" LEA "]) + device->lmpVersion = -11; + else if ([services containsString:@" GATT "]) + device->lmpVersion = -6; + + device->discoverable = ![props[@"controller_discoverable"] isEqualToString:@"attrib_off"]; + ffStrbufSetS(&device->vendor, ((NSString*) props[@"controller_vendorID"]).UTF8String); + ffStrbufSubstrAfterFirstC(&device->vendor, '('); + ffStrbufTrimRight(&device->vendor, ')'); + break; + } + } + + return NULL; +} diff --git a/src/detection/bluetoothradio/bluetoothradio_linux.c b/src/detection/bluetoothradio/bluetoothradio_linux.c new file mode 100644 index 0000000..a7d92a4 --- /dev/null +++ b/src/detection/bluetoothradio/bluetoothradio_linux.c @@ -0,0 +1,153 @@ +#include "bluetoothradio.h" +#include "common/strutil.h" + +#ifdef FF_HAVE_DBUS + #include "common/dbus.h" + #include "common/io.h" + +/* Example dbus reply: +array [ + dict entry( + string "Address" + variant string "XX:XX:XX:XX:XX:XX" + ) + dict entry( + string "Name" + variant string "xxxxxxxx" + ) + dict entry( + string "Powered" + variant boolean true + ) + dict entry( + string "PowerState" + variant string "on" + ) + dict entry( + string "Manufacturer" + variant uint16 2 + ) + dict entry( + string "Version" + variant byte 12 + ) +] +*/ + +static const char* detectBluetoothProperty(FFBluetoothRadioResult* device, FFDBusData* dbus, DBusMessageIter* iter) { + if (dbus->lib->ffdbus_message_iter_get_arg_type(iter) != DBUS_TYPE_DICT_ENTRY) { + return "Expected dict entry"; + } + + DBusMessageIter dictIter; + dbus->lib->ffdbus_message_iter_recurse(iter, &dictIter); + + if (dbus->lib->ffdbus_message_iter_get_arg_type(&dictIter) != DBUS_TYPE_STRING) { + return "Expected dict entry key to be a string"; + } + + const char* deviceProperty; + dbus->lib->ffdbus_message_iter_get_basic(&dictIter, &deviceProperty); + + dbus->lib->ffdbus_message_iter_next(&dictIter); + + if (ffStrEquals(deviceProperty, "Address")) { + ffDBusGetString(dbus, &dictIter, &device->address); + } else if (ffStrEquals(deviceProperty, "Alias")) { + ffDBusGetString(dbus, &dictIter, &device->name); + } else if (ffStrEquals(deviceProperty, "Manufacturer")) { + uint64_t vendorId; + if (ffDBusGetUint(dbus, &dictIter, &vendorId)) { + ffStrbufSetStatic(&device->vendor, ffBluetoothRadioGetVendor((uint32_t) vendorId)); + } + } else if (ffStrEquals(deviceProperty, "Version")) { + uint64_t version; + if (ffDBusGetUint(dbus, &dictIter, &version)) { + device->lmpVersion = (int32_t) version; + } + } else if (ffStrEquals(deviceProperty, "Powered")) { + ffDBusGetBool(dbus, &dictIter, &device->enabled); + } else if (ffStrEquals(deviceProperty, "Discoverable")) { + ffDBusGetBool(dbus, &dictIter, &device->discoverable); + } else if (ffStrEquals(deviceProperty, "Pairable")) { + ffDBusGetBool(dbus, &dictIter, &device->connectable); + } + + return NULL; +} + +static const char* detectBluetoothRoot(FFBluetoothRadioResult* device, const char* hciName, FFDBusData* dbus) { + char objPath[300]; + snprintf(objPath, sizeof(objPath), "/org/bluez/%s", hciName); + + DBusMessage* properties = ffDBusGetMethodReply(dbus, "org.bluez", objPath, "org.freedesktop.DBus.Properties", "GetAll", "org.bluez.Adapter1", NULL); + if (!properties) { + return "Failed to call org.freedesktop.DBus.Properties.GetAll"; + } + + DBusMessageIter rootIter; + if (!dbus->lib->ffdbus_message_iter_init(properties, &rootIter)) { + dbus->lib->ffdbus_message_unref(properties); + return "Failed to get root iterator of org.freedesktop.DBus.Properties.GetAll"; + } + + if (dbus->lib->ffdbus_message_iter_get_arg_type(&rootIter) != DBUS_TYPE_ARRAY) { + dbus->lib->ffdbus_message_unref(properties); + return "Expected array"; + } + + DBusMessageIter arrayIter; + dbus->lib->ffdbus_message_iter_recurse(&rootIter, &arrayIter); + + do { + detectBluetoothProperty(device, dbus, &arrayIter); + } while (dbus->lib->ffdbus_message_iter_next(&arrayIter)); + + dbus->lib->ffdbus_message_unref(properties); + return NULL; +} + +static const char* detectBluetooth(FFlist* devices) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/bluetooth"); + if (dirp == NULL) { + return "Failed to open /sys/class/bluetooth"; + } + + FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {}; + const char* error = ffDBusLoadData(DBUS_BUS_SYSTEM, &dbus); + if (error) { + return error; + } + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + if (strchr(entry->d_name, ':') != NULL) { // ignore connected devices + continue; + } + + FFBluetoothRadioResult* device = FF_LIST_ADD(FFBluetoothRadioResult, *devices); + ffStrbufInit(&device->name); + ffStrbufInit(&device->address); + ffStrbufInitStatic(&device->vendor, "Unknown"); + device->lmpVersion = INT_MIN; + device->lmpSubversion = INT_MIN; + device->enabled = false; + detectBluetoothRoot(device, entry->d_name, &dbus); + } + return NULL; +} + +#endif + +const char* ffDetectBluetoothRadio(FFlist* devices /* FFBluetoothRadioResult */) { +#ifdef FF_HAVE_DBUS + return detectBluetooth(devices); +#else + FF_UNUSED(devices) + return "Fastfetch was compiled without DBus support"; +#endif +} diff --git a/src/detection/bluetoothradio/bluetoothradio_nosupport.c b/src/detection/bluetoothradio/bluetoothradio_nosupport.c new file mode 100644 index 0000000..08fb6f8 --- /dev/null +++ b/src/detection/bluetoothradio/bluetoothradio_nosupport.c @@ -0,0 +1,5 @@ +#include "bluetoothradio.h" + +const char* ffDetectBluetoothRadio(FF_A_UNUSED FFlist* devices /* FFBluetoothRadioResult */) { + return "Not supported on this platform"; +} diff --git a/src/detection/bluetoothradio/bluetoothradio_windows.c b/src/detection/bluetoothradio/bluetoothradio_windows.c new file mode 100644 index 0000000..f0b55d4 --- /dev/null +++ b/src/detection/bluetoothradio/bluetoothradio_windows.c @@ -0,0 +1,99 @@ +#include "bluetoothradio.h" +#include "common/library.h" +#include "common/io.h" +#include "common/windows/unicode.h" + +#include <windows.h> +#include <bluetoothapis.h> +#include <winioctl.h> + +// #include <bthioctl.h> + +#define BTH_IOCTL_BASE 0 +#define BTH_CTL(id) CTL_CODE(FILE_DEVICE_BLUETOOTH, (id), METHOD_BUFFERED, FILE_ANY_ACCESS) +#define IOCTL_BTH_GET_LOCAL_INFO BTH_CTL(BTH_IOCTL_BASE + 0x00) +#define LMP_LE_SUPPORTED(x) ((x >> 38) & 1) + +typedef struct _BTH_RADIO_INFO { + // Supported LMP features of the radio. Use LMP_XXX() to extract + // the desired bits. + ULONGLONG lmpSupportedFeatures; + + // Manufacturer ID (possibly BTH_MFG_XXX) + USHORT mfg; + + // LMP subversion + USHORT lmpSubversion; + + // LMP version + UCHAR lmpVersion; +} FF_A_PACKED BTH_RADIO_INFO; + +typedef struct _BTH_LOCAL_RADIO_INFO { + // Local BTH_ADDR, class of device, and radio name + BTH_DEVICE_INFO localInfo; + + // Combo of LOCAL_RADIO_XXX values + ULONG flags; + + // HCI revision, see core spec + USHORT hciRevision; + + // HCI version, see core spec + UCHAR hciVersion; + + // More information about the local radio (LMP, MFG) + BTH_RADIO_INFO radioInfo; +} FF_A_PACKED BTH_LOCAL_RADIO_INFO; +static_assert(sizeof(BTH_LOCAL_RADIO_INFO) == 292, "BTH_LOCAL_RADIO_INFO should be 292 bytes"); + +#pragma GCC diagnostic ignored "-Wpointer-sign" + +const char* ffDetectBluetoothRadio(FFlist* devices /* FFBluetoothRadioResult */) { + // Actually bluetoothapis.dll, but it's missing on Windows 7 + FF_LIBRARY_LOAD_MESSAGE(bluetoothapis, "bluetoothapis.dll", 1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindFirstRadio) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindNextRadio) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothFindRadioClose) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothIsConnectable) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(bluetoothapis, BluetoothIsDiscoverable) + + HANDLE hRadio = NULL; + HBLUETOOTH_DEVICE_FIND hFind = ffBluetoothFindFirstRadio(&(BLUETOOTH_FIND_RADIO_PARAMS) { + .dwSize = sizeof(BLUETOOTH_FIND_RADIO_PARAMS) }, + &hRadio); + if (!hFind) { + if (GetLastError() == ERROR_NO_MORE_ITEMS) { + return "No Bluetooth radios found or service disabled"; + } else { + return "BluetoothFindFirstRadio() failed"; + } + } + + do { + BTH_LOCAL_RADIO_INFO blri; + DWORD returned; + if (!DeviceIoControl(hRadio, IOCTL_BTH_GET_LOCAL_INFO, NULL, 0, &blri, sizeof(blri), &returned, NULL)) { + continue; + } + + FFBluetoothRadioResult* device = FF_LIST_ADD(FFBluetoothRadioResult, *devices); + ffStrbufInitS(&device->name, blri.localInfo.name); + + BLUETOOTH_ADDRESS_STRUCT addr = { .ullLong = blri.localInfo.address }; + ffStrbufInitF(&device->address, "%02X:%02X:%02X:%02X:%02X:%02X", addr.rgBytes[5], addr.rgBytes[4], addr.rgBytes[3], addr.rgBytes[2], addr.rgBytes[1], addr.rgBytes[0]); + + device->lmpVersion = blri.radioInfo.lmpVersion; + device->lmpSubversion = blri.radioInfo.lmpSubversion; + ffStrbufInitStatic(&device->vendor, ffBluetoothRadioGetVendor(blri.radioInfo.mfg)); + device->enabled = true; + device->connectable = ffBluetoothIsConnectable(hRadio); + device->discoverable = ffBluetoothIsDiscoverable(hRadio); + + NtClose(hRadio); + } while (ffBluetoothFindNextRadio(hFind, &hRadio)); + + ffBluetoothFindRadioClose(hFind); + + return NULL; +} diff --git a/src/detection/board/board.h b/src/detection/board/board.h new file mode 100644 index 0000000..c092a7f --- /dev/null +++ b/src/detection/board/board.h @@ -0,0 +1,13 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/board/option.h" + +typedef struct FFBoardResult { + FFstrbuf name; + FFstrbuf vendor; + FFstrbuf version; + FFstrbuf serial; +} FFBoardResult; + +const char* ffDetectBoard(FFBoardResult* board); diff --git a/src/detection/board/board_android.c b/src/detection/board/board_android.c new file mode 100644 index 0000000..b5978d1 --- /dev/null +++ b/src/detection/board/board_android.c @@ -0,0 +1,9 @@ +#include "board.h" +#include "common/settings.h" + +const char* ffDetectBoard(FFBoardResult* board) { + if (!ffSettingsGetAndroidProperty("ro.product.board", &board->name)) { + ffSettingsGetAndroidProperty("ro.board.platform", &board->name); + } + return NULL; +} diff --git a/src/detection/board/board_apple.c b/src/detection/board/board_apple.c new file mode 100644 index 0000000..e7c4d38 --- /dev/null +++ b/src/detection/board/board_apple.c @@ -0,0 +1,32 @@ +#include "board.h" + +#include "common/apple/cf_helpers.h" + +const char* ffDetectBoard(FFBoardResult* result) { + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t service = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceMatching("IOPlatformExpertDevice")); + if (!service) { + return "No IOPlatformExpertDevice found"; + } + + FF_CFTYPE_AUTO_RELEASE CFTypeRef boardId = IORegistryEntryCreateCFProperty(service, CFSTR("board-id"), kCFAllocatorDefault, kNilOptions); + if (boardId) { + ffCfStrGetString(boardId, &result->name); + } else { + io_name_t name; + if (IORegistryEntryGetName(service, name) == kIOReturnSuccess) { + ffStrbufSetS(&result->name, name); + } + } + + FF_CFTYPE_AUTO_RELEASE CFStringRef version = IORegistryEntryCreateCFProperty(service, CFSTR("version"), kCFAllocatorDefault, kNilOptions); + if (version) { + ffCfStrGetString(version, &result->version); + } + + FF_CFTYPE_AUTO_RELEASE CFTypeRef manufacturer = IORegistryEntryCreateCFProperty(service, CFSTR("manufacturer"), kCFAllocatorDefault, kNilOptions); + if (manufacturer) { + ffCfStrGetString(manufacturer, &result->vendor); + } + + return NULL; +} diff --git a/src/detection/board/board_bsd.c b/src/detection/board/board_bsd.c new file mode 100644 index 0000000..2e0e417 --- /dev/null +++ b/src/detection/board/board_bsd.c @@ -0,0 +1,15 @@ +#include "board.h" +#include "common/settings.h" +#include "common/smbios.h" + +const char* ffDetectBoard(FFBoardResult* result) { + ffSettingsGetFreeBSDKenv("smbios.planar.product", &result->name); + ffCleanUpSmbiosValue(&result->name); + ffSettingsGetFreeBSDKenv("smbios.planar.serial", &result->serial); + ffCleanUpSmbiosValue(&result->serial); + ffSettingsGetFreeBSDKenv("smbios.planar.maker", &result->vendor); + ffCleanUpSmbiosValue(&result->vendor); + ffSettingsGetFreeBSDKenv("smbios.planar.version", &result->version); + ffCleanUpSmbiosValue(&result->version); + return NULL; +} diff --git a/src/detection/board/board_linux.c b/src/detection/board/board_linux.c new file mode 100644 index 0000000..52b3df2 --- /dev/null +++ b/src/detection/board/board_linux.c @@ -0,0 +1,26 @@ +#include "board.h" +#include "common/io.h" +#include "common/smbios.h" + +const char* ffDetectBoard(FFBoardResult* board) { + if (ffGetSmbiosValue("/sys/devices/virtual/dmi/id/board_name", "/sys/class/dmi/id/board_name", &board->name)) { + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/board_serial", "/sys/class/dmi/id/board_serial", &board->serial); + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/board_vendor", "/sys/class/dmi/id/board_vendor", &board->vendor); + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/board_version", "/sys/class/dmi/id/board_version", &board->version); + } else if (ffReadFileBuffer("/sys/firmware/devicetree/base/smbios/smbios/baseboard/product", &board->name)) { + ffStrbufTrimRight(&board->name, '\0'); + if (ffReadFileBuffer("/sys/firmware/devicetree/base/smbios/smbios/baseboard/manufacturer", &board->vendor)) { + ffStrbufTrimRight(&board->vendor, '\0'); + } + } else if (ffReadFileBuffer("/sys/firmware/devicetree/base/board", &board->name)) { + ffStrbufTrimRightSpace(&board->name); + } else if (ffReadFileBuffer("/sys/firmware/devicetree/base/compatible", &board->vendor)) { + uint32_t comma = ffStrbufFirstIndexC(&board->vendor, ','); + if (comma < board->vendor.length) { + ffStrbufSetS(&board->name, board->vendor.chars + comma + 1); + ffStrbufTrimRightSpace(&board->name); + ffStrbufSubstrBefore(&board->vendor, comma); + } + } + return NULL; +} diff --git a/src/detection/board/board_nbsd.c b/src/detection/board/board_nbsd.c new file mode 100644 index 0000000..cc3ff93 --- /dev/null +++ b/src/detection/board/board_nbsd.c @@ -0,0 +1,20 @@ +#include "board.h" +#include "common/sysctl.h" +#include "common/smbios.h" + +const char* ffDetectBoard(FFBoardResult* board) { + if (ffSysctlGetString("machdep.dmi.board-product", &board->name) == NULL) { + ffCleanUpSmbiosValue(&board->name); + } + if (ffSysctlGetString("machdep.dmi.board-version", &board->version) == NULL) { + ffCleanUpSmbiosValue(&board->version); + } + if (ffSysctlGetString("machdep.dmi.board-vendor", &board->vendor) == NULL) { + ffCleanUpSmbiosValue(&board->vendor); + } + if (ffSysctlGetString("machdep.dmi.board-serial", &board->serial) == NULL) { + ffCleanUpSmbiosValue(&board->serial); + } + + return NULL; +} diff --git a/src/detection/board/board_nosupport.c b/src/detection/board/board_nosupport.c new file mode 100644 index 0000000..befc630 --- /dev/null +++ b/src/detection/board/board_nosupport.c @@ -0,0 +1,5 @@ +#include "board.h" + +const char* ffDetectBoard(FF_A_UNUSED FFBoardResult* board) { + return "Not supported on this platform"; +} diff --git a/src/detection/board/board_windows.c b/src/detection/board/board_windows.c new file mode 100644 index 0000000..079e64d --- /dev/null +++ b/src/detection/board/board_windows.c @@ -0,0 +1,46 @@ +#include "board.h" +#include "common/smbios.h" + +typedef struct FFSmbiosBaseboard { + FFSmbiosHeader Header; + + uint8_t Manufacturer; // string + uint8_t Product; // string + uint8_t Version; // string + uint8_t SerialNumber; // string + uint8_t AssetTag; // string + uint8_t FeatureFlags; // bit field + uint8_t LocationInChassis; // string + uint16_t ChassisHandle; // varies + uint8_t BoardType; // enum + uint8_t NumberOfContainedObjectHandles; // varies + uint16_t ContainedObjectHandles[]; // varies +} FF_A_PACKED FFSmbiosBaseboard; + +static_assert(offsetof(FFSmbiosBaseboard, ContainedObjectHandles) == 0x0F, + "FFSmbiosBaseboard: Wrong struct alignment"); + +const char* ffDetectBoard(FFBoardResult* board) { + const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable(); + if (!smbiosTable) { + return "Failed to get SMBIOS data"; + } + + const FFSmbiosBaseboard* data = (const FFSmbiosBaseboard*) (*smbiosTable)[FF_SMBIOS_TYPE_BASEBOARD_INFO]; + if (!data) { + return "Baseboard information section is not found in SMBIOS data"; + } + + const char* strings = (const char*) data + data->Header.Length; + + ffStrbufSetStatic(&board->name, ffSmbiosLocateString(strings, data->Product)); + ffCleanUpSmbiosValue(&board->name); + ffStrbufSetStatic(&board->serial, ffSmbiosLocateString(strings, data->SerialNumber)); + ffCleanUpSmbiosValue(&board->serial); + ffStrbufSetStatic(&board->vendor, ffSmbiosLocateString(strings, data->Manufacturer)); + ffCleanUpSmbiosValue(&board->vendor); + ffStrbufSetStatic(&board->version, ffSmbiosLocateString(strings, data->Version)); + ffCleanUpSmbiosValue(&board->version); + + return NULL; +} diff --git a/src/detection/bootmgr/bootmgr.c b/src/detection/bootmgr/bootmgr.c new file mode 100644 index 0000000..31838d4 --- /dev/null +++ b/src/detection/bootmgr/bootmgr.c @@ -0,0 +1,44 @@ +#include "efi_helper.h" + +static inline uint8_t evBits(uint16_t val, uint8_t mask, uint8_t shift) { + return (uint8_t) ((val & (mask << shift)) >> shift); +} + +static void ffEfiUcs2ToUtf8(const uint16_t* const chars, FFstrbuf* result) { + for (uint32_t i = 0; chars[i]; i++) { + if (chars[i] <= 0x007f) { + ffStrbufAppendC(result, (char) chars[i]); + } else if (chars[i] > 0x007f && chars[i] <= 0x07ff) { + ffStrbufAppendC(result, (char) (0xc0 | evBits(chars[i], 0x1f, 6))); + ffStrbufAppendC(result, (char) (0x80 | evBits(chars[i], 0x3f, 0))); + } else { + ffStrbufAppendC(result, (char) (0xe0 | evBits(chars[i], 0xf, 12))); + ffStrbufAppendC(result, (char) (0x80 | evBits(chars[i], 0x3f, 6))); + ffStrbufAppendC(result, (char) (0x80 | evBits(chars[i], 0x3f, 0))); + } + } +} + +bool ffEfiFillLoadOption(const FFEfiLoadOption* efiOption, FFBootmgrResult* result) { + uint32_t descLen = 0; + while (efiOption->Description[descLen]) { + ++descLen; + } + + if (descLen) { + ffEfiUcs2ToUtf8(efiOption->Description, &result->name); + } + + for ( + ffEfiDevicePathProtocol* filePathList = (ffEfiDevicePathProtocol*) &efiOption->Description[descLen + 1]; + filePathList->Type != 0x7F; // End of Hardware Device Path + filePathList = (ffEfiDevicePathProtocol*) ((uint8_t*) filePathList + filePathList->Length)) { + if (filePathList->Type == 4 && filePathList->SubType == 4) { + // https://uefi.org/specs/UEFI/2.10/10_Protocols_Device_Path_Protocol.html#file-path-media-device-path + ffEfiUcs2ToUtf8((uint16_t*) filePathList->SpecificDevicePathData, &result->firmware); + return true; + } + } + + return false; +} diff --git a/src/detection/bootmgr/bootmgr.h b/src/detection/bootmgr/bootmgr.h new file mode 100644 index 0000000..055b0ee --- /dev/null +++ b/src/detection/bootmgr/bootmgr.h @@ -0,0 +1,13 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/bootmgr/option.h" + +typedef struct FFBootmgrResult { + FFstrbuf name; + FFstrbuf firmware; + uint16_t order; + bool secureBoot; +} FFBootmgrResult; + +const char* ffDetectBootmgr(FFBootmgrResult* bios); diff --git a/src/detection/bootmgr/bootmgr_apple.c b/src/detection/bootmgr/bootmgr_apple.c new file mode 100644 index 0000000..d2b24e0 --- /dev/null +++ b/src/detection/bootmgr/bootmgr_apple.c @@ -0,0 +1,69 @@ +#include "bootmgr.h" +#include "common/io.h" +#include "common/apple/cf_helpers.h" + +#include <IOKit/IOKitLib.h> + +static const char* detectSecureBoot(bool* result) { +#if __aarch64__ + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryDevice = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/chosen"); + if (!entryDevice) { + return "IORegistryEntryFromPath() failed"; + } + + FF_CFTYPE_AUTO_RELEASE CFTypeRef prop = IORegistryEntryCreateCFProperty(entryDevice, CFSTR("secure-boot"), kCFAllocatorDefault, kNilOptions); + if (!prop) { + return "IORegistryEntryCreateCFProperty() failed"; + } + + if (CFGetTypeID(prop) != CFDataGetTypeID() || CFDataGetLength((CFDataRef) prop) == 0) { + return "Invalid secure-boot property"; + } + + *result = (bool) *CFDataGetBytePtr((CFDataRef) prop); +#else + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryDevice = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/options"); + if (!entryDevice) { + return "IORegistryEntryFromPath() failed"; + } + + FF_CFTYPE_AUTO_RELEASE CFTypeRef prop = IORegistryEntryCreateCFProperty(entryDevice, CFSTR("94b73556-2197-4702-82a8-3e1337dafbfb:AppleSecureBootPolicy"), kCFAllocatorDefault, 0); + if (!prop) { + return "IORegistryEntryCreateCFProperty() failed"; + } + + if (CFGetTypeID(prop) != CFDataGetTypeID() || CFDataGetLength((CFDataRef) prop) == 0) { + return "Invalid secure-boot property"; + } + + *result = *CFDataGetBytePtr((CFDataRef) prop) != 0x02 /* Permissive Security */; +#endif + + return NULL; +} + +const char* ffDetectBootmgr(FFBootmgrResult* result) { + if (ffPathExists("/System/Library/CoreServices/boot.efi", FF_PATHTYPE_FILE)) { + ffStrbufSetStatic(&result->firmware, "/System/Library/CoreServices/boot.efi"); + } + + #ifdef __aarch64__ + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t deviceChosen = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/chosen"); + if (deviceChosen) { + FF_CFTYPE_AUTO_RELEASE CFStringRef tag = IORegistryEntryCreateCFProperty(deviceChosen, CFSTR("iboot-stage-two-tag"), kCFAllocatorDefault, kNilOptions) + ?: IORegistryEntryCreateCFProperty(deviceChosen, CFSTR("system-firmware-version"), kCFAllocatorDefault, kNilOptions); + if (tag) { + ffCfStrGetString(tag, &result->name); + ffStrbufSubstrBeforeFirstC(&result->name, '-'); + } + } + #endif + + if (!result->name.length) { + ffStrbufSetStatic(&result->name, "iBoot"); + } + + detectSecureBoot(&result->secureBoot); + + return NULL; +} diff --git a/src/detection/bootmgr/bootmgr_bsd.c b/src/detection/bootmgr/bootmgr_bsd.c new file mode 100644 index 0000000..cfaa41d --- /dev/null +++ b/src/detection/bootmgr/bootmgr_bsd.c @@ -0,0 +1,62 @@ +#include "bootmgr.h" +#include "efi_helper.h" +#include "common/io.h" + +#ifdef __OpenBSD__ + #include <dev/efi/efiio.h> +#else + #include <sys/efiio.h> +#endif +#include <sys/ioctl.h> +#include <fcntl.h> +#include <stdalign.h> + +#ifdef __NetBSD__ +typedef uint16_t efi_char; +#endif + +#ifndef EFI_GLOBAL_VARIABLE + #define EFI_GLOBAL_VARIABLE { 0x8be4df61, 0x93ca, 0x11d2, 0xaa, 0x0d, { 0x00, 0xe0, 0x98, 0x03, 0x2b, 0x8c } } +#endif + +const char* ffDetectBootmgr(FFBootmgrResult* result) { + FF_AUTO_CLOSE_FD int efifd = open("/dev/efi", O_RDWR | O_CLOEXEC); + if (efifd < 0) { + return "open(/dev/efi) failed"; + } + + alignas(uint16_t) uint8_t buffer[2048]; + struct efi_var_ioc ioc = { + .vendor = EFI_GLOBAL_VARIABLE, + .data = buffer, + }; + + ioc.datasize = sizeof(buffer); + ioc.name = (efi_char[]) { 'B', 'o', 'o', 't', 'C', 'u', 'r', 'r', 'e', 'n', 't', '\0' }; + ioc.namesize = sizeof("BootCurrent") * 2; + if (ioctl(efifd, EFIIOC_VAR_GET, &ioc) < 0 || ioc.datasize != 2) { + return "ioctl(EFIIOC_VAR_GET, BootCurrent) failed"; + } + + result->order = *(uint16_t*) buffer; + + unsigned char hex[5]; + snprintf((char*) hex, sizeof(hex), "%04X", result->order); + ioc.datasize = sizeof(buffer); + ioc.name = (efi_char[]) { 'B', 'o', 'o', 't', hex[0], hex[1], hex[2], hex[3], '\0' }; + ioc.namesize = sizeof("Boot####") * 2; + if (ioctl(efifd, EFIIOC_VAR_GET, &ioc) < 0 || ioc.datasize == sizeof(buffer)) { + return "ioctl(EFIIOC_VAR_GET, Boot####) failed"; + } + + ffEfiFillLoadOption((FFEfiLoadOption*) buffer, result); + + ioc.name = (efi_char[]) { 'S', 'e', 'c', 'u', 'r', 'e', 'B', 'o', 'o', 't', '\0' }; + ioc.namesize = sizeof("SecureBoot") * 2; + ioc.datasize = sizeof(buffer); + if (ioctl(efifd, EFIIOC_VAR_GET, &ioc) == 0 && ioc.datasize == 1) { + result->secureBoot = !!buffer[0]; + } + + return NULL; +} diff --git a/src/detection/bootmgr/bootmgr_haiku.cpp b/src/detection/bootmgr/bootmgr_haiku.cpp new file mode 100644 index 0000000..72358fc --- /dev/null +++ b/src/detection/bootmgr/bootmgr_haiku.cpp @@ -0,0 +1,17 @@ +extern "C" { + #include "bootmgr.h" + #include "common/io.h" +} + +const char* ffDetectBootmgr(FFBootmgrResult* result) { + // TODO: glob haiku_loader.* + check EFI partition + if (ffPathExists("/system/haiku_loader.bios_ia32", FF_PATHTYPE_FILE)) { + ffStrbufSetStatic(&result->firmware, "/system/haiku_loader.bios_ia32"); + } + + ffStrbufSetStatic(&result->name, "haiku_loader"); + + // TODO: detectSecureBoot(&result->secureBoot); + + return NULL; +} diff --git a/src/detection/bootmgr/bootmgr_linux.c b/src/detection/bootmgr/bootmgr_linux.c new file mode 100644 index 0000000..0fd019a --- /dev/null +++ b/src/detection/bootmgr/bootmgr_linux.c @@ -0,0 +1,32 @@ +#include "bootmgr.h" +#include "common/io.h" +#include "efi_helper.h" + +#include <stdalign.h> + +#define FF_EFIVARS_PATH_PREFIX "/sys/firmware/efi/efivars/" + +const char* ffDetectBootmgr(FFBootmgrResult* result) { + alignas(uint16_t) uint8_t buffer[2048]; + + if (ffReadFileData(FF_EFIVARS_PATH_PREFIX "BootCurrent-" FF_EFI_GLOBAL_GUID, sizeof(buffer), buffer) != 6) { + return "Failed to read efivar: BootCurrent"; + } + + result->order = *(uint16_t*) &buffer[4]; + + snprintf((char*) buffer, sizeof(buffer), FF_EFIVARS_PATH_PREFIX "Boot%04X-" FF_EFI_GLOBAL_GUID, result->order); + + ssize_t size = ffReadFileData((const char*) buffer, sizeof(buffer), buffer); + if (size < 5 + (int) sizeof(FFEfiLoadOption) || size == (ssize_t) sizeof(buffer)) { + return "Failed to read efivar: Boot####"; + } + + ffEfiFillLoadOption((FFEfiLoadOption*) &buffer[4], result); + + if (ffReadFileData(FF_EFIVARS_PATH_PREFIX "SecureBoot-" FF_EFI_GLOBAL_GUID, sizeof(buffer), buffer) >= 5) { + result->secureBoot = buffer[4] == 1; + } + + return NULL; +} diff --git a/src/detection/bootmgr/bootmgr_nosupport.c b/src/detection/bootmgr/bootmgr_nosupport.c new file mode 100644 index 0000000..bb01714 --- /dev/null +++ b/src/detection/bootmgr/bootmgr_nosupport.c @@ -0,0 +1,5 @@ +#include "bootmgr.h" + +const char* ffDetectBootmgr(FF_A_UNUSED FFBootmgrResult* result) { + return "Not supported on this platform"; +} diff --git a/src/detection/bootmgr/bootmgr_windows.c b/src/detection/bootmgr/bootmgr_windows.c new file mode 100644 index 0000000..e83e1e2 --- /dev/null +++ b/src/detection/bootmgr/bootmgr_windows.c @@ -0,0 +1,74 @@ +#include "bootmgr.h" +#include "efi_helper.h" +#include "common/io.h" +#include "common/windows/nt.h" + +#include <ntstatus.h> +#include <windows.h> + +const char* enablePrivilege(const wchar_t* privilege) { + FF_AUTO_CLOSE_FD HANDLE token = NULL; + if (!NT_SUCCESS(NtOpenProcessToken(NtCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token))) { + return "NtOpenProcessToken() failed"; + } + + TOKEN_PRIVILEGES tp = { + .PrivilegeCount = 1, + .Privileges = { + (LUID_AND_ATTRIBUTES) { .Attributes = SE_PRIVILEGE_ENABLED } }, + }; + if (!LookupPrivilegeValueW(NULL, privilege, &tp.Privileges[0].Luid)) { + return "LookupPrivilegeValue() failed"; + } + + NTSTATUS status = NtAdjustPrivilegesToken(token, false, &tp, sizeof(tp), NULL, NULL); + if (!NT_SUCCESS(status)) { + return "NtAdjustPrivilegesToken() failed"; + } + + if (status == STATUS_NOT_ALL_ASSIGNED) { + return "The token does not have the specified privilege; try sudo please"; + } + + return NULL; +} + +const char* ffDetectBootmgr(FFBootmgrResult* result) { + const char* err = enablePrivilege(L"SeSystemEnvironmentPrivilege"); + if (err != NULL) { + return err; + } + + GUID efiGlobalGuid; + if (!NT_SUCCESS(RtlGUIDFromString(&(UNICODE_STRING) RTL_CONSTANT_STRING(L"{" FF_EFI_GLOBAL_GUID L"}"), &efiGlobalGuid))) { + return "RtlGUIDFromString() failed"; + } + + ULONG size = sizeof(result->order); + if (!NT_SUCCESS(NtQuerySystemEnvironmentValueEx(&(UNICODE_STRING) RTL_CONSTANT_STRING(L"BootCurrent"), &efiGlobalGuid, &result->order, &size, NULL))) { + return "NtQuerySystemEnvironmentValueEx(BootCurrent) failed"; + } + if (size != sizeof(result->order)) { + return "NtQuerySystemEnvironmentValueEx(BootCurrent) returned unexpected size"; + } + + uint8_t buffer[2048]; + wchar_t key[9]; + swprintf(key, ARRAY_SIZE(key), L"Boot%04X", result->order); + size = sizeof(buffer); + if (!NT_SUCCESS(NtQuerySystemEnvironmentValueEx(&(UNICODE_STRING) RTL_CONSTANT_STRING(key), &efiGlobalGuid, buffer, &size, NULL))) { + return "NtQuerySystemEnvironmentValueEx(Boot####) failed"; + } + if (size < sizeof(FFEfiLoadOption) || size == ARRAY_SIZE(buffer)) { + return "NtQuerySystemEnvironmentValueEx(Boot####) returned unexpected size"; + } + + ffEfiFillLoadOption((FFEfiLoadOption*) buffer, result); + + SYSTEM_SECUREBOOT_INFORMATION ssi; + if (NT_SUCCESS(NtQuerySystemInformation(SystemSecureBootInformation, &ssi, sizeof(ssi), NULL))) { + result->secureBoot = ssi.SecureBootEnabled; + } + + return NULL; +} diff --git a/src/detection/bootmgr/efi_helper.h b/src/detection/bootmgr/efi_helper.h new file mode 100644 index 0000000..b52ec8a --- /dev/null +++ b/src/detection/bootmgr/efi_helper.h @@ -0,0 +1,22 @@ +#include "bootmgr.h" + +// https://uefi.org/specs/UEFI/2.10/10_Protocols_Device_Path_Protocol.html#generic-device-path-structures +typedef struct ffEfiDevicePathProtocol { + uint8_t Type; + uint8_t SubType; + uint16_t Length; + uint8_t SpecificDevicePathData[]; +} ffEfiDevicePathProtocol; + +// https://uefi.org/specs/UEFI/2.10/03_Boot_Manager.html#load-options +typedef struct FFEfiLoadOption { + uint32_t Attributes; + uint16_t FilePathListLength; + uint16_t Description[]; + // ffEfiDevicePathProtocol FilePathList[]; + // uint8_t OptionalData[]; +} FFEfiLoadOption; + +bool ffEfiFillLoadOption(const FFEfiLoadOption* efiOption, FFBootmgrResult* result); + +#define FF_EFI_GLOBAL_GUID "8be4df61-93ca-11d2-aa0d-00e098032b8c" diff --git a/src/detection/brightness/brightness.h b/src/detection/brightness/brightness.h new file mode 100644 index 0000000..abb9478 --- /dev/null +++ b/src/detection/brightness/brightness.h @@ -0,0 +1,26 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/brightness/option.h" + +typedef struct FFBrightnessResult { + FFstrbuf name; + double min, max, current; + bool builtin; +} FFBrightnessResult; + +enum { + FF_DDC_EDID_ADDR = 0x50u, + FF_DDC_CI_ADDR = 0x37u, + FF_DDC_CI_WRITE_ADDR = FF_DDC_CI_ADDR << 1, + FF_DDC_CI_READ_ADDR = FF_DDC_CI_WRITE_ADDR | 1, + FF_DDC_CI_VCP_COMMAND = 0x51u, + FF_DDC_CI_GET_VCP = 0x01u, + FF_DDC_CI_SET_VCP = 0x03u, + FF_DDC_CI_COMMAND_PACKET = 0x80u, + FF_DDC_CI_LUMINANCE_OPCODE = 0x10u, +}; +#define FF_BRIGHTNESS_DDCCI_SLEEP_SKIP ((uint32_t) -1) +#define FF_DDC_CI_MAKE_HEADER(len) (FF_DDC_CI_COMMAND_PACKET | ((len) & 0x7F)) + +const char* ffDetectBrightness(FFBrightnessOptions* options, FFlist* result); // list of FFBrightnessResult diff --git a/src/detection/brightness/brightness_apple.c b/src/detection/brightness/brightness_apple.c new file mode 100644 index 0000000..5dd12b2 --- /dev/null +++ b/src/detection/brightness/brightness_apple.c @@ -0,0 +1,244 @@ +#include "brightness.h" +#include "detection/displayserver/displayserver.h" +#include "common/apple/cf_helpers.h" +#include "common/edidHelper.h" +#include "common/time.h" + +#include <CoreGraphics/CoreGraphics.h> + +// DDC/CI +#ifdef __aarch64__ +typedef CFTypeRef IOAVServiceRef; +extern IOAVServiceRef IOAVServiceCreate(CFAllocatorRef allocator) FF_A_WEAK_IMPORT; +extern IOAVServiceRef IOAVServiceCreateWithService(CFAllocatorRef allocator, io_service_t service) FF_A_WEAK_IMPORT; +extern IOReturn IOAVServiceCopyEDID(IOAVServiceRef service, CFDataRef* x2) FF_A_WEAK_IMPORT; +extern IOReturn IOAVServiceReadI2C(IOAVServiceRef service, uint32_t chipAddress, uint32_t offset, void* outputBuffer, uint32_t outputBufferSize) FF_A_WEAK_IMPORT; +extern IOReturn IOAVServiceWriteI2C(IOAVServiceRef service, uint32_t chipAddress, uint32_t dataAddress, void* inputBuffer, uint32_t inputBufferSize) FF_A_WEAK_IMPORT; +#else + // DDC/CI (Intel) + #include <IOKit/IOKitLib.h> + #include <IOKit/graphics/IOGraphicsLib.h> + #include <IOKit/i2c/IOI2CInterface.h> +extern void CGSServiceForDisplayNumber(CGDirectDisplayID display, io_service_t* service) FF_A_WEAK_IMPORT; +#endif + +// ACPI +extern int DisplayServicesGetBrightness(CGDirectDisplayID display, float* brightness) FF_A_WEAK_IMPORT; + +// Works for internal display +static const char* detectWithDisplayServices(const FFDisplayServerResult* displayServer, FFlist* result) { + if (DisplayServicesGetBrightness == NULL) { + return "DisplayServices function DisplayServicesGetBrightness is not available"; + } + + FF_LIST_FOR_EACH (FFDisplayResult, display, displayServer->displays) { + if (display->type == FF_DISPLAY_TYPE_BUILTIN || display->type == FF_DISPLAY_TYPE_UNKNOWN) { + float value; + if (DisplayServicesGetBrightness((CGDirectDisplayID) display->id, &value) == kCGErrorSuccess) { + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + brightness->current = value; + brightness->max = 1; + brightness->min = 0; + ffStrbufInitCopy(&brightness->name, &display->name); + brightness->builtin = true; + } + } + } + + return NULL; +} + +#ifdef __aarch64__ +// https://github.com/waydabber/m1ddc +// Works for Apple Silicon and USB-C adapter connection ( but not HTMI ) +static const char* detectWithDdcci(FF_A_UNUSED const FFDisplayServerResult* displayServer, FFBrightnessOptions* options, FFlist* result) { + if (!IOAVServiceCreate || !IOAVServiceReadI2C || !IOAVServiceWriteI2C) { + return "IOAVService is not available"; + } + + FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL; + if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching("DCPAVServiceProxy"), &iterator) != kIOReturnSuccess) { + return "IOServiceGetMatchingServices() failed"; + } + + io_registry_entry_t registryEntry; + while ((registryEntry = IOIteratorNext(iterator)) != IO_OBJECT_NULL) { + FF_CFTYPE_AUTO_RELEASE IOAVServiceRef service = NULL; + { + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryAv = registryEntry; + + FF_CFTYPE_AUTO_RELEASE CFBooleanRef IOAVServiceUserInterfaceSupported = IORegistryEntryCreateCFProperty(entryAv, CFSTR("IOAVServiceUserInterfaceSupported"), kCFAllocatorDefault, kNilOptions); + if (IOAVServiceUserInterfaceSupported && !CFBooleanGetValue(IOAVServiceUserInterfaceSupported)) { + // IOAVServiceCreateWithService won't work + continue; + } + + FF_CFTYPE_AUTO_RELEASE CFStringRef location = IORegistryEntryCreateCFProperty(entryAv, CFSTR("Location"), kCFAllocatorDefault, kNilOptions); + if (location && CFStringCompare(location, CFSTR("Embedded"), 0) == 0) { + // Builtin display should be handled by DisplayServices + continue; + } + + service = IOAVServiceCreateWithService(kCFAllocatorDefault, (io_service_t) registryEntry); + if (!service) { + continue; + } + } + + { + uint8_t i2cIn[4] = { FF_DDC_CI_MAKE_HEADER(2), FF_DDC_CI_GET_VCP, FF_DDC_CI_LUMINANCE_OPCODE }; + i2cIn[3] = FF_DDC_CI_WRITE_ADDR ^ i2cIn[0] ^ i2cIn[1] ^ i2cIn[2]; + + for (uint32_t i = 0; i < 2; ++i) { + IOAVServiceWriteI2C(service, FF_DDC_CI_ADDR, FF_DDC_CI_VCP_COMMAND, i2cIn, ARRAY_SIZE(i2cIn)); + ffTimeSleep(options->ddcciSleep); + } + } + + uint8_t i2cOut[12] = {}; + if (IOAVServiceReadI2C(service, FF_DDC_CI_ADDR, FF_DDC_CI_VCP_COMMAND, i2cOut, ARRAY_SIZE(i2cOut)) == KERN_SUCCESS) { + if (i2cOut[2] != 0x02 || i2cOut[3] != 0x00) { + continue; + } + + uint32_t current = ((uint32_t) i2cOut[8] << 8u) + (uint32_t) i2cOut[9]; + uint32_t max = ((uint32_t) i2cOut[6] << 8u) + (uint32_t) i2cOut[7]; + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + brightness->max = max; + brightness->min = 0; + brightness->current = current; + ffStrbufInit(&brightness->name); + brightness->builtin = false; + + uint8_t edid[128] = {}; + if (IOAVServiceReadI2C(service, 0x50, 0x00, edid, ARRAY_SIZE(edid)) == KERN_SUCCESS) { + ffEdidGetName(edid, &brightness->name); + } + } + } + + return NULL; +} +#else +static IOOptionBits getSupportedTransactionType(void) { + FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL; + + if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceNameMatching("IOFramebufferI2CInterface"), &iterator) != KERN_SUCCESS) { + return kIOI2CNoTransactionType; + } + + io_registry_entry_t registryEntry; + while ((registryEntry = IOIteratorNext(iterator)) != MACH_PORT_NULL) { + FF_IOOBJECT_AUTO_RELEASE io_service_t io_service = registryEntry; + FF_CFTYPE_AUTO_RELEASE CFNumberRef IOI2CTransactionTypes = IORegistryEntryCreateCFProperty(io_service, CFSTR(kIOI2CTransactionTypesKey), kCFAllocatorDefault, kNilOptions); + + if (IOI2CTransactionTypes) { + int64_t types = 0; + ffCfNumGetInt64(IOI2CTransactionTypes, &types); + + if (types) { + if ((1 << kIOI2CDDCciReplyTransactionType) & (uint64_t) types) { + return kIOI2CDDCciReplyTransactionType; + } + if ((1 << kIOI2CSimpleTransactionType) & (uint64_t) types) { + return kIOI2CSimpleTransactionType; + } + } + } + break; + } + + return kIOI2CNoTransactionType; +} + +static const char* detectWithDdcci(const FFDisplayServerResult* displayServer, FFBrightnessOptions* options, FFlist* result) { + if (!CGSServiceForDisplayNumber) { + return "CGSServiceForDisplayNumber is not available"; + } + IOOptionBits transactionType = getSupportedTransactionType(); + if (transactionType == kIOI2CNoTransactionType) { + return "No supported IOI2C transaction type found"; + } + + FF_LIST_FOR_EACH (FFDisplayResult, display, displayServer->displays) { + if (display->type == FF_DISPLAY_TYPE_EXTERNAL) { + FF_IOOBJECT_AUTO_RELEASE io_service_t framebuffer = IO_OBJECT_NULL; + CGSServiceForDisplayNumber((CGDirectDisplayID) display->id, &framebuffer); + if (framebuffer == IO_OBJECT_NULL) { + continue; + } + + IOItemCount count; + if (IOFBGetI2CInterfaceCount(framebuffer, &count) != KERN_SUCCESS || count == 0) { + continue; + } + + for (IOItemCount bus = 0; bus < count; ++bus) { + FF_IOOBJECT_AUTO_RELEASE io_service_t interface = IO_OBJECT_NULL; + if (IOFBCopyI2CInterfaceForBus(framebuffer, bus, &interface) != KERN_SUCCESS) { + continue; + } + + uint8_t i2cOut[12] = {}; + IOI2CConnectRef connect = NULL; + if (IOI2CInterfaceOpen(interface, kNilOptions, &connect) != KERN_SUCCESS) { + continue; + } + + uint8_t i2cIn[] = { FF_DDC_CI_VCP_COMMAND, FF_DDC_CI_MAKE_HEADER(2), FF_DDC_CI_GET_VCP, FF_DDC_CI_LUMINANCE_OPCODE, 0 }; + i2cIn[4] = FF_DDC_CI_WRITE_ADDR ^ i2cIn[0] ^ i2cIn[1] ^ i2cIn[2] ^ i2cIn[3]; + + IOI2CRequest request = { + .commFlags = kNilOptions, + .sendAddress = FF_DDC_CI_WRITE_ADDR, + .sendTransactionType = kIOI2CSimpleTransactionType, + .sendBuffer = (vm_address_t) i2cIn, + .sendBytes = ARRAY_SIZE(i2cIn), + .minReplyDelay = options->ddcciSleep * 1000ULL, + .replyAddress = FF_DDC_CI_READ_ADDR, + .replySubAddress = FF_DDC_CI_VCP_COMMAND, + .replyTransactionType = transactionType, + .replyBytes = ARRAY_SIZE(i2cOut), + .replyBuffer = (vm_address_t) i2cOut, + }; + IOReturn ret = IOI2CSendRequest(connect, kNilOptions, &request); + IOI2CInterfaceClose(connect, kNilOptions); + + if (ret != KERN_SUCCESS || request.result != kIOReturnSuccess || request.replyBytes < 10) { + continue; + } + if (i2cOut[2] != 0x02 || i2cOut[3] != 0x00) { + continue; + } + + uint32_t current = ((uint32_t) i2cOut[8] << 8u) + (uint32_t) i2cOut[9]; + uint32_t max = ((uint32_t) i2cOut[6] << 8u) + (uint32_t) i2cOut[7]; + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + brightness->max = max; + brightness->min = 0; + brightness->current = current; + ffStrbufInitCopy(&brightness->name, &display->name); + brightness->builtin = false; + + break; + } + } + } + + return NULL; +} +#endif + +const char* ffDetectBrightness(FFBrightnessOptions* options, FFlist* result) { + const FFDisplayServerResult* displayServer = ffConnectDisplayServer(); + + detectWithDisplayServices(displayServer, result); + + if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP && displayServer->displays.length > result->length) { + detectWithDdcci(displayServer, options, result); + } + + return NULL; +} diff --git a/src/detection/brightness/brightness_bsd.c b/src/detection/brightness/brightness_bsd.c new file mode 100644 index 0000000..5ff411c --- /dev/null +++ b/src/detection/brightness/brightness_bsd.c @@ -0,0 +1,147 @@ +#include "brightness.h" +#include "common/arrutil.h" +#include "common/io.h" +#include "common/kmod.h" +#include "common/debug.h" +#include "common/time.h" + +#include <sys/ioctl.h> +#include <sys/fcntl.h> +#include <unistd.h> + +#if __has_include(<dev/iicbus/iic.h>) + #include <dev/iicbus/iic.h> + +const char* detectWithDdcci(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) { + // FIXME: doesn't work for me + for (char i = '0'; i <= '9'; ++i) { + char path[] = "/dev/iic0"; + path[ARRAY_SIZE(path) - 2] = i; + + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + int err = errno; + if (err == ENOENT) { + FF_DEBUG("No more I2C devices"); + break; + } else if (i == '0' && !ffKmodLoaded("iicbus")) { + FF_DEBUG("iicbus module is not loaded"); + return "No I2C devices found and iicbus module is not loaded"; + } + FF_DEBUG("open(/dev/iic%c) failed: %s", i, strerror(errno)); + continue; + } + + uint8_t i2cIn[] = { FF_DDC_CI_VCP_COMMAND, FF_DDC_CI_MAKE_HEADER(2), FF_DDC_CI_GET_VCP, FF_DDC_CI_LUMINANCE_OPCODE, 0 }; + i2cIn[4] = FF_DDC_CI_WRITE_ADDR ^ i2cIn[0] ^ i2cIn[1] ^ i2cIn[2] ^ i2cIn[3]; + + int ret = ioctl(fd, I2CRDWR, &(struct iic_rdwr_data){ .msgs = &(struct iic_msg){ .slave = FF_DDC_CI_WRITE_ADDR, .flags = IIC_M_WR, .len = ARRAY_SIZE(i2cIn), .buf = i2cIn }, .nmsgs = 1 }); + if (ret < 0) { + FF_DEBUG("First ioctl(/dev/iic%c, I2CRDWR) failed: %s", i, strerror(errno)); + continue; + } + + ffTimeSleep(options->ddcciSleep); + + uint8_t i2cOut[12] = {}; + ret = ioctl(fd, I2CRDWR, &(struct iic_rdwr_data){ .msgs = &(struct iic_msg){ .slave = FF_DDC_CI_READ_ADDR, // LSB will be overridden by kernel to set read bit + .flags = IIC_M_RD, + .len = ARRAY_SIZE(i2cOut), + .buf = i2cOut }, + .nmsgs = 1 }); + if (ret < 0) { + FF_DEBUG("Second ioctl(/dev/iic%c, I2CRDWR) failed: %s", i, strerror(errno)); + continue; + } + if (i2cOut[2] != 0x02 || i2cOut[3] != 0x00) { + FF_DEBUG("i2c out validation failed: i2cOut[2] = 0x%02x, i2cOut[3] = 0x%02x", i2cOut[2], i2cOut[3]); + continue; + } + + uint32_t current = ((uint32_t) i2cOut[8] << 8u) + (uint32_t) i2cOut[9]; + uint32_t max = ((uint32_t) i2cOut[6] << 8u) + (uint32_t) i2cOut[7]; + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + brightness->max = max; + brightness->min = 0; + brightness->current = current; + ffStrbufInitS(&brightness->name, path + strlen("/dev/")); + brightness->builtin = false; + } + + return NULL; +} + +#else + +const char* detectWithDdcci(FF_A_UNUSED FFBrightnessOptions* options, FF_A_UNUSED FFlist* result) { + FF_DEBUG("DDC/CI support is not available on this system"); + return "DDC/CI is supported only on FreeBSD"; +} + +#endif + +#if __has_include(<sys/backlight.h>) + #include <sys/backlight.h> + +const char* detectWithBacklight(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) { + // https://man.freebsd.org/cgi/man.cgi?query=backlight&sektion=9 + char path[] = "/dev/backlight/backlight0"; + + for (char i = '0'; i <= '9'; ++i) { + path[ARRAY_SIZE(path) - 2] = i; + + FF_AUTO_CLOSE_FD int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + int err = errno; + if (err == ENOENT) { + FF_DEBUG("No more backlight devices"); + break; + } else if (i == '0' && !ffKmodLoaded("backlight")) { + FF_DEBUG("backlight module is not loaded"); + return "No backlight devices found and backlight module is not loaded"; + } + FF_DEBUG("open(/dev/backlight/backlight%c) failed: %s", i, strerror(errno)); + continue; + } + + struct backlight_props status; + if (ioctl(fd, BACKLIGHTGETSTATUS, &status) < 0) { + continue; + } + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + ffStrbufInit(&brightness->name); + + brightness->max = BACKLIGHTMAXLEVELS; + brightness->min = 0; + brightness->current = status.brightness; + brightness->builtin = true; + + struct backlight_info info; + if (ioctl(fd, BACKLIGHTGETINFO, &info) == 0) { + ffStrbufAppendS(&brightness->name, info.name); + } else { + ffStrbufAppendS(&brightness->name, path + strlen("/dev/backlight/")); + } + } + return NULL; +} + +#else + +const char* detectWithBacklight(FF_A_UNUSED FFBrightnessOptions* options, FF_A_UNUSED FFlist* result) { + FF_DEBUG("Backlight support is not available on this system"); + return "Backlight is supported only on FreeBSD 13 and newer"; +} + +#endif + +const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) { + detectWithBacklight(options, result); + + if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP && result->length == 0) { + detectWithDdcci(options, result); + } + return NULL; +} diff --git a/src/detection/brightness/brightness_haiku.cpp b/src/detection/brightness/brightness_haiku.cpp new file mode 100644 index 0000000..86fbe6d --- /dev/null +++ b/src/detection/brightness/brightness_haiku.cpp @@ -0,0 +1,46 @@ +extern "C" { +#include "brightness.h" +#include "common/strutil.h" +} + +#include <Application.h> +#include <Screen.h> + +const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) { + // We need a valid be_app to query the app_server here. + BApplication app("application/x-vnd.fastfetch-cli-fastfetch"); + BScreen screen{}; // default screen is the main one + + do { + if (!screen.IsValid()) { + continue; + } + + float value = 1.0f; + status_t status = screen.GetBrightness(&value); + if (status != B_OK) { + continue; + } + + monitor_info monitor; + // WARNING: This is experimental new Haiku API + status_t err = screen.GetMonitorInfo(&monitor); + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + + if (err == B_OK) { + ffStrbufInitF(&brightness->name, "%s %s (%d)", monitor.vendor, monitor.name, screen.ID().id); + ffStrbufTrimRightSpace(&brightness->name); + } else { + ffStrbufInitF(&brightness->name, "Screen %d", screen.ID().id); + } + + brightness->max = 1.0f; + brightness->min = 0.0f; + brightness->current = value; + brightness->builtin = true; + + } while (screen.SetToNext() == B_OK); + + return NULL; +} diff --git a/src/detection/brightness/brightness_linux.c b/src/detection/brightness/brightness_linux.c new file mode 100644 index 0000000..e7125f8 --- /dev/null +++ b/src/detection/brightness/brightness_linux.c @@ -0,0 +1,176 @@ +#include "brightness.h" +#include "common/io.h" +#include "common/edidHelper.h" +#include "common/strutil.h" + +#include <dirent.h> +#include <limits.h> + +static const char* detectWithBacklight(FFlist* result) { + // https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-class-backlight + const char* backlightDirPath = "/sys/class/backlight/"; + + DIR* dirp = opendir(backlightDirPath); + if (dirp == NULL) { + return "Failed to open `/sys/class/backlight/`"; + } + + FF_STRBUF_AUTO_DESTROY backlightDir = ffStrbufCreateA(64); + ffStrbufAppendS(&backlightDir, backlightDirPath); + + uint32_t backlightDirLength = backlightDir.length; + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + ffStrbufAppendS(&backlightDir, entry->d_name); + ffStrbufAppendS(&backlightDir, "/brightness"); + if (ffReadFileBuffer(backlightDir.chars, &buffer)) { + double actualBrightness = ffStrbufToDouble(&buffer, 0); + ffStrbufSubstrBefore(&backlightDir, backlightDirLength); + ffStrbufAppendS(&backlightDir, entry->d_name); + ffStrbufAppendS(&backlightDir, "/max_brightness"); + if (ffReadFileBuffer(backlightDir.chars, &buffer)) { + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + ffStrbufSubstrBeforeLastC(&backlightDir, '/'); + ffStrbufAppendS(&backlightDir, "/device"); + ffStrbufInitA(&brightness->name, PATH_MAX); + if (realpath(backlightDir.chars, brightness->name.chars)) { + ffStrbufRecalculateLength(&brightness->name); + // if we managed to get edid, use it + ffStrbufAppendS(&brightness->name, "/edid"); + uint8_t edidData[128]; + if (ffReadFileData(brightness->name.chars, ARRAY_SIZE(edidData), edidData) == ARRAY_SIZE(edidData)) { + ffStrbufClear(&brightness->name); + ffEdidGetName(edidData, &brightness->name); + } else { + ffStrbufSubstrBeforeLastC(&brightness->name, '/'); // remove "/edid" + ffStrbufSubstrAfterLastC(&brightness->name, '/'); // try getting DRM connector name + if (ffCharIsDigit(brightness->name.chars[0])) { + // PCI address or some unknown path, give up + ffStrbufSetS(&brightness->name, entry->d_name); + } else { + if (ffStrbufStartsWithS(&brightness->name, "card") && ffCharIsDigit(brightness->name.chars[4])) { + ffStrbufSubstrAfterFirstC(&brightness->name, '-'); + } + } + } + } else { + ffStrbufInitS(&brightness->name, entry->d_name); + } + brightness->max = ffStrbufToDouble(&buffer, 0); + brightness->min = 0; + brightness->current = actualBrightness; + brightness->builtin = true; + } + } + ffStrbufSubstrBefore(&backlightDir, backlightDirLength); + } + + closedir(dirp); + + return NULL; +} + +#ifdef FF_HAVE_DDCUTIL + #include "detection/displayserver/displayserver.h" + #include "common/library.h" + #include "common/mallocHelper.h" + + #include <ddcutil_macros.h> + #include <ddcutil_c_api.h> + + // Try to be compatible with ddcutil 2.0 + #if DDCUTIL_VMAJOR >= 2 +double ddca_set_default_sleep_multiplier(double multiplier); // ddcutil 1.4 + #else +DDCA_Status ddca_init(const char* libopts, int syslog_level, int opts); + #endif + +static const char* detectWithDdcci(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) { + FF_LIBRARY_LOAD_MESSAGE(libddcutil, "libddcutil" FF_LIBRARY_EXTENSION, 5); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_get_display_info_list2) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_open_display2) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_get_any_vcp_value_using_explicit_type) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_free_any_vcp_value) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libddcutil, ddca_close_display) + + #ifndef FF_DISABLE_DLOPEN + FF_LIBRARY_LOAD_SYMBOL_LAZY(libddcutil, ddca_init) + if (ffddca_init) { + FF_SUPPRESS_IO(); + // Ref: https://github.com/rockowitz/ddcutil/issues/344 + if (ffddca_init(NULL, -1 /*DDCA_SYSLOG_NOT_SET*/, 1 /*DDCA_INIT_OPTIONS_DISABLE_CONFIG_FILE*/) < 0) { + return "ddca_init() failed"; + } + } else { + FF_LIBRARY_LOAD_SYMBOL_LAZY(libddcutil, ddca_set_default_sleep_multiplier); + if (ffddca_set_default_sleep_multiplier) { + ffddca_set_default_sleep_multiplier(options->ddcciSleep / 40.0); + } + + libddcutil = NULL; // Don't dlclose libddcutil. See https://github.com/rockowitz/ddcutil/issues/330 + } + #else + #if DDCUTIL_VMAJOR >= 2 + if (ddca_init(NULL, -1 /*DDCA_SYSLOG_NOT_SET*/, 1 /*DDCA_INIT_OPTIONS_DISABLE_CONFIG_FILE*/) < 0) { + return "ddca_init() failed"; + } + #else + ddca_set_default_sleep_multiplier(options->ddcciSleep / 40.0); + #endif + #endif + + FF_AUTO_FREE DDCA_Display_Info_List* infoList = NULL; + if (ffddca_get_display_info_list2(false, &infoList) < 0) { + return "ddca_get_display_info_list2(false, &infoList) failed"; + } + + if (infoList->ct == 0) { + return "No DDC/CI compatible displays found"; + } + + for (int index = 0; index < infoList->ct; ++index) { + const DDCA_Display_Info* display = &infoList->info[index]; + + DDCA_Display_Handle handle; + if (ffddca_open_display2(display->dref, false, &handle) >= 0) { + DDCA_Any_Vcp_Value* vcpValue = NULL; + if (ffddca_get_any_vcp_value_using_explicit_type(handle, 0x10 /*brightness*/, DDCA_NON_TABLE_VCP_VALUE, &vcpValue) >= 0) { + assert(vcpValue->value_type == DDCA_NON_TABLE_VCP_VALUE); + int current = VALREC_CUR_VAL(vcpValue), max = VALREC_MAX_VAL(vcpValue); + ffddca_free_any_vcp_value(vcpValue); + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + brightness->max = max; + brightness->min = 0; + brightness->current = current; + ffStrbufInitS(&brightness->name, display->model_name); + } + ffddca_close_display(handle); + } + } + + return NULL; +} +#endif + +const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) { + detectWithBacklight(result); + +#ifdef FF_HAVE_DDCUTIL + if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP) { + const FFDisplayServerResult* displayServer = ffConnectDisplayServer(); + if (result->length < displayServer->displays.length) { + detectWithDdcci(options, result); + } + } +#endif + + return NULL; +} diff --git a/src/detection/brightness/brightness_nbsd.c b/src/detection/brightness/brightness_nbsd.c new file mode 100644 index 0000000..afd6656 --- /dev/null +++ b/src/detection/brightness/brightness_nbsd.c @@ -0,0 +1,26 @@ +#include "brightness.h" + +#include "common/sysctl.h" + +const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) { + // https://man.netbsd.org/NetBSD-10.1/acpiout.4#DESCRIPTION + char key[] = "hw.acpi.acpiout0.brightness"; + char* pn = key + strlen("hw.acpi.acpiout"); + + for (uint32_t i = 0; i <= 9; ++i) { + *pn = (char) ('0' + i); + int value = ffSysctlGetInt(key, -1); + if (value == -1) { + continue; + } + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + ffStrbufInitF(&brightness->name, "acpiout%d", i); + + brightness->max = 100; + brightness->min = 0; + brightness->current = value; + brightness->builtin = true; + } + return NULL; +} diff --git a/src/detection/brightness/brightness_nosupport.c b/src/detection/brightness/brightness_nosupport.c new file mode 100644 index 0000000..6e9284e --- /dev/null +++ b/src/detection/brightness/brightness_nosupport.c @@ -0,0 +1,5 @@ +#include "brightness.h" + +const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FF_A_UNUSED FFlist* result) { + return "Not supported on this platform"; +} diff --git a/src/detection/brightness/brightness_obsd.c b/src/detection/brightness/brightness_obsd.c new file mode 100644 index 0000000..102a8c2 --- /dev/null +++ b/src/detection/brightness/brightness_obsd.c @@ -0,0 +1,44 @@ +#include "brightness.h" +#include "common/io.h" + +#include <dev/wscons/wsconsio.h> +#include <sys/ioctl.h> +#include <unistd.h> +#include <fcntl.h> + +const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) { + char path[] = "/dev/ttyCX"; + for (char i = '0'; i <= '9'; ++i) { + path[strlen("/dev/ttyC")] = i; + + FF_AUTO_CLOSE_FD int devfd = open(path, O_RDONLY | O_CLOEXEC); + + if (devfd < 0) { + if (errno == EACCES && i == '0') { + return "Permission denied when opening tty device"; + } + if (errno == ENOENT) { + break; + } + continue; + } + + struct wsdisplay_param param = { + .param = WSDISPLAYIO_PARAM_BRIGHTNESS, + }; + + if (ioctl(devfd, WSDISPLAYIO_GETPARAM, ¶m) < 0) { + continue; + } + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + ffStrbufInitF(&brightness->name, "ttyC%c", i); + + brightness->max = param.max; + brightness->min = param.min; + brightness->current = param.curval; + brightness->builtin = true; + } + + return NULL; +} diff --git a/src/detection/brightness/brightness_windows.c b/src/detection/brightness/brightness_windows.c new file mode 100644 index 0000000..70b7f01 --- /dev/null +++ b/src/detection/brightness/brightness_windows.c @@ -0,0 +1,223 @@ +#include "brightness.h" +#include "detection/displayserver/displayserver.h" +#include "common/debug.h" +#include "common/library.h" +#include "common/mallocHelper.h" +#include "common/windows/wmi.h" +#include "common/windows/unicode.h" + +#include <inttypes.h> +#include <winternl.h> +#include <windows.h> + +NTSYSAPI NTSTATUS WINAPI GetPhysicalMonitors( + _In_ UNICODE_STRING* pstrDeviceName, + _In_ DWORD dwPhysicalMonitorArraySize, + _Out_ DWORD* pdwNumPhysicalMonitorHandlesInArray, + _Out_ HANDLE* phPhysicalMonitorArray); + +typedef enum _MC_VCP_CODE_TYPE { + MC_MOMENTARY, + MC_SET_PARAMETER +} MC_VCP_CODE_TYPE, + *LPMC_VCP_CODE_TYPE; + +NTSYSAPI NTSTATUS WINAPI DDCCIGetVCPFeature( + _In_ HANDLE hMonitor, + _In_ DWORD dwVCPCode, + _Out_opt_ LPMC_VCP_CODE_TYPE pvct, + _Out_ DWORD* pdwCurrentValue, + _Out_opt_ DWORD* pdwMaximumValue); + +NTSYSAPI NTSTATUS WINAPI DestroyPhysicalMonitorInternal( + _In_ HANDLE hMonitor); + +NTSTATUS WINAPI GetPhysicalMonitorDescription( + _In_ HANDLE hMonitor, + _In_ DWORD dwPhysicalMonitorDescriptionSizeInChars, + _Out_ LPWSTR szPhysicalMonitorDescription); + +static const char* detectWithWmi(FFlist* result) { + FF_DEBUG("WMI: start detection"); + + // https://github.com/tpn/winsdk-10/blob/master/Include/10.0.16299.0/km/wmicore.mof#L21200 + const GUID WmiMonitorBrightnessGuid = { + 0xd43412ac, 0x67f9, 0x4fbb, { 0xa0, 0x81, 0x17, 0x52, 0xa2, 0xc3, 0x3e, 0x84 } + }; + + FF_AUTO_CLOSE_WMI_BLOCK HANDLE hBlock = NULL; + + ULONG status = WmiOpenBlock(&WmiMonitorBrightnessGuid, WMIGUID_QUERY, &hBlock); + if (status != 0) { + FF_DEBUG("WMI: WmiOpenBlock failed: %s", ffDebugWin32Error(status)); + return "WmiOpenBlock() failed"; + } + + ULONG bufferSize = 0; + status = WmiQueryAllDataW(hBlock, &bufferSize, NULL); + if (status != ERROR_SUCCESS && status != ERROR_INSUFFICIENT_BUFFER) { + FF_DEBUG("WMI: first WmiQueryAllDataW failed, bufferSize=%lu: %s", bufferSize, ffDebugWin32Error(status)); + return "WmiQueryAllDataW() failed"; + } + + FF_DEBUG("WMI: initial query bufferSize=%lu", bufferSize); + + if (bufferSize == 0) { + FF_DEBUG("WMI: WmiQueryAllDataW returned empty buffer"); + return "WmiQueryAllDataW() returned no data"; + } + + FF_AUTO_FREE PWNODE_ALL_DATA pAllData = (PWNODE_ALL_DATA) malloc(bufferSize); + + status = WmiQueryAllDataW(hBlock, &bufferSize, pAllData); + if (status != ERROR_SUCCESS) { + FF_DEBUG("WMI: second WmiQueryAllDataW failed, bufferSize=%lu: %s", bufferSize, ffDebugWin32Error(status)); + return "WmiQueryAllDataW() failed"; + } + + if (bufferSize < sizeof(WNODE_ALL_DATA)) { + FF_DEBUG("WMI: insufficient buffer for WNODE_ALL_DATA, bufferSize=%lu", bufferSize); + return "WmiQueryAllDataW() returned insufficient data for WNODE_ALL_DATA"; + } + + FF_DEBUG("WMI: instanceCount=%lu, flags=0x%lX", pAllData->InstanceCount, pAllData->WnodeHeader.Flags); + + PULONG pNameOffsets = (PULONG) ((PUCHAR) pAllData + pAllData->OffsetInstanceNameOffsets); + + for (ULONG i = 0; i < pAllData->InstanceCount; i++) { + ULONG dataOffset = 0; + ULONG dataLength = 0; + + if (pAllData->WnodeHeader.Flags & WNODE_FLAG_FIXED_INSTANCE_SIZE) { + dataLength = pAllData->FixedInstanceSize; + dataOffset = pAllData->DataBlockOffset + i * dataLength; + } else { + dataOffset = pAllData->OffsetInstanceDataAndLength[i].OffsetInstanceData; + dataLength = pAllData->OffsetInstanceDataAndLength[i].LengthInstanceData; + } + + if (dataLength == 0 || dataOffset >= bufferSize || dataLength > bufferSize - dataOffset) { + FF_DEBUG("WMI: skip invalid instance %lu (dataOffset=%lu, dataLength=%lu, bufferSize=%lu)", i, dataOffset, dataLength, bufferSize); + continue; + } + + USHORT nameCharsCount = *(PUSHORT) ((PUCHAR) pAllData + pNameOffsets[i]) / sizeof(WCHAR); + PCWSTR pNameChars = (PCWSTR) ((PUCHAR) pAllData + (pNameOffsets[i] + sizeof(USHORT))); + + PUCHAR pDataBlock = (PUCHAR) pAllData + dataOffset; + UCHAR currentBrightness = pDataBlock[0]; + + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + brightness->max = 100; + brightness->min = 0; + brightness->current = currentBrightness; + brightness->builtin = true; + ffStrbufInitNWS(&brightness->name, nameCharsCount, pNameChars); + ffStrbufSubstrAfterFirstC(&brightness->name, '\\'); + ffStrbufSubstrBeforeFirstC(&brightness->name, '\\'); + + FF_DEBUG("WMI: detected builtin display '%s', current=%u", brightness->name.chars, (unsigned) currentBrightness); + } + + FF_DEBUG("WMI: finished detection, total results=%u", result->length); + + return NULL; +} + +static const char* detectWithDdcci(const FFDisplayServerResult* displayServer, FFlist* result) { + FF_DEBUG("DDC/CI: start detection, displayCount=%u", displayServer->displays.length); + + void* gdi32 = ffLibraryGetModule(L"gdi32.dll"); + if (!gdi32) { + FF_DEBUG("DDC/CI: failed to load gdi32.dll"); + return "ffLibraryGetModule(gdi32.dll) failed"; + } + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(gdi32, GetPhysicalMonitors) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(gdi32, DDCCIGetVCPFeature) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(gdi32, DestroyPhysicalMonitorInternal) + + FF_LIST_FOR_EACH (FFDisplayResult, display, displayServer->displays) { + if (display->type == FF_DISPLAY_TYPE_BUILTIN) { + FF_DEBUG("DDC/CI: skip builtin display id=%" PRIu64, display->id); + continue; + } + + MONITORINFOEXW mi; + mi.cbSize = sizeof(mi); + if (!GetMonitorInfoW((HMONITOR) (uintptr_t) display->id, (LPMONITORINFO) &mi)) { + FF_DEBUG("DDC/CI: GetMonitorInfoW failed for display id=%" PRIu64 ": %s", display->id, ffDebugWin32Error(GetLastError())); + continue; + } + + UNICODE_STRING deviceName = { + .Length = (USHORT) (wcslen(mi.szDevice) * sizeof(wchar_t)), + .MaximumLength = 0, + .Buffer = mi.szDevice, + }; + HANDLE physicalMonitor; + DWORD monitorCount = 0; + NTSTATUS monitorStatus = ffGetPhysicalMonitors(&deviceName, 1, &monitorCount, &physicalMonitor); + if (NT_SUCCESS(monitorStatus) && monitorCount >= 1) { + DWORD curr = 0, max = 0; + if (NT_SUCCESS(ffDDCCIGetVCPFeature(physicalMonitor, FF_DDC_CI_LUMINANCE_OPCODE, NULL, &curr, &max))) { + FFBrightnessResult* brightness = FF_LIST_ADD(FFBrightnessResult, *result); + if (display->name.length > 0) { + ffStrbufInitCopy(&brightness->name, &display->name); + } else { + FF_LIBRARY_LOAD_SYMBOL_LAZY(gdi32, GetPhysicalMonitorDescription) + if (ffGetPhysicalMonitorDescription) { + wchar_t description[128 /*MUST be PHYSICAL_MONITOR_DESCRIPTION_SIZE*/]; + if (NT_SUCCESS(ffGetPhysicalMonitorDescription(physicalMonitor, ARRAY_SIZE(description), description))) { + ffStrbufInitWS(&brightness->name, description); + } + } + if (brightness->name.length == 0) { + ffStrbufSetNWS(&brightness->name, deviceName.Length / 2, deviceName.Buffer); + } + } + brightness->max = max; + brightness->min = 0; + brightness->current = curr; + brightness->builtin = false; + + FF_DEBUG("DDC/CI: detected external display '%s', current=%u, max=%u", brightness->name.chars, (unsigned) curr, (unsigned) max); + } else { + FF_DEBUG("DDC/CI: DDCCIGetVCPFeature failed for monitor '%ls': %s", deviceName.Buffer, ffDebugWin32Error(GetLastError())); + } + + ffDestroyPhysicalMonitorInternal(physicalMonitor); + } else { + FF_DEBUG("DDC/CI: GetPhysicalMonitors failed for '%ls', status=0x%08X, monitorCount=%lu, error=%s", deviceName.Buffer, (unsigned) monitorStatus, monitorCount, ffDebugWin32Error(GetLastError())); + } + } + + FF_DEBUG("DDC/CI: finished detection, total results=%u", result->length); + return NULL; +} + +static bool hasBuiltinDisplay(const FFDisplayServerResult* displayServer) { + FF_LIST_FOR_EACH (FFDisplayResult, display, displayServer->displays) { + if (display->type == FF_DISPLAY_TYPE_BUILTIN || display->type == FF_DISPLAY_TYPE_UNKNOWN) { + return true; + } + } + return false; +} + +const char* ffDetectBrightness(FF_A_UNUSED FFBrightnessOptions* options, FFlist* result) { + const FFDisplayServerResult* displayServer = ffConnectDisplayServer(); + FF_DEBUG("start, displayCount=%u", displayServer->displays.length); + + if (hasBuiltinDisplay(displayServer)) { + FF_DEBUG("builtin display detected, trying WMI"); + detectWithWmi(result); + } + + if (options->ddcciSleep != FF_BRIGHTNESS_DDCCI_SLEEP_SKIP && result->length < displayServer->displays.length) { + FF_DEBUG("resultCount=%u < displayCount=%u, trying DDC/CI", result->length, displayServer->displays.length); + detectWithDdcci(displayServer, result); + } + + FF_DEBUG("finished, resultCount=%u", result->length); + return NULL; +} diff --git a/src/detection/btrfs/btrfs.h b/src/detection/btrfs/btrfs.h new file mode 100644 index 0000000..08ea4e9 --- /dev/null +++ b/src/detection/btrfs/btrfs.h @@ -0,0 +1,28 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/btrfs/option.h" + +typedef struct FFBtrfsDiskUsage { + uint64_t total; + uint64_t used; + const char* type; + const char* profile; // single / dup / raidx + uint8_t copies; +} FFBtrfsDiskUsage; + +typedef struct FFBtrfsResult { + FFstrbuf name; + FFstrbuf uuid; + FFstrbuf devices; + FFstrbuf features; + uint32_t generation; + uint32_t nodeSize; + uint32_t sectorSize; + uint64_t totalSize; + uint64_t globalReservationUsed; + uint64_t globalReservationTotal; + FFBtrfsDiskUsage allocation[3]; +} FFBtrfsResult; + +const char* ffDetectBtrfs(FFlist* result /* list of FFBtrfsResult */); diff --git a/src/detection/btrfs/btrfs_linux.c b/src/detection/btrfs/btrfs_linux.c new file mode 100644 index 0000000..41914d7 --- /dev/null +++ b/src/detection/btrfs/btrfs_linux.c @@ -0,0 +1,178 @@ +#include "btrfs.h" + +#include "common/io.h" +#include <fcntl.h> + +enum { uuidLen = (uint32_t) __builtin_strlen("00000000-0000-0000-0000-000000000000") }; + +static const char* enumerateDevices(FFBtrfsResult* item, int dfd, FFstrbuf* buffer) { + int subfd = openat(dfd, "devices", O_RDONLY | O_CLOEXEC | O_DIRECTORY); + if (subfd < 0) { + return "openat(\"/sys/fs/btrfs/UUID/devices\") == -1"; + } + + FF_AUTO_CLOSE_DIR DIR* dirp = fdopendir(subfd); + if (dirp == NULL) { + close(subfd); + return "fdopendir(\"/sys/fs/btrfs/UUID/devices\") == NULL"; + } + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + if (item->devices.length) { + ffStrbufAppendC(&item->devices, ','); + } + ffStrbufAppendS(&item->devices, entry->d_name); + + char path[sizeof(entry->d_name) + sizeof("/size") + 1]; + snprintf(path, ARRAY_SIZE(path), "%s/size", entry->d_name); + + if (ffReadFileBufferRelative(subfd, path, buffer)) { + item->totalSize += ffStrbufToUInt(buffer, 0) * 512; + } + } + + return NULL; +} + +static const char* enumerateFeatures(FFBtrfsResult* item, int dfd) { + int subfd = openat(dfd, "features", O_RDONLY | O_CLOEXEC | O_DIRECTORY); + if (subfd < 0) { + return "openat(\"/sys/fs/btrfs/UUID/features\") == -1"; + } + + FF_AUTO_CLOSE_DIR DIR* dirp = fdopendir(subfd); + if (dirp == NULL) { + return "fdopendir(\"/sys/fs/btrfs/UUID/features\") == NULL"; + } + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + if (item->features.length) { + ffStrbufAppendC(&item->features, ','); + } + ffStrbufAppendS(&item->features, entry->d_name); + } + + return NULL; +} + +static const char* detectAllocation(FFBtrfsResult* item, int dfd, FFstrbuf* buffer) { + FF_AUTO_CLOSE_FD int subfd = openat(dfd, "allocation", O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY); + if (subfd < 0) { + return "openat(\"/sys/fs/btrfs/UUID/allocation\") == -1"; + } + + if (ffReadFileBufferRelative(subfd, "global_rsv_size", buffer)) { + item->globalReservationTotal = ffStrbufToUInt(buffer, 0); + } else { + return "ffReadFileBuffer(\"/sys/fs/btrfs/UUID/allocation/global_rsv_size\") == NULL"; + } + + if (ffReadFileBufferRelative(subfd, "global_rsv_reserved", buffer)) { + item->globalReservationUsed = ffStrbufToUInt(buffer, 0); + } + item->globalReservationUsed = item->globalReservationTotal - item->globalReservationUsed; + +#define FF_BTRFS_DETECT_PROFILE(_index, _type, _profile, _copies) \ + else if (faccessat(subfd, _type "/" _profile "/", F_OK, 0) == 0) { \ + item->allocation[_index].profile = _profile; \ + item->allocation[_index].copies = _copies; \ + } + +#define FF_BTRFS_DETECT_TYPE(_index, _type) \ + do { \ + item->allocation[_index].type = _type; \ + if (ffReadFileBufferRelative(subfd, _type "/total_bytes", buffer)) \ + item->allocation[_index].total = ffStrbufToUInt(buffer, 0); \ + \ + if (ffReadFileBufferRelative(subfd, _type "/bytes_used", buffer)) \ + item->allocation[_index].used = ffStrbufToUInt(buffer, 0); \ + \ + if (false) {} \ + FF_BTRFS_DETECT_PROFILE(_index, _type, "single", 1) \ + FF_BTRFS_DETECT_PROFILE(_index, _type, "dup", 2) \ + FF_BTRFS_DETECT_PROFILE(_index, _type, "raid0", 1) \ + FF_BTRFS_DETECT_PROFILE(_index, _type, "raid1", 2) \ + FF_BTRFS_DETECT_PROFILE(_index, _type, "raid10", 2) \ + FF_BTRFS_DETECT_PROFILE(_index, _type, "raid1c3", 3) \ + FF_BTRFS_DETECT_PROFILE(_index, _type, "raid1c4", 4) \ + FF_BTRFS_DETECT_PROFILE(_index, _type, "raid5", 1) /* (n-1)/n */ \ + FF_BTRFS_DETECT_PROFILE(_index, _type, "raid6", 1) /* (n-2)/n */ \ + else { \ + item->allocation[_index].profile = "unknown"; \ + item->allocation[_index].copies = 1; \ + } \ + } while (0) + + FF_BTRFS_DETECT_TYPE(0, "data"); + FF_BTRFS_DETECT_TYPE(1, "metadata"); + FF_BTRFS_DETECT_TYPE(2, "system"); + +#undef FF_BTRFS_DETECT_TYPE + + return NULL; +} + +const char* ffDetectBtrfs(FFlist* result) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/fs/btrfs/"); + if (dirp == NULL) { + return "opendir(\"/sys/fs/btrfs\") == NULL"; + } + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + if (strlen(entry->d_name) != uuidLen) { + continue; + } + + FFBtrfsResult* item = FF_LIST_ADD(FFBtrfsResult, *result); + (*item) = (FFBtrfsResult) { + .uuid = ffStrbufCreateNS(uuidLen, entry->d_name), + .name = ffStrbufCreate(), + .devices = ffStrbufCreate(), + .features = ffStrbufCreate(), + }; + + FF_AUTO_CLOSE_FD int dfd = openat(dirfd(dirp), entry->d_name, O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY); + if (dfd < 0) { + continue; + } + + if (ffAppendFileBufferRelative(dfd, "label", &item->name)) { + ffStrbufTrimRightSpace(&item->name); + } + + enumerateDevices(item, dfd, &buffer); + + enumerateFeatures(item, dfd); + + if (ffReadFileBufferRelative(dfd, "generation", &buffer)) { + item->generation = (uint32_t) ffStrbufToUInt(&buffer, 0); + } + + if (ffReadFileBufferRelative(dfd, "nodesize", &buffer)) { + item->nodeSize = (uint32_t) ffStrbufToUInt(&buffer, 0); + } + + if (ffReadFileBufferRelative(dfd, "sectorsize", &buffer)) { + item->sectorSize = (uint32_t) ffStrbufToUInt(&buffer, 0); + } + + detectAllocation(item, dfd, &buffer); + } + + return NULL; +} diff --git a/src/detection/btrfs/btrfs_nosupport.c b/src/detection/btrfs/btrfs_nosupport.c new file mode 100644 index 0000000..b51d2c5 --- /dev/null +++ b/src/detection/btrfs/btrfs_nosupport.c @@ -0,0 +1,5 @@ +#include "btrfs.h" + +const char* ffDetectBtrfs(FF_A_UNUSED FFlist* result) { + return "Not supported on this platform"; +} diff --git a/src/detection/camera/camera.h b/src/detection/camera/camera.h new file mode 100644 index 0000000..52036b5 --- /dev/null +++ b/src/detection/camera/camera.h @@ -0,0 +1,15 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/camera/option.h" + +typedef struct FFCameraResult { + FFstrbuf name; + FFstrbuf vendor; + FFstrbuf id; + FFstrbuf colorspace; + uint32_t width; + uint32_t height; +} FFCameraResult; + +const char* ffDetectCamera(FFlist* result /* list of FFCameraResult */); diff --git a/src/detection/camera/camera_android.c b/src/detection/camera/camera_android.c new file mode 100644 index 0000000..b09314d --- /dev/null +++ b/src/detection/camera/camera_android.c @@ -0,0 +1,58 @@ +#include "camera.h" + +#include "common/processing.h" +#include "common/properties.h" + +#define FF_TERMUX_API_PATH FASTFETCH_TARGET_DIR_ROOT "/libexec/termux-api" +#define FF_TERMUX_API_PARAM "CameraInfo" + +static inline void wrapYyjsonFree(yyjson_doc** doc) { + assert(doc); + if (*doc) { + yyjson_doc_free(*doc); + } +} + +const char* ffDetectCamera(FF_A_UNUSED FFlist* result) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + if (ffProcessAppendStdOut(&buffer, (char* const[]) { FF_TERMUX_API_PATH, FF_TERMUX_API_PARAM, NULL })) { + return "Starting `" FF_TERMUX_API_PATH " " FF_TERMUX_API_PARAM "` failed"; + } + + yyjson_doc* FF_A_CLEANUP(wrapYyjsonFree) doc = yyjson_read_opts(buffer.chars, buffer.length, 0, NULL, NULL); + if (!doc) { + return "Failed to parse camera info"; + } + + yyjson_val* root = yyjson_doc_get_root(doc); + if (!yyjson_is_arr(root)) { + return "Camera info result is not a JSON array"; + } + + yyjson_val* device; + size_t idx, max; + yyjson_arr_foreach (root, idx, max, device) { + FFCameraResult* camera = FF_LIST_ADD(FFCameraResult, *result); + { + const char* facing = yyjson_get_str(yyjson_obj_get(device, "facing")); + if (facing) { + ffStrbufInitF(&camera->name, "builtin-%s", facing); + } else { + ffStrbufInitStatic(&camera->name, "Unknown"); + } + } + ffStrbufInit(&camera->vendor); + ffStrbufInitJsonVal(&camera->id, yyjson_obj_get(device, "id")); + yyjson_val* sizes = yyjson_arr_get_first(yyjson_obj_get(device, "jpeg_output_sizes")); + if (yyjson_is_obj(sizes)) { + camera->width = (uint32_t) yyjson_get_uint(yyjson_obj_get(sizes, "width")); + camera->height = (uint32_t) yyjson_get_uint(yyjson_obj_get(sizes, "height")); + } else { + camera->width = camera->height = 0; + } + ffStrbufInit(&camera->colorspace); + } + + return NULL; +} diff --git a/src/detection/camera/camera_apple.m b/src/detection/camera/camera_apple.m new file mode 100644 index 0000000..aa5c1cf --- /dev/null +++ b/src/detection/camera/camera_apple.m @@ -0,0 +1,61 @@ +#include "camera.h" +#include "common/io.h" + +#import <AVFoundation/AVCaptureDevice.h> + +// warning: 'AVCaptureDeviceTypeExternalUnknown' is deprecated +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +#ifdef MAC_OS_VERSION_14_0 +// To make fastfetch compiled on newer macOS versions runs on older ones +AVF_EXPORT FF_A_WEAK_IMPORT AVCaptureDeviceType const AVCaptureDeviceTypeExternal; +#endif + +const char* ffDetectCamera(FFlist* result) +{ + #ifdef MAC_OS_X_VERSION_10_15 + FF_SUPPRESS_IO(); // #822 + + AVCaptureDeviceType deviceType = NULL; + + #ifdef MAC_OS_VERSION_14_0 + // Strangely `@available(macOS 14.0, *)` doesn't work here (#1594) + if (@available(macOS 14.0, *)) + { + if (&AVCaptureDeviceTypeExternal) + deviceType = AVCaptureDeviceTypeExternal; + } + #endif + if (deviceType == NULL) + deviceType = AVCaptureDeviceTypeExternalUnknown; + + AVCaptureDeviceDiscoverySession* session = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:@[AVCaptureDeviceTypeBuiltInWideAngleCamera, deviceType] + mediaType:AVMediaTypeVideo + position:AVCaptureDevicePositionUnspecified]; + if (!session) + return "Failed to create AVCaptureDeviceDiscoverySession"; + + for (AVCaptureDevice* device in session.devices) + { + FFCameraResult* camera = FF_LIST_ADD(FFCameraResult, *result); + ffStrbufInitS(&camera->name, device.localizedName.UTF8String); + ffStrbufInitS(&camera->vendor, device.manufacturer.UTF8String); + ffStrbufInitS(&camera->id, device.uniqueID.UTF8String); + switch (device.activeColorSpace) + { + case AVCaptureColorSpace_sRGB: ffStrbufInitStatic(&camera->colorspace, "sRGB"); break; + case AVCaptureColorSpace_P3_D65: ffStrbufInitStatic(&camera->colorspace, "P3-D65"); break; + case 2 /*AVCaptureColorSpace_HLG_BT2020*/: ffStrbufInitStatic(&camera->colorspace, "BT2020-HLG"); break; + case 3 /*AVCaptureColorSpace_AppleLog*/: ffStrbufInitStatic(&camera->colorspace, "AppleLog"); break; + case 4 /*AVCaptureColorSpace_AppleLog2*/: ffStrbufInitStatic(&camera->colorspace, "AppleLog2"); break; + } + + CMVideoDimensions size = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription); + camera->width = size.width < 0 ? 0 : (uint32_t) size.width; + camera->height = size.height < 0 ? 0 : (uint32_t) size.height; + } + return NULL; + #else + return "No support for old MacOS version"; + #endif +} diff --git a/src/detection/camera/camera_linux.c b/src/detection/camera/camera_linux.c new file mode 100644 index 0000000..2fea2b4 --- /dev/null +++ b/src/detection/camera/camera_linux.c @@ -0,0 +1,94 @@ +#include "camera.h" +#include "common/io.h" + +#include <unistd.h> +#include <fcntl.h> +#include <sys/ioctl.h> + +#if FF_HAVE_LINUX_VIDEODEV2 + #include <linux/videodev2.h> +#elif __has_include(<sys/videoio.h>) // OpenBSD + #include <sys/videoio.h> + #define FF_HAVE_LINUX_VIDEODEV2 1 +#endif + +const char* ffDetectCamera(FFlist* result) { +#if FF_HAVE_LINUX_VIDEODEV2 + char path[] = "/dev/videoN"; + + for (uint32_t i = 0; i <= 9; ++i) { + path[ARRAY_SIZE(path) - 2] = (char) (i + '0'); + FF_AUTO_CLOSE_FD int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + if (errno == ENOENT) { + break; + } + if (errno == ENXIO) { + continue; + } + return "Failed to open /dev/videoN"; + } + + struct v4l2_capability cap = {}; + if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0 || !(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { + continue; + } + + struct v4l2_format fmt = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE }; + if (ioctl(fd, VIDIOC_G_FMT, &fmt) < 0) { + continue; + } + + FFCameraResult* camera = FF_LIST_ADD(FFCameraResult, *result); + ffStrbufInitS(&camera->name, (const char*) cap.card); + ffStrbufInit(&camera->vendor); + ffStrbufInitS(&camera->id, (const char*) cap.bus_info); + switch (fmt.fmt.pix.colorspace) { + case V4L2_COLORSPACE_SMPTE170M: + ffStrbufInitStatic(&camera->colorspace, "SMPTE 170M"); + break; + case V4L2_COLORSPACE_SMPTE240M: + ffStrbufInitStatic(&camera->colorspace, "SMPTE 240M"); + break; + case V4L2_COLORSPACE_BT878: + ffStrbufInitStatic(&camera->colorspace, "BT.878"); + break; + case V4L2_COLORSPACE_470_SYSTEM_M: + ffStrbufInitStatic(&camera->colorspace, "NTSC"); + break; + case V4L2_COLORSPACE_470_SYSTEM_BG: + ffStrbufInitStatic(&camera->colorspace, "EBU 3213"); + break; + case V4L2_COLORSPACE_JPEG: + ffStrbufInitStatic(&camera->colorspace, "JPEG"); + break; + case V4L2_COLORSPACE_REC709: + case V4L2_COLORSPACE_SRGB: + ffStrbufInitStatic(&camera->colorspace, "sRGB"); + break; + case 9 /* V4L2_COLORSPACE_OPRGB */: + ffStrbufInitStatic(&camera->colorspace, "Adobe RGB"); + break; + case 10 /* V4L2_COLORSPACE_BT2020 */: + ffStrbufInitStatic(&camera->colorspace, "BT.2020"); + break; + case 11 /* V4L2_COLORSPACE_RAW */: + ffStrbufInitStatic(&camera->colorspace, "RAW"); + break; + case 12 /* V4L2_COLORSPACE_DCI_P3 */: + ffStrbufInitStatic(&camera->colorspace, "DCI-P3"); + break; + default: + ffStrbufInit(&camera->colorspace); + break; + } + camera->width = fmt.fmt.pix.width; + camera->height = fmt.fmt.pix.height; + } + + return NULL; +#else + FF_UNUSED(result); + return "Fastfetch was compiled without <linux/videodev2.h>"; +#endif +} diff --git a/src/detection/camera/camera_nosupport.c b/src/detection/camera/camera_nosupport.c new file mode 100644 index 0000000..ae5b243 --- /dev/null +++ b/src/detection/camera/camera_nosupport.c @@ -0,0 +1,5 @@ +#include "camera.h" + +const char* ffDetectCamera(FF_A_UNUSED FFlist* result) { + return "Not support on this platform"; +} diff --git a/src/detection/camera/camera_windows.cpp b/src/detection/camera/camera_windows.cpp new file mode 100644 index 0000000..1f42306 --- /dev/null +++ b/src/detection/camera/camera_windows.cpp @@ -0,0 +1,154 @@ +extern "C" { +#include "camera.h" +#include "common/library.h" +#include "common/windows/com.h" +} +#include "common/windows/unicode.hpp" +#include "common/windows/util.hpp" + +#include <initguid.h> +#include <mfapi.h> +#include <mfidl.h> + +extern "C" const char* ffDetectCamera(FF_A_UNUSED FFlist* result) { + FF_LIBRARY_LOAD_MESSAGE(mfplat, "mfplat" FF_LIBRARY_EXTENSION, 1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mfplat, MFCreateAttributes) + FF_LIBRARY_LOAD_MESSAGE(mf, "mf" FF_LIBRARY_EXTENSION, 1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mf, MFEnumDeviceSources) + + const char* error = ffInitCom(); + if (error) { + return error; + } + + IMFAttributes* FF_AUTO_RELEASE_COM_OBJECT attrs = nullptr; + if (FAILED(ffMFCreateAttributes(&attrs, 1))) { + return "MFCreateAttributes() failed"; + } + + if (FAILED(attrs->SetGUID( + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID))) { + return "SetGUID(MF_*) failed"; + } + + IMFActivate** devices = NULL; + uint32_t count; + + if (FAILED(ffMFEnumDeviceSources(attrs, &devices, &count))) { + return "MFEnumDeviceSources() failed"; + } + + for (uint32_t i = 0; i < count; i++) { + IMFActivate* FF_AUTO_RELEASE_COM_OBJECT device = devices[i]; + + wchar_t buffer[256]; + uint32_t length = 0; + if (FAILED(device->GetString(MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, buffer, ARRAY_SIZE(buffer), &length)) || length == 0) { + continue; + } + + FFCameraResult* camera = FF_LIST_ADD(FFCameraResult, *result); + ffStrbufInitNWS(&camera->name, length, buffer); + ffStrbufInit(&camera->colorspace); + ffStrbufInit(&camera->vendor); + ffStrbufInit(&camera->id); + camera->width = 0; + camera->height = 0; + + if (SUCCEEDED(device->GetString(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, buffer, ARRAY_SIZE(buffer), &length)) && length > 0) { + ffStrbufSetNWS(&camera->id, length, buffer); + } + + IMFMediaSource* FF_AUTO_RELEASE_COM_OBJECT source = nullptr; + if (FAILED(device->ActivateObject(IID_PPV_ARGS(&source)))) { + continue; + } + + on_scope_exit destroySource([&] { source->Shutdown(); }); + + IMFPresentationDescriptor* FF_AUTO_RELEASE_COM_OBJECT pd = nullptr; + if (FAILED(source->CreatePresentationDescriptor(&pd))) { + continue; + } + + IMFStreamDescriptor* FF_AUTO_RELEASE_COM_OBJECT sd = nullptr; + BOOL selected; + if (FAILED(pd->GetStreamDescriptorByIndex(0, &selected, &sd))) { + continue; + } + + IMFMediaTypeHandler* FF_AUTO_RELEASE_COM_OBJECT handler = nullptr; + if (FAILED(sd->GetMediaTypeHandler(&handler))) { + continue; + } + + DWORD mediaTypeCount; + if (FAILED(handler->GetMediaTypeCount(&mediaTypeCount))) { + continue; + } + + // Assume first type is the maximum resolution + IMFMediaType* type = NULL; + for (DWORD idx = 0; SUCCEEDED(handler->GetMediaTypeByIndex(idx, &type)); ++idx) { + on_scope_exit destroyType([=] { type->Release(); }); + + GUID majorType; + if (FAILED(type->GetMajorType(&majorType)) || majorType != MFMediaType_Video) { + continue; + } + + MFVideoPrimaries primaries; + static_assert(sizeof(primaries) == sizeof(uint32_t), ""); + if (SUCCEEDED(type->GetUINT32(MF_MT_VIDEO_PRIMARIES, (uint32_t*) &primaries))) { + switch (primaries) { + case MFVideoPrimaries_BT709: + ffStrbufSetStatic(&camera->colorspace, "sRGB"); + break; + case MFVideoPrimaries_BT470_2_SysM: + case MFVideoPrimaries_BT470_2_SysBG: + ffStrbufSetStatic(&camera->colorspace, "NTSC"); + break; + case MFVideoPrimaries_SMPTE170M: + ffStrbufSetStatic(&camera->colorspace, "SMPTE 170M"); + break; + case MFVideoPrimaries_SMPTE240M: + ffStrbufSetStatic(&camera->colorspace, "SMPTE 240M"); + break; + case MFVideoPrimaries_EBU3213: + ffStrbufSetStatic(&camera->colorspace, "EBU 3213"); + break; + case MFVideoPrimaries_SMPTE_C: + ffStrbufSetStatic(&camera->colorspace, "SMPTE C"); + break; + case MFVideoPrimaries_BT2020: + ffStrbufSetStatic(&camera->colorspace, "BT.2020"); + break; + case MFVideoPrimaries_XYZ: + ffStrbufSetStatic(&camera->colorspace, "XYZ"); + break; + case MFVideoPrimaries_DCI_P3: + ffStrbufSetStatic(&camera->colorspace, "DCI-P3"); + break; + case MFVideoPrimaries_ACES: + ffStrbufSetStatic(&camera->colorspace, "ACES"); + break; + case (MFVideoPrimaries) 13: // MFVideoPrimaries_Display_P3 + ffStrbufSetStatic(&camera->colorspace, "Display P3"); + break; + default: + break; + } + } + + MFGetAttributeSize(type, MF_MT_FRAME_SIZE, &camera->width, &camera->height); + break; + } + } + + if (devices) { + CoTaskMemFree(devices); + } + + return nullptr; +} diff --git a/src/detection/chassis/chassis.c b/src/detection/chassis/chassis.c new file mode 100644 index 0000000..d4d1fd3 --- /dev/null +++ b/src/detection/chassis/chassis.c @@ -0,0 +1,81 @@ +#include "chassis.h" + +const char* ffChassisTypeToString(uint32_t type) { + // 7.4.1 System Enclosure or Chassis Types + switch (type & 0b01111111) { + case 0x01: + return "Other"; + case 0x02: + return "Unknown"; + case 0x03: + return "Desktop"; + case 0x04: + return "Low Profile Desktop"; + case 0x05: + return "Pizza Box"; + case 0x06: + return "Mini Tower"; + case 0x07: + return "Tower"; + case 0x08: + return "Portable"; + case 0x09: + return "Laptop"; + case 0x0A: + return "Notebook"; + case 0x0B: + return "Hand Held"; + case 0x0C: + return "Docking Station"; + case 0x0D: + return "All in One"; + case 0x0E: + return "Sub Notebook"; + case 0x0F: + return "Space-saving"; + case 0x10: + return "Lunch Box"; + case 0x11: + return "Main Server Chassis"; + case 0x12: + return "Expansion Chassis"; + case 0x13: + return "SubChassis"; + case 0x14: + return "Bus Expansion Chassis"; + case 0x15: + return "Peripheral Chassis"; + case 0x16: + return "RAID Chassis"; + case 0x17: + return "Rack Mount Chassis"; + case 0x18: + return "Sealed-case PC"; + case 0x19: + return "Multi-system chassis"; + case 0x1A: + return "Compact PCI"; + case 0x1B: + return "Advanced TCA"; + case 0x1C: + return "Blade"; + case 0x1D: + return "Blade Enclosure"; + case 0x1E: + return "Tablet"; + case 0x1F: + return "Convertible"; + case 0x20: + return "Detachable"; + case 0x21: + return "IoT Gateway"; + case 0x22: + return "Embedded PC"; + case 0x23: + return "Mini PC"; + case 0x24: + return "Stick PC"; + default: + return NULL; + } +} diff --git a/src/detection/chassis/chassis.h b/src/detection/chassis/chassis.h new file mode 100644 index 0000000..4e57ad0 --- /dev/null +++ b/src/detection/chassis/chassis.h @@ -0,0 +1,14 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/chassis/option.h" + +typedef struct FFChassisResult { + FFstrbuf type; + FFstrbuf serial; + FFstrbuf vendor; + FFstrbuf version; +} FFChassisResult; + +const char* ffDetectChassis(FFChassisResult* result); +const char* ffChassisTypeToString(uint32_t type); diff --git a/src/detection/chassis/chassis_apple.c b/src/detection/chassis/chassis_apple.c new file mode 100644 index 0000000..1ff4fee --- /dev/null +++ b/src/detection/chassis/chassis_apple.c @@ -0,0 +1,40 @@ +#include "chassis.h" +#include "detection/host/host.h" + +const char* ffDetectChassis(FFChassisResult* result) { + FFHostResult host = { + .family = ffStrbufCreate(), + .name = ffStrbufCreate(), + .version = ffStrbufCreate(), + .sku = ffStrbufCreate(), + .serial = ffStrbufCreate(), + .uuid = ffStrbufCreate(), + .vendor = ffStrbufCreate(), + }; + if (ffDetectHost(&host) != NULL) { + return "Failed to detect host"; + } + + if (ffStrbufStartsWithS(&host.name, "MacBook ")) { + ffStrbufSetStatic(&result->type, "Laptop"); + } else if (ffStrbufStartsWithS(&host.name, "Mac mini ") || + ffStrbufStartsWithS(&host.name, "Mac Studio ")) { + ffStrbufSetStatic(&result->type, "Mini PC"); + } else if (ffStrbufStartsWithS(&host.name, "iMac ")) { + ffStrbufSetStatic(&result->type, "All in One"); + } else { + ffStrbufSetStatic(&result->type, "Desktop"); + } + + ffStrbufSet(&result->vendor, &host.vendor); + + ffStrbufDestroy(&host.family); + ffStrbufDestroy(&host.name); + ffStrbufDestroy(&host.version); + ffStrbufDestroy(&host.sku); + ffStrbufDestroy(&host.serial); + ffStrbufDestroy(&host.uuid); + ffStrbufDestroy(&host.vendor); + + return NULL; +} diff --git a/src/detection/chassis/chassis_bsd.c b/src/detection/chassis/chassis_bsd.c new file mode 100644 index 0000000..fa13718 --- /dev/null +++ b/src/detection/chassis/chassis_bsd.c @@ -0,0 +1,16 @@ +#include "chassis.h" +#include "common/settings.h" +#include "common/smbios.h" + +const char* ffDetectChassis(FFChassisResult* result) { + // Unlike other platforms, `smbios.chassis.type` return display string directly on my machine + ffSettingsGetFreeBSDKenv("smbios.chassis.type", &result->type); + ffCleanUpSmbiosValue(&result->type); + ffSettingsGetFreeBSDKenv("smbios.chassis.maker", &result->vendor); + ffCleanUpSmbiosValue(&result->vendor); + ffSettingsGetFreeBSDKenv("smbios.chassis.serial", &result->serial); + ffCleanUpSmbiosValue(&result->serial); + ffSettingsGetFreeBSDKenv("smbios.chassis.version", &result->version); + ffCleanUpSmbiosValue(&result->version); + return NULL; +} diff --git a/src/detection/chassis/chassis_linux.c b/src/detection/chassis/chassis_linux.c new file mode 100644 index 0000000..af7ed0a --- /dev/null +++ b/src/detection/chassis/chassis_linux.c @@ -0,0 +1,39 @@ +#include "chassis.h" +#include "common/io.h" +#include "common/smbios.h" + +#include <ctype.h> + +const char* ffDetectChassis(FFChassisResult* result) { + if (ffGetSmbiosValue("/sys/devices/virtual/dmi/id/chassis_type", "/sys/class/dmi/id/chassis_type", &result->type)) { + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/chassis_serial", "/sys/class/dmi/id/chassis_serial", &result->serial); + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/chassis_vendor", "/sys/class/dmi/id/chassis_vendor", &result->vendor); + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/chassis_version", "/sys/class/dmi/id/chassis_version", &result->version); + + if (result->type.length) { + const char* typeStr = ffChassisTypeToString((uint32_t) ffStrbufToUInt(&result->type, 9999)); + if (typeStr) { + ffStrbufSetStatic(&result->type, typeStr); + } + } + } else { + // Available on Asahi Linux + uint32_t chassisType = 0; + if (ffReadFileData("/sys/firmware/devicetree/base/smbios/smbios/chassis/chassis-type", sizeof(chassisType), &chassisType)) // big endian + { + chassisType = __builtin_bswap32(chassisType); + const char* typeStr = ffChassisTypeToString(chassisType); + if (typeStr) { + ffStrbufSetStatic(&result->type, typeStr); + } + + if (ffReadFileBuffer("/sys/firmware/devicetree/base/smbios/smbios/chassis/manufacturer", &result->vendor) && result->vendor.length > 0) { + ffStrbufTrimRight(&result->vendor, '\0'); + } + } else if (ffReadFileBuffer("/sys/firmware/devicetree/base/chassis-type", &result->type) && result->type.length > 0) { + ffStrbufTrimRight(&result->type, '\0'); + result->type.chars[0] = (char) toupper(result->type.chars[0]); + } + } + return NULL; +} diff --git a/src/detection/chassis/chassis_nbsd.c b/src/detection/chassis/chassis_nbsd.c new file mode 100644 index 0000000..5644798 --- /dev/null +++ b/src/detection/chassis/chassis_nbsd.c @@ -0,0 +1,20 @@ +#include "chassis.h" +#include "common/sysctl.h" +#include "common/smbios.h" + +const char* ffDetectChassis(FFChassisResult* chassis) { + if (ffSysctlGetString("machdep.dmi.chassis-type", &chassis->type) == NULL) { + ffCleanUpSmbiosValue(&chassis->type); + } + if (ffSysctlGetString("machdep.dmi.chassis-version", &chassis->version) == NULL) { + ffCleanUpSmbiosValue(&chassis->version); + } + if (ffSysctlGetString("machdep.dmi.chassis-vendor", &chassis->vendor) == NULL) { + ffCleanUpSmbiosValue(&chassis->vendor); + } + if (ffSysctlGetString("machdep.dmi.chassis-serial", &chassis->serial) == NULL) { + ffCleanUpSmbiosValue(&chassis->serial); + } + + return NULL; +} diff --git a/src/detection/chassis/chassis_nosupport.c b/src/detection/chassis/chassis_nosupport.c new file mode 100644 index 0000000..0fc4dd9 --- /dev/null +++ b/src/detection/chassis/chassis_nosupport.c @@ -0,0 +1,5 @@ +#include "chassis.h" + +const char* ffDetectChassis(FF_A_UNUSED FFChassisResult* result) { + return "Not supported on this platform"; +} diff --git a/src/detection/chassis/chassis_windows.c b/src/detection/chassis/chassis_windows.c new file mode 100644 index 0000000..51a4c6c --- /dev/null +++ b/src/detection/chassis/chassis_windows.c @@ -0,0 +1,54 @@ +#include "chassis.h" +#include "common/smbios.h" + +// 7.4 +typedef struct FFSmbiosSystemEnclosure { + FFSmbiosHeader Header; + + uint8_t Manufacturer; // string + uint8_t Type; // varies + uint8_t Version; // string + uint8_t SerialNumber; // string + uint8_t AssetTagNumber; // string + + // 2.1+ + uint8_t BootupState; // enum + uint8_t PowerSupplyState; // enum + uint8_t ThermalState; // enum + uint8_t SecurityStatus; // enum + + // 2.3+ + uint32_t OEMDefined; // varies + uint8_t Height; // varies + uint8_t NumberOfPowerCords; // varies + uint8_t ContainedElementCount; // varies + uint8_t ContainedRecordLength; // varies + uint8_t ContainedElements[]; // varies +} FF_A_PACKED FFSmbiosSystemEnclosure; + +static_assert(offsetof(FFSmbiosSystemEnclosure, ContainedElements) == 0x15, + "FFSmbiosSystemEnclosure: Wrong struct alignment"); + +const char* ffDetectChassis(FFChassisResult* result) { + const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable(); + if (!smbiosTable) { + return "Failed to get SMBIOS data"; + } + + const FFSmbiosSystemEnclosure* data = (const FFSmbiosSystemEnclosure*) (*smbiosTable)[FF_SMBIOS_TYPE_SYSTEM_ENCLOSURE]; + if (!data) { + return "System enclosure is not found in SMBIOS data"; + } + + const char* strings = (const char*) data + data->Header.Length; + + ffStrbufSetStatic(&result->vendor, ffSmbiosLocateString(strings, data->Manufacturer)); + ffCleanUpSmbiosValue(&result->vendor); + ffStrbufSetStatic(&result->serial, ffSmbiosLocateString(strings, data->SerialNumber)); + ffCleanUpSmbiosValue(&result->serial); + ffStrbufSetStatic(&result->version, ffSmbiosLocateString(strings, data->Version)); + ffCleanUpSmbiosValue(&result->version); + ffStrbufSetStatic(&result->type, ffChassisTypeToString(data->Type)); + + return NULL; +} diff --git a/src/detection/codec/codec.c b/src/detection/codec/codec.c new file mode 100644 index 0000000..c7b3c45 --- /dev/null +++ b/src/detection/codec/codec.c @@ -0,0 +1,14 @@ +#include "codec.h" + +const char* ffDetectCodec(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/) { + if (options->useVulkan) { +#ifdef FF_HAVE_VULKAN + return ffDetectCodecVulkan(options, result); +#else + FF_UNUSED(options, result); + return "Fastfetch was built without Vulkan support"; +#endif + } + + return ffDetectCodecNative(options, result); +} diff --git a/src/detection/codec/codec.h b/src/detection/codec/codec.h new file mode 100644 index 0000000..d0f48d9 --- /dev/null +++ b/src/detection/codec/codec.h @@ -0,0 +1,43 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/codec/option.h" + +typedef enum FF_A_PACKED FFCodecType { + FF_CODEC_TYPE_NONE = 0, + FF_CODEC_TYPE_UNKNOWN = UINT32_C(1) << 0, + FF_CODEC_TYPE_H261 = UINT32_C(1) << 1, + FF_CODEC_TYPE_H263 = UINT32_C(1) << 2, + FF_CODEC_TYPE_MJPEG = UINT32_C(1) << 3, + FF_CODEC_TYPE_MPEG1 = UINT32_C(1) << 4, + FF_CODEC_TYPE_MPEG2 = UINT32_C(1) << 5, + FF_CODEC_TYPE_DIVX_XVID = UINT32_C(1) << 6, + FF_CODEC_TYPE_H264 = UINT32_C(1) << 7, + FF_CODEC_TYPE_WMV8 = UINT32_C(1) << 8, + FF_CODEC_TYPE_WMV9 = UINT32_C(1) << 9, + FF_CODEC_TYPE_VC1 = UINT32_C(1) << 10, + FF_CODEC_TYPE_VP8 = UINT32_C(1) << 11, + FF_CODEC_TYPE_HEVC = UINT32_C(1) << 12, + FF_CODEC_TYPE_VP9 = UINT32_C(1) << 13, + FF_CODEC_TYPE_AV1 = UINT32_C(1) << 14, + FF_CODEC_TYPE_VVC = UINT32_C(1) << 15, + FF_CODEC_TYPE_DOLBY_VISION_HEVC = UINT32_C(1) << 16, + FF_CODEC_TYPE_PRORES = UINT32_C(1) << 17, + FF_CODEC_TYPE_PRORES_RAW = UINT32_C(1) << 18, + FF_CODEC_TYPE_MAX = FF_CODEC_TYPE_PRORES_RAW, + FF_CODEC_TYPE_FORCE_UNSIGNED = UINT32_MAX, +} FFCodecType; + +typedef struct FFCodecResult { + FFstrbuf gpu; + FFCodecType decoders; + FFCodecType encoders; + const char* platformApi; +} FFCodecResult; + +const char* ffDetectCodec(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/); +const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/); + +#ifdef FF_HAVE_VULKAN +const char* ffDetectCodecVulkan(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/); +#endif diff --git a/src/detection/codec/codec_android.c b/src/detection/codec/codec_android.c new file mode 100644 index 0000000..238dab7 --- /dev/null +++ b/src/detection/codec/codec_android.c @@ -0,0 +1,102 @@ +#include "codec.h" + +#include "common/library.h" +#include "common/strutil.h" +#undef __INTRODUCED_IN +#define __INTRODUCED_IN(...) +#include <media/NdkMediaCodec.h> + +static const struct { + const char* mime; + FFCodecType type; +} FF_CODEC_MIME_TO_TYPE[] = { + { "video/3gpp", FF_CODEC_TYPE_H263 }, + { "video/h263", FF_CODEC_TYPE_H263 }, + { "video/mjpeg", FF_CODEC_TYPE_MJPEG }, + { "video/mpeg2", FF_CODEC_TYPE_MPEG2 }, + { "video/mp2v-es", FF_CODEC_TYPE_MPEG2 }, + { "video/mp4v-es", FF_CODEC_TYPE_DIVX_XVID }, + { "video/avc", FF_CODEC_TYPE_H264 }, + { "video/hevc", FF_CODEC_TYPE_HEVC }, + { "video/x-vnd.on2.vp8", FF_CODEC_TYPE_VP8 }, + { "video/x-vnd.on2.vp9", FF_CODEC_TYPE_VP9 }, + { "video/av01", FF_CODEC_TYPE_AV1 }, + { "video/vvc", FF_CODEC_TYPE_VVC }, +}; + +static bool ffCodecIsLikelySoftware(const char* codecName) { + if (!codecName) { + return true; + } + + return ffStrStartsWith(codecName, "OMX.google.") || + ffStrStartsWith(codecName, "c2.android.") || + ffStrStartsWith(codecName, "OMX.ffmpeg.") || + ffStrStartsWith(codecName, "OMX.PV."); +} + +static bool ffCodecIsHardwareAccelerated( + AMediaCodec* codec, + __typeof__(&AMediaCodec_getName) ffAMediaCodec_getName, + __typeof__(&AMediaCodec_releaseName) ffAMediaCodec_releaseName) { + if (!codec) { + return false; + } + + char* codecName = NULL; + media_status_t status = ffAMediaCodec_getName(codec, &codecName); + if (status != AMEDIA_OK || !codecName) { + return false; + } + + bool isHardware = !ffCodecIsLikelySoftware(codecName); + ffAMediaCodec_releaseName(codec, codecName); + return isHardware; +} + +const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/) { + FF_LIBRARY_LOAD_MESSAGE(mediandk, "libmediandk.so", 0) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_createDecoderByType) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_createEncoderByType) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_delete) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_getName) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mediandk, AMediaCodec_releaseName) + + FFCodecType decoders = FF_CODEC_TYPE_NONE; + FFCodecType encoders = FF_CODEC_TYPE_NONE; + + for (uint32_t i = 0; i < ARRAY_SIZE(FF_CODEC_MIME_TO_TYPE); ++i) { + const char* mime = FF_CODEC_MIME_TO_TYPE[i].mime; + FFCodecType type = FF_CODEC_MIME_TO_TYPE[i].type; + + if ((options->showType & FF_CODEC_SHOW_TYPE_DECODER) && !(decoders & type)) { + AMediaCodec* decoder = ffAMediaCodec_createDecoderByType(mime); + if (decoder) { + if (ffCodecIsHardwareAccelerated(decoder, ffAMediaCodec_getName, ffAMediaCodec_releaseName)) { + decoders |= type; + } + ffAMediaCodec_delete(decoder); + } + } + + if ((options->showType & FF_CODEC_SHOW_TYPE_ENCODER) && !(encoders & type)) { + AMediaCodec* encoder = ffAMediaCodec_createEncoderByType(mime); + if (encoder) { + if (ffCodecIsHardwareAccelerated(encoder, ffAMediaCodec_getName, ffAMediaCodec_releaseName)) { + encoders |= type; + } + ffAMediaCodec_delete(encoder); + } + } + } + + if (decoders != FF_CODEC_TYPE_NONE || encoders != FF_CODEC_TYPE_NONE) { + FFCodecResult* item = FF_LIST_ADD(FFCodecResult, *result); + ffStrbufInitStatic(&item->gpu, "Default"); + item->decoders = decoders; + item->encoders = encoders; + item->platformApi = "AMediaCodec"; + } + + return NULL; +} diff --git a/src/detection/codec/codec_apple.c b/src/detection/codec/codec_apple.c new file mode 100644 index 0000000..3778373 --- /dev/null +++ b/src/detection/codec/codec_apple.c @@ -0,0 +1,96 @@ +#include "codec.h" + +#include <VideoToolbox/VideoToolbox.h> +#include "common/apple/cf_helpers.h" + +static const struct { + CMVideoCodecType codec; + FFCodecType type; +} FF_CODEC_CODECS[] = { + { 'h263', FF_CODEC_TYPE_H263 }, // kCMVideoCodecType_H263 + { 'jpeg', FF_CODEC_TYPE_MJPEG }, // kCMVideoCodecType_JPEG + { 'dmb1', FF_CODEC_TYPE_MJPEG }, // kCMVideoCodecType_JPEG_OpenDML + { 'mp1v', FF_CODEC_TYPE_MPEG1 }, // kCMVideoCodecType_MPEG1Video + { 'mp2v', FF_CODEC_TYPE_MPEG2 }, // kCMVideoCodecType_MPEG2Video + { 'mp4v', FF_CODEC_TYPE_DIVX_XVID }, // kCMVideoCodecType_MPEG4Video + { 'avc1', FF_CODEC_TYPE_H264 }, // kCMVideoCodecType_H264 + { 'hvc1', FF_CODEC_TYPE_HEVC }, // kCMVideoCodecType_HEVC + { 'muxa', FF_CODEC_TYPE_HEVC }, // kCMVideoCodecType_HEVCWithAlpha + { 'dvh1', FF_CODEC_TYPE_DOLBY_VISION_HEVC }, // kCMVideoCodecType_DolbyVisionHEVC + { 'dish', FF_CODEC_TYPE_HEVC }, // kCMVideoCodecType_DisparityHEVC + { 'deph', FF_CODEC_TYPE_HEVC }, // kCMVideoCodecType_DepthHEVC + { 'vp09', FF_CODEC_TYPE_VP9 }, // kCMVideoCodecType_VP9 + { 'av01', FF_CODEC_TYPE_AV1 }, // kCMVideoCodecType_AV1 + { 'ap4x', FF_CODEC_TYPE_PRORES }, // kCMVideoCodecType_AppleProRes4444XQ + { 'ap4h', FF_CODEC_TYPE_PRORES }, // kCMVideoCodecType_AppleProRes4444 + { 'apch', FF_CODEC_TYPE_PRORES }, // kCMVideoCodecType_AppleProRes422HQ + { 'apcn', FF_CODEC_TYPE_PRORES }, // kCMVideoCodecType_AppleProRes422 + { 'apcs', FF_CODEC_TYPE_PRORES }, // kCMVideoCodecType_AppleProRes422LT + { 'apco', FF_CODEC_TYPE_PRORES }, // kCMVideoCodecType_AppleProRes422Proxy + { 'aprn', FF_CODEC_TYPE_PRORES_RAW }, // kCMVideoCodecType_AppleProResRAW + { 'aprh', FF_CODEC_TYPE_PRORES_RAW }, // kCMVideoCodecType_AppleProResRAWHQ +}; + +static FFCodecType ffCodecCodecToType(CMVideoCodecType codec) { + for (uint32_t i = 0; i < ARRAY_SIZE(FF_CODEC_CODECS); ++i) { + if (FF_CODEC_CODECS[i].codec == codec) { + return FF_CODEC_CODECS[i].type; + } + } + return FF_CODEC_TYPE_NONE; +} + +static FFCodecType ffCodecDetectEncoders(void) { + CFArrayRef encoderList = NULL; + if (VTCopyVideoEncoderList(NULL, &encoderList) != noErr || !encoderList) { + return FF_CODEC_TYPE_NONE; + } + + FFCodecType types = FF_CODEC_TYPE_NONE; + for (uint32_t i = 0; i < CFArrayGetCount(encoderList); ++i) { + CFDictionaryRef encoder = CFArrayGetValueAtIndex(encoderList, i); + bool isHardwareAccelerated; + int codec; + if (ffCfDictGetBool(encoder, CFSTR("IsHardwareAccelerated"), &isHardwareAccelerated) != NULL || + !isHardwareAccelerated || + ffCfDictGetInt(encoder, CFSTR("CodecType"), &codec) != NULL) { + continue; + } + types |= ffCodecCodecToType((CMVideoCodecType) codec); + } + + return types; +} + +static FFCodecType ffCodecDetectDecoders() { + FFCodecType types = FF_CODEC_TYPE_NONE; + for (uint32_t i = 0; i < ARRAY_SIZE(FF_CODEC_CODECS); ++i) { + if (types & FF_CODEC_CODECS[i].type) { + continue; + } + + bool supported = VTIsHardwareDecodeSupported(FF_CODEC_CODECS[i].codec); + if (!supported) { + continue; + } + + types |= FF_CODEC_CODECS[i].type; + } + + return types; +} + +const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /* list of FFCodecResult */) { + FFCodecType decoders = options->showType & FF_CODEC_SHOW_TYPE_DECODER ? ffCodecDetectDecoders() : FF_CODEC_TYPE_NONE; + FFCodecType encoders = options->showType & FF_CODEC_SHOW_TYPE_ENCODER ? ffCodecDetectEncoders() : FF_CODEC_TYPE_NONE; + + if (decoders != FF_CODEC_TYPE_NONE || encoders != FF_CODEC_TYPE_NONE) { + FFCodecResult* item = FF_LIST_ADD(FFCodecResult, *result); + ffStrbufInitStatic(&item->gpu, "Default"); + item->decoders = decoders; + item->encoders = encoders; + item->platformApi = "VideoToolbox"; + } + + return NULL; +} diff --git a/src/detection/codec/codec_linux.c b/src/detection/codec/codec_linux.c new file mode 100644 index 0000000..c25614e --- /dev/null +++ b/src/detection/codec/codec_linux.c @@ -0,0 +1,470 @@ +#include "codec.h" + +#if (FF_HAVE_DRM && FF_HAVE_VA) || FF_HAVE_VDPAU + + #include "common/library.h" + #include "common/mallocHelper.h" + #include "common/io.h" + + #if FF_HAVE_DRM && FF_HAVE_VA + #include "detection/gpu/gpu.h" + + #include <fcntl.h> + #include <va/va.h> + #include <va/va_drm.h> + #include <xf86drm.h> + #endif + + #include <string.h> + + #if FF_HAVE_VDPAU + #include <stdlib.h> + #include <vdpau/vdpau.h> + +VdpStatus vdp_device_create_x11(void* display, int screen, VdpDevice* device, VdpGetProcAddress** get_proc_address); +void* XOpenDisplay(const char* display_name); +int XCloseDisplay(void* display); +int XDefaultScreen(void* display); + #endif + + #if FF_HAVE_DRM && FF_HAVE_VA + +static FFCodecType ffCodecProfileToType(VAProfile profile) { + switch (profile) { + case 11: // VAProfileH263Baseline + return FF_CODEC_TYPE_H263; + + case 12: // VAProfileJPEGBaseline + return FF_CODEC_TYPE_MJPEG; + + case 0: // VAProfileMPEG2Simple + case 1: // VAProfileMPEG2Main + return FF_CODEC_TYPE_MPEG2; + + case 2: // VAProfileMPEG4Simple + case 3: // VAProfileMPEG4AdvancedSimple + case 4: // VAProfileMPEG4Main + return FF_CODEC_TYPE_DIVX_XVID; + + case 5: // VAProfileH264Baseline + case 6: // VAProfileH264Main + case 7: // VAProfileH264High + case 13: // VAProfileH264ConstrainedBaseline + case 15: // VAProfileH264MultiviewHigh + case 16: // VAProfileH264StereoHigh + case 36: // VAProfileH264High10 + case 40: // VAProfileH264High422 + return FF_CODEC_TYPE_H264; + + case 8: // VAProfileVC1Simple + case 9: // VAProfileVC1Main + case 10: // VAProfileVC1Advanced + return FF_CODEC_TYPE_VC1; + + case 14: // VAProfileVP8Version0_3 + return FF_CODEC_TYPE_VP8; + + case 17: // VAProfileHEVCMain + case 18: // VAProfileHEVCMain10 + case 23: // VAProfileHEVCMain12 + case 24: // VAProfileHEVCMain422_10 + case 25: // VAProfileHEVCMain422_12 + case 26: // VAProfileHEVCMain444 + case 27: // VAProfileHEVCMain444_10 + case 28: // VAProfileHEVCMain444_12 + case 29: // VAProfileHEVCSccMain + case 30: // VAProfileHEVCSccMain10 + case 31: // VAProfileHEVCSccMain444 + case 34: // VAProfileHEVCSccMain444_10 + return FF_CODEC_TYPE_HEVC; + + case 19: // VAProfileVP9Profile0 + case 20: // VAProfileVP9Profile1 + case 21: // VAProfileVP9Profile2 + case 22: // VAProfileVP9Profile3 + return FF_CODEC_TYPE_VP9; + + case 32: // VAProfileAV1Profile0 + case 33: // VAProfileAV1Profile1 + case 39: // VAProfileAV1Profile2 + return FF_CODEC_TYPE_AV1; + + case 37: // VAProfileVVCMain10 + case 38: // VAProfileVVCMultilayerMain10 + return FF_CODEC_TYPE_VVC; + + default: + return FF_CODEC_TYPE_UNKNOWN; + } +} + +static FFCodecShowType ffCodecGetEntrypointType(VAEntrypoint entrypoint) { + switch (entrypoint) { + case VAEntrypointVLD: + case VAEntrypointIDCT: + case VAEntrypointMoComp: + return FF_CODEC_SHOW_TYPE_DECODER; + case VAEntrypointEncSlice: + case VAEntrypointEncSliceLP: + case VAEntrypointFEI: + return FF_CODEC_SHOW_TYPE_ENCODER; + default: + return FF_CODEC_SHOW_TYPE_NONE; + } +} + +static bool ffCodecProfileHasOutput( + VADisplay display, + __typeof__(vaQueryConfigEntrypoints)* ffvaQueryConfigEntrypoints, + __typeof__(vaGetConfigAttributes)* ffvaGetConfigAttributes, + int maxEntrypoints, + VAEntrypoint* entrypoints, + VAProfile profile, + FFCodecShowType entrypointType) { + int numEntrypoints = maxEntrypoints; + if (ffvaQueryConfigEntrypoints(display, profile, entrypoints, &numEntrypoints) != VA_STATUS_SUCCESS) { + return false; + } + + for (int i = 0; i < numEntrypoints; ++i) { + if (ffCodecGetEntrypointType(entrypoints[i]) != entrypointType) { + continue; + } + + VAConfigAttrib attrib = { + .type = VAConfigAttribRTFormat, + .value = 0, + }; + + if (ffvaGetConfigAttributes(display, profile, entrypoints[i], &attrib, 1) == VA_STATUS_SUCCESS && + attrib.value != VA_ATTRIB_NOT_SUPPORTED && + attrib.value != 0) { + return true; + } + } + + return false; +} + +static void ffCodecFillGpuName(const drmDevice* dev, const char* path, FFstrbuf* name) { + ffStrbufInit(name); + + switch (dev->bustype) { + case DRM_BUS_PCI: { + FFGPUResult gpu = { + .vendor = ffStrbufCreateStatic(ffGPUGetVendorString(dev->deviceinfo.pci->vendor_id)), + .name = ffStrbufCreate(), + }; + + ffGPUFillVendorAndName(0, dev->deviceinfo.pci->vendor_id, dev->deviceinfo.pci->device_id, &gpu); + ffStrbufSetF(name, "%s %s", gpu.vendor.chars, gpu.name.chars); + + ffStrbufDestroy(&gpu.vendor); + ffStrbufDestroy(&gpu.name); + break; + } + case DRM_BUS_PLATFORM: + ffStrbufSetS(name, dev->deviceinfo.platform->compatible[0]); + return; + case DRM_BUS_HOST1X: + ffStrbufSetS(name, dev->deviceinfo.host1x->compatible[0]); + return; + case DRM_BUS_USB: + ffStrbufSetF(name, "0x%04X 0x%04X", dev->deviceinfo.usb->vendor, dev->deviceinfo.usb->product); + return; + default: + ffStrbufSetStatic(name, "Unknown GPU"); + return; + } + + if (!name->length && path && *path) { + const char* base = strrchr(path, '/'); + ffStrbufSetS(name, base ? base + 1 : path); + } + + if (!name->length) { + ffStrbufSetStatic(name, "Unknown GPU"); + } +} + +static const char* ffDetectCodecByVa(FFCodecOptions* options, FFlist* result) { + FF_LIBRARY_LOAD_MESSAGE(libdrm, "libdrm" FF_LIBRARY_EXTENSION, 2) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmGetDevices) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmFreeDevices) + + FF_LIBRARY_LOAD_MESSAGE(libva, "libva" FF_LIBRARY_EXTENSION, 2) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libva, vaInitialize) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libva, vaTerminate) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libva, vaMaxNumProfiles) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libva, vaMaxNumEntrypoints) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libva, vaQueryConfigProfiles) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libva, vaQueryConfigEntrypoints) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libva, vaGetConfigAttributes) + + FF_LIBRARY_LOAD_MESSAGE(libvaDrm, "libva-drm" FF_LIBRARY_EXTENSION, 2) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libvaDrm, vaGetDisplayDRM) + + drmDevicePtr devices[64]; + int numDevices = ffdrmGetDevices(devices, ARRAY_SIZE(devices)); + if (numDevices < 0) { + return "drmGetDevices() failed"; + } + if (numDevices == 0) { + return "No DRM devices found"; + } + + const char* error = "No DRM device could initialize VA-API"; + + for (int i = 0; i < numDevices; ++i) { + drmDevice* dev = devices[i]; + + const char* path = NULL; + if (dev->available_nodes & (1 << DRM_NODE_RENDER)) { + path = dev->nodes[DRM_NODE_RENDER]; + } else if (dev->available_nodes & (1 << DRM_NODE_PRIMARY)) { + path = dev->nodes[DRM_NODE_PRIMARY]; + } else { + continue; + } + + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + fd = open(path, O_RDONLY | O_CLOEXEC); + } + if (fd < 0) { + continue; + } + + VADisplay display = ffvaGetDisplayDRM(fd); + if (!display) { + continue; + } + + int major = 0, minor = 0; + if (ffvaInitialize(display, &major, &minor) != VA_STATUS_SUCCESS) { + continue; + } + error = NULL; + + int maxProfiles = ffvaMaxNumProfiles(display); + int maxEntrypoints = ffvaMaxNumEntrypoints(display); + if (maxProfiles <= 0 || maxEntrypoints <= 0) { + ffvaTerminate(display); + continue; + } + + FF_AUTO_FREE VAProfile* profiles = (VAProfile*) malloc(sizeof(VAProfile) * (size_t) maxProfiles); + FF_AUTO_FREE VAEntrypoint* entrypoints = (VAEntrypoint*) malloc(sizeof(VAEntrypoint) * (size_t) maxEntrypoints); + + int numProfiles = maxProfiles; + if (ffvaQueryConfigProfiles(display, profiles, &numProfiles) != VA_STATUS_SUCCESS) { + ffvaTerminate(display); + continue; + } + + FFCodecType decoderTypes = FF_CODEC_TYPE_NONE; + FFCodecType encoderTypes = FF_CODEC_TYPE_NONE; + for (int j = 0; j < numProfiles; ++j) { + FFCodecType type = ffCodecProfileToType(profiles[j]); + + bool hasDecoder = (options->showType & FF_CODEC_SHOW_TYPE_DECODER) && + !(decoderTypes & type) && + ffCodecProfileHasOutput( + display, + ffvaQueryConfigEntrypoints, + ffvaGetConfigAttributes, + maxEntrypoints, + entrypoints, + profiles[j], + FF_CODEC_SHOW_TYPE_DECODER); + + bool hasEncoder = (options->showType & FF_CODEC_SHOW_TYPE_ENCODER) && + !(encoderTypes & type) && + ffCodecProfileHasOutput( + display, + ffvaQueryConfigEntrypoints, + ffvaGetConfigAttributes, + maxEntrypoints, + entrypoints, + profiles[j], + FF_CODEC_SHOW_TYPE_ENCODER); + + if (!hasDecoder && !hasEncoder) { + continue; + } + + if (hasDecoder) { + decoderTypes |= type; + } + if (hasEncoder) { + encoderTypes |= type; + } + } + + if (decoderTypes != FF_CODEC_TYPE_NONE || encoderTypes != FF_CODEC_TYPE_NONE) { + FFCodecResult* item = FF_LIST_ADD(FFCodecResult, *result); + ffCodecFillGpuName(dev, path, &item->gpu); + item->decoders = decoderTypes; + item->encoders = encoderTypes; + item->platformApi = "VA-API"; + } + + ffvaTerminate(display); + } + + ffdrmFreeDevices(devices, numDevices); + return error; +} + #endif + + #if FF_HAVE_VDPAU + +static const struct FFCodecVdpauCodec { + VdpDecoderProfile profile; + FFCodecType type; +} FF_CODEC_VDPAU_CODECS[] = { + { 0, FF_CODEC_TYPE_MPEG1 }, // VDP_DECODER_PROFILE_MPEG1 + { 1, FF_CODEC_TYPE_MPEG2 }, // VDP_DECODER_PROFILE_MPEG2_SIMPLE + { 2, FF_CODEC_TYPE_MPEG2 }, // VDP_DECODER_PROFILE_MPEG2_MAIN + { 12, FF_CODEC_TYPE_DIVX_XVID }, // VDP_DECODER_PROFILE_MPEG4_PART2_SP + { 13, FF_CODEC_TYPE_DIVX_XVID }, // VDP_DECODER_PROFILE_MPEG4_PART2_ASP + { 14, FF_CODEC_TYPE_DIVX_XVID }, // VDP_DECODER_PROFILE_DIVX4_QMOBILE + { 15, FF_CODEC_TYPE_DIVX_XVID }, // VDP_DECODER_PROFILE_DIVX4_MOBILE + { 16, FF_CODEC_TYPE_DIVX_XVID }, // VDP_DECODER_PROFILE_DIVX4_HOME_THEATER + { 17, FF_CODEC_TYPE_DIVX_XVID }, // VDP_DECODER_PROFILE_DIVX4_HD_1080P + { 18, FF_CODEC_TYPE_DIVX_XVID }, // VDP_DECODER_PROFILE_DIVX5_QMOBILE + { 19, FF_CODEC_TYPE_DIVX_XVID }, // VDP_DECODER_PROFILE_DIVX5_MOBILE + { 20, FF_CODEC_TYPE_DIVX_XVID }, // VDP_DECODER_PROFILE_DIVX5_HOME_THEATER + { 21, FF_CODEC_TYPE_DIVX_XVID }, // VDP_DECODER_PROFILE_DIVX5_HD_1080P + { 6, FF_CODEC_TYPE_H264 }, // VDP_DECODER_PROFILE_H264_BASELINE + { 7, FF_CODEC_TYPE_H264 }, // VDP_DECODER_PROFILE_H264_MAIN + { 8, FF_CODEC_TYPE_H264 }, // VDP_DECODER_PROFILE_H264_HIGH + { 22, FF_CODEC_TYPE_H264 }, // VDP_DECODER_PROFILE_H264_CONSTRAINED_BASELINE + { 23, FF_CODEC_TYPE_H264 }, // VDP_DECODER_PROFILE_H264_EXTENDED + { 24, FF_CODEC_TYPE_H264 }, // VDP_DECODER_PROFILE_H264_PROGRESSIVE_HIGH + { 25, FF_CODEC_TYPE_H264 }, // VDP_DECODER_PROFILE_H264_CONSTRAINED_HIGH + { 26, FF_CODEC_TYPE_H264 }, // VDP_DECODER_PROFILE_H264_HIGH_444_PREDICTIVE + { 9, FF_CODEC_TYPE_VC1 }, // VDP_DECODER_PROFILE_VC1_SIMPLE + { 10, FF_CODEC_TYPE_VC1 }, // VDP_DECODER_PROFILE_VC1_MAIN + { 11, FF_CODEC_TYPE_VC1 }, // VDP_DECODER_PROFILE_VC1_ADVANCED + { 100, FF_CODEC_TYPE_HEVC }, // VDP_DECODER_PROFILE_HEVC_MAIN + { 101, FF_CODEC_TYPE_HEVC }, // VDP_DECODER_PROFILE_HEVC_MAIN_10 + { 102, FF_CODEC_TYPE_HEVC }, // VDP_DECODER_PROFILE_HEVC_MAIN_STILL + { 103, FF_CODEC_TYPE_HEVC }, // VDP_DECODER_PROFILE_HEVC_MAIN_12 + { 104, FF_CODEC_TYPE_HEVC }, // VDP_DECODER_PROFILE_HEVC_MAIN_444 + { 105, FF_CODEC_TYPE_HEVC }, // VDP_DECODER_PROFILE_HEVC_MAIN_444_10 + { 106, FF_CODEC_TYPE_HEVC }, // VDP_DECODER_PROFILE_HEVC_MAIN_444_12 + { 27, FF_CODEC_TYPE_VP9 }, // VDP_DECODER_PROFILE_VP9_PROFILE_0 + { 28, FF_CODEC_TYPE_VP9 }, // VDP_DECODER_PROFILE_VP9_PROFILE_1 + { 29, FF_CODEC_TYPE_VP9 }, // VDP_DECODER_PROFILE_VP9_PROFILE_2 + { 30, FF_CODEC_TYPE_VP9 }, // VDP_DECODER_PROFILE_VP9_PROFILE_3 + { 107, FF_CODEC_TYPE_AV1 }, // VDP_DECODER_PROFILE_AV1_MAIN + { 108, FF_CODEC_TYPE_AV1 }, // VDP_DECODER_PROFILE_AV1_HIGH + { 109, FF_CODEC_TYPE_AV1 }, // VDP_DECODER_PROFILE_AV1_PROFESSIONAL +}; + +static const char* ffDetectCodecByVdpau(FFCodecOptions* options, FFlist* result) { + if (options->showType == FF_CODEC_SHOW_TYPE_DECODER) { + return "VDPAU only supports decoding"; + } + + FF_LIBRARY_LOAD_MESSAGE(libX11, "libX11" FF_LIBRARY_EXTENSION, 6) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libX11, XOpenDisplay) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libX11, XCloseDisplay) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libX11, XDefaultScreen) + + FF_LIBRARY_LOAD_MESSAGE(libvdpau, "libvdpau" FF_LIBRARY_EXTENSION, 1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libvdpau, vdp_device_create_x11) + + void* x11Display = ffXOpenDisplay(NULL); + if (!x11Display) { + return "XOpenDisplay() failed"; + } + + VdpDevice device = VDP_INVALID_HANDLE; + VdpGetProcAddress* ffvdp_get_proc_address = NULL; + if (ffvdp_device_create_x11(x11Display, ffXDefaultScreen(x11Display), &device, &ffvdp_get_proc_address) != VDP_STATUS_OK || + device == VDP_INVALID_HANDLE || + ffvdp_get_proc_address == NULL) { + ffXCloseDisplay(x11Display); + return "vdp_device_create_x11() failed"; + } + + VdpDeviceDestroy* ffvdp_device_destroy = NULL; + VdpDecoderQueryCapabilities* ffvdp_decoder_query_capabilities = NULL; + if (ffvdp_get_proc_address(device, VDP_FUNC_ID_DEVICE_DESTROY, (void**) &ffvdp_device_destroy) != VDP_STATUS_OK || + ffvdp_get_proc_address(device, VDP_FUNC_ID_DECODER_QUERY_CAPABILITIES, (void**) &ffvdp_decoder_query_capabilities) != VDP_STATUS_OK || + ffvdp_device_destroy == NULL || + ffvdp_decoder_query_capabilities == NULL) { + if (ffvdp_device_destroy) { + ffvdp_device_destroy(device); + } + ffXCloseDisplay(x11Display); + return "ffvdp_get_proc_address() failed"; + } + + FFCodecType decoderTypes = FF_CODEC_TYPE_NONE; + + for (uint32_t i = 0; i < ARRAY_SIZE(FF_CODEC_VDPAU_CODECS); ++i) { + const struct FFCodecVdpauCodec* codec = &FF_CODEC_VDPAU_CODECS[i]; + if (decoderTypes & codec->type) { + continue; + } + VdpBool isSupported = VDP_FALSE; + uint32_t maxLevel = 0, maxMacroblocks = 0, maxWidth = 0, maxHeight = 0; + if (ffvdp_decoder_query_capabilities(device, codec->profile, &isSupported, &maxLevel, &maxMacroblocks, &maxWidth, &maxHeight) != VDP_STATUS_OK || + !isSupported) { + continue; + } + + decoderTypes |= codec->type; + } + + ffvdp_device_destroy(device); + ffXCloseDisplay(x11Display); + + if (decoderTypes == FF_CODEC_TYPE_NONE) { + return NULL; + } + + FFCodecResult* item = FF_LIST_ADD(FFCodecResult, *result); + ffStrbufInit(&item->gpu); + const char* driver = getenv("VDPAU_DRIVER"); + if (driver && *driver) { + ffStrbufSetS(&item->gpu, driver); + } else { + ffStrbufSetStatic(&item->gpu, "Default"); + } + item->decoders = decoderTypes; + item->encoders = FF_CODEC_TYPE_NONE; + item->platformApi = "VDPAU"; + + return NULL; +} + #endif + +const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /* list of FFCodecResult */) { + FF_SUPPRESS_IO(); + + #if FF_HAVE_DRM && FF_HAVE_VA + if (ffDetectCodecByVa(options, result) == NULL) { + return NULL; + } + #endif + #if FF_HAVE_VDPAU + if (ffDetectCodecByVdpau(options, result) == NULL) { + return NULL; + } + #endif + + return "Both libva and libvdpau fail to initialize"; +} + +#else + +const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /* list of FFCodecResult */) { + FF_UNUSED(options, result); + return "Fastfetch was built without DRM / VA-API / VDPAU headers"; +} + +#endif diff --git a/src/detection/codec/codec_vulkan.c b/src/detection/codec/codec_vulkan.c new file mode 100644 index 0000000..eabfe26 --- /dev/null +++ b/src/detection/codec/codec_vulkan.c @@ -0,0 +1,270 @@ +#include "codec.h" + +#ifdef FF_HAVE_VULKAN + #include <vulkan/vulkan.h> + #include "common/strutil.h" + + #if VK_KHR_video_decode_queue && VK_KHR_video_encode_queue + + #include "common/debug.h" + #include "common/io.h" + #include "common/library.h" + #include "common/mallocHelper.h" + +static FFCodecType ffCodecDecodeOperationsToTypes(VkVideoCodecOperationFlagsKHR operations) { + FFCodecType types = FF_CODEC_TYPE_NONE; + + if (operations & 0x00000001 /*VK_VIDEO_CODEC_OPERATION_DECODE_H264_BIT_KHR*/) { + types |= FF_CODEC_TYPE_H264; + } + if (operations & 0x00000002 /*VK_VIDEO_CODEC_OPERATION_DECODE_H265_BIT_KHR*/) { + types |= FF_CODEC_TYPE_HEVC; + } + if (operations & 0x00000004 /*VK_VIDEO_CODEC_OPERATION_DECODE_AV1_BIT_KHR*/) { + types |= FF_CODEC_TYPE_AV1; + } + if (operations & 0x00000008 /*VK_VIDEO_CODEC_OPERATION_DECODE_VP9_BIT_KHR*/) { + types |= FF_CODEC_TYPE_VP9; + } + + return types; +} + +static FFCodecType ffCodecEncodeOperationsToTypes(VkVideoCodecOperationFlagsKHR operations) { + FFCodecType types = FF_CODEC_TYPE_NONE; + + if (operations & 0x00010000 /*VK_VIDEO_CODEC_OPERATION_ENCODE_H264_BIT_KHR*/) { + types |= FF_CODEC_TYPE_H264; + } + if (operations & 0x00020000 /*VK_VIDEO_CODEC_OPERATION_ENCODE_H265_BIT_KHR*/) { + types |= FF_CODEC_TYPE_HEVC; + } + if (operations & 0x00040000 /*VK_VIDEO_CODEC_OPERATION_ENCODE_AV1_BIT_KHR*/) { + types |= FF_CODEC_TYPE_AV1; + } + + return types; +} + +static bool ffCodecHasDeviceExtension(const VkExtensionProperties* extensions, uint32_t extensionCount, const char* extensionName) { + for (uint32_t i = 0; i < extensionCount; ++i) { + if (ffStrEquals(extensions[i].extensionName, extensionName)) { + return true; + } + } + + return false; +} + +const char* ffDetectCodecVulkan(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/) { + FF_DEBUG("Starting Vulkan codec detection"); + + FF_LIBRARY_LOAD_MESSAGE(vulkan, + #if __APPLE__ + "libMoltenVK" FF_LIBRARY_EXTENSION, + -1 + #elif _WIN32 + "vulkan-1" FF_LIBRARY_EXTENSION, + -1 + #else + "libvulkan" FF_LIBRARY_EXTENSION, + 2 + #endif + ) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(vulkan, vkGetInstanceProcAddr) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(vulkan, vkCreateInstance) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(vulkan, vkDestroyInstance) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(vulkan, vkEnumeratePhysicalDevices) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(vulkan, vkEnumerateDeviceExtensionProperties) + + FF_SUPPRESS_IO(); + + uint32_t apiVersion = VK_API_VERSION_1_0; + PFN_vkEnumerateInstanceVersion ffvkEnumerateInstanceVersion = (PFN_vkEnumerateInstanceVersion) ffvkGetInstanceProcAddr(NULL, "vkEnumerateInstanceVersion"); + if (ffvkEnumerateInstanceVersion != NULL) { + uint32_t detectedApiVersion = 0; + if (ffvkEnumerateInstanceVersion(&detectedApiVersion) == VK_SUCCESS) { + apiVersion = detectedApiVersion; + } + } + + const uint32_t projectVersion = VK_MAKE_VERSION( + FASTFETCH_PROJECT_VERSION_MAJOR, + FASTFETCH_PROJECT_VERSION_MINOR, + FASTFETCH_PROJECT_VERSION_PATCH); + + VkInstance vkInstance = VK_NULL_HANDLE; + VkResult res = ffvkCreateInstance(&(VkInstanceCreateInfo){ + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pApplicationInfo = &(VkApplicationInfo){ + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pApplicationName = FASTFETCH_PROJECT_NAME, + .applicationVersion = projectVersion, + .pEngineName = "fastfetch-codec-vulkan", + .engineVersion = projectVersion, + .apiVersion = apiVersion, + }, + }, + NULL, + &vkInstance); + if (res != VK_SUCCESS) { + FF_DEBUG("ffvkCreateInstance() failed with VkResult=%d", res); + switch (res) { + case VK_ERROR_OUT_OF_HOST_MEMORY: + return "ffvkCreateInstance() failed: VK_ERROR_OUT_OF_HOST_MEMORY"; + case VK_ERROR_OUT_OF_DEVICE_MEMORY: + return "ffvkCreateInstance() failed: VK_ERROR_OUT_OF_DEVICE_MEMORY"; + case VK_ERROR_INITIALIZATION_FAILED: + return "ffvkCreateInstance() failed: VK_ERROR_INITIALIZATION_FAILED"; + case VK_ERROR_LAYER_NOT_PRESENT: + return "ffvkCreateInstance() failed: VK_ERROR_LAYER_NOT_PRESENT"; + case VK_ERROR_EXTENSION_NOT_PRESENT: + return "ffvkCreateInstance() failed: VK_ERROR_EXTENSION_NOT_PRESENT"; + case VK_ERROR_INCOMPATIBLE_DRIVER: + return "ffvkCreateInstance() failed: VK_ERROR_INCOMPATIBLE_DRIVER"; + default: + return "ffvkCreateInstance() failed: unknown error"; + } + } + + PFN_vkGetPhysicalDeviceProperties ffvkGetPhysicalDeviceProperties = (PFN_vkGetPhysicalDeviceProperties) ffvkGetInstanceProcAddr(vkInstance, "vkGetPhysicalDeviceProperties"); + if (!ffvkGetPhysicalDeviceProperties) { + ffvkDestroyInstance(vkInstance, NULL); + return "vkGetPhysicalDeviceProperties is not available"; + } + + PFN_vkGetPhysicalDeviceQueueFamilyProperties2 ffvkGetPhysicalDeviceQueueFamilyProperties2 = (PFN_vkGetPhysicalDeviceQueueFamilyProperties2) ffvkGetInstanceProcAddr(vkInstance, "vkGetPhysicalDeviceQueueFamilyProperties2"); + if (!ffvkGetPhysicalDeviceQueueFamilyProperties2) { + ffvkDestroyInstance(vkInstance, NULL); + return "vkGetPhysicalDeviceQueueFamilyProperties2 is not available"; + } + + uint32_t physicalDeviceCount = 0; + res = ffvkEnumeratePhysicalDevices(vkInstance, &physicalDeviceCount, NULL); + if (res != VK_SUCCESS) { + FF_DEBUG("ffvkEnumeratePhysicalDevices(count) failed with VkResult=%d", res); + ffvkDestroyInstance(vkInstance, NULL); + return "ffvkEnumeratePhysicalDevices() failed during Vulkan codec detection"; + } + if (physicalDeviceCount == 0) { + ffvkDestroyInstance(vkInstance, NULL); + return "No Vulkan physical devices found"; + } + + FF_AUTO_FREE VkPhysicalDevice* physicalDevices = (VkPhysicalDevice*) malloc(sizeof(VkPhysicalDevice) * (size_t) physicalDeviceCount); + + res = ffvkEnumeratePhysicalDevices(vkInstance, &physicalDeviceCount, physicalDevices); + if (res != VK_SUCCESS) { + FF_DEBUG("ffvkEnumeratePhysicalDevices(list) failed with VkResult=%d", res); + ffvkDestroyInstance(vkInstance, NULL); + return "ffvkEnumeratePhysicalDevices() failed during Vulkan codec detection"; + } + + bool sawVideoQueueExtension = false; + + for (uint32_t i = 0; i < physicalDeviceCount; ++i) { + VkPhysicalDeviceProperties properties = {}; + ffvkGetPhysicalDeviceProperties(physicalDevices[i], &properties); + + uint32_t extensionCount = 0; + res = ffvkEnumerateDeviceExtensionProperties(physicalDevices[i], NULL, &extensionCount, NULL); + if (res != VK_SUCCESS) { + FF_DEBUG("vkEnumerateDeviceExtensionProperties(count) failed for '%s' with VkResult=%d", properties.deviceName, res); + continue; + } + + if (extensionCount == 0) { + FF_DEBUG("Skipping Vulkan device '%s' because it has no extensions", properties.deviceName); + continue; + } + + FF_AUTO_FREE VkExtensionProperties* extensions = (VkExtensionProperties*) malloc(sizeof(VkExtensionProperties) * (size_t) extensionCount); + + res = ffvkEnumerateDeviceExtensionProperties(physicalDevices[i], NULL, &extensionCount, extensions); + if (res != VK_SUCCESS) { + FF_DEBUG("vkEnumerateDeviceExtensionProperties(list) failed for '%s' with VkResult=%d", properties.deviceName, res); + continue; + } + + bool hasVideoDecode = (options->showType & FF_CODEC_SHOW_TYPE_DECODER) && + ffCodecHasDeviceExtension(extensions, extensionCount, VK_KHR_VIDEO_DECODE_QUEUE_EXTENSION_NAME); + bool hasVideoEncode = (options->showType & FF_CODEC_SHOW_TYPE_ENCODER) && + ffCodecHasDeviceExtension(extensions, extensionCount, VK_KHR_VIDEO_ENCODE_QUEUE_EXTENSION_NAME); + + if (!hasVideoDecode && !hasVideoEncode) { + FF_DEBUG("Skipping Vulkan device '%s' because it does not support video queue extensions", properties.deviceName); + continue; + } + sawVideoQueueExtension = true; + + uint32_t queueFamilyCount = 0; + ffvkGetPhysicalDeviceQueueFamilyProperties2(physicalDevices[i], &queueFamilyCount, NULL); + if (queueFamilyCount == 0) { + FF_DEBUG("Skipping Vulkan device '%s' because it has no queue families", properties.deviceName); + continue; + } + + FF_AUTO_FREE VkQueueFamilyProperties2* queueFamilyProperties = (VkQueueFamilyProperties2*) malloc(sizeof(VkQueueFamilyProperties2) * (size_t) queueFamilyCount); + FF_AUTO_FREE VkQueueFamilyVideoPropertiesKHR* queueFamilyVideoProperties = (VkQueueFamilyVideoPropertiesKHR*) malloc(sizeof(VkQueueFamilyVideoPropertiesKHR) * (size_t) queueFamilyCount); + + for (uint32_t queueIndex = 0; queueIndex < queueFamilyCount; ++queueIndex) { + queueFamilyVideoProperties[queueIndex] = (VkQueueFamilyVideoPropertiesKHR){ + .sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_VIDEO_PROPERTIES_KHR, + }; + queueFamilyProperties[queueIndex] = (VkQueueFamilyProperties2){ + .sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_PROPERTIES_2, + .pNext = &queueFamilyVideoProperties[queueIndex], + }; + } + + ffvkGetPhysicalDeviceQueueFamilyProperties2(physicalDevices[i], &queueFamilyCount, queueFamilyProperties); + + FFCodecType decoders = FF_CODEC_TYPE_NONE; + FFCodecType encoders = FF_CODEC_TYPE_NONE; + + for (uint32_t queueIndex = 0; queueIndex < queueFamilyCount; ++queueIndex) { + const VkQueueFlags queueFlags = queueFamilyProperties[queueIndex].queueFamilyProperties.queueFlags; + const VkVideoCodecOperationFlagsKHR operations = queueFamilyVideoProperties[queueIndex].videoCodecOperations; + + if (hasVideoDecode && (queueFlags & VK_QUEUE_VIDEO_DECODE_BIT_KHR)) { + decoders |= ffCodecDecodeOperationsToTypes(operations); + } + + if (hasVideoEncode && (queueFlags & VK_QUEUE_VIDEO_ENCODE_BIT_KHR)) { + encoders |= ffCodecEncodeOperationsToTypes(operations); + } + } + + if (decoders == FF_CODEC_TYPE_NONE && encoders == FF_CODEC_TYPE_NONE) { + FF_DEBUG("Skipping Vulkan device '%s' because no supported codec operations were reported", properties.deviceName); + continue; + } + + FFCodecResult* item = FF_LIST_ADD(FFCodecResult, *result); + ffStrbufInitS(&item->gpu, properties.deviceName); + item->decoders = decoders; + item->encoders = encoders; + item->platformApi = "Vulkan Video"; + FF_DEBUG("Added Vulkan codec result for '%s': decoders=%u encoders=%u", properties.deviceName, (unsigned) decoders, (unsigned) encoders); + } + + ffvkDestroyInstance(vkInstance, NULL); + + if (result->length > 0) { + return NULL; + } + + return sawVideoQueueExtension ? "No supported Vulkan video codec operations found" + : "VK_KHR_video_queue is not supported by any Vulkan physical device"; +} + + #else + +const char* ffDetectCodecVulkan(FFCodecOptions* options, FFlist* result) { + FF_UNUSED(options, result); + return "Vulkan video queue extensions are not supported by this Vulkan implementation"; +} + + #endif + +#endif diff --git a/src/detection/codec/codec_windows.cpp b/src/detection/codec/codec_windows.cpp new file mode 100644 index 0000000..df01a63 --- /dev/null +++ b/src/detection/codec/codec_windows.cpp @@ -0,0 +1,553 @@ +extern "C" { +#include "codec.h" +#include "common/library.h" +#include "common/windows/com.h" +#include "common/windows/unicode.h" +#include "common/windows/nt.h" +} + +#include <d3d11.h> +#include <d3d12.h> +#include <dxgi.h> +#include <initguid.h> +#include <dxva.h> +#include <d3d12video.h> +#include <mfapi.h> + +typedef struct D3D12_FEATURE_DATA_VIDEO_ENCODER_CODEC { + UINT NodeIndex; + D3D12_VIDEO_ENCODER_CODEC Codec; + BOOL IsSupported; +} D3D12_FEATURE_DATA_VIDEO_ENCODER_CODEC; + +HRESULT MFTEnum2( + _In_ GUID guidCategory, + _In_ UINT32 Flags, + _In_ const MFT_REGISTER_TYPE_INFO* pInputType, + _In_ const MFT_REGISTER_TYPE_INFO* pOutputType, + _In_opt_ IMFAttributes* pAttributes, + _Out_ IMFActivate*** pppMFTActivate, + _Out_ UINT32* pnumMFTActivate); + +// clang-format off +#ifndef MFT_ENUM_ADAPTER_LUID +// {1D39518C-E220-4DA8-A07F-BA172552D6B1} +DEFINE_GUID(MFT_ENUM_ADAPTER_LUID, + 0x1d39518c, 0xe220, 0x4da8, 0xa0, 0x7f, 0xba, 0x17, 0x25, 0x52, 0xd6, 0xb1); +#endif +// clang-format on + +static const DXGI_FORMAT FF_NATIVE_CODEC_FORMATS[] = { + DXGI_FORMAT_420_OPAQUE, + DXGI_FORMAT_NV12, + DXGI_FORMAT_P010, + DXGI_FORMAT_P016, + DXGI_FORMAT_YUY2, + DXGI_FORMAT_Y210, + DXGI_FORMAT_Y216, + DXGI_FORMAT_AYUV, + DXGI_FORMAT_Y410, + DXGI_FORMAT_Y416, +}; + +static FFCodecType ffCodecProfileToTypeDx11(const GUID& profile) { + // clang-format off + if (IsEqualGUID(profile, DXVA_ModeH261_A) || + IsEqualGUID(profile, DXVA_ModeH261_B)) { + return FF_CODEC_TYPE_H261; + } + + if (IsEqualGUID(profile, DXVA_ModeH263_A) || + IsEqualGUID(profile, DXVA_ModeH263_B) || + IsEqualGUID(profile, DXVA_ModeH263_C) || + IsEqualGUID(profile, DXVA_ModeH263_D) || + IsEqualGUID(profile, DXVA_ModeH263_E) || + IsEqualGUID(profile, DXVA_ModeH263_F)) { + return FF_CODEC_TYPE_H263; + } + + if (IsEqualGUID(profile, DXVA_ModeMJPEG_VLD_420) || + IsEqualGUID(profile, DXVA_ModeMJPEG_VLD_422) || + IsEqualGUID(profile, DXVA_ModeMJPEG_VLD_444) || + IsEqualGUID(profile, DXVA_ModeMJPEG_VLD_4444) || + IsEqualGUID(profile, DXVA_ModeJPEG_VLD_420) || + IsEqualGUID(profile, DXVA_ModeJPEG_VLD_422) || + IsEqualGUID(profile, DXVA_ModeJPEG_VLD_444)) { + return FF_CODEC_TYPE_MJPEG; + } + + if (IsEqualGUID(profile, DXVA_ModeMPEG1_A) || + IsEqualGUID(profile, DXVA_ModeMPEG1_VLD)) { + return FF_CODEC_TYPE_MPEG1; + } + + if (IsEqualGUID(profile, DXVA_ModeMPEG2_A) || + IsEqualGUID(profile, DXVA_ModeMPEG2_B) || + IsEqualGUID(profile, DXVA_ModeMPEG2_C) || + IsEqualGUID(profile, DXVA_ModeMPEG2_D) || + IsEqualGUID(profile, DXVA_ModeMPEG2and1_VLD)) { + return FF_CODEC_TYPE_MPEG2; + } + + if (IsEqualGUID(profile, DXVA_ModeMPEG4pt2_VLD_Simple) || + IsEqualGUID(profile, DXVA_ModeMPEG4pt2_VLD_AdvSimple_NoGMC) || + IsEqualGUID(profile, DXVA_ModeMPEG4pt2_VLD_AdvSimple_GMC)) { + return FF_CODEC_TYPE_DIVX_XVID; + } + + if (IsEqualGUID(profile, DXVA_ModeH264_E) || + IsEqualGUID(profile, DXVA_ModeH264_F) || + IsEqualGUID(profile, DXVA_ModeH264_VLD_WithFMOASO_NoFGT) || + IsEqualGUID(profile, DXVA_ModeH264_VLD_Stereo_Progressive_NoFGT) || + IsEqualGUID(profile, DXVA_ModeH264_VLD_Stereo_NoFGT) || + IsEqualGUID(profile, DXVA_ModeH264_VLD_Multiview_NoFGT)) { + return FF_CODEC_TYPE_H264; + } + + if (IsEqualGUID(profile, DXVA_ModeWMV8_A) || + IsEqualGUID(profile, DXVA_ModeWMV8_B)) { + return FF_CODEC_TYPE_WMV8; + } + + if (IsEqualGUID(profile, DXVA_ModeWMV9_A) || + IsEqualGUID(profile, DXVA_ModeWMV9_B) || + IsEqualGUID(profile, DXVA_ModeWMV9_C)) { + return FF_CODEC_TYPE_WMV9; + } + + if (IsEqualGUID(profile, DXVA_ModeVC1_A) || + IsEqualGUID(profile, DXVA_ModeVC1_B) || + IsEqualGUID(profile, DXVA_ModeVC1_C) || + IsEqualGUID(profile, DXVA_ModeVC1_D) || + IsEqualGUID(profile, DXVA_ModeVC1_D2010)) { + return FF_CODEC_TYPE_VC1; + } + + if (IsEqualGUID(profile, DXVA_ModeVP8_VLD)) { + return FF_CODEC_TYPE_VP8; + } + + if (IsEqualGUID(profile, DXVA_ModeHEVC_VLD_Main) || + IsEqualGUID(profile, DXVA_ModeHEVC_VLD_Main10) || + IsEqualGUID(profile, DXVA_ModeHEVC_VLD_Main12) || + IsEqualGUID(profile, DXVA_ModeHEVC_VLD_Main10_422) || + IsEqualGUID(profile, DXVA_ModeHEVC_VLD_Main12_422) || + IsEqualGUID(profile, DXVA_ModeHEVC_VLD_Main_444) || + IsEqualGUID(profile, DXVA_ModeHEVC_VLD_Main10_Ext) || + IsEqualGUID(profile, DXVA_ModeHEVC_VLD_Main10_444) || + IsEqualGUID(profile, DXVA_ModeHEVC_VLD_Main12_444) || + IsEqualGUID(profile, DXVA_ModeHEVC_VLD_Main16)) { + return FF_CODEC_TYPE_HEVC; + } + + if (IsEqualGUID(profile, DXVA_ModeVP9_VLD_Profile0) || + IsEqualGUID(profile, DXVA_ModeVP9_VLD_10bit_Profile2)) { + return FF_CODEC_TYPE_VP9; + } + + if (IsEqualGUID(profile, DXVA_ModeAV1_VLD_Profile0) || + IsEqualGUID(profile, DXVA_ModeAV1_VLD_Profile1) || + IsEqualGUID(profile, DXVA_ModeAV1_VLD_Profile2) || + IsEqualGUID(profile, DXVA_ModeAV1_VLD_12bit_Profile2) || + IsEqualGUID(profile, DXVA_ModeAV1_VLD_12bit_Profile2_420)) { + return FF_CODEC_TYPE_AV1; + } + // clang-format on + + return FF_CODEC_TYPE_UNKNOWN; +} + +static FFCodecType ffCodecProfileToTypeDx12(const GUID& profile) { + // clang-format off + if (IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_MPEG1_AND_MPEG2)) { + return FFCodecType(FF_CODEC_TYPE_MPEG1 | FF_CODEC_TYPE_MPEG2); + } + + if (IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_MPEG2)) { + return FF_CODEC_TYPE_MPEG2; + } + + if (IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_MPEG4PT2_SIMPLE) || + IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_MPEG4PT2_ADVSIMPLE_NOGMC)) { + return FF_CODEC_TYPE_DIVX_XVID; + } + + if (IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_H264) || + IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_H264_STEREO_PROGRESSIVE) || + IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_H264_STEREO) || + IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_H264_MULTIVIEW)) { + return FF_CODEC_TYPE_H264; + } + + if (IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_VC1) || + IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_VC1_D2010)) { + return FF_CODEC_TYPE_VC1; + } + + if (IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_VP8)) { + return FF_CODEC_TYPE_VP8; + } + + if (IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN) || + IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN10)) { + return FF_CODEC_TYPE_HEVC; + } + + if (IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_VP9) || + IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_VP9_10BIT_PROFILE2)) { + return FF_CODEC_TYPE_VP9; + } + + if (IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_AV1_PROFILE0) || + IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_AV1_PROFILE1) || + IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_AV1_PROFILE2) || + IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_AV1_12BIT_PROFILE2) || + IsEqualGUID(profile, D3D12_VIDEO_DECODE_PROFILE_AV1_12BIT_PROFILE2_420)) { + return FF_CODEC_TYPE_AV1; + } + // clang-format on + + return FF_CODEC_TYPE_UNKNOWN; +} + +static bool ffCodecProfileHasNativeOutput(ID3D11VideoDevice* videoDevice, const GUID& profile) { + for (uint32_t i = 0; i < ARRAY_SIZE(FF_NATIVE_CODEC_FORMATS); ++i) { + BOOL supported = FALSE; + if (SUCCEEDED(videoDevice->CheckVideoDecoderFormat(&profile, FF_NATIVE_CODEC_FORMATS[i], &supported)) && supported) { + return true; + } + } + + return false; +} + +static bool ffCodecProfileHasNativeOutput(ID3D12VideoDevice* videoDevice, const GUID& profile, UINT nodeIndex) { + for (uint32_t i = 0; i < ARRAY_SIZE(FF_NATIVE_CODEC_FORMATS); ++i) { + D3D12_FEATURE_DATA_VIDEO_DECODE_SUPPORT support = { + .NodeIndex = nodeIndex, + .Configuration = { + .DecodeProfile = profile, + .BitstreamEncryption = D3D12_BITSTREAM_ENCRYPTION_TYPE_NONE, + .InterlaceType = D3D12_VIDEO_FRAME_CODED_INTERLACE_TYPE_NONE, + }, + .Width = 1920, + .Height = 1080, + .DecodeFormat = FF_NATIVE_CODEC_FORMATS[i], + .FrameRate = { + .Numerator = 30, + .Denominator = 1, + }, + .BitRate = 10000000, + .SupportFlags = D3D12_VIDEO_DECODE_SUPPORT_FLAG_NONE, + .ConfigurationFlags = D3D12_VIDEO_DECODE_CONFIGURATION_FLAG_NONE, + .DecodeTier = D3D12_VIDEO_DECODE_TIER_NOT_SUPPORTED, + }; + + if (SUCCEEDED(videoDevice->CheckFeatureSupport(D3D12_FEATURE_VIDEO_DECODE_SUPPORT, &support, sizeof(support))) && + (support.SupportFlags & D3D12_VIDEO_DECODE_SUPPORT_FLAG_SUPPORTED)) { + return true; + } + } + + return false; +} + +static bool ffCodecEncoderSupportedD3d12(ID3D12VideoDevice* videoDevice, D3D12_VIDEO_ENCODER_CODEC codec, UINT nodeIndex) { + D3D12_FEATURE_DATA_VIDEO_ENCODER_CODEC encoderCodec = { + .NodeIndex = nodeIndex, + .Codec = codec, + .IsSupported = FALSE, + }; + + return SUCCEEDED(videoDevice->CheckFeatureSupport( + D3D12_FEATURE_VIDEO_ENCODER_CODEC, + &encoderCodec, + sizeof(encoderCodec))) && + encoderCodec.IsSupported; +} + +typedef struct FFCodecMftEncoderSubtype { + const GUID* subtype; + FFCodecType codecType; +} FFCodecMftEncoderSubtype; + +static const FFCodecMftEncoderSubtype FF_D3D11VA_MFT_ENCODER_SUBTYPES[] = { + { &MFVideoFormat_H264, FF_CODEC_TYPE_H264 }, + { &MFVideoFormat_HEVC, FF_CODEC_TYPE_HEVC }, + { &MFVideoFormat_AV1, FF_CODEC_TYPE_AV1 }, +}; + +static FFCodecType ffDetectD3d11vaDecoders(IDXGIAdapter1* adapter, __typeof__(&D3D11CreateDevice) ffD3D11CreateDevice) { + ID3D11Device* FF_AUTO_RELEASE_COM_OBJECT d3dDevice = nullptr; + D3D_FEATURE_LEVEL featureLevel; + if (FAILED(ffD3D11CreateDevice( + adapter, + D3D_DRIVER_TYPE_UNKNOWN, + nullptr, + D3D11_CREATE_DEVICE_VIDEO_SUPPORT, + nullptr, + 0, + D3D11_SDK_VERSION, + &d3dDevice, + &featureLevel, + nullptr)) || + !d3dDevice) { + return FF_CODEC_TYPE_NONE; + } + + ID3D11VideoDevice* FF_AUTO_RELEASE_COM_OBJECT videoDevice = nullptr; + if (FAILED(d3dDevice->QueryInterface(__uuidof(ID3D11VideoDevice), (void**) &videoDevice)) || !videoDevice) { + return FF_CODEC_TYPE_NONE; + } + + FFCodecType decoders = FF_CODEC_TYPE_NONE; + UINT profileCount = videoDevice->GetVideoDecoderProfileCount(); + for (UINT profileIndex = 0; profileIndex < profileCount; ++profileIndex) { + GUID profile; + if (FAILED(videoDevice->GetVideoDecoderProfile(profileIndex, &profile))) { + continue; + } + + FFCodecType type = ffCodecProfileToTypeDx11(profile); + + if ((decoders & type) || !ffCodecProfileHasNativeOutput(videoDevice, profile)) { + continue; + } + + decoders = (FFCodecType) (((uint32_t) decoders) | ((uint32_t) type)); + } + return decoders; +} + +static FFCodecType ffDetectD3d11MftEncoders(const LUID& adapterLuid, __typeof__(&MFCreateAttributes) ffMFCreateAttributes, __typeof__(&MFTEnum2) ffMFTEnum2) { + IMFAttributes* FF_AUTO_RELEASE_COM_OBJECT attributes = nullptr; + if (FAILED(ffMFCreateAttributes(&attributes, 1)) || !attributes) { + return FF_CODEC_TYPE_NONE; + } + + if (FAILED(attributes->SetBlob(MFT_ENUM_ADAPTER_LUID, (const UINT8*) &adapterLuid, sizeof(adapterLuid)))) { + return FF_CODEC_TYPE_NONE; + } + + FFCodecType encoders = FF_CODEC_TYPE_NONE; + + for (uint32_t subtypeIndex = 0; subtypeIndex < ARRAY_SIZE(FF_D3D11VA_MFT_ENCODER_SUBTYPES); ++subtypeIndex) { + const FFCodecMftEncoderSubtype& subtype = FF_D3D11VA_MFT_ENCODER_SUBTYPES[subtypeIndex]; + if (encoders & subtype.codecType) { + continue; + } + + MFT_REGISTER_TYPE_INFO outputType = { + .guidMajorType = MFMediaType_Video, + .guidSubtype = *subtype.subtype, + }; + + IMFActivate** activateList = nullptr; + UINT32 activateCount = 0; + + if (SUCCEEDED(ffMFTEnum2( + MFT_CATEGORY_VIDEO_ENCODER, + MFT_ENUM_FLAG_HARDWARE | MFT_ENUM_FLAG_SORTANDFILTER, + nullptr, + &outputType, + attributes, + &activateList, + &activateCount)) && + activateCount > 0) { + encoders = (FFCodecType) (((uint32_t) encoders) | ((uint32_t) subtype.codecType)); + } + + for (uint32_t i = 0; i < activateCount; ++i) { + activateList[i]->Release(); + } + CoTaskMemFree(activateList); + } + + return encoders; +} + +static FFCodecType ffCodecEncoderToType(D3D12_VIDEO_ENCODER_CODEC codec) { + switch (codec) { + case D3D12_VIDEO_ENCODER_CODEC_H264: + return FF_CODEC_TYPE_H264; + case D3D12_VIDEO_ENCODER_CODEC_HEVC: + return FF_CODEC_TYPE_HEVC; + case D3D12_VIDEO_ENCODER_CODEC_AV1: + return FF_CODEC_TYPE_AV1; + default: + return FF_CODEC_TYPE_UNKNOWN; + } +} + +template <typename Func> +static void ffEnumHardwareAdapters(IDXGIFactory1* factory, Func&& onAdapter) { + for (UINT adapterIndex = 0;; ++adapterIndex) { + IDXGIAdapter1* FF_AUTO_RELEASE_COM_OBJECT adapter = nullptr; + HRESULT adapterStatus = factory->EnumAdapters1(adapterIndex, &adapter); + if (adapterStatus == DXGI_ERROR_NOT_FOUND) { + break; + } + if (FAILED(adapterStatus) || !adapter) { + continue; + } + + DXGI_ADAPTER_DESC1 desc; + if (FAILED(adapter->GetDesc1(&desc))) { + continue; + } + if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) { + continue; + } + + onAdapter(adapter, desc); + } +} + +const char* detectD3d11va(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/, IDXGIFactory1* factory) { + FF_LIBRARY_LOAD_MESSAGE(d3d11, "d3d11" FF_LIBRARY_EXTENSION, 1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(d3d11, D3D11CreateDevice) + FF_LIBRARY_LOAD_MESSAGE(mfplat, "mfplat" FF_LIBRARY_EXTENSION, 1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mfplat, MFCreateAttributes) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(mfplat, MFTEnum2) + + ffEnumHardwareAdapters(factory, [&](IDXGIAdapter1* adapter, const DXGI_ADAPTER_DESC1& desc) { + FFCodecType decoders = (options->showType & FF_CODEC_SHOW_TYPE_DECODER) + ? ffDetectD3d11vaDecoders(adapter, ffD3D11CreateDevice) + : FF_CODEC_TYPE_NONE; + FFCodecType encoders = (options->showType & FF_CODEC_SHOW_TYPE_ENCODER) + ? ffDetectD3d11MftEncoders(desc.AdapterLuid, ffMFCreateAttributes, ffMFTEnum2) + : FF_CODEC_TYPE_NONE; + + if (decoders == FF_CODEC_TYPE_NONE && encoders == FF_CODEC_TYPE_NONE) { + return; + } + + FFCodecResult* gpuResult = FF_LIST_ADD(FFCodecResult, *result); + ffStrbufInitWS(&gpuResult->gpu, desc.Description); + gpuResult->decoders = decoders; + gpuResult->encoders = encoders; + gpuResult->platformApi = "D3D11VA+MFT"; + }); + + return nullptr; +} + +const char* detectD3d12va(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/, IDXGIFactory1* factory) { + FF_LIBRARY_LOAD_MESSAGE(d3d12, "d3d12" FF_LIBRARY_EXTENSION, 1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(d3d12, D3D12CreateDevice) + + static const GUID FF_D3D12_DECODE_PROFILES[] = { + D3D12_VIDEO_DECODE_PROFILE_MPEG2, + D3D12_VIDEO_DECODE_PROFILE_MPEG1_AND_MPEG2, + D3D12_VIDEO_DECODE_PROFILE_H264, + D3D12_VIDEO_DECODE_PROFILE_H264_STEREO_PROGRESSIVE, + D3D12_VIDEO_DECODE_PROFILE_H264_STEREO, + D3D12_VIDEO_DECODE_PROFILE_H264_MULTIVIEW, + D3D12_VIDEO_DECODE_PROFILE_VC1, + D3D12_VIDEO_DECODE_PROFILE_VC1_D2010, + D3D12_VIDEO_DECODE_PROFILE_MPEG4PT2_SIMPLE, + D3D12_VIDEO_DECODE_PROFILE_MPEG4PT2_ADVSIMPLE_NOGMC, + D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN, + D3D12_VIDEO_DECODE_PROFILE_HEVC_MAIN10, + D3D12_VIDEO_DECODE_PROFILE_VP9, + D3D12_VIDEO_DECODE_PROFILE_VP9_10BIT_PROFILE2, + D3D12_VIDEO_DECODE_PROFILE_VP8, + D3D12_VIDEO_DECODE_PROFILE_AV1_PROFILE0, + D3D12_VIDEO_DECODE_PROFILE_AV1_PROFILE1, + D3D12_VIDEO_DECODE_PROFILE_AV1_PROFILE2, + D3D12_VIDEO_DECODE_PROFILE_AV1_12BIT_PROFILE2, + D3D12_VIDEO_DECODE_PROFILE_AV1_12BIT_PROFILE2_420, + }; + + static const D3D12_VIDEO_ENCODER_CODEC FF_D3D12_ENCODER_CODECS[] = { + D3D12_VIDEO_ENCODER_CODEC_H264, + D3D12_VIDEO_ENCODER_CODEC_HEVC, + D3D12_VIDEO_ENCODER_CODEC_AV1, + }; + + const uint32_t resultLengthBefore = result->length; + + ffEnumHardwareAdapters(factory, [&](IDXGIAdapter1* adapter, const DXGI_ADAPTER_DESC1& desc) { + ID3D12Device* FF_AUTO_RELEASE_COM_OBJECT d3dDevice = nullptr; + if (FAILED(ffD3D12CreateDevice( + adapter, + D3D_FEATURE_LEVEL_11_0, + __uuidof(ID3D12Device), + (void**) &d3dDevice)) || + !d3dDevice) { + return; + } + + ID3D12VideoDevice* FF_AUTO_RELEASE_COM_OBJECT videoDevice = nullptr; + if (FAILED(d3dDevice->QueryInterface(__uuidof(ID3D12VideoDevice), (void**) &videoDevice)) || !videoDevice) { + return; + } + + FFCodecType decoders = FF_CODEC_TYPE_NONE; + if (options->showType & FF_CODEC_SHOW_TYPE_DECODER) { + for (uint32_t profileIndex = 0; profileIndex < ARRAY_SIZE(FF_D3D12_DECODE_PROFILES); ++profileIndex) { + const GUID& profile = FF_D3D12_DECODE_PROFILES[profileIndex]; + FFCodecType codecType = ffCodecProfileToTypeDx12(profile); + + if ((decoders & codecType) || !ffCodecProfileHasNativeOutput(videoDevice, profile, 0)) { + continue; + } + + decoders = (FFCodecType) (((uint32_t) decoders) | ((uint32_t) codecType)); + } + } + + FFCodecType encoders = FF_CODEC_TYPE_NONE; + if (options->showType & FF_CODEC_SHOW_TYPE_ENCODER) { + for (uint32_t codecIndex = 0; codecIndex < ARRAY_SIZE(FF_D3D12_ENCODER_CODECS); ++codecIndex) { + D3D12_VIDEO_ENCODER_CODEC codec = FF_D3D12_ENCODER_CODECS[codecIndex]; + FFCodecType codecType = ffCodecEncoderToType(codec); + + if ((encoders & codecType) || !ffCodecEncoderSupportedD3d12(videoDevice, codec, 0)) { + continue; + } + + encoders = (FFCodecType) (((uint32_t) encoders) | ((uint32_t) codecType)); + } + } + + if (decoders == FF_CODEC_TYPE_NONE && encoders == FF_CODEC_TYPE_NONE) { + return; + } + + FFCodecResult* gpuResult = FF_LIST_ADD(FFCodecResult, *result); + ffStrbufInitWS(&gpuResult->gpu, desc.Description); + gpuResult->decoders = decoders; + gpuResult->encoders = encoders; + gpuResult->platformApi = "D3D12VA"; + }); + + if (result->length == resultLengthBefore) { + return "No D3D12 video acceleration support"; + } + + return nullptr; +} + +const char* ffDetectCodecNative(FFCodecOptions* options, FFlist* result /*list of FFCodecResult*/) { + FF_LIBRARY_LOAD_MESSAGE(dxgi, "dxgi" FF_LIBRARY_EXTENSION, 1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(dxgi, CreateDXGIFactory1) + + const char* error = ffInitCom(); + if (error) { + return error; + } + + IDXGIFactory1* FF_AUTO_RELEASE_COM_OBJECT factory = nullptr; + if (FAILED(ffCreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**) &factory)) || !factory) { + return "CreateDXGIFactory1() failed"; + } + + if (ffIsWindows11OrGreater()) { + // D3D12 video encoding is supported only on Windows 11 + if (detectD3d12va(options, result, factory) == nullptr) { + return nullptr; + } + } + return detectD3d11va(options, result, factory); +} diff --git a/src/detection/command/command.c b/src/detection/command/command.c new file mode 100644 index 0000000..28c6f4b --- /dev/null +++ b/src/detection/command/command.c @@ -0,0 +1,58 @@ +#include "detection/command/command.h" +#include "common/processing.h" +#include "common/FFstrbuf.h" + +typedef struct FFCommandResultBundle { + FFProcessHandle handle; + const char* error; +} FFCommandResultBundle; + +// FIFO, non-thread-safe list of running commands +static FFlist commandQueue; + +static const char* spawnProcess(FFCommandOptions* options, FFProcessHandle* handle) { + if (options->text.length == 0) { + return "No command text specified"; + } + + return ffProcessSpawn(options->param.length ? (char* const[]) { + options->shell.chars, + options->param.chars, + options->text.chars, + NULL } + : (char* const[]) { options->shell.chars, options->text.chars, NULL }, + options->useStdErr, + handle); +} + +bool ffPrepareCommand(FFCommandOptions* options) { + if (!options->parallel) { + return false; + } + + FFCommandResultBundle* bundle = FF_LIST_ADD(FFCommandResultBundle, commandQueue); + bundle->error = spawnProcess(options, &bundle->handle); + + return true; +} + +const char* ffDetectCommand(FFCommandOptions* options, FFstrbuf* result) { + FFCommandResultBundle bundle = {}; + if (!options->parallel) { + bundle.error = spawnProcess(options, &bundle.handle); + } else if (!FF_LIST_SHIFT(commandQueue, &bundle)) { + return "[BUG] command queue is empty"; + } + + if (bundle.error) { + return bundle.error; + } + + bundle.error = ffProcessReadOutput(&bundle.handle, result); + if (bundle.error) { + return bundle.error; + } + + ffStrbufTrimRightSpace(result); + return NULL; +} diff --git a/src/detection/command/command.h b/src/detection/command/command.h new file mode 100644 index 0000000..73acc0a --- /dev/null +++ b/src/detection/command/command.h @@ -0,0 +1,6 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/command/option.h" + +const char* ffDetectCommand(FFCommandOptions* options, FFstrbuf* result); diff --git a/src/detection/cpu/cpu.c b/src/detection/cpu/cpu.c new file mode 100644 index 0000000..aadcdc4 --- /dev/null +++ b/src/detection/cpu/cpu.c @@ -0,0 +1,519 @@ +#include "cpu.h" + +const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu); + +const char* ffDetectCPU(const FFCPUOptions* options, FFCPUResult* cpu) { + const char* error = ffDetectCPUImpl(options, cpu); + if (error) { + return error; + } + + const char* removeStrings[] = { + " CPU", " FPU", " APU", " Processor", " Dual-Core", " Quad-Core", " Six-Core", " Eight-Core", " Ten-Core", " 2-Core", " 4-Core", " 6-Core", " 8-Core", " 10-Core", " 12-Core", " 14-Core", " 16-Core" + }; + ffStrbufRemoveStrings(&cpu->name, ARRAY_SIZE(removeStrings), removeStrings); + uint32_t radeonGraphics = ffStrbufFirstIndexS(&cpu->name, " w/ Radeon "); // w/ Radeon 780M Graphics + if (radeonGraphics >= cpu->name.length) { + radeonGraphics = ffStrbufFirstIndexS(&cpu->name, " with Radeon "); + } + if (radeonGraphics < cpu->name.length) { + ffStrbufSubstrBefore(&cpu->name, radeonGraphics); + } + ffStrbufSubstrBeforeFirstC(&cpu->name, '@'); // Cut the speed output in the name as we append our own + ffStrbufTrimRight(&cpu->name, ' '); // If we removed the @ in previous step there was most likely a space before it + ffStrbufRemoveDupWhitespaces(&cpu->name); + return NULL; +} + +const char* ffCPUAppleCodeToName(uint32_t code) { + // https://github.com/AsahiLinux/docs/wiki/Codenames + switch (code) { + case 8103: + return "Apple M1"; + case 6000: + return "Apple M1 Pro"; + case 6001: + return "Apple M1 Max"; + case 6002: + return "Apple M1 Ultra"; + case 8112: + return "Apple M2"; + case 6020: + return "Apple M2 Pro"; + case 6021: + return "Apple M2 Max"; + case 6022: + return "Apple M2 Ultra"; + case 8122: + return "Apple M3"; + case 6030: + return "Apple M3 Pro"; + case 6031: + case 6034: + return "Apple M3 Max"; + case 8132: + return "Apple M4"; + case 6040: + return "Apple M4 Pro"; + case 6041: + return "Apple M4 Max"; + default: + return NULL; + } +} + +const char* ffCPUQualcommCodeToName(uint32_t code) { + // https://github.com/AsahiLinux/docs/wiki/Codenames + switch (code) { + case 7180: + return "Qualcomm Snapdragon 7c"; + case 7280: + return "Qualcomm Snapdragon 7c+ Gen 3"; + case 8180: + return "Qualcomm Snapdragon 8cx Gen 2 5G"; + case 8280: + return "Qualcomm Snapdragon 8cx Gen 3"; + default: + return NULL; + } +} + +#if defined(__x86_64__) || defined(__i386__) + + #include <cpuid.h> + +void ffCPUDetectByCpuid(FFCPUResult* cpu) { + uint32_t eax = 0, ebx = 0, ecx = 0, edx = 0; + if (__get_cpuid(0x16, &eax, &ebx, &ecx, &edx)) { + // WARNING: CPUID may report frequencies of efficient cores + // cpuid returns 0 MHz when hypervisor is enabled + if (eax) { + cpu->frequencyBase = eax; + } + if (ebx) { + cpu->frequencyMax = ebx; + } + } + + if (__get_cpuid(1, &eax, &ebx, &ecx, &edx)) { + // Feature tests (leaf1.ecx, leaf7.ebx) + bool sse2 = (ecx & bit_SSE2) != 0; + bool sse4_2 = (ecx & bit_SSE4_2) != 0; + bool pclmul = (ecx & bit_PCLMUL) != 0; + bool popcnt = (ecx & bit_POPCNT) != 0; + bool fma = (ecx & bit_FMA) != 0; + bool osxsave = (ecx & bit_OSXSAVE) != 0; + + unsigned int eax7 = 0, ebx7 = 0, ecx7 = 0, edx7 = 0; + __get_cpuid_count(7, 0, &eax7, &ebx7, &ecx7, &edx7); + + bool avx2 = (ebx7 & bit_AVX2) != 0; + bool bmi2 = (ebx7 & bit_BMI2) != 0; + bool avx512f = (ebx7 & bit_AVX512F) != 0; + bool avx512bw = (ebx7 & bit_AVX512BW) != 0; + bool avx512dq = (ebx7 & bit_AVX512DQ) != 0; + + // OS support for AVX/AVX512: check XGETBV (requires OSXSAVE) + bool avx_os = false; + bool avx512_os = false; + if (osxsave) { + __asm__ __volatile__( + "xgetbv" + : "=a"(eax), "=d"(edx) + : "c"(0) + :); + uint64_t xcr0 = ((uint64_t) edx << 32) | eax; + + // AVX requires XCR0[1:2] == 11b (XMM and YMM state) + avx_os = (xcr0 & 0x6ULL) == 0x6ULL; + // AVX512 requires XCR0[7,5,6] etc. common mask 0xE6 (bits 1,2,5,6,7) + avx512_os = (xcr0 & 0xE6ULL) == 0xE6ULL; + } + + cpu->march = "unknown"; + if (avx512f && avx512bw && avx512dq && avx512_os) { + cpu->march = "x86_64-v4"; + } else if (avx2 && fma && bmi2 && avx_os) { + cpu->march = "x86_64-v3"; + } else if (sse4_2 && popcnt && pclmul) { + cpu->march = "x86_64-v2"; + } else if (sse2) { + cpu->march = "x86_64-v1"; + } + } +} + +#elif defined(__aarch64__) + +// This is not accurate because a lot of flags are optional from old versions +// https://developer.arm.com/documentation/109697/2025_06/Feature-descriptions?lang=en +// https://en.wikipedia.org/wiki/AArch64#ARM-A_(application_architecture) +// Worth noting: Apple M1 is marked as ARMv8.5-A on Wikipedia, but it lacks BTI (mandatory in v8.5) + + #ifdef __linux__ + #include "common/io.h" + #include <elf.h> +// #include <asm/hwcap.h> + +void ffCPUDetectByCpuid(FFCPUResult* cpu) { + char buf[PROC_FILE_BUFFSIZ]; + ssize_t nRead = ffReadFileData("/proc/self/auxv", ARRAY_SIZE(buf), buf); + + if (nRead < (ssize_t) sizeof(Elf64_auxv_t)) { + return; + } + + uint64_t hwcap = 0, hwcap2 = 0; + + for (Elf64_auxv_t* auxv = (Elf64_auxv_t*) buf; (char*) auxv < buf + nRead; ++auxv) { + if (auxv->a_type == AT_HWCAP) { + hwcap = auxv->a_un.a_val; + } else if (auxv->a_type == AT_HWCAP2) { + hwcap2 = auxv->a_un.a_val; + } + } + + if (!hwcap) { + return; + } + + cpu->march = "unknown"; + + // ARMv8-A + bool has_fp = (hwcap & (1 << 0) /* HWCAP_FP */) != 0; + bool has_asimd = (hwcap & (1 << 1) /* HWCAP_ASIMD */) != 0; + + // ARMv8.1-A + bool has_atomics = (hwcap & (1 << 8) /* HWCAP_ATOMICS */) != 0; // optional from v8.0 + bool has_crc32 = (hwcap & (1 << 7) /* HWCAP_CRC32 */) != 0; // optional from v8.0 + bool has_asimdrdm = (hwcap & (1 << 12) /* HWCAP_ASIMDRDM */) != 0; // optional from v8.0 + + // ARMv8.2-A + bool has_fphp = (hwcap & (1 << 9) /* HWCAP_FPHP */) != 0; // optional + bool has_dcpop = (hwcap & (1 << 16) /* HWCAP_DCPOP */) != 0; // DC CVAP, optional from v8.1 + + // ARMv8.3-A + bool has_paca = (hwcap & (1 << 30) /* HWCAP_PACA */) != 0; // optional from v8.2 + bool has_lrcpc = (hwcap & (1 << 15) /* HWCAP_LRCPC */) != 0; // optional from v8.2 + bool has_fcma = (hwcap & (1 << 14) /* HWCAP_FCMA */) != 0; // optional from v8.2 + bool has_jscvt = (hwcap & (1 << 13) /* HWCAP_JSCVT */) != 0; // optional from v8.2 + + // ARMv8.4-A + bool has_dit = (hwcap & (1 << 24) /* HWCAP_DIT */) != 0; // optional from v8.3 + bool has_flagm = (hwcap & (1 << 27) /* HWCAP_FLAGM */) != 0; // optional from v8.1 + bool has_ilrcpc = (hwcap & (1 << 26) /* HWCAP_ILRCPC */) != 0; // optional from v8.2 + + // ARMv8.5-A + bool has_bti = (hwcap2 & (1 << 17) /* HWCAP2_BTI */) != 0; // optional from v8.4 + bool has_sb = (hwcap & (1 << 29) /* HWCAP_SB */) != 0; // optional from v8.0 + bool has_dcpodp = (hwcap2 & (1 << 0) /* HWCAP2_DCPODP */) != 0; // optional from v8.1 + bool has_flagm2 = (hwcap2 & (1 << 7) /* HWCAP2_FLAGM2 */) != 0; // optional from v8.4 + bool has_frint = (hwcap2 & (1 << 8) /* HWCAP2_FRINT */) != 0; // optional from v8.4 + + // ARMv9.0-A + bool has_sve2 = (hwcap2 & (1 << 1) /* HWCAP2_SVE2 */) != 0; + + // ARMv9.1-A + // ARMv8.6-A + bool has_bf16 = (hwcap2 & (1 << 14) /* HWCAP2_BF16 */) != 0; // optional from v8.2 + bool has_i8mm = (hwcap2 & (1 << 13) /* HWCAP2_I8MM */) != 0; // optional from v8.1 + + // ARMv8.7-A + bool has_afp = (hwcap2 & (1 << 20) /* HWCAP2_AFP */) != 0; // optional from v8.6 + + // ARMv9.2-A + bool has_sme = (hwcap2 & (1 << 23) /* HWCAP2_SME */) != 0; + + // ARMv9.3-A + bool has_sme2 = (hwcap2 & (1UL << 37) /* HWCAP2_SME2 */) != 0; // optional from v9.2 + + // ARMv8.8-A + bool has_mops = (hwcap2 & (1UL << 43) /* HWCAP2_MOPS */) != 0; // optional from v8.7 + + // ARMv8.9-A + bool has_cssc = (hwcap2 & (1UL << 34) /* HWCAP2_CSSC */) != 0; // optional from v8.7 + + // ARMv9.4-A + bool has_sme2p1 = (hwcap2 & (1UL << 38) /* HWCAP2_SME2P1 */) != 0; // optional from v9.2 + + // ARMv9.5-A + bool has_f8e4m3 = (hwcap2 & (1UL << 55) /* HWCAP2_F8E4M3 */) != 0; // optional from v9.2 + bool has_f8e5m2 = (hwcap2 & (1UL << 56) /* HWCAP2_F8E5M2 */) != 0; // optional from v9.2 + + // ARMv9.6-A + bool has_cmpbr = (hwcap & (1UL << 33) /* HWCAP_CMPBR */) != 0; // optional from v9.5 + bool has_fprcvt = (hwcap & (1UL << 34) /* HWCAP_FPRCVT */) != 0; // optional from v9.5 + + if (has_sve2 || has_sme) { + // ARMv9 + if (has_cmpbr && has_fprcvt) { + cpu->march = "ARMv9.6-A"; + } else if (has_f8e5m2 && has_f8e4m3) { + cpu->march = "ARMv9.5-A"; + } else if (has_sme2p1) { + cpu->march = "ARMv9.4-A"; + } else if (has_sme2) { + cpu->march = "ARMv9.3-A"; + } else if (has_sme) { + cpu->march = "ARMv9.2-A"; + } else if (has_i8mm && has_bf16) { + cpu->march = "ARMv9.1-A"; + } else { + cpu->march = "ARMv9.0-A"; + } + } else { + // ARMv8 + if (has_cssc) { + cpu->march = "ARMv8.9-A"; + } else if (has_mops) { + cpu->march = "ARMv8.8-A"; + } else if (has_afp) { + cpu->march = "ARMv8.7-A"; + } else if (has_i8mm && has_bf16) { + cpu->march = "ARMv8.6-A"; + } else if (has_bti && has_sb && has_dcpodp && has_flagm2 && has_frint) { + cpu->march = "ARMv8.5-A"; + } else if (has_dit && has_flagm && has_ilrcpc) { + cpu->march = "ARMv8.4-A"; + } else if (has_paca && has_lrcpc && has_fcma && has_jscvt) { + cpu->march = "ARMv8.3-A"; + } else if (has_fphp && has_dcpop) { + cpu->march = "ARMv8.2-A"; + } else if (has_atomics && has_crc32 && has_asimdrdm) { + cpu->march = "ARMv8.1-A"; + } else if (has_asimd && has_fp) { + cpu->march = "ARMv8-A"; + } + } +} + #elif __APPLE__ + #include <sys/sysctl.h> +// #include <arm/cpu_capabilities_public.h> // Not available in macOS 14- + +void ffCPUDetectByCpuid(FFCPUResult* cpu) { + uint64_t caps[2] = { 0 }; // 80-bit capability mask, split into two 64-bit values + size_t size = sizeof(caps); + + if (sysctlbyname("hw.optional.arm.caps", caps, &size, NULL, 0) != 0) { + return; + } + + // Helper macro to test bit in 80-bit capability mask + #define FF_HAS_CAP(bit) \ + (((bit) < 64) ? ((caps[0] >> (bit)) & 1ULL) : ((caps[1] >> ((bit) - 64U)) & 1ULL)) + + cpu->march = "unknown"; + + // ARMv8-A + bool has_fp = FF_HAS_CAP(50); /* CAP_BIT_AdvSIMD_HPFPCvt */ // Full FP16 support (implies FP/ASIMD) + bool has_asimd = FF_HAS_CAP(49); /* CAP_BIT_AdvSIMD */ // Advanced SIMD (NEON) + + // ARMv8.1-A + bool has_lse = FF_HAS_CAP(6); /* CAP_BIT_FEAT_LSE */ // Large System Extensions, optional in v8.0 + bool has_crc32 = FF_HAS_CAP(51); /* CAP_BIT_FEAT_CRC32 */ // CRC32 instructions, optional in v8.0 + bool has_rdm = FF_HAS_CAP(5); /* CAP_BIT_FEAT_RDM */ // AdvSIMD rounding double multiply accumulate, optional in v8.0 + + // ARMv8.2-A + bool has_fp16 = FF_HAS_CAP(34); /* CAP_BIT_FEAT_FP16 */ // Half-precision FP support, optional + bool has_dpb = FF_HAS_CAP(22); /* CAP_BIT_FEAT_DPB */ // DC CVAP, optional from v8.1 + + // ARMv8.3-A + bool has_pauth = FF_HAS_CAP(19); /* CAP_BIT_FEAT_PAuth */ // Pointer Authentication (PAC), optional from v8.2 + bool has_lrcpc = FF_HAS_CAP(15); /* CAP_BIT_FEAT_LRCPC */ // LDAPR/LR with RCPC semantics, optional from v8.2 + bool has_fcma = FF_HAS_CAP(17); /* CAP_BIT_FEAT_FCMA */ // Complex number multiply-add, optional from v8.2 + bool has_jscvt = FF_HAS_CAP(18); /* CAP_BIT_FEAT_JSCVT */ // JavaScript-style conversion (FJCVTZS), optional from v8.2 + + // ARMv8.4-A + bool has_lse2 = FF_HAS_CAP(30); /* CAP_BIT_FEAT_LSE2 */ // Large System Extensions version 2, optional from v8.2 + bool has_dit = FF_HAS_CAP(33); /* CAP_BIT_FEAT_DIT */ // Data Independent Timing, optional from v8.3 + bool has_flagm = FF_HAS_CAP(0); /* CAP_BIT_FEAT_FlagM */ // Flag manipulation (FMOV/FCVT), optional from v8.1 + bool has_lrcpc2 = FF_HAS_CAP(16); /* CAP_BIT_FEAT_LRCPC2 */ // Enhanced RCPC (LDAPUR/LDAPST), optional from v8.2 + + // ARMv8.5-A + bool has_bti = FF_HAS_CAP(36); /* CAP_BIT_FEAT_BTI */ // Branch Target Identification, optional from v8.4 + bool has_sb = FF_HAS_CAP(13); /* CAP_BIT_FEAT_SB */ // Speculative Barrier, optional from v8.0 + bool has_dpb2 = FF_HAS_CAP(23); /* CAP_BIT_FEAT_DPB2 */ // DC CVADP (DPB2), optional from v8.1 + bool has_flagm2 = FF_HAS_CAP(1); /* CAP_BIT_FEAT_FlagM2 */ // Enhanced FlagM, optional from v8.4 + bool has_frintts = FF_HAS_CAP(14); /* CAP_BIT_FEAT_FRINTTS */ // Floating-point to integer instructions, optional from v8.4 + + // ARMv9.0-A + bool has_sve2 = false; // Not exposed and not supported by Apple M4 + + // ARMv9.1-A + // ARMv8.6-A + bool has_bf16 = FF_HAS_CAP(24); /* CAP_BIT_FEAT_BF16 */ // Brain float16, optional from v8.2 + bool has_i8mm = FF_HAS_CAP(25); /* CAP_BIT_FEAT_I8MM */ // Int8 Matrix Multiply, optional from v8.1 + + // ARMv8.7-A + bool has_afp = FF_HAS_CAP(29); /* CAP_BIT_FEAT_AFP */ // Alternate FP16 (FEXPA), optional from v8.6 + + // ARMv9.2-A + bool has_sme = FF_HAS_CAP(40); /* CAP_BIT_FEAT_SME */ // Scalable Matrix Extension, optional from v9.2 + + // ARMv9.3-A + bool has_sme2 = FF_HAS_CAP(41); /* CAP_BIT_FEAT_SME2 */ // SME2, optional from v9.2 + + // ARMv8.8-A + bool has_hbc = FF_HAS_CAP(64); /* CAP_BIT_FEAT_HBC */ // Hinted conditional branches, optional from v8.7 + + // ARMv8.9-A + bool has_cssc = FF_HAS_CAP(67); /* CAP_BIT_FEAT_CSSC */ // Common Short String Compare, optional from v8.7 + + // ARMv9.4-A+ are not exposed yet + + if (has_sve2 || has_sme) { + // ARMv9 family + if (has_sme2) { + cpu->march = "ARMv9.3-A"; + } else if (has_sme) { + cpu->march = "ARMv9.2-A"; + } else if (has_i8mm && has_bf16) { + cpu->march = "ARMv9.1-A"; + } else { + cpu->march = "ARMv9.0-A"; + } + } else { + // ARMv8 family + if (has_cssc) { + cpu->march = "ARMv8.9-A"; + } else if (has_hbc) { + cpu->march = "ARMv8.8-A"; + } else if (has_afp) { + cpu->march = "ARMv8.7-A"; + } else if (has_i8mm && has_bf16) { + cpu->march = "ARMv8.6-A"; + } else if (has_bti && has_sb && has_dpb2 && has_flagm2 && has_frintts) { + cpu->march = "ARMv8.5-A"; + } else if (has_lse2 && has_dit && has_flagm && has_lrcpc2) { + cpu->march = "ARMv8.4-A"; + } else if (has_pauth && has_lrcpc && has_fcma && has_jscvt) { + cpu->march = "ARMv8.3-A"; + } else if (has_fp16 && has_dpb) { + cpu->march = "ARMv8.2-A"; + } else if (has_lse && has_crc32 && has_rdm) { + cpu->march = "ARMv8.1-A"; + } else if (has_asimd && has_fp) { + cpu->march = "ARMv8-A"; + } + } + + #undef HAS_CAP +} + #elif _WIN32 + #include <processthreadsapi.h> + + // Missing from winnt.h of MinGW-w64 + #define PF_ARM_LSE2_AVAILABLE 62 + #define PF_RESERVED_FEATURE 63 + #define PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE 64 + #define PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE 65 + #define PF_ARM_V82_I8MM_INSTRUCTIONS_AVAILABLE 66 + #define PF_ARM_V82_FP16_INSTRUCTIONS_AVAILABLE 67 + #define PF_ARM_V86_BF16_INSTRUCTIONS_AVAILABLE 68 + #define PF_ARM_V86_EBF16_INSTRUCTIONS_AVAILABLE 69 + #define PF_ARM_SME_INSTRUCTIONS_AVAILABLE 70 + #define PF_ARM_SME2_INSTRUCTIONS_AVAILABLE 71 + #define PF_ARM_SME2_1_INSTRUCTIONS_AVAILABLE 72 + #define PF_ARM_SME2_2_INSTRUCTIONS_AVAILABLE 73 + #define PF_ARM_SME_AES_INSTRUCTIONS_AVAILABLE 74 + #define PF_ARM_SME_SBITPERM_INSTRUCTIONS_AVAILABLE 75 + #define PF_ARM_SME_SF8MM4_INSTRUCTIONS_AVAILABLE 76 + #define PF_ARM_SME_SF8MM8_INSTRUCTIONS_AVAILABLE 77 + #define PF_ARM_SME_SF8DP2_INSTRUCTIONS_AVAILABLE 78 + #define PF_ARM_SME_SF8DP4_INSTRUCTIONS_AVAILABLE 79 + #define PF_ARM_SME_SF8FMA_INSTRUCTIONS_AVAILABLE 80 + #define PF_ARM_SME_F8F32_INSTRUCTIONS_AVAILABLE 81 + #define PF_ARM_SME_F8F16_INSTRUCTIONS_AVAILABLE 82 + #define PF_ARM_SME_F16F16_INSTRUCTIONS_AVAILABLE 83 + #define PF_ARM_SME_B16B16_INSTRUCTIONS_AVAILABLE 84 + #define PF_ARM_SME_F64F64_INSTRUCTIONS_AVAILABLE 85 + #define PF_ARM_SME_I16I64_INSTRUCTIONS_AVAILABLE 86 + #define PF_ARM_SME_LUTv2_INSTRUCTIONS_AVAILABLE 87 + #define PF_ARM_SME_FA64_INSTRUCTIONS_AVAILABLE 88 + +void ffCPUDetectByCpuid(FFCPUResult* cpu) { + // ARMv8-A + bool has_vfp = IsProcessorFeaturePresent(PF_ARM_VFP_32_REGISTERS_AVAILABLE); // Implies basic FP support + bool has_neon = IsProcessorFeaturePresent(PF_ARM_NEON_INSTRUCTIONS_AVAILABLE); // NEON (ASIMD) + + // ARMv8.1-A + bool has_atomics = IsProcessorFeaturePresent(PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE); // LSE atomics + bool has_crc32 = IsProcessorFeaturePresent(PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE); // CRC32 + + // ARMv8.2-A + bool has_fp16 = IsProcessorFeaturePresent(PF_ARM_V82_FP16_INSTRUCTIONS_AVAILABLE); // Half-precision FP + + // ARMv8.3-A + bool has_lrcpc = IsProcessorFeaturePresent(PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE); // LDAPR/LR with RCPC semantics + bool has_jscvt = IsProcessorFeaturePresent(PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE); // FJCVTZS + + // ARMv8.4-A + // My CPU (Apple M1 Pro in VM) does support LSE2, but Windows doesn't detect it for some reason + bool has_lse2 = IsProcessorFeaturePresent(PF_ARM_LSE2_AVAILABLE); // Large System Extensions version 2, optional from v8.2 + bool has_dp = IsProcessorFeaturePresent(PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE); // DotProd, optional from v8.1 (*) + + // ARMv9.0-A + bool has_sve2 = IsProcessorFeaturePresent(PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE); // SVE2 + + // ARMv9.1-A + // ARMv8.6-A + bool has_bf16 = IsProcessorFeaturePresent(PF_ARM_V86_BF16_INSTRUCTIONS_AVAILABLE); // BF16, optional from v8.2 + bool has_i8mm = IsProcessorFeaturePresent(PF_ARM_V82_I8MM_INSTRUCTIONS_AVAILABLE); // Int8 matrix multiply, optional from v8.2 + + // ARMv8.7-A + bool has_ebf16 = IsProcessorFeaturePresent(PF_ARM_V86_EBF16_INSTRUCTIONS_AVAILABLE); // Extended BFloat16 behaviors, optional from v8.2 + + // ARMv9.2-A + bool has_sme = IsProcessorFeaturePresent(PF_ARM_SME_INSTRUCTIONS_AVAILABLE); // SME + + // ARMv9.3-A + bool has_sme2 = IsProcessorFeaturePresent(PF_ARM_SME2_INSTRUCTIONS_AVAILABLE); // SME2 + + // ARMv9.4-A + bool has_sme2p1 = IsProcessorFeaturePresent(PF_ARM_SME2_1_INSTRUCTIONS_AVAILABLE); // SME2.1 + + if (has_sve2 || has_sme) { + // ARMv9 family + if (has_sme2p1) { + cpu->march = "ARMv9.4-A"; + } else if (has_sme2) { + cpu->march = "ARMv9.3-A"; + } else if (has_sme) { + cpu->march = "ARMv9.2-A"; + } else if (has_i8mm && has_bf16) { + cpu->march = "ARMv9.1-A"; + } else { + cpu->march = "ARMv9.0-A"; + } + } else { + // ARMv8 family + if (has_ebf16) { + cpu->march = "ARMv8.7-A"; + } else if (has_i8mm && has_bf16) { + cpu->march = "ARMv8.6-A"; + } else if (has_dp && has_lse2) { + cpu->march = "ARMv8.4-A"; + } else if (has_lrcpc && has_jscvt) { + cpu->march = "ARMv8.3-A"; + } else if (has_fp16) { + cpu->march = "ARMv8.2-A"; + } else if (has_atomics && has_crc32) { + cpu->march = "ARMv8.1-A"; + } else if (has_neon && has_vfp) { + cpu->march = "ARMv8-A"; + } + } +} + #else +void ffCPUDetectByCpuid(FF_A_UNUSED FFCPUResult* cpu) { + // Unsupported system +} + #endif + +#else + +void ffCPUDetectByCpuid(FF_A_UNUSED FFCPUResult* cpu) { + // Unsupported architecture +} + +#endif diff --git a/src/detection/cpu/cpu.h b/src/detection/cpu/cpu.h new file mode 100644 index 0000000..0f7eece --- /dev/null +++ b/src/detection/cpu/cpu.h @@ -0,0 +1,35 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/cpu/option.h" + +#define FF_CPU_TEMP_UNSET (-DBL_MAX) + +typedef struct FFCPUCore { + uint32_t freq; + uint32_t count; +} FFCPUCore; + +typedef struct FFCPUResult { + FFstrbuf name; + FFstrbuf vendor; + const char* march; // Microarchitecture + + uint16_t packages; + uint16_t coresPhysical; + uint16_t coresLogical; + uint16_t coresOnline; + uint16_t numaNodes; + + uint32_t frequencyBase; // GHz + uint32_t frequencyMax; // GHz + + FFCPUCore coreTypes[16]; // number of P cores, E cores, etc. + + double temperature; +} FFCPUResult; + +const char* ffDetectCPU(const FFCPUOptions* options, FFCPUResult* cpu); +const char* ffCPUAppleCodeToName(uint32_t code); +const char* ffCPUQualcommCodeToName(uint32_t code); +void ffCPUDetectByCpuid(FFCPUResult* cpu); diff --git a/src/detection/cpu/cpu_apple.c b/src/detection/cpu/cpu_apple.c new file mode 100644 index 0000000..f078b0c --- /dev/null +++ b/src/detection/cpu/cpu_apple.c @@ -0,0 +1,158 @@ +#include "cpu.h" +#include "common/sysctl.h" +#include "common/apple/smc_temps.h" +#include "common/strutil.h" + +static double detectCpuTemp(const FFCPUOptions* options, const FFstrbuf* cpuName) { + double result = 0; + + const char* error = NULL; + + if (options->tempSensor.length) { + error = ffDetectSmcSpecificTemp(options->tempSensor.chars, &result); + } else { + if (ffStrbufStartsWithS(cpuName, "Apple M")) { + switch (strtol(cpuName->chars + strlen("Apple M"), NULL, 10)) { + case 1: + error = ffDetectSmcTemps(FF_TEMP_CPU_M1X, &result); + break; + case 2: + error = ffDetectSmcTemps(FF_TEMP_CPU_M2X, &result); + break; + case 3: + error = ffDetectSmcTemps(FF_TEMP_CPU_M3X, &result); + break; + case 4: + error = ffDetectSmcTemps(FF_TEMP_CPU_M4X, &result); + break; + default: + error = "Unsupported Apple Silicon CPU"; + } + } else { // PPC? + error = ffDetectSmcTemps(FF_TEMP_CPU_X64, &result); + } + } + + if (error) { + return FF_CPU_TEMP_UNSET; + } + + return result; +} + +#ifdef __aarch64__ + #include "common/apple/cf_helpers.h" + + #include <IOKit/IOKitLib.h> + +static const char* detectFrequency(FFCPUResult* cpu) { + // https://github.com/giampaolo/psutil/pull/2222/files + + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryDevice = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceNameMatching("pmgr")); + if (!entryDevice) { + return "IOServiceGetMatchingService() failed"; + } + + if (!IOObjectConformsTo(entryDevice, "AppleARMIODevice")) { + return "\"pmgr\" should conform to \"AppleARMIODevice\""; + } + + FF_CFTYPE_AUTO_RELEASE CFDataRef freqProperty = (CFDataRef) IORegistryEntryCreateCFProperty(entryDevice, CFSTR("voltage-states5-sram"), kCFAllocatorDefault, kNilOptions); + if (!freqProperty || CFGetTypeID(freqProperty) != CFDataGetTypeID()) { + return "\"voltage-states5-sram\" in \"pmgr\" is not found"; + } + + // voltage-states5-sram stores supported <frequency / voltage> pairs of pcores from the lowest to the highest + // voltage-states1-sram stores ecores' + CFIndex propLength = CFDataGetLength(freqProperty); + if (propLength == 0 || propLength % (CFIndex) sizeof(uint32_t) * 2 != 0) { + return "Invalid \"voltage-states5-sram\" length"; + } + + uint32_t* pStart = (uint32_t*) CFDataGetBytePtr(freqProperty); + uint32_t pMax = *pStart; + for (CFIndex i = 2; i < propLength / (CFIndex) sizeof(uint32_t) && pStart[i] > 0; i += 2 /* skip voltage */) { + pMax = pMax > pStart[i] ? pMax : pStart[i]; + } + + if (pMax > 0) { + if (pMax > 100000000) { // Assume that pMax is in Hz, M1~M3 + cpu->frequencyMax = pMax / 1000 / 1000; + } else { // Assume that pMax is in kHz, M4 and later (#1394) + cpu->frequencyMax = pMax / 1000; + } + } + + return NULL; +} +#else +static const char* detectFrequency(FFCPUResult* cpu) { + cpu->frequencyBase = (uint32_t) (ffSysctlGetInt64("hw.cpufrequency", 0) / 1000 / 1000); + cpu->frequencyMax = (uint32_t) (ffSysctlGetInt64("hw.cpufrequency_max", 0) / 1000 / 1000); + if (cpu->frequencyBase == 0) { + unsigned current = 0; + size_t size = sizeof(current); + if (sysctl((int[]) { CTL_HW, HW_CPU_FREQ }, 2, ¤t, &size, NULL, 0) == 0) { + cpu->frequencyBase = (uint32_t) (current / 1000 / 1000); + } + } + return NULL; +} +#endif + +static const char* detectCoreCount(FFCPUResult* cpu) { + uint32_t nPerfLevels = (uint32_t) ffSysctlGetInt("hw.nperflevels", 0); + if (nPerfLevels <= 0) { + return "sysctl(hw.nperflevels) failed"; + } + + char sysctlKey[] = "hw.perflevelN.logicalcpu"; + if (nPerfLevels > ARRAY_SIZE(cpu->coreTypes)) { + nPerfLevels = ARRAY_SIZE(cpu->coreTypes); + } + for (uint32_t i = 0; i < nPerfLevels; ++i) { + sysctlKey[strlen("hw.perflevel")] = (char) ('0' + i); + cpu->coreTypes[i] = (FFCPUCore) { + .freq = nPerfLevels - i, + .count = (uint32_t) ffSysctlGetInt(sysctlKey, 0), + }; + } + return NULL; +} + +const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu) { + if (ffSysctlGetString("machdep.cpu.brand_string", &cpu->name) != NULL) { + return "sysctlbyname(machdep.cpu.brand_string) failed"; + } + + ffSysctlGetString("machdep.cpu.vendor", &cpu->vendor); + cpu->packages = (uint16_t) ffSysctlGetInt("hw.packages", 1); + if (cpu->vendor.length == 0 && ffStrbufStartsWithS(&cpu->name, "Apple ")) { + ffStrbufAppendS(&cpu->vendor, "Apple"); + } + + cpu->coresPhysical = (uint16_t) ffSysctlGetInt("hw.physicalcpu_max", 1); + if (cpu->coresPhysical == 1) { + cpu->coresPhysical = (uint16_t) ffSysctlGetInt("hw.physicalcpu", 1); + } + + cpu->coresLogical = (uint16_t) ffSysctlGetInt("hw.logicalcpu_max", 1); + if (cpu->coresLogical == 1) { + cpu->coresLogical = (uint16_t) ffSysctlGetInt("hw.ncpu", 1); + } + + cpu->coresOnline = (uint16_t) ffSysctlGetInt("hw.logicalcpu", 1); + if (cpu->coresOnline == 1) { + cpu->coresOnline = (uint16_t) ffSysctlGetInt("hw.activecpu", 1); + } + + ffCPUDetectByCpuid(cpu); + detectFrequency(cpu); + if (options->showPeCoreCount) { + detectCoreCount(cpu); + } + + cpu->temperature = options->temp ? detectCpuTemp(options, &cpu->name) : FF_CPU_TEMP_UNSET; + + return NULL; +} diff --git a/src/detection/cpu/cpu_arm.h b/src/detection/cpu/cpu_arm.h new file mode 100644 index 0000000..571761a --- /dev/null +++ b/src/detection/cpu/cpu_arm.h @@ -0,0 +1,588 @@ +#pragma once + +#include "fastfetch.h" + +// https://github.com/util-linux/util-linux/blob/master/sys-utils/lscpu-arm.c +// We use the util-linux's data but not its code. Call me if it violates util-linux's GPL license. + +static const char* hwImplId2Vendor(uint32_t implId) { + switch (implId) { + case 0x41: + return "ARM"; + case 0x42: + return "Broadcom"; + case 0x43: + return "Cavium"; + case 0x44: + return "DEC"; + case 0x46: + return "FUJITSU"; + case 0x48: + return "HiSilicon"; + case 0x49: + return "Infineon"; + case 0x4d: + return "Motorola"; + case 0x4e: + return "NVIDIA"; + case 0x50: + return "APM"; + case 0x51: + return "Qualcomm"; + case 0x53: + return "Samsung"; + case 0x56: + return "Marvell"; + case 0x61: + return "Apple"; + case 0x66: + return "Faraday"; + case 0x69: + return "Intel"; + case 0x6D: + return "Microsoft"; + case 0x70: + return "Phytium"; + case 0xc0: + return "Ampere"; + default: + return "Unknown"; + } +} + +static const char* armPartId2name(uint32_t partId) { + switch (partId) { + case 0x810: + return "ARM810"; + case 0x920: + return "ARM920"; + case 0x922: + return "ARM922"; + case 0x926: + return "ARM926"; + case 0x940: + return "ARM940"; + case 0x946: + return "ARM946"; + case 0x966: + return "ARM966"; + case 0xa20: + return "ARM1020"; + case 0xa22: + return "ARM1022"; + case 0xa26: + return "ARM1026"; + case 0xb02: + return "ARM11-MPCore"; + case 0xb36: + return "ARM1136"; + case 0xb56: + return "ARM1156"; + case 0xb76: + return "ARM1176"; + case 0xc05: + return "Cortex-A5"; + case 0xc07: + return "Cortex-A7"; + case 0xc08: + return "Cortex-A8"; + case 0xc09: + return "Cortex-A9"; + case 0xc0d: + return "Cortex-A17"; /* Originally A12 */ + case 0xc0f: + return "Cortex-A15"; + case 0xc0e: + return "Cortex-A17"; + case 0xc14: + return "Cortex-R4"; + case 0xc15: + return "Cortex-R5"; + case 0xc17: + return "Cortex-R7"; + case 0xc18: + return "Cortex-R8"; + case 0xc20: + return "Cortex-M0"; + case 0xc21: + return "Cortex-M1"; + case 0xc23: + return "Cortex-M3"; + case 0xc24: + return "Cortex-M4"; + case 0xc27: + return "Cortex-M7"; + case 0xc60: + return "Cortex-M0+"; + case 0xd01: + return "Cortex-A32"; + case 0xd02: + return "Cortex-A34"; + case 0xd03: + return "Cortex-A53"; + case 0xd04: + return "Cortex-A35"; + case 0xd05: + return "Cortex-A55"; + case 0xd06: + return "Cortex-A65"; + case 0xd07: + return "Cortex-A57"; + case 0xd08: + return "Cortex-A72"; + case 0xd09: + return "Cortex-A73"; + case 0xd0a: + return "Cortex-A75"; + case 0xd0b: + return "Cortex-A76"; + case 0xd0c: + return "Neoverse-N1"; + case 0xd0d: + return "Cortex-A77"; + case 0xd0e: + return "Cortex-A76AE"; + case 0xd13: + return "Cortex-R52"; + case 0xd14: + return "Cortex-R82AE"; + case 0xd15: + return "Cortex-R82"; + case 0xd16: + return "Cortex-R52+"; + case 0xd20: + return "Cortex-M23"; + case 0xd21: + return "Cortex-M33"; + case 0xd24: + return "Cortex-M52"; + case 0xd22: + return "Cortex-M55"; + case 0xd23: + return "Cortex-M85"; + case 0xd40: + return "Neoverse-V1"; + case 0xd41: + return "Cortex-A78"; + case 0xd42: + return "Cortex-A78AE"; + case 0xd43: + return "Cortex-A65AE"; + case 0xd44: + return "Cortex-X1"; + case 0xd46: + return "Cortex-A510"; + case 0xd47: + return "Cortex-A710"; + case 0xd48: + return "Cortex-X2"; + case 0xd49: + return "Neoverse-N2"; + case 0xd4a: + return "Neoverse-E1"; + case 0xd4b: + return "Cortex-A78C"; + case 0xd4c: + return "Cortex-X1C"; + case 0xd4d: + return "Cortex-A715"; + case 0xd4e: + return "Cortex-X3"; + case 0xd4f: + return "Neoverse-V2"; + case 0xd80: + return "Cortex-A520"; + case 0xd81: + return "Cortex-A720"; + case 0xd82: + return "Cortex-X4"; + case 0xd83: + return "Neoverse-V3AE"; + case 0xd84: + return "Neoverse-V3"; + case 0xd85: + return "Cortex-X925"; + case 0xd87: + return "Cortex-A725"; + case 0xd88: + return "Cortex-A520AE"; + case 0xd89: + return "Cortex-A720AE"; + case 0xd8a: + return "C1-Nano"; + case 0xd8b: + return "C1-Pro"; + case 0xd8c: + return "C1-Ultra"; + case 0xd8e: + return "Neoverse-N3"; + case 0xd8f: + return "Cortex-A320"; + case 0xd90: + return "C1-Premium"; + default: + return NULL; + } +} + +static const char* brcmPartId2name(uint32_t partId) { + switch (partId) { + case 0x0f: + return "Brahma-B15"; + case 0x100: + return "Brahma-B53"; + case 0x516: + return "ThunderX2"; + default: + return NULL; + } +} + +static const char* decPartId2name(uint32_t partId) { + switch (partId) { + case 0xa10: + return "SA110"; + case 0xa11: + return "SA1100"; + default: + return NULL; + } +} + +static const char* caviumPartId2name(uint32_t partId) { + switch (partId) { + case 0x0a0: + return "ThunderX"; + case 0x0a1: + return "ThunderX-88XX"; + case 0x0a2: + return "ThunderX-81XX"; + case 0x0a3: + return "ThunderX-83XX"; + case 0x0af: + return "ThunderX2-99xx"; + case 0x0b0: + return "OcteonTX2"; + case 0x0b1: + return "OcteonTX2-98XX"; + case 0x0b2: + return "OcteonTX2-96XX"; + case 0x0b3: + return "OcteonTX2-95XX"; + case 0x0b4: + return "OcteonTX2-95XXN"; + case 0x0b5: + return "OcteonTX2-95XXMM"; + case 0x0b6: + return "OcteonTX2-95XXO"; + case 0x0b8: + return "ThunderX3-T110"; + default: + return NULL; + } +} + +static const char* apmPartId2name(uint32_t partId) { + switch (partId) { + case 0x000: + return "X-Gene"; + default: + return NULL; + } +} + +static const char* qcomPartId2name(uint32_t partId) { + switch (partId) { + case 0x001: + return "Oryon 1"; + case 0x002: + return "Oryon 2"; + case 0x00f: + return "Scorpion"; + case 0x02d: + return "Scorpion"; + case 0x04d: + return "Krait"; + case 0x06f: + return "Krait"; + case 0x201: + return "Kryo"; + case 0x205: + return "Kryo"; + case 0x211: + return "Kryo"; + case 0x800: + return "Falkor-V1/Kryo"; + case 0x801: + return "Kryo-V2"; + case 0x802: + return "Kryo-3XX-Gold"; + case 0x803: + return "Kryo-3XX-Silver"; + case 0x804: + return "Kryo-4XX-Gold"; + case 0x805: + return "Kryo-4XX-Silver"; + case 0xc00: + return "Falkor"; + case 0xc01: + return "Saphira"; + default: + return NULL; + } +} + +static const char* samsungPartId2name(uint32_t partId) { + switch (partId) { + case 0x001: + return "Exynos-M1"; + case 0x002: + return "Exynos-M3"; + case 0x003: + return "Exynos-M4"; + case 0x004: + return "Exynos-M5"; + default: + return NULL; + } +} + +static const char* nvidiaPartId2name(uint32_t partId) { + switch (partId) { + case 0x000: + return "Denver"; + case 0x003: + return "Denver-2"; + case 0x004: + return "Carmel"; + case 0x010: + return "Olympus"; + default: + return NULL; + } +} + +static const char* marvellPartId2name(uint32_t partId) { + switch (partId) { + case 0x131: + return "Feroceon-88FR131"; + case 0x581: + return "PJ4/PJ4b"; + case 0x584: + return "PJ4B-MP"; + default: + return NULL; + } +} + +static const char* applePartId2name(uint32_t partId) { + switch (partId) { + case 0x000: + return "Swift"; + case 0x001: + return "Cyclone"; + case 0x002: + return "Typhoon"; + case 0x003: + return "Typhoon/Capri"; + case 0x004: + return "Twister"; + case 0x005: + return "Twister/Elba/Malta"; + case 0x006: + return "Hurricane"; + case 0x007: + return "Hurricane/Myst"; + case 0x008: + return "Monsoon"; + case 0x009: + return "Mistral"; + case 0x00b: + return "Vortex"; + case 0x00c: + return "Tempest"; + case 0x00f: + return "Tempest-M9"; + case 0x010: + return "Vortex/Aruba"; + case 0x011: + return "Tempest/Aruba"; + case 0x012: + return "Lightning"; + case 0x013: + return "Thunder"; + case 0x020: + return "Icestorm-A14"; + case 0x021: + return "Firestorm-A14"; + case 0x022: + return "Icestorm-M1"; + case 0x023: + return "Firestorm-M1"; + case 0x024: + return "Icestorm-M1-Pro"; + case 0x025: + return "Firestorm-M1-Pro"; + case 0x026: + return "Thunder-M10"; + case 0x028: + return "Icestorm-M1-Max"; + case 0x029: + return "Firestorm-M1-Max"; + case 0x030: + return "Blizzard-A15"; + case 0x031: + return "Avalanche-A15"; + case 0x032: + return "Blizzard-M2"; + case 0x033: + return "Avalanche-M2"; + case 0x034: + return "Blizzard-M2-Pro"; + case 0x035: + return "Avalanche-M2-Pro"; + case 0x036: + return "Sawtooth-A16"; + case 0x037: + return "Everest-A16"; + case 0x038: + return "Blizzard-M2-Max"; + case 0x039: + return "Avalanche-M2-Max"; + case 0x046: + return "Sawtooth-M11"; + case 0x048: + return "Sawtooth-M3-Max"; + case 0x049: + return "Everest-M3-Max"; + default: + return NULL; + } +} + +static const char* faradayPartId2name(uint32_t partId) { + switch (partId) { + case 0x526: + return "FA526"; + case 0x626: + return "FA626"; + default: + return NULL; + } +} + +static const char* intelPartId2name(uint32_t partId) { + switch (partId) { + case 0x200: + return "i80200"; + case 0x210: + return "PXA250A"; + case 0x212: + return "PXA210A"; + case 0x242: + return "i80321-400"; + case 0x243: + return "i80321-600"; + case 0x290: + return "PXA250B/PXA26x"; + case 0x292: + return "PXA210B"; + case 0x2c2: + return "i80321-400-B0"; + case 0x2c3: + return "i80321-600-B0"; + case 0x2d0: + return "PXA250C/PXA255/PXA26x"; + case 0x2d2: + return "PXA210C"; + case 0x411: + return "PXA27x"; + case 0x41c: + return "IPX425-533"; + case 0x41d: + return "IPX425-400"; + case 0x41f: + return "IPX425-266"; + case 0x682: + return "PXA32x"; + case 0x683: + return "PXA930/PXA935"; + case 0x688: + return "PXA30x"; + case 0x689: + return "PXA31x"; + case 0xb11: + return "SA1110"; + case 0xc12: + return "IPX1200"; + default: + return NULL; + } +} + +static const char* fujitsuPartId2name(uint32_t partId) { + switch (partId) { + case 0x001: + return "A64FX"; + case 0x003: + return "MONAKA"; + default: + return NULL; + } +} + +static const char* hisiPartId2name(uint32_t partId) { + switch (partId) { + case 0xd01: + return "TaiShan-v110"; /* used in Kunpeng-920 SoC */ + case 0xd02: + return "TaiShan-v120"; /* used in Kirin 990A and 9000S SoCs */ + case 0xd40: + return "Cortex-A76"; /* HiSilicon uses this ID though advertises A76 */ + case 0xd41: + return "Cortex-A77"; /* HiSilicon uses this ID though advertises A77 */ + default: + return NULL; + } +} + +static const char* amperePartId2name(uint32_t partId) { + switch (partId) { + case 0xac3: + return "Ampere-1"; + case 0xac4: + return "Ampere-1a"; + default: + return NULL; + } +} + +static const char* ftPartId2name(uint32_t partId) { + switch (partId) { + case 0x303: + return "FTC310"; + case 0x660: + return "FTC660"; + case 0x661: + return "FTC661"; + case 0x662: + return "FTC662"; + case 0x663: + return "FTC663"; + case 0x664: + return "FTC664"; + case 0x862: + return "FTC862"; + default: + return NULL; + } +} + +static const char* msPartId2name(uint32_t partId) { + switch (partId) { + case 0xd49: + return "Azure-Cobalt-100"; + default: + return NULL; + } +} diff --git a/src/detection/cpu/cpu_bsd.c b/src/detection/cpu/cpu_bsd.c new file mode 100644 index 0000000..4cca30e --- /dev/null +++ b/src/detection/cpu/cpu_bsd.c @@ -0,0 +1,109 @@ +#include "cpu.h" +#include "common/sysctl.h" +#include "common/strutil.h" + +#include <sys/param.h> +#if __has_include(<sys/cpuset.h>) + #include <sys/cpuset.h> + #define FF_HAVE_CPUSET 1 +#endif + +static const char* detectCpuTemp(const FFCPUOptions* options, double* current) { + int temp; + if (options->tempSensor.length > 0) { + temp = ffSysctlGetInt(options->tempSensor.chars, -999999); + if (temp == -999999) { + return "ffSysctlGetInt(options->tempSensor) failed"; + } + } else { + temp = ffSysctlGetInt("dev.cpu.0.temperature", -999999); + if (temp == -999999) { + // Thermal zone temperature + temp = ffSysctlGetInt("hw.acpi.thermal.tz0.temperature", -999999); + if (temp == -999999) { + return "ffSysctlGetInt(\"dev.cpu.0.temperature\" or \"hw.acpi.thermal.tz0.temperature\") failed"; + } + } + } + + // In tenth of degrees Kelvin + *current = (double) temp / 10 - 273.15; + return NULL; +} + +const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu) { + if (ffSysctlGetString("hw.model", &cpu->name) != NULL) { + return "sysctlbyname(hw.model) failed"; + } + + cpu->coresLogical = (uint16_t) ffSysctlGetInt("hw.ncpu", 1); + cpu->coresPhysical = (uint16_t) ffSysctlGetInt("kern.smp.cores", 0); + cpu->coresOnline = (uint16_t) ffSysctlGetInt("kern.smp.cpus", cpu->coresLogical); + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + if (ffSysctlGetString("kern.sched.topology_spec", &buffer) == NULL && buffer.length > 0) { + // <groups> + // <group level="1" cache-level="3"> + // <cpu count="4" mask="f,0,0,0">0, 1, 2, 3</cpu> + // <children> + // <group level="2" cache-level="2"> + // <cpu count="2" mask="3,0,0,0">0, 1</cpu> + // <flags><flag name="THREAD">THREAD group</flag><flag name="SMT">SMT group</flag></flags> + // </group> + // <group level="2" cache-level="2"> + // <cpu count="2" mask="c,0,0,0">2, 3</cpu> + // <flags><flag name="THREAD">THREAD group</flag><flag name="SMT">SMT group</flag></flags> + // </group> + // </children> + // </group> + // </groups> + for (char* p = buffer.chars; (p = strstr(p, "\n </group>\n")); ++p) { + cpu->packages++; + } + } + +#if FF_HAVE_CPUSET && (__x86_64__ || __i386__) + // Bind current process to the first two cores, which is *usually* a performance core + cpuset_t currentCPU; + CPU_ZERO(¤tCPU); + CPU_SET(1, ¤tCPU); + CPU_SET(2, ¤tCPU); + cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1, sizeof(cpuset_t), ¤tCPU); +#endif + + ffCPUDetectByCpuid(cpu); + + uint32_t clockrate = (uint32_t) ffSysctlGetInt("hw.clockrate", 0); + if (clockrate > cpu->frequencyBase) { + cpu->frequencyBase = clockrate; + } + + for (uint16_t i = 0; i < cpu->coresLogical; ++i) { + ffStrbufClear(&buffer); + char key[32]; + snprintf(key, sizeof(key), "dev.cpu.%u.freq_levels", i); + if (ffSysctlGetString(key, &buffer) == NULL) { + if (buffer.length == 0) { + continue; + } + + // MHz/Watts pairs like: 2501/32000 2187/27125 2000/24000 + uint32_t fmax = (uint32_t) strtoul(buffer.chars, NULL, 10); + if (cpu->frequencyMax < fmax) { + cpu->frequencyMax = fmax; + } + } else { + break; + } + } + + cpu->temperature = FF_CPU_TEMP_UNSET; + + if (options->temp) { + detectCpuTemp(options, &cpu->temperature); + } + + cpu->numaNodes = (uint16_t) ffSysctlGetInt("vm.ndomains", 0); + + return NULL; +} diff --git a/src/detection/cpu/cpu_haiku.c b/src/detection/cpu/cpu_haiku.c new file mode 100644 index 0000000..9a8acb5 --- /dev/null +++ b/src/detection/cpu/cpu_haiku.c @@ -0,0 +1,68 @@ +#include "cpu.h" +#include "common/mallocHelper.h" + +#include <OS.h> +#include <private/shared/cpu_type.h> + +const char* ffDetectCPUImpl(FF_A_UNUSED const FFCPUOptions* options, FFCPUResult* cpu) { + system_info sysInfo; + if (get_system_info(&sysInfo) != B_OK) { + return "get_system_info() failed"; + } + + uint32 topoNodeCount = 0; + get_cpu_topology_info(NULL, &topoNodeCount); + if (topoNodeCount == 0) { + return "get_cpu_topology_info(NULL) failed"; + } + + FF_AUTO_FREE cpu_topology_node_info* topology = malloc(sizeof(*topology) * topoNodeCount); + if (get_cpu_topology_info(topology, &topoNodeCount) != B_OK) { + return "get_cpu_topology_info(topology) failed"; + } + + enum cpu_platform platform = B_CPU_UNKNOWN; + enum cpu_vendor cpuVendor = B_CPU_VENDOR_UNKNOWN; + uint32 cpuModel = 0, frequency = 0; + uint16_t packages = 0, cores = 0; + + for (uint32 i = 0; i < topoNodeCount; i++) { + switch (topology[i].type) { + case B_TOPOLOGY_ROOT: + platform = topology[i].data.root.platform; + break; + + case B_TOPOLOGY_PACKAGE: + cpuVendor = topology[i].data.package.vendor; + ++packages; + break; + + case B_TOPOLOGY_CORE: + cpuModel = topology[i].data.core.model; + uint32_t freq = (uint32_t) (topology[i].data.core.default_frequency / 1000000); + frequency = freq > frequency ? freq : frequency; + ++cores; + break; + + default: + break; + } + } + + const char* model = get_cpu_model_string(platform, cpuVendor, cpuModel); + if (model) { + ffStrbufSetS(&cpu->name, model); + } else { + ffStrbufSetF(&cpu->name, "(Unknown %" B_PRIx32 ")", cpuModel); + } + ffStrbufSetS(&cpu->vendor, get_cpu_vendor_string(cpuVendor)); + + ffCPUDetectByCpuid(cpu); + if (cpu->frequencyBase < frequency) { + cpu->frequencyBase = frequency; + } + cpu->packages = packages; + cpu->coresPhysical = cores; + cpu->coresOnline = cpu->coresLogical = (uint16_t) sysInfo.cpu_count; + return NULL; +} diff --git a/src/detection/cpu/cpu_linux.c b/src/detection/cpu/cpu_linux.c new file mode 100644 index 0000000..b228d52 --- /dev/null +++ b/src/detection/cpu/cpu_linux.c @@ -0,0 +1,1098 @@ +#include "cpu.h" +#include "common/io.h" +#include "common/processing.h" +#include "common/properties.h" +#include "common/mallocHelper.h" +#include "common/strutil.h" +#include "common/path.h" + +#include <sys/sysinfo.h> +#include <stdlib.h> +#include <unistd.h> +#include <dirent.h> +#include <fcntl.h> + +#define FF_CPUINFO_PATH "/proc/cpuinfo" + +static double readTempFile(int dfd, const char* filename, FFstrbuf* buffer) { + if (filename ? !ffReadFileBufferRelative(dfd, filename, buffer) : !ffReadFDBuffer(dfd, buffer)) { + return FF_CPU_TEMP_UNSET; + } + + double value = ffStrbufToDouble(buffer, FF_CPU_TEMP_UNSET); // millidegree Celsius + if (value == FF_CPU_TEMP_UNSET) { + return FF_CPU_TEMP_UNSET; + } + + return value / 1000.; +} + +static double parseTZDir(int dfd, FFstrbuf* buffer) { + if (!ffReadFileBufferRelative(dfd, "type", buffer)) { + return FF_CPU_TEMP_UNSET; + } + + if (!ffStrbufStartsWithS(buffer, "cpu") && + !ffStrbufStartsWithS(buffer, "soc") && +#if __x86_64__ || __i386__ + !ffStrbufEqualS(buffer, "x86_pkg_temp") && +#endif + true) + return FF_CPU_TEMP_UNSET; + + return readTempFile(dfd, "temp", buffer); +} + +static double parseHwmonDir(int dfd, FFstrbuf* buffer) { + // https://www.kernel.org/doc/Documentation/hwmon/sysfs-interface + if (!ffReadFileBufferRelative(dfd, "name", buffer)) { + return FF_CPU_TEMP_UNSET; + } + + ffStrbufTrimRightSpace(buffer); + + if ( + !ffStrbufContainS(buffer, "cpu") && +#if __x86_64__ || __i386__ + !ffStrbufEqualS(buffer, "k10temp") && // AMD + !ffStrbufEqualS(buffer, "fam15h_power") && // AMD + !ffStrbufEqualS(buffer, "coretemp") && // Intel +#endif + true) + return FF_CPU_TEMP_UNSET; + + return readTempFile(dfd, "temp1_input", buffer); +} + +static double detectCPUTemp(const FFCPUOptions* options) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + if (options->tempSensor.length > 0) { + FF_AUTO_CLOSE_FD int subfd = -1; + const char* fileName = NULL; + if (ffStrbufStartsWithS(&options->tempSensor, "hwmon") && ffCharIsDigit(options->tempSensor.chars[strlen("hwmon")])) { + FF_AUTO_CLOSE_FD int dfd = open("/sys/class/hwmon/", O_PATH | O_CLOEXEC); + subfd = openat(dfd, options->tempSensor.chars, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (subfd >= 0) { + fileName = "temp1_input"; + } else { + subfd = openat(dfd, options->tempSensor.chars, O_RDONLY | O_CLOEXEC); + } + } else if (ffStrbufStartsWithS(&options->tempSensor, "thermal_zone") && ffCharIsDigit(options->tempSensor.chars[strlen("thermal_zone")])) { + FF_AUTO_CLOSE_FD int dfd = open("/sys/class/thermal/", O_PATH | O_CLOEXEC); + subfd = openat(dfd, options->tempSensor.chars, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (subfd >= 0) { + fileName = "temp"; + } else { + subfd = openat(dfd, options->tempSensor.chars, O_RDONLY | O_CLOEXEC); + } + } else if (ffStrbufStartsWithS(&options->tempSensor, "cputemp.") && ffCharIsDigit(options->tempSensor.chars[strlen("cputemp.")])) { + FF_AUTO_CLOSE_FD int dfd = open("/sys/class/platform/", O_PATH | O_CLOEXEC); + subfd = openat(dfd, options->tempSensor.chars, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (subfd >= 0) { + fileName = "temp1_input"; + } else { + subfd = openat(dfd, options->tempSensor.chars, O_RDONLY | O_CLOEXEC); + } + } else if (ffIsAbsolutePath(options->tempSensor.chars)) { + subfd = open(options->tempSensor.chars, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (subfd >= 0) { + fileName = "temp1_input"; + } else { + subfd = open(options->tempSensor.chars, O_RDONLY | O_CLOEXEC); + } + } + if (subfd < 0) { + return FF_CPU_TEMP_UNSET; + } + + return readTempFile(subfd, fileName, &buffer); + } + + { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/hwmon/"); + if (dirp) { + int dfd = dirfd(dirp); + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + FF_AUTO_CLOSE_FD int subfd = openat(dfd, entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (subfd < 0) { + continue; + } + + double result = parseHwmonDir(subfd, &buffer); + if (result != FF_CPU_TEMP_UNSET) { + return result; + } + } + } + } + { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/thermal/"); + if (dirp) { + int dfd = dirfd(dirp); + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + if (!ffStrStartsWith(entry->d_name, "thermal_zone")) { + continue; + } + + FF_AUTO_CLOSE_FD int subfd = openat(dfd, entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (subfd < 0) { + continue; + } + + double result = parseTZDir(subfd, &buffer); + if (result != FF_CPU_TEMP_UNSET) { + return result; + } + } + } + } + { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/devices/platform/"); + if (dirp) { + int dfd = dirfd(dirp); + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + if (!ffStrStartsWith(entry->d_name, "cputemp.")) { + continue; + } + + FF_AUTO_CLOSE_FD int subfd = openat(dfd, entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (subfd < 0) { + continue; + } + + double result = parseHwmonDir(subfd, &buffer); + if (result != FF_CPU_TEMP_UNSET) { + return result; + } + } + } + } + + return FF_CPU_TEMP_UNSET; +} + +static void detectNumaNodes(FFCPUResult* cpu) { + FF_AUTO_CLOSE_DIR DIR* dir = opendir("/sys/devices/system/node/"); + if (!dir) { + return; + } + + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN) { + continue; + } + if (ffStrStartsWith(entry->d_name, "node") && ffCharIsDigit(entry->d_name[strlen("node")])) { + cpu->numaNodes++; + } + } +} + +#ifdef __ANDROID__ + #include "common/settings.h" + +static void detectQualcomm(FFCPUResult* cpu) { + // https://en.wikipedia.org/wiki/List_of_Qualcomm_Snapdragon_systems_on_chips + + assert(cpu->name.length >= 2); + uint32_t code = (uint32_t) strtoul(cpu->name.chars + 2, NULL, 10); + const char* name = NULL; + + switch (code) { + case 8845: + name = "8 Gen 5"; + break; // ? + case 8850: + name = "8 Elite Gen 5"; + break; + case 8735: + name = "8s Gen 4"; + break; + case 8750: + name = "8 Elite"; + break; + case 8635: + name = "8s Gen 3"; + break; + case 8650: + name = "8 Gen 3"; + break; + case 8550: + name = "8 Gen 2"; + break; + case 8475: + name = "8+ Gen 1"; + break; + case 8450: + name = "8 Gen 1"; + break; + case 7750: + name = "7 Gen 4"; + break; + case 7675: + name = "7+ Gen 3"; + break; + case 7635: + name = "7s Gen 3"; + break; + case 7550: + name = "7 Gen 3"; + break; + case 7475: + name = "7+ Gen 2"; + break; + case 7435: + name = "7s Gen 2"; + break; + case 7450: + name = "7 Gen 1"; + break; + case 6650: + name = "6 Gen 4"; + break; + case 6375: + name = "6s Gen 3"; + break; + case 6475: + name = "6 Gen 3"; + break; + case 6115: + name = "6s Gen 1"; + break; + case 6450: + name = "6 Gen 1"; + break; + case 4635: + name = "4s Gen 2"; + break; + case 4450: + name = "4 Gen 2"; + break; + case 4375: + name = "4 Gen 1"; + break; + } + + if (name) { + char str[32]; + ffStrCopy(str, cpu->name.chars, sizeof(str)); + ffStrbufSetF(&cpu->name, "Qualcomm Snapdragon %s [%s]", name, str); + return; + } +} + +static void detectMediaTek(FFCPUResult* cpu) { + // https://en.wikipedia.org/wiki/List_of_MediaTek_systems_on_chips + + assert(cpu->name.length >= 2); + uint32_t code = (uint32_t) strtoul(cpu->name.chars + 2, NULL, 10); + const char* name = NULL; + + switch (code) // The SOC code of MTK Dimensity series is full of mess + { + case 6993: + name = "9500"; + break; + case 6991: + name = "9400"; + break; + case 6989: + case 8796: + name = "9300"; + break; + case 6985: + name = "9200"; + break; + case 6983: + case 8798: + name = "9000"; + break; + + case 6899: + name = "8400"; + break; + case 6897: + case 8792: + name = "8300"; + break; + case 6896: + name = "8200"; + break; + case 8795: + name = "8100"; + break; + case 6895: + name = "8000"; + break; + } + + if (name) { + char str[32]; + ffStrCopy(str, cpu->name.chars, sizeof(str)); + ffStrbufSetF(&cpu->name, "MediaTek Dimensity %s [%s]", name, str); + return; + } +} + +static void detectExynos(FFCPUResult* cpu) { + // https://en.wikipedia.org/wiki/Exynos + + assert(cpu->name.length > 3); + uint32_t code = (uint32_t) strtoul(cpu->name.chars + 3, NULL, 10); + const char* name = NULL; + + switch (code) { + case 9965: + name = "2600"; + break; + case 9955: + name = "2500"; + break; + case 9945: + name = "2400"; + break; + // No 2300 + case 9925: + name = "2200"; + break; + case 9840: + name = "2100"; + break; + + case 8855: + name = "1580"; + break; + case 8845: + name = "1480"; + break; + case 8835: + name = "1380"; + break; + case 8535: + name = "1330"; + break; + case 8825: + name = "1280"; + break; + case 9815: + name = "1080"; + break; + + case 9830: + name = "990"; + break; + case 9630: + name = "980"; + break; + + case 8805: + name = "880"; + break; + case 3830: + name = "850"; + break; + } + + if (name) { + char str[32]; + ffStrCopy(str, cpu->name.chars, sizeof(str)); + ffStrbufSetF(&cpu->name, "Samsung Exynos %s [%s]", name, str); + return; + } +} + +static void detectAndroid(FFCPUResult* cpu) { + if (cpu->name.length == 0) { + if (ffSettingsGetAndroidProperty("ro.soc.model", &cpu->name)) { + ffStrbufClear(&cpu->vendor); // We usually detect the vendor of CPU core as ARM, but instead we want the vendor of SOC + } + } + if (cpu->vendor.length == 0) { + if (!ffSettingsGetAndroidProperty("ro.soc.manufacturer", &cpu->vendor)) { + if (!ffSettingsGetAndroidProperty("ro.product.product.manufacturer", &cpu->vendor)) { + if (!ffSettingsGetAndroidProperty("ro.product.vendor.manufacturer", &cpu->vendor)) { + if (ffSettingsGetAndroidProperty("ro.mediatek.platform", &cpu->name)) { + ffStrbufSetStatic(&cpu->vendor, "MediaTek"); + } + } + } + } + } + + if (ffStrbufEqualS(&cpu->vendor, "QTI")) { + ffStrbufSetStatic(&cpu->vendor, "Qualcomm"); + } else if (ffStrbufIgnCaseEqualS(&cpu->vendor, "MediaTek")) { // sometimes "Mediatek" + ffStrbufSetStatic(&cpu->vendor, "MediaTek"); + } else if (cpu->vendor.length > 0) { + cpu->vendor.chars[0] = (char) toupper(cpu->vendor.chars[0]); + } + + if (ffStrbufEqualS(&cpu->vendor, "Qualcomm") && ffStrbufStartsWithS(&cpu->name, "SM")) { + detectQualcomm(cpu); + } else if (ffStrbufEqualS(&cpu->vendor, "MediaTek") && ffStrbufStartsWithS(&cpu->name, "MT")) { + detectMediaTek(cpu); + } else if (ffStrbufEqualS(&cpu->vendor, "Samsung") && ffStrbufStartsWithS(&cpu->name, "s5e")) { + cpu->name.chars[0] = 'S'; + cpu->name.chars[2] = 'E'; + detectExynos(cpu); + } +} +#endif + +#if __arm__ || __aarch64__ + #include "cpu_arm.h" + +static void detectArmName(FFstrbuf* cpuinfo, FFCPUResult* cpu, uint32_t implId) { + char* line = NULL; + size_t len = 0; + uint32_t lastPartId = UINT32_MAX; + uint32_t num = 0; + while (ffStrbufGetline(&line, &len, cpuinfo)) { + if (!ffStrStartsWith(line, "CPU part\t: ")) { + continue; + } + uint32_t partId = (uint32_t) strtoul(line + strlen("CPU part\t: "), NULL, 16); + const char* name = NULL; + switch (implId) { + case 0x41: + name = armPartId2name(partId); + break; + case 0x42: + name = brcmPartId2name(partId); + break; + case 0x43: + name = caviumPartId2name(partId); + break; + case 0x44: + name = decPartId2name(partId); + break; + case 0x46: + name = fujitsuPartId2name(partId); + break; + case 0x48: + name = hisiPartId2name(partId); + break; + case 0x4e: + name = nvidiaPartId2name(partId); + break; + case 0x50: + name = apmPartId2name(partId); + break; + case 0x51: + name = qcomPartId2name(partId); + break; + case 0x53: + name = samsungPartId2name(partId); + break; + case 0x56: + name = marvellPartId2name(partId); + break; + case 0x61: + if (partId == 0) { + // https://github.com/Dr-Noob/cpufetch/issues/213#issuecomment-1927782105 + ffStrbufSetStatic(&cpu->name, "Virtualized Apple Silicon"); + ffStrbufGetlineRestore(&line, &len, cpuinfo); + return; + } + name = applePartId2name(partId); + break; + case 0x66: + name = faradayPartId2name(partId); + break; + case 0x69: + name = intelPartId2name(partId); + break; + case 0x6d: + name = msPartId2name(partId); + break; + case 0x70: + name = ftPartId2name(partId); + break; + case 0xc0: + name = amperePartId2name(partId); + break; + } + if (lastPartId != partId) { + if (lastPartId != UINT32_MAX) { + if (num > 1) { + ffStrbufAppendF(&cpu->name, "*%u", num); + } + ffStrbufAppendS(&cpu->name, " + "); + } + if (name) { + ffStrbufAppendS(&cpu->name, name); + } else if (partId) { + ffStrbufAppendF(&cpu->name, "%s-%X", cpu->vendor.chars, partId); + } else { + ffStrbufAppend(&cpu->name, &cpu->vendor); + } + lastPartId = partId; + num = 1; + } else { + ++num; + } + } + if (num > 1) { + ffStrbufAppendF(&cpu->name, "*%u", num); + } +} +#endif + +static const char* parseCpuInfo( + FFstrbuf* cpuinfo, + FFCPUResult* cpu, + FF_A_UNUSED FFstrbuf* physicalCoresBuffer, + FF_A_UNUSED FFstrbuf* cpuMHz, + FF_A_UNUSED FFstrbuf* cpuIsa, + FF_A_UNUSED FFstrbuf* cpuUarch, + FF_A_UNUSED FFstrbuf* cpuImplementer) { + char* line = NULL; + size_t len = 0; + + while (ffStrbufGetline(&line, &len, cpuinfo)) { + // Stop after reasonable information is acquired + if ((*line == '\0' || *line == '\n') && cpu->name.length > 0) { + ffStrbufGetlineRestore(&line, &len, cpuinfo); + break; + } + + (void) ( +// arm64 doesn't have "model name"; arm32 does have "model name" but its value is not useful. +// "Hardware" should always be used in this case +#if __x86_64__ || __i386__ + (cpu->name.length == 0 && ffParsePropLine(line, "model name :", &cpu->name)) || + (cpu->vendor.length == 0 && ffParsePropLine(line, "vendor_id :", &cpu->vendor)) || + (physicalCoresBuffer->length == 0 && ffParsePropLine(line, "cpu cores :", physicalCoresBuffer)) || + (cpuMHz->length == 0 && ffParsePropLine(line, "cpu MHz :", cpuMHz)) || +#elif __arm__ || __aarch64__ + (cpuImplementer->length == 0 && ffParsePropLine(line, "CPU implementer :", cpuImplementer)) || + (cpu->name.length == 0 && ffParsePropLine(line, "Hardware :", &cpu->name)) || // For Android devices +#elif __powerpc__ || __powerpc + (cpuMHz->length == 0 && ffParsePropLine(line, "clock :", cpuMHz)) || + (cpu->name.length == 0 && ffParsePropLine(line, "cpu :", &cpu->name)) || +#elif __mips__ || __mips + (cpu->name.length == 0 && ffParsePropLine(line, "cpu model :", &cpu->name)) || +#elif __loongarch__ + (cpu->name.length == 0 && ffParsePropLine(line, "Model Name :", &cpu->name)) || + (cpuMHz->length == 0 && ffParsePropLine(line, "CPU MHz :", cpuMHz)) || +#elif __riscv__ || __riscv + (cpuIsa->length == 0 && ffParsePropLine(line, "isa :", cpuIsa)) || + (cpuUarch->length == 0 && ffParsePropLine(line, "uarch :", cpuUarch)) || +#elif __s390x__ + (cpu->name.length == 0 && ffParsePropLine(line, "machine :", &cpu->name)) || + (cpu->vendor.length == 0 && ffParsePropLine(line, "vendor_id :", &cpu->vendor)) || + (cpuMHz->length == 0 && ffParsePropLine(line, "cpu MHz static :", cpuMHz)) || +#elif __ia64__ + (cpu->name.length == 0 && ffParsePropLine(line, "model name :", &cpu->name)) || + (cpu->vendor.length == 0 && ffParsePropLine(line, "vendor :", &cpu->vendor)) || + (cpuMHz->length == 0 && ffParsePropLine(line, "cpu MHz :", cpuMHz)) || +#elif __hppa__ + (cpu->name.length == 0 && ffParsePropLine(line, "cpu :", &cpu->name)) || +#elif __sh__ + (cpu->name.length == 0 && ffParsePropLine(line, "cpu type :", &cpu->name)) || +#else + (cpu->name.length == 0 && ffParsePropLine(line, "model name :", &cpu->name)) || + (cpu->name.length == 0 && ffParsePropLine(line, "model :", &cpu->name)) || + (cpu->name.length == 0 && ffParsePropLine(line, "cpu model :", &cpu->name)) || + (cpu->name.length == 0 && ffParsePropLine(line, "hardware :", &cpu->name)) || + (cpu->name.length == 0 && ffParsePropLine(line, "processor :", &cpu->name)) || +#endif + + false); + } + + return NULL; +} + +static uint32_t getFrequency(FFstrbuf* basePath, const char* cpuinfoFileName, const char* scalingFileName, FFstrbuf* buffer) { + uint32_t baseLen = basePath->length; + ffStrbufAppendS(basePath, cpuinfoFileName); + bool ok = ffReadFileBuffer(basePath->chars, buffer); + ffStrbufSubstrBefore(basePath, baseLen); + if (ok) { + return (uint32_t) (ffStrbufToUInt(buffer, 0) / 1000); + } + + if (scalingFileName) { + ffStrbufAppendS(basePath, scalingFileName); + ok = ffReadFileBuffer(basePath->chars, buffer); + ffStrbufSubstrBefore(basePath, baseLen); + if (ok) { + return (uint32_t) (ffStrbufToUInt(buffer, 0) / 1000); + } + } + + return 0; +} + +static uint8_t getNumCores(FFstrbuf* basePath, FFstrbuf* buffer) { + uint32_t baseLen = basePath->length; + ffStrbufAppendS(basePath, "/affected_cpus"); + bool ok = ffReadFileBuffer(basePath->chars, buffer); + ffStrbufSubstrBefore(basePath, baseLen); + if (ok) { + return (uint8_t) (ffStrbufCountC(buffer, ' ') + 1); + } + + ffStrbufAppendS(basePath, "/related_cpus"); + ok = ffReadFileBuffer(basePath->chars, buffer); + ffStrbufSubstrBefore(basePath, baseLen); + if (ok) { + return (uint8_t) (ffStrbufCountC(buffer, ' ') + 1); + } + + return 0; +} + +static bool detectFrequency(FFCPUResult* cpu, const FFCPUOptions* options) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateS("/sys/devices/system/cpu/cpufreq/"); + FF_AUTO_CLOSE_DIR DIR* dir = opendir(path.chars); + if (!dir) { + return false; + } + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + uint32_t baseLen = path.length; + + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) { + if (ffStrStartsWith(entry->d_name, "policy") && ffCharIsDigit(entry->d_name[strlen("policy")])) { + ffStrbufAppendS(&path, entry->d_name); + + uint32_t fmax = getFrequency(&path, "/cpuinfo_max_freq", "/scaling_max_freq", &buffer); + if (fmax == 0) { + continue; + } + + if (cpu->frequencyMax >= fmax) { + if (!options->showPeCoreCount) { + ffStrbufSubstrBefore(&path, baseLen); + continue; + } + } else { + cpu->frequencyMax = fmax; + } + + uint32_t fbase = getFrequency(&path, "/base_frequency", NULL, &buffer); + if (fbase > 0) { + cpu->frequencyBase = cpu->frequencyBase > fbase ? cpu->frequencyBase : fbase; + } + + if (options->showPeCoreCount) { + uint32_t freq = fbase == 0 ? fmax : fbase; // seems base frequencies are more stable + uint32_t ifreq = 0; + while (cpu->coreTypes[ifreq].freq != freq && cpu->coreTypes[ifreq].freq > 0) { + ++ifreq; + } + if (cpu->coreTypes[ifreq].freq == 0) { + cpu->coreTypes[ifreq].freq = freq; + } + cpu->coreTypes[ifreq].count += getNumCores(&path, &buffer); + } + ffStrbufSubstrBefore(&path, baseLen); + } + } + return true; +} + +#if __i386__ || __x86_64__ + +FF_A_UNUSED static uint16_t getPackageCount(FFstrbuf* cpuinfo) { + const char* p = cpuinfo->chars; + uint64_t low = 0, high = 0; + + while ((p = memmem(p, cpuinfo->length - (uint32_t) (p - cpuinfo->chars), "\nphysical id\t:", strlen("\nphysical id\t:")))) { + p += strlen("\nphysical id\t:"); + char* pend; + unsigned long long id = strtoul(p, &pend, 10); + if (__builtin_expect(id > 64, false)) { // Do 129-socket boards exist? + high |= 1ULL << (id - 64); + } else { + low |= 1ULL << id; + } + p = pend; + } + + return (uint16_t) (__builtin_popcountll(low) + __builtin_popcountll(high)); +} + +FF_A_UNUSED static const char* detectCPUX86(const FFCPUOptions* options, FFCPUResult* cpu) { + FF_STRBUF_AUTO_DESTROY cpuinfo = ffStrbufCreateA(PROC_FILE_BUFFSIZ); + if (!ffReadFileBuffer(FF_CPUINFO_PATH, &cpuinfo) || cpuinfo.length == 0) { + return "ffReadFileBuffer(\"" FF_CPUINFO_PATH "\") failed"; + } + + FF_STRBUF_AUTO_DESTROY physicalCoresBuffer = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY cpuMHz = ffStrbufCreate(); + const char* error = parseCpuInfo(&cpuinfo, cpu, &physicalCoresBuffer, &cpuMHz, NULL, NULL, NULL); + if (error) { + return error; + } + + cpu->coresLogical = (uint16_t) get_nprocs_conf(); + cpu->coresOnline = (uint16_t) get_nprocs(); + cpu->packages = getPackageCount(&cpuinfo); + cpu->coresPhysical = (uint16_t) ffStrbufToUInt(&physicalCoresBuffer, 0); // physical cores in single package + if (cpu->coresPhysical > 0 && cpu->packages > 1) { + cpu->coresPhysical *= cpu->packages; + } + + // Ref https://github.com/fastfetch-cli/fastfetch/issues/1194#issuecomment-2295058252 + ffCPUDetectByCpuid(cpu); + if (!detectFrequency(cpu, options) || cpu->frequencyBase == 0) { + cpu->frequencyBase = (uint32_t) ffStrbufToUInt(&cpuMHz, 0); + } + + detectNumaNodes(cpu); + + return NULL; +} + +#else + +static const char* detectPhysicalCores(FFCPUResult* cpu) { + int dfd = open("/sys/devices/system/cpu/", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (dfd < 0) { + return "open(\"/sys/devices/system/cpu/\") failed"; + } + + FF_AUTO_CLOSE_DIR DIR* dir = fdopendir(dfd); + if (!dir) { + return "fdopendir(dfd) failed"; + } + + uint64_t pkgLow = 0, pkgHigh = 0; + + struct dirent* entry; + FF_LIST_AUTO_DESTROY cpuList = ffListCreate(); + while ((entry = readdir(dir)) != NULL) { + if (entry->d_type != DT_DIR || !ffStrStartsWith(entry->d_name, "cpu") || !ffCharIsDigit(entry->d_name[strlen("cpu")])) { + continue; + } + + FF_AUTO_CLOSE_FD int cpuxfd = openat(dirfd(dir), entry->d_name, O_RDONLY | O_DIRECTORY); + if (cpuxfd < 0) { + continue; + } + + char buf[128]; + + // Check if the directory contains a file named "topology/physical_package_id" + // that lists the physical package id of the CPU. + + ssize_t len = ffReadFileDataRelative(cpuxfd, "topology/physical_package_id", sizeof(buf) - 1, buf); + if (len > 0) { + buf[len] = '\0'; + unsigned long long id = strtoul(buf, NULL, 10); + if (__builtin_expect(id > 64, false)) { // Do 129-socket boards exist? + pkgHigh |= 1ULL << (id - 64); + } else { + pkgLow |= 1ULL << id; + } + } + + // Check if the directory contains a file named "topology/core_cpus_list" + // that lists the physical cores in the package. + + len = ffReadFileDataRelative(cpuxfd, "topology/core_cpus_list", sizeof(buf) - 1, buf); + if (len > 0) { + buf[len] = '\0'; // low-high or low + + for (const char* p = buf; *p;) { + char* pend; + uint32_t coreId = (uint32_t) strtoul(p, &pend, 10); + if (pend == p) { + break; + } + + bool found = false; + FF_LIST_FOR_EACH (uint32_t, id, cpuList) { + if (*id == coreId) { + // This core is already counted + found = true; + break; + } + } + if (!found) { + *FF_LIST_ADD(uint32_t, cpuList) = coreId; + } + + p = strchr(pend, ','); + if (!p) { + break; + } + ++p; + } + } + } + + cpu->coresPhysical = (uint16_t) cpuList.length; + cpu->packages = (uint16_t) (__builtin_popcountll(pkgLow) + __builtin_popcountll(pkgHigh)); + return NULL; +} + +FF_A_UNUSED static void parseIsa(FFstrbuf* cpuIsa) { + // Always use the last part of the ISA string. Ref: #590 #1204 + ffStrbufSubstrAfterLastC(cpuIsa, ' '); + + if (ffStrbufStartsWithS(cpuIsa, "rv")) { + // RISC-V ISA string example: "rv64imafdch_zicsr_zifencei". + // The _z parts are not important for CPU showcasing, so we remove them. + if (ffStrbufContainC(cpuIsa, '_')) { + ffStrbufSubstrBeforeFirstC(cpuIsa, '_'); + } + // Then we replace "imafd" with "g" since "g" is a shorthand. + if (ffStrbufContainS(cpuIsa, "imafd")) { + // Remove 4 of the 5 characters and replace the remaining one with "g". + ffStrbufRemoveSubstr(cpuIsa, 4, 8); + cpuIsa->chars[4] = 'g'; + } + // The final ISA output of the above example is "rv64gch". + } +} + +FF_A_UNUSED static void detectSocName(FFCPUResult* cpu) { + if (cpu->name.length > 0) { + return; + } + + // [x-vendor,x-model\0]*N + char content[512]; + ssize_t length = ffReadFileData("/sys/firmware/devicetree/base/compatible", ARRAY_SIZE(content), content); + if (length < 4) { + return; // v,m\0 + } + + if (content[length - 1] != '\0') { + return; // must end with \0 + } + + --length; + + char* vendor = NULL; + char* model = NULL; + + for (char* p; length > 0; length = p ? (ssize_t) (p - content) - 1 : 0) { + p = memrchr(content, '\0', (size_t) length); + + vendor = p /* first entry */ ? p + 1 : content; + + size_t partLen = (size_t) (length - (vendor - content)); + if (partLen < 3) { + continue; + } + + char* comma = memchr(vendor, ',', partLen); + if (!comma) { + continue; + } + + size_t vendorLen = (size_t) (comma - vendor); + if (vendorLen == 0) { + continue; + } + + model = comma + 1; + size_t modelLen = (size_t) (partLen - (size_t) (model - vendor)); + if (modelLen == 0) { + continue; + } + + if ((modelLen >= strlen("-platform") && ffStrEndsWith(model, "-platform")) || + (modelLen >= strlen("-soc") && ffStrEndsWith(model, "-soc"))) { + continue; + } + + *comma = '\0'; + break; + } + + if (!length) { + return; + } + + if (false) { + } + #if __aarch64__ + else if (ffStrEquals(vendor, "apple")) { + // https://elixir.bootlin.com/linux/v6.11/source/arch/arm64/boot/dts/apple + if (model[0] == 't') { + uint32_t deviceId = (uint32_t) strtoul(model + 1, NULL, 10); + ffStrbufSetStatic(&cpu->name, ffCPUAppleCodeToName(deviceId)); + + if (!cpu->name.length) { + ffStrbufSetS(&cpu->name, "Apple Silicon "); + ffStrbufAppendS(&cpu->name, model); + } + } else { + ffStrbufSetS(&cpu->name, model); + } + + ffStrbufSetStatic(&cpu->vendor, "Apple"); + } + #endif + else if (ffStrEquals(vendor, "qcom")) { + // https://elixir.bootlin.com/linux/v6.11/source/arch/arm64/boot/dts/qcom + if (ffStrStartsWith(model, "x")) { + ffStrbufSetS(&cpu->name, "Qualcomm Snapdragon X Elite "); + for (const char* p = model + 1; *p; ++p) { + ffStrbufAppendC(&cpu->name, (char) toupper(*p)); + } + } else if (ffStrStartsWith(model, "sc")) { + const char* code = model + 2; + uint32_t deviceId = (uint32_t) strtoul(code, NULL, 10); + ffStrbufSetStatic(&cpu->name, ffCPUQualcommCodeToName(deviceId)); + if (!cpu->name.length) { + ffStrbufAppendS(&cpu->name, "Qualcomm Snapdragon SC"); + ffStrbufAppendS(&cpu->name, code); + } + } else { + ffStrbufSetS(&cpu->name, model); + } + + ffStrbufSetStatic(&cpu->vendor, "Qualcomm"); + } else if (ffStrEquals(vendor, "brcm")) { + // Raspberry Pi + ffStrbufSetStatic(&cpu->vendor, "Broadcom"); + for (const char* p = model; *p; ++p) { + ffStrbufAppendC(&cpu->name, (char) toupper(*p)); + } + } else if (ffStrEquals(vendor, "thead")) { + // Lichee Pi? + ffStrbufSetStatic(&cpu->vendor, "T-Head"); + for (const char* p = model; *p; ++p) { + ffStrbufAppendC(&cpu->name, (char) toupper(*p)); + } + } else { + ffStrbufSetS(&cpu->name, model); + ffStrbufSetS(&cpu->vendor, vendor); + cpu->vendor.chars[0] = (char) toupper(vendor[0]); + } +} + + #ifdef __loongarch__ +FF_A_UNUSED static uint16_t getLoongarchPropCount(FFstrbuf* cpuinfo, const char* key) { + const char* p = cpuinfo->chars; + uint64_t low = 0, high = 0; + uint32_t keylen = (uint32_t) strlen(key); + + while ((p = memmem(p, cpuinfo->length - (uint32_t) (p - cpuinfo->chars), key, keylen))) { + p += keylen; + char* pend; + unsigned long id = strtoul(p, &pend, 10); + if (__builtin_expect(id > 64, false)) { + high |= 1UL << (id - 64); + } else { + low |= 1UL << id; + } + p = pend; + } + + return (uint16_t) (__builtin_popcountll(low) + __builtin_popcountll(high)); +} + #endif + +FF_A_UNUSED static const char* detectCPUOthers(const FFCPUOptions* options, FFCPUResult* cpu) { + cpu->coresLogical = (uint16_t) get_nprocs_conf(); + cpu->coresOnline = (uint16_t) get_nprocs(); + + #if __ANDROID__ + detectAndroid(cpu); + #elif !__powerpc__ && !__powerpc + detectSocName(cpu); + #endif + + detectFrequency(cpu, options); + + if (cpu->name.length == 0) { + FF_STRBUF_AUTO_DESTROY cpuinfo = ffStrbufCreateA(PROC_FILE_BUFFSIZ); + if (!ffReadFileBuffer(FF_CPUINFO_PATH, &cpuinfo) || cpuinfo.length == 0) { + return "ffReadFileBuffer(\"" FF_CPUINFO_PATH "\") failed"; + } + + FF_STRBUF_AUTO_DESTROY cpuMHz = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY cpuIsa = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY cpuUarch = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY cpuImplementerStr = ffStrbufCreate(); + + const char* error = parseCpuInfo(&cpuinfo, cpu, NULL, &cpuMHz, &cpuIsa, &cpuUarch, &cpuImplementerStr); + if (error) { + return error; + } + + if (cpu->frequencyBase == 0) { + cpu->frequencyBase = (uint32_t) ffStrbufToUInt(&cpuMHz, 0); + } + + #if __arm__ || __aarch64__ + uint32_t cpuImplementer = (uint32_t) strtoul(cpuImplementerStr.chars, NULL, 16); + ffStrbufSetStatic(&cpu->vendor, hwImplId2Vendor(cpuImplementer)); + + if (cpu->name.length == 0) { + detectArmName(&cpuinfo, cpu, cpuImplementer); + } + #elif __riscv__ || __riscv + if (cpu->name.length == 0) { + if (cpuUarch.length > 0) { + if (cpu->name.length > 0) { + ffStrbufAppendC(&cpu->name, ' '); + } + ffStrbufAppend(&cpu->name, &cpuUarch); + } + + if (cpuIsa.length > 0) { + parseIsa(&cpuIsa); + if (cpu->name.length > 0) { + ffStrbufAppendC(&cpu->name, ' '); + } + ffStrbufAppend(&cpu->name, &cpuIsa); + } + } + #elif __loongarch__ + cpu->packages = getLoongarchPropCount(&cpuinfo, "\npackage\t\t\t:"); + cpu->coresPhysical = getLoongarchPropCount(&cpuinfo, "\ncore\t\t\t:"); + if (cpu->packages > 1) { + cpu->coresPhysical *= cpu->packages; + } + #elif __s390x__ + if (cpu->name.length) { + ffStrbufPrependS(&cpu->name, "Machine "); + } + #endif + } + + if (cpu->coresPhysical == 0) { + detectPhysicalCores(cpu); + } + + ffCPUDetectByCpuid(cpu); + detectNumaNodes(cpu); + + return NULL; +} +#endif + +const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu) { + cpu->temperature = options->temp ? detectCPUTemp(options) : FF_CPU_TEMP_UNSET; + +#if __x86_64__ || __i386__ + return detectCPUX86(options, cpu); +#else + return detectCPUOthers(options, cpu); +#endif +} diff --git a/src/detection/cpu/cpu_nbsd.c b/src/detection/cpu/cpu_nbsd.c new file mode 100644 index 0000000..9c88c0e --- /dev/null +++ b/src/detection/cpu/cpu_nbsd.c @@ -0,0 +1,109 @@ +#include "cpu.h" +#include "common/sysctl.h" +#include "common/io.h" + +#include <sys/envsys.h> +#include <prop/proplib.h> +#include <paths.h> +#include <time.h> +#include <unistd.h> +#include <fcntl.h> + +static void freePropDict(prop_dictionary_t* pdict) { + assert(pdict != NULL); + if (*pdict == NULL) { + return; + } + prop_object_release(*pdict); +} + +static const char* detectCpuTemp(const FFCPUOptions* options, double* current) { + FF_AUTO_CLOSE_FD int fd = open(_PATH_SYSMON, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return "open(_PATH_SYSMON, O_RDONLY | O_CLOEXEC) failed"; + } + + FF_A_CLEANUP(freePropDict) prop_dictionary_t root = NULL; + if (prop_dictionary_recv_ioctl(fd, ENVSYS_GETDICTIONARY, &root) < 0) { + return "prop_dictionary_recv_ioctl(ENVSYS_GETDICTIONARY) failed"; + } + + prop_array_t array; + + if (options->tempSensor.length > 0) { + array = prop_dictionary_get(root, options->tempSensor.chars); + if (!array) { + return "No temp data found in specified sensor"; + } + } else { + array = prop_dictionary_get(root, "coretemp0"); + if (!array) { + array = prop_dictionary_get(root, "amdzentemp0"); + } + if (!array) { + array = prop_dictionary_get(root, "viac7temp0"); + } + if (!array) { + array = prop_dictionary_get(root, "acpitz0"); // Thermal Zones + } + if (!array) { + return "No temp data found in root dictionary"; + } + } + + if (prop_array_count(array) != 2) { + return "Unexpected `xtemp0` data"; + } + + prop_dictionary_t dict = prop_array_get(array, 0); + if (prop_object_type(dict) != PROP_TYPE_DICTIONARY) { + return "Unexpected `xtemp0[0]`"; + } + + int temp = 0; // in µK + if (!prop_dictionary_get_int(dict, "cur-value", &temp)) { + return "Failed to get temperature"; + } + + *current = temp / 1e6 - 273.15; + + return NULL; +} + +const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu) { + if (ffSysctlGetString("machdep.cpu_brand", &cpu->name) != NULL && + ffSysctlGetString("machdep.dmi.processor-version", &cpu->name) != NULL && + ffSysctlGetString("hw.cpu0.name", &cpu->name) != NULL && + ffSysctlGetString("hw.model", &cpu->name) != NULL) { + ffStrbufSetS(&cpu->name, "Unknown CPU"); + } + + if (ffSysctlGetString("machdep.dmi.processor-vendor", &cpu->vendor) == NULL) { + ffStrbufTrimRightSpace(&cpu->vendor); + } + + cpu->coresPhysical = (uint16_t) ffSysctlGetInt("hw.ncpu", 1); + cpu->coresLogical = cpu->coresPhysical; + cpu->coresOnline = (uint16_t) ffSysctlGetInt("hw.ncpuonline", cpu->coresLogical); + + ffCPUDetectByCpuid(cpu); + + uint32_t freq = (uint32_t) ffSysctlGetInt("machdep.cpu.frequency.target", 0); + if (freq == 0) { + freq = (uint32_t) (ffSysctlGetInt64("hw.cpu0.clock_frequency", 0) / 1000000); + } + if (freq == 0) { + freq = (uint32_t) ffSysctlGetInt("machdep.dmi.processor-frequency", 0); + } + if (freq > cpu->frequencyBase) { + cpu->frequencyBase = freq; + } + + cpu->temperature = FF_CPU_TEMP_UNSET; + + if (options->temp) { + detectCpuTemp(options, &cpu->temperature); + } + + return NULL; +} diff --git a/src/detection/cpu/cpu_nosupport.c b/src/detection/cpu/cpu_nosupport.c new file mode 100644 index 0000000..309883d --- /dev/null +++ b/src/detection/cpu/cpu_nosupport.c @@ -0,0 +1,5 @@ +#include "cpu.h" + +const char* ffDetectCPUImpl(FF_A_UNUSED const FFCPUOptions* options, FF_A_UNUSED FFCPUResult* cpu) { + return "Not supported on this platform"; +} diff --git a/src/detection/cpu/cpu_obsd.c b/src/detection/cpu/cpu_obsd.c new file mode 100644 index 0000000..e5b8d2f --- /dev/null +++ b/src/detection/cpu/cpu_obsd.c @@ -0,0 +1,78 @@ +#include "cpu.h" +#include "common/sysctl.h" +#include "common/strutil.h" + +#include <errno.h> +#include <sys/time.h> +#include <sys/sensors.h> + +static const char* detectCPUTemp(const FFCPUOptions* options, FFCPUResult* cpu) { + int mib[5] = { CTL_HW, HW_SENSORS, 0, SENSOR_TEMP, 0 }; + + for (mib[2] = 0; mib[2] < 1024; mib[2]++) { + struct sensordev sensordev; + size_t sdlen = sizeof(struct sensordev); + if (sysctl(mib, 3, &sensordev, &sdlen, NULL, 0) < 0) { + if (errno == ENOENT) { + break; + } + if (errno == ENXIO) { + continue; + } + return "sysctl(sensordev) failed"; + } + + if (options->tempSensor.length > 0) { + if (!ffStrbufEqualS(&options->tempSensor, sensordev.xname)) { + continue; + } + } else { + if (!ffStrStartsWith(sensordev.xname, "cpu")) { + continue; + } + } + + for (mib[4] = 0; mib[4] < sensordev.maxnumt[SENSOR_TEMP]; mib[4]++) { + struct sensor sensor; + size_t slen = sizeof(struct sensor); + if (sysctl(mib, 5, &sensor, &slen, NULL, 0) < 0) { + if (errno != ENOENT) { + return "sysctl(sensor) failed"; + } + continue; + } + if (sensor.flags & SENSOR_FINVALID) { + continue; + } + + cpu->temperature = (double) (sensor.value - 273150000) / 1E6; + return NULL; + } + } + + return "No sensor for CPU temp found"; +} + +const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu) { + if (ffSysctlGetString(CTL_HW, HW_MODEL, &cpu->name)) { + return "sysctl(hw.model) failed"; + } + + cpu->coresPhysical = (uint16_t) ffSysctlGetInt(CTL_HW, HW_NCPU, 1); + cpu->coresLogical = cpu->coresPhysical; + cpu->coresOnline = (uint16_t) ffSysctlGetInt(CTL_HW, HW_NCPUONLINE, cpu->coresLogical); + + ffCPUDetectByCpuid(cpu); + + uint32_t cpuspeed = (uint32_t) ffSysctlGetInt(CTL_HW, HW_CPUSPEED, 0); + if (cpuspeed > cpu->frequencyBase) { + cpu->frequencyBase = cpuspeed; + } + + cpu->temperature = FF_CPU_TEMP_UNSET; + if (options->temp) { + detectCPUTemp(options, cpu); + } + + return NULL; +} diff --git a/src/detection/cpu/cpu_sunos.c b/src/detection/cpu/cpu_sunos.c new file mode 100644 index 0000000..9b1cab5 --- /dev/null +++ b/src/detection/cpu/cpu_sunos.c @@ -0,0 +1,154 @@ +#include "cpu.h" +#include "common/processing.h" +#include "common/strutil.h" +#include <kstat.h> + +static const char* detectCPUTempByKstat(const FFCPUOptions* options, kstat_ctl_t* kc, FFCPUResult* cpu) { + const char* possibleModules[] = { "temperature", "cpu_temp", "acpi_thermal", NULL }; + + if (options->tempSensor.length > 0) { + possibleModules[0] = options->tempSensor.chars; + possibleModules[1] = NULL; + } + + for (int i = 0; possibleModules[i] != NULL; i++) { + kstat_t* ks = kstat_lookup(kc, possibleModules[i], -1, NULL); + if (ks && kstat_read(kc, ks, NULL) >= 0) { + kstat_named_t* kn = kstat_data_lookup(ks, "temperature"); + if (kn) { + switch (kn->data_type) { + case KSTAT_DATA_INT32: + cpu->temperature = (float) kn->value.i32; + return NULL; + case KSTAT_DATA_UINT32: + cpu->temperature = (float) kn->value.ui32; + return NULL; + case KSTAT_DATA_FLOAT: + cpu->temperature = kn->value.f; + return NULL; + } + } + } + } + + return "Failed to find CPU temperature using kstat"; +} + +static const char* detectCPUTempByIpmiTool(FFCPUResult* cpu) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + const char* error = ffProcessAppendStdOut(&buffer, (char* const[]) { "ipmitool", "-c", "sdr", "list", NULL }); + + if (error) { + return error; + } + + char* line = NULL; + size_t len = 0; + while (ffStrbufGetline(&line, &len, &buffer)) { + if (sscanf(line, "CPU%*d Temp,%lf,degrees C,ok", &cpu->temperature) == 1) { + return NULL; + } + } + + return "ipmitool sdr list failed to find CPU temperature"; +} + +static inline void kstatFreeWrap(kstat_ctl_t** pkc) { + assert(pkc); + if (*pkc) { + kstat_close(*pkc); + } +} + +static inline uint16_t countTypeId(kstat_ctl_t* kc, const char* type) { + uint64_t low = 0, high = 0; + for (kstat_t* ksp = kc->kc_chain; ksp; ksp = ksp->ks_next) { + if (ffStrStartsWith(ksp->ks_module, "cpu_info")) { + if (kstat_read(kc, ksp, NULL) < 0) { + continue; + } + + kstat_named_t* stat = kstat_data_lookup(ksp, type); + if (!stat) { + continue; + } + + uint32_t id = 0; + switch (stat->data_type) { +#ifdef _INT64_TYPE + case KSTAT_DATA_INT64: + case KSTAT_DATA_UINT64: + id = (uint32_t) stat->value.ui64; + break; +#endif + case KSTAT_DATA_INT32: + case KSTAT_DATA_UINT32: + id = stat->value.ui32; + break; + default: + continue; + } + if (__builtin_expect(id > 64, false)) { + high |= 1ULL << (id - 64); + } else { + low |= 1ULL << id; + } + } + } + return (uint16_t) (__builtin_popcountll(low) + __builtin_popcountll(high)); +} + +const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu) { + FF_A_CLEANUP(kstatFreeWrap) kstat_ctl_t* kc = kstat_open(); + if (!kc) { + return "kstat_open() failed"; + } + + kstat_t* ks = kstat_lookup(kc, "cpu_info", -1, NULL); + if (!ks) { + return "kstat_lookup() failed"; + } + + if (kstat_read(kc, ks, NULL) < 0) { + return "kstat_read() failed"; + } + + { + kstat_named_t* kn = kstat_data_lookup(ks, "brand"); + if (kn) { + ffStrbufSetNS(&cpu->name, KSTAT_NAMED_STR_BUFLEN(kn) - 1, KSTAT_NAMED_STR_PTR(kn)); + } + } + { + kstat_named_t* kn = kstat_data_lookup(ks, "vendor_id"); + if (kn) { + ffStrbufSetNS(&cpu->vendor, KSTAT_NAMED_STR_BUFLEN(kn) - 1, KSTAT_NAMED_STR_PTR(kn)); + } + } + ffCPUDetectByCpuid(cpu); + { + kstat_named_t* kn = kstat_data_lookup(ks, "clock_MHz"); + if (kn && kn->value.ui32 > cpu->frequencyBase) { + cpu->frequencyBase = kn->value.ui32; + } + } + + ks = kstat_lookup(kc, "unix", -1, "system_misc"); + if (ks && kstat_read(kc, ks, NULL) >= 0) { + kstat_named_t* kn = kstat_data_lookup(ks, "ncpus"); + if (kn) { + cpu->coresLogical = cpu->coresOnline = (uint16_t) kn->value.ui32; + } + } + + cpu->packages = countTypeId(kc, "chip_id"); + cpu->coresPhysical = countTypeId(kc, "core_id"); + + if (options->temp) { + if (detectCPUTempByKstat(options, kc, cpu) != NULL) { + detectCPUTempByIpmiTool(cpu); + } + } + + return NULL; +} diff --git a/src/detection/cpu/cpu_windows.c b/src/detection/cpu/cpu_windows.c new file mode 100644 index 0000000..86edce9 --- /dev/null +++ b/src/detection/cpu/cpu_windows.c @@ -0,0 +1,320 @@ +#include "cpu.h" +#include "common/windows/registry.h" +#include "common/windows/nt.h" +#include "common/mallocHelper.h" +#include "common/smbios.h" + +#include <windows.h> +#include "common/windows/perflib_.h" +#include "common/windows/nt.h" +#include <wchar.h> + +static inline void ffPerfCloseQueryHandle(HANDLE* phQuery) { + if (*phQuery != NULL) { + PerfCloseQueryHandle(*phQuery); + *phQuery = NULL; + } +} + +const char* detectThermalTemp(const FFCPUOptions* options, double* result) { + struct FFPerfQuerySpec { + PERF_COUNTER_IDENTIFIER Identifier; + WCHAR Name[16]; + } querySpec = { + .Identifier = { + // Thermal Zone Information + // HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\_V2Providers\{383487a6-3676-4870-a4e7-d45b30c35629}\{52bc5412-dac2-449c-8bc2-96443888fe6b} + .CounterSetGuid = { 0x52bc5412, 0xdac2, 0x449c, { 0x8b, 0xc2, 0x96, 0x44, 0x38, 0x88, 0xfe, 0x6b } }, + .Size = sizeof(querySpec), + .CounterId = PERF_WILDCARD_COUNTER, + .InstanceId = PERF_WILDCARD_COUNTER, + }, + .Name = L"\\_TZ.CPUZ", // The standard(?) instance name for CPU temperature in the thermal provider + }; + + if (options->tempSensor.length > 0) { + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(querySpec.Name, (ULONG) sizeof(querySpec.Name), NULL, options->tempSensor.chars, (ULONG) options->tempSensor.length + 1))) { + return "Invalid temp sensor string"; + } + } + + DWORD dataSize = 0; + if (PerfEnumerateCounterSetInstances(NULL, &querySpec.Identifier.CounterSetGuid, NULL, 0, &dataSize) != ERROR_NOT_ENOUGH_MEMORY) { + return "PerfEnumerateCounterSetInstances() failed"; + } + + if (dataSize <= sizeof(PERF_INSTANCE_HEADER)) { + return "No `Thermal Zone Information` instances found"; + } + + { + FF_AUTO_FREE PERF_INSTANCE_HEADER* const pHead = malloc(dataSize); + if (PerfEnumerateCounterSetInstances(NULL, &querySpec.Identifier.CounterSetGuid, pHead, dataSize, &dataSize) != ERROR_SUCCESS) { + return "PerfEnumerateCounterSetInstances() failed to get instance headers"; + } + + PERF_INSTANCE_HEADER* pInstanceHeader = pHead; + while (1) { + const wchar_t* instanceName = (const wchar_t*) ((BYTE*) pInstanceHeader + sizeof(*pInstanceHeader)); + if (wcscmp(instanceName, querySpec.Name) == 0) { + break; + } + + dataSize -= pInstanceHeader->Size; + if (dataSize == 0) { + break; + } + pInstanceHeader = (PERF_INSTANCE_HEADER*) ((BYTE*) pInstanceHeader + pInstanceHeader->Size); + } + + if (dataSize == 0) { + if (options->tempSensor.length > 0) { + return "Unable to find CPU sensor"; + } + + const wchar_t* instanceName = (const wchar_t*) ((BYTE*) pHead + sizeof(*pHead)); + wcscpy(querySpec.Name, instanceName); // Use the first instance name if the specific one is not found + } + } + + FF_A_CLEANUP(ffPerfCloseQueryHandle) + HANDLE hQuery = NULL; + + if (PerfOpenQueryHandle(NULL, &hQuery) != ERROR_SUCCESS) { + return "PerfOpenQueryHandle() failed"; + } + + if (PerfAddCounters(hQuery, &querySpec.Identifier, sizeof(querySpec)) != ERROR_SUCCESS) { + return "PerfAddCounters() failed"; + } + + if (querySpec.Identifier.Status != ERROR_SUCCESS) { + return "PerfAddCounters() reports invalid identifier"; + } + + if (PerfQueryCounterData(hQuery, NULL, 0, &dataSize) != ERROR_NOT_ENOUGH_MEMORY) { + return "PerfQueryCounterData(NULL) failed"; + } + + if (dataSize <= sizeof(PERF_DATA_HEADER) + sizeof(PERF_COUNTER_HEADER)) { // PERF_ERROR_RETURN, should not happen + return "instance doesn't exist"; + } + + FF_AUTO_FREE PERF_DATA_HEADER* const pDataHeader = malloc(dataSize); + + if (PerfQueryCounterData(hQuery, pDataHeader, dataSize, &dataSize) != ERROR_SUCCESS) { + return "PerfQueryCounterData(pDataHeader) failed"; + } + + PERF_COUNTER_HEADER* pCounterHeader = (PERF_COUNTER_HEADER*) (pDataHeader + 1); + if (pCounterHeader->dwType != PERF_MULTIPLE_COUNTERS) { + return "Invalid counter type"; + } + + PERF_MULTI_COUNTERS* pMultiCounters = (PERF_MULTI_COUNTERS*) (pCounterHeader + 1); + PERF_COUNTER_DATA* pCounterData = (PERF_COUNTER_DATA*) ((BYTE*) pMultiCounters + pMultiCounters->dwSize); + + for (ULONG iCounter = 0; iCounter != pMultiCounters->dwCounters; iCounter++) { + if (pCounterData->dwDataSize == sizeof(int32_t)) { + DWORD* pCounterIds = (DWORD*) (pMultiCounters + 1); + int32_t value = *(int32_t*) (pCounterData + 1); + if (value == 0) { + return "Temperature data is zero"; + } + + switch (pCounterIds[iCounter]) { + case 0: // Temperature + *result = value - 273; + break; + case 3: // High Precision Temperature + *result = value / 10.0 - 273; + break; + } + } + + pCounterData = (PERF_COUNTER_DATA*) ((BYTE*) pCounterData + pCounterData->dwSize); + } + + return NULL; +} + +// 7.5 +typedef struct FFSmbiosProcessorInfo { + FFSmbiosHeader Header; + + uint8_t SocketDesignation; // string + uint8_t ProcessorType; // enum + uint8_t ProcessorFamily; // enum + uint8_t ProcessorManufacturer; // string + uint64_t ProcessorID; // varies + uint8_t ProcessorVersion; // string + uint8_t Voltage; // varies + uint16_t ExternalClock; // varies + uint16_t MaxSpeed; // varies + uint16_t CurrentSpeed; // varies + uint8_t Status; // varies + uint8_t ProcessorUpgrade; // enum + + // 2.1+ + uint16_t L1CacheHandle; // varies + uint16_t L2CacheHandle; // varies + uint16_t L3CacheHandle; // varies + + // 2.3+ + uint8_t SerialNumber; // string + uint8_t AssertTag; // string + uint8_t PartNumber; // string + + // 2.5+ + uint8_t CoreCount; // varies + uint8_t CoreEnabled; // varies + uint8_t ThreadCount; // varies + uint16_t ProcessorCharacteristics; // bit field + + // 2.6+ + uint16_t ProcessorFamily2; // enum + + // 3.0+ + uint16_t CoreCount2; // varies + uint16_t CoreEnabled2; // varies + uint16_t ThreadCount2; // varies + + // 3.6+ + uint16_t ThreadEnabled; // varies +} FF_A_PACKED FFSmbiosProcessorInfo; + +static_assert(offsetof(FFSmbiosProcessorInfo, ThreadEnabled) == 0x30, + "FFSmbiosProcessorInfo: Wrong struct alignment"); + +static const char* detectMaxSpeedBySmbios(FFCPUResult* cpu) { + const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable(); + if (!smbiosTable) { + return "Failed to get SMBIOS data"; + } + + const FFSmbiosProcessorInfo* data = (const FFSmbiosProcessorInfo*) (*smbiosTable)[FF_SMBIOS_TYPE_PROCESSOR_INFO]; + + if (!data) { + return "Processor information is not found in SMBIOS data"; + } + + while (data->ProcessorType != 0x03 /*Central Processor*/ || (data->Status & 0b00000111) != 1 /*Enabled*/) { + data = (const FFSmbiosProcessorInfo*) ffSmbiosNextEntry(&data->Header); + if (data->Header.Type != FF_SMBIOS_TYPE_PROCESSOR_INFO) { + return "No active CPU is found in SMBIOS data"; + } + } + + uint32_t speed = data->MaxSpeed; + // Sometimes SMBIOS reports invalid value. We assume that max speed is small than 2x of base + if (speed < cpu->frequencyBase || speed > cpu->frequencyBase * 2) { + return "Possible invalid CPU max speed in SMBIOS data. See #800"; + } + + cpu->frequencyMax = speed; + + return NULL; +} + +static const char* detectNCores(FFCPUResult* cpu) { + LOGICAL_PROCESSOR_RELATIONSHIP lpr = RelationAll; + ULONG length = 0; + NtQuerySystemInformationEx(SystemLogicalProcessorAndGroupInformation, &lpr, sizeof(lpr), NULL, 0, &length); + if (length == 0) { + return "GetLogicalProcessorInformationEx(RelationAll, NULL, &length) failed"; + } + + SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX* FF_AUTO_FREE + pProcessorInfo = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) malloc(length); + + if (!NT_SUCCESS(NtQuerySystemInformationEx(SystemLogicalProcessorAndGroupInformation, &lpr, sizeof(lpr), pProcessorInfo, length, &length))) { + return "GetLogicalProcessorInformationEx(RelationAll, pProcessorInfo, &length) failed"; + } + + for ( + SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX* ptr = pProcessorInfo; + (uint8_t*) ptr < ((uint8_t*) pProcessorInfo) + length; + ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) (((uint8_t*) ptr) + ptr->Size)) { + if (ptr->Relationship == RelationGroup) { + for (uint32_t index = 0; index < ptr->Group.ActiveGroupCount; ++index) { + cpu->coresOnline += ptr->Group.GroupInfo[index].ActiveProcessorCount; + cpu->coresLogical += ptr->Group.GroupInfo[index].MaximumProcessorCount; + } + } else if (ptr->Relationship == RelationProcessorCore) { + ++cpu->coresPhysical; + } else if (ptr->Relationship == RelationProcessorPackage) { + ++cpu->packages; + } else if (ptr->Relationship == RelationNumaNode) { + ++cpu->numaNodes; + } + } + + return NULL; +} + +static const char* detectByRegistry(FFCPUResult* cpu) { + FF_AUTO_CLOSE_FD HANDLE hKey = NULL; + if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", &hKey, NULL)) { + return "ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, L\"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0\", &hKey, NULL) failed"; + } + + if (ffRegReadValues(hKey, 3, (FFRegValueArg[]) { + FF_ARG(cpu->name, L"ProcessorNameString"), + FF_ARG(cpu->vendor, L"VendorIdentifier"), + FF_ARG(cpu->frequencyBase, L"~MHz"), + }, + NULL)) { + ffStrbufTrimRightSpace(&cpu->vendor); + } else { + return "ffRegReadValues() failed for CPU registry key"; + } + + return NULL; +} + +static const char* detectCoreTypes(FFCPUResult* cpu) { + FF_AUTO_FREE PROCESSOR_POWER_INFORMATION* pinfo = calloc(cpu->coresLogical, sizeof(PROCESSOR_POWER_INFORMATION)); + if (!NT_SUCCESS(NtPowerInformation(ProcessorInformation, NULL, 0, pinfo, (ULONG) sizeof(PROCESSOR_POWER_INFORMATION) * cpu->coresLogical))) { + return "NtPowerInformation(ProcessorInformation, NULL, 0, pinfo, size) failed"; + } + + for (uint32_t icore = 0; icore < cpu->coresLogical && pinfo[icore].MhzLimit; ++icore) { + uint32_t ifreq = 0; + while (cpu->coreTypes[ifreq].freq != pinfo[icore].MhzLimit && cpu->coreTypes[ifreq].freq > 0) { + ++ifreq; + } + if (cpu->coreTypes[ifreq].freq == 0) { + cpu->coreTypes[ifreq].freq = pinfo[icore].MhzLimit; + } + ++cpu->coreTypes[ifreq].count; + } + + if (cpu->frequencyBase == 0) { + cpu->frequencyBase = pinfo->MaxMhz; + } + return NULL; +} + +const char* ffDetectCPUImpl(const FFCPUOptions* options, FFCPUResult* cpu) { + detectNCores(cpu); + + const char* error = detectByRegistry(cpu); + if (error) { + return error; + } + + ffCPUDetectByCpuid(cpu); + if (options->showPeCoreCount) { + detectCoreTypes(cpu); + } + + if (cpu->frequencyMax == 0) { + detectMaxSpeedBySmbios(cpu); + } + + if (options->temp) { + detectThermalTemp(options, &cpu->temperature); + } + + return NULL; +} diff --git a/src/detection/cpucache/cpucache.h b/src/detection/cpucache/cpucache.h new file mode 100644 index 0000000..287154b --- /dev/null +++ b/src/detection/cpucache/cpucache.h @@ -0,0 +1,44 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/cpucache/option.h" + +typedef enum FF_A_PACKED FFCPUCacheType { + FF_CPU_CACHE_TYPE_UNIFIED = 0, + FF_CPU_CACHE_TYPE_INSTRUCTION = 1, + FF_CPU_CACHE_TYPE_DATA = 2, + FF_CPU_CACHE_TYPE_TRACE = 3, +} FFCPUCacheType; + +typedef struct FFCPUCache { + uint32_t size; + uint32_t num; + uint32_t lineSize; + FFCPUCacheType type; +} FFCPUCache; + +typedef struct FFCPUCacheResult { + FFlist caches[4]; // L1, L2, L3, L4(?) +} FFCPUCacheResult; + +const char* ffDetectCPUCache(FFCPUCacheResult* result); + +static inline FFCPUCache* ffCPUCacheAddItem(FFCPUCacheResult* result, uint32_t level, uint32_t size, uint32_t lineSize, FFCPUCacheType type) { + FFlist* cacheLevel = &result->caches[level - 1]; + + FF_LIST_FOR_EACH (FFCPUCache, item, *cacheLevel) { + if (item->type == type && item->size == size && item->lineSize == lineSize) { + item->num++; + return item; + } + } + + FFCPUCache* item = FF_LIST_ADD(FFCPUCache, *cacheLevel); + *item = (FFCPUCache) { + .size = size, + .num = 1, + .lineSize = lineSize, + .type = type, + }; + return item; +} diff --git a/src/detection/cpucache/cpucache_apple.c b/src/detection/cpucache/cpucache_apple.c new file mode 100644 index 0000000..3d1562c --- /dev/null +++ b/src/detection/cpucache/cpucache_apple.c @@ -0,0 +1,63 @@ +#include "cpucache.h" +#include "common/sysctl.h" +#include "common/strutil.h" + +const char* ffDetectCPUCache(FFCPUCacheResult* result) { + // https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_system_capabilities#3901385 + uint32_t nPerfLevels = (uint32_t) ffSysctlGetInt("hw.nperflevels", 0); + if (nPerfLevels <= 0) { + return "sysctl(hw.nperflevels) failed"; + } + + // macOS provides the global system cache line size + uint32_t lineSize = (uint32_t) ffSysctlGetInt64("hw.cachelinesize", 0); + + char sysctlKey[128] = "hw.perflevelN."; + char* pNum = sysctlKey + strlen("hw.perflevel"); + char* pSubkey = sysctlKey + strlen("hw.perflevelN."); + const size_t lenLeft = ARRAY_SIZE(sysctlKey) - strlen("hw.perflevelN."); + + for (uint32_t i = 0; i < nPerfLevels; ++i) { + *pNum = (char) ('0' + i); + + ffStrCopy(pSubkey, "physicalcpu", lenLeft); + uint32_t ncpu = (uint32_t) ffSysctlGetInt(sysctlKey, 0); + if (ncpu <= 0) { + continue; + } + + ffStrCopy(pSubkey, "l1icachesize", lenLeft); + uint32_t size = (uint32_t) ffSysctlGetInt(sysctlKey, 0); + if (size) { + ffCPUCacheAddItem(result, 1, size, lineSize, FF_CPU_CACHE_TYPE_INSTRUCTION)->num = ncpu; + } + + ffStrCopy(pSubkey, "l1dcachesize", lenLeft); + size = (uint32_t) ffSysctlGetInt(sysctlKey, 0); + if (size) { + ffCPUCacheAddItem(result, 1, size, lineSize, FF_CPU_CACHE_TYPE_DATA)->num = ncpu; + } + + ffStrCopy(pSubkey, "l2cachesize", lenLeft); + size = (uint32_t) ffSysctlGetInt(sysctlKey, 0); + if (size) { + ffStrCopy(pSubkey, "cpusperl2", lenLeft); + uint32_t cpuSper = (uint32_t) ffSysctlGetInt(sysctlKey, 0); + if (cpuSper) { + ffCPUCacheAddItem(result, 2, size, lineSize, FF_CPU_CACHE_TYPE_UNIFIED)->num = ncpu / cpuSper; + } + } + + ffStrCopy(pSubkey, "l3cachesize", lenLeft); + size = (uint32_t) ffSysctlGetInt(sysctlKey, 0); + if (size) { + ffStrCopy(pSubkey, "cpusperl3", lenLeft); + uint32_t cpuSper = (uint32_t) ffSysctlGetInt(sysctlKey, 0); + if (cpuSper) { + ffCPUCacheAddItem(result, 3, size, lineSize, FF_CPU_CACHE_TYPE_UNIFIED)->num = ncpu / cpuSper; + } + } + } + + return NULL; +} diff --git a/src/detection/cpucache/cpucache_linux.c b/src/detection/cpucache/cpucache_linux.c new file mode 100644 index 0000000..0657c84 --- /dev/null +++ b/src/detection/cpucache/cpucache_linux.c @@ -0,0 +1,132 @@ +#include "cpucache.h" +#include "common/io.h" +#include "common/strutil.h" + +static const char* parseCpuCacheIndex(FFstrbuf* path, FFCPUCacheResult* result, FFstrbuf* buffer, FFstrbuf* added) { + uint32_t baseLen = path->length; + ffStrbufAppendS(path, "/level"); + if (!ffReadFileBuffer(path->chars, buffer)) { + return "ffReadFileBuffer(\"/sys/devices/system/cpu/cpuX/cache/indexX/level\") == NULL"; + } + + uint32_t level = (uint32_t) ffStrbufToUInt(buffer, 0); + if (level < 1 || level > 4) { + return "level < 1 || level > 4"; + } + + ffStrbufSubstrBefore(path, baseLen); + ffStrbufAppendS(path, "/size"); + if (!ffReadFileBuffer(path->chars, buffer)) { + return "ffReadFileBuffer(\"/sys/devices/system/cpu/cpuX/cache/indexX/size\") == NULL"; + } + + uint32_t sizeKb = (uint32_t) ffStrbufToUInt(buffer, 0); + if (sizeKb == 0) { + return "size == 0"; + } + + ffStrbufSubstrBefore(path, baseLen); + ffStrbufAppendS(path, "/type"); + if (!ffReadFileBuffer(path->chars, buffer)) { + return "ffReadFileBuffer(\"/sys/devices/system/cpu/cpuX/cache/indexX/type\") == NULL"; + } + ffStrbufTrimRightSpace(buffer); + + FFCPUCacheType cacheType = 0; + switch (buffer->chars[0]) { + case 'I': + cacheType = FF_CPU_CACHE_TYPE_INSTRUCTION; + break; + case 'D': + cacheType = FF_CPU_CACHE_TYPE_DATA; + break; + case 'U': + cacheType = FF_CPU_CACHE_TYPE_UNIFIED; + break; + case 'T': + cacheType = FF_CPU_CACHE_TYPE_TRACE; + break; + default: + return "unknown cache type"; + } + + uint32_t lineSize = 0; + ffStrbufSubstrBefore(path, baseLen); + ffStrbufAppendS(path, "/coherency_line_size"); + if (ffReadFileBuffer(path->chars, buffer)) { + lineSize = (uint32_t) ffStrbufToUInt(buffer, 0); + } + + ffStrbufSubstrBefore(path, baseLen); + ffStrbufAppendS(path, "/shared_cpu_list"); + ffStrbufClear(buffer); + ffStrbufAppendC(buffer, '['); + if (!ffAppendFileBuffer(path->chars, buffer)) { + return "ffAppendFileBuffer(\"/sys/devices/system/cpu/cpuX/cache/indexX/shared_cpu_list\") == NULL"; + } + ffStrbufTrimRightSpace(buffer); + + // deduplicate shared caches + ffStrbufAppendF(buffer, "_%u_%u_%u_%u]", level, sizeKb, lineSize, cacheType); + + if (ffStrbufContain(added, buffer)) { + return NULL; + } + ffStrbufAppend(added, buffer); + ffCPUCacheAddItem(result, level, sizeKb * 1024, lineSize, cacheType); + return NULL; +} + +static const char* parseCpuCache(FFstrbuf* path, FFCPUCacheResult* result, FFstrbuf* buffer, FFstrbuf* added) { + ffStrbufAppendS(path, "/cache/"); + uint32_t baseLen = path->length; + FF_AUTO_CLOSE_DIR DIR* pathCacheDir = opendir(path->chars); + if (!pathCacheDir) { + return "opendir(\"/sys/devices/system/cpu/cpuX/cache/\") == NULL"; + } + + struct dirent* pathCacheEntry; + while ((pathCacheEntry = readdir(pathCacheDir)) != NULL) { + if (!ffStrStartsWith(pathCacheEntry->d_name, "index") || !ffCharIsDigit(pathCacheEntry->d_name[strlen("index")])) { + continue; + } + + ffStrbufAppendS(path, pathCacheEntry->d_name); + const char* error = parseCpuCacheIndex(path, result, buffer, added); + if (error) { + return error; + } + ffStrbufSubstrBefore(path, baseLen); + } + + return NULL; +} + +const char* ffDetectCPUCache(FFCPUCacheResult* result) { + // https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateS("/sys/devices/system/cpu/"); + uint32_t baseLen = path.length; + FF_AUTO_CLOSE_DIR DIR* pathCpuDir = opendir(path.chars); + if (!pathCpuDir) { + return "opendir(\"/sys/devices/system/cpu/\") == NULL"; + } + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY added = ffStrbufCreate(); + + struct dirent* pathCpuEntry; + while ((pathCpuEntry = readdir(pathCpuDir)) != NULL) { + if (!ffStrStartsWith(pathCpuEntry->d_name, "cpu") || + !ffCharIsDigit(pathCpuEntry->d_name[strlen("cpu")])) { + continue; + } + + ffStrbufAppendS(&path, pathCpuEntry->d_name); + const char* error = parseCpuCache(&path, result, &buffer, &added); + if (error) { + return error; + } + ffStrbufSubstrBefore(&path, baseLen); + } + return NULL; +} diff --git a/src/detection/cpucache/cpucache_nosupport.c b/src/detection/cpucache/cpucache_nosupport.c new file mode 100644 index 0000000..51d2256 --- /dev/null +++ b/src/detection/cpucache/cpucache_nosupport.c @@ -0,0 +1,5 @@ +#include "cpucache.h" + +const char* ffDetectCPUCache(FF_A_UNUSED FFCPUCacheResult* result) { + return "Not supported on this platform"; +} diff --git a/src/detection/cpucache/cpucache_shared.c b/src/detection/cpucache/cpucache_shared.c new file mode 100644 index 0000000..c13e351 --- /dev/null +++ b/src/detection/cpucache/cpucache_shared.c @@ -0,0 +1,82 @@ +#include "cpucache.h" +#include "common/smbios.h" +#include "common/strutil.h" + +typedef struct FFSmbiosCacheInfo { + FFSmbiosHeader Header; + + uint8_t SocketDesignation; // string + uint16_t CacheConfiguration; // varies + uint16_t MaximumCacheSize; // varies + uint16_t InstalledSize; // varies + uint16_t SupportedSramType; // bit field + uint16_t CurrentSramType; // bit field + + // 2.1+ + uint8_t CacheSpeed; // varies + uint8_t ErrorCorrectionType; // enum + uint8_t SystemCacheType; // enum + uint8_t Associativity; // enum + + // 3.1+ + uint32_t MaximumCacheSize2; // bit field + uint32_t InstalledCacheSize2; // bit field +} FF_A_PACKED FFSmbiosCacheInfo; + +static_assert(offsetof(FFSmbiosCacheInfo, InstalledCacheSize2) == 0x17, + "FFSmbiosCacheInfo: Wrong struct alignment"); + +const char* ffDetectCPUCache(FFCPUCacheResult* result) { + const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable(); + if (!smbiosTable) { + return "Failed to get SMBIOS data"; + } + + const FFSmbiosCacheInfo* data = (const FFSmbiosCacheInfo*) (*smbiosTable)[FF_SMBIOS_TYPE_CACHE_INFO]; + if (!data) { + return "Cache information is not found in SMBIOS data"; + } + + const FFSmbiosCacheInfo* endOfTable = (const FFSmbiosCacheInfo*) (*smbiosTable)[FF_SMBIOS_TYPE_END_OF_TABLE]; + for (; data != endOfTable; data = (const FFSmbiosCacheInfo*) ffSmbiosNextEntry(&data->Header)) { + if (data->Header.Type != FF_SMBIOS_TYPE_CACHE_INFO) { + continue; + } + + bool enabled = !!(data->CacheConfiguration & (1 << 7)); + if (!enabled) { + continue; + } + + uint32_t size = data->InstalledSize; + if (size == 0) { + continue; + } + + if (data->InstalledSize != 0xFFFF) { + size *= (size >> 15 ? 64 : 1) * 1024u; + } else if (data->Header.Length > offsetof(FFSmbiosCacheInfo, InstalledCacheSize2)) { + size = data->InstalledCacheSize2; + size *= (size >> 31 ? 64 : 1) * 1024u; + } + + uint32_t level = (data->CacheConfiguration & 0b111u) + 1; + + FFCPUCacheType type; + switch (data->SystemCacheType) { + case 3: + type = FF_CPU_CACHE_TYPE_INSTRUCTION; + break; + case 4: + type = FF_CPU_CACHE_TYPE_DATA; + break; + default: + type = FF_CPU_CACHE_TYPE_UNIFIED; + break; + } + + ffCPUCacheAddItem(result, level, size, 0, type); + } + + return NULL; +} diff --git a/src/detection/cpucache/cpucache_windows.c b/src/detection/cpucache/cpucache_windows.c new file mode 100644 index 0000000..b9d3815 --- /dev/null +++ b/src/detection/cpucache/cpucache_windows.c @@ -0,0 +1,47 @@ +#include "cpucache.h" +#include "common/mallocHelper.h" +#include "common/windows/nt.h" + +const char* ffDetectCPUCache(FFCPUCacheResult* result) { + LOGICAL_PROCESSOR_RELATIONSHIP lpr = RelationCache; + DWORD length = 0; + NtQuerySystemInformationEx(SystemLogicalProcessorAndGroupInformation, &lpr, sizeof(lpr), NULL, 0, &length); + if (length == 0) { + return "GetLogicalProcessorInformationEx(RelationCache, NULL, &length) failed"; + } + + SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX* FF_AUTO_FREE + pProcessorInfo = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) malloc(length); + + if (!NT_SUCCESS(NtQuerySystemInformationEx(SystemLogicalProcessorAndGroupInformation, &lpr, sizeof(lpr), pProcessorInfo, length, &length))) { + return "GetLogicalProcessorInformationEx(RelationCache, pProcessorInfo, &length) failed"; + } + + for ( + SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX* ptr = pProcessorInfo; + (uint8_t*) ptr < ((uint8_t*) pProcessorInfo) + length; + ptr = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX*) (((uint8_t*) ptr) + ptr->Size)) { + if (__builtin_expect(ptr->Relationship == RelationCache && ptr->Cache.Level > 0 && ptr->Cache.Level <= 4, true)) { + FFCPUCacheType cacheType = 0; + switch (ptr->Cache.Type) { + case CacheUnified: + cacheType = FF_CPU_CACHE_TYPE_UNIFIED; + break; + case CacheInstruction: + cacheType = FF_CPU_CACHE_TYPE_INSTRUCTION; + break; + case CacheData: + cacheType = FF_CPU_CACHE_TYPE_DATA; + break; + case CacheTrace: + cacheType = FF_CPU_CACHE_TYPE_TRACE; + break; + default: + break; + } + ffCPUCacheAddItem(result, ptr->Cache.Level, ptr->Cache.CacheSize, ptr->Cache.LineSize, cacheType); + } + } + + return NULL; +} diff --git a/src/detection/cpuusage/cpuusage.c b/src/detection/cpuusage/cpuusage.c new file mode 100644 index 0000000..a78ae7b --- /dev/null +++ b/src/detection/cpuusage/cpuusage.c @@ -0,0 +1,74 @@ +#include "fastfetch.h" +#include "detection/cpuusage/cpuusage.h" +#include "common/time.h" + +#include <stdint.h> + +static FFlist cpuTimes1; +static uint64_t startTime; + +void ffPrepareCPUUsage(void) { + if (startTime != 0) { + return; // Already prepared + } + + ffListInit(&cpuTimes1); + ffGetCpuUsageInfo(&cpuTimes1); + startTime = ffTimeGetNow(); +} + +const char* ffGetCpuUsageResult(FFCPUUsageOptions* options, FFlist* result) { + const char* error = NULL; + if (startTime == 0) { + ffListInit(&cpuTimes1); + error = ffGetCpuUsageInfo(&cpuTimes1); + if (error) { + return error; + } + ffTimeSleep(options->waitTime); + } else { + uint64_t elapsedTime = ffTimeGetNow() - startTime; + if (elapsedTime < options->waitTime) { + ffTimeSleep(options->waitTime - (uint32_t) elapsedTime); + } + } + + if (cpuTimes1.length == 0) { + return "No CPU cores found"; + } + + FF_LIST_AUTO_DESTROY cpuTimes2 = ffListCreate(); + uint32_t retryCount = 0; + +retry: + error = ffGetCpuUsageInfo(&cpuTimes2); + if (error) { + return error; + } + if (cpuTimes1.length != cpuTimes2.length) { + return "Unexpected CPU usage result"; + } + + for (uint32_t i = 0; i < cpuTimes1.length; ++i) { + FFCpuUsageInfo* cpuTime1 = FF_LIST_GET(FFCpuUsageInfo, cpuTimes1, i); + FFCpuUsageInfo* cpuTime2 = FF_LIST_GET(FFCpuUsageInfo, cpuTimes2, i); + if (cpuTime2->totalAll <= cpuTime1->totalAll) { + if (++retryCount <= 3) { + ffListClear(&cpuTimes2); + ffTimeSleep(options->waitTime); + goto retry; + } + return "CPU time did not increase. Try increasing wait time."; + } + } + + for (uint32_t i = 0; i < cpuTimes1.length; ++i) { + FFCpuUsageInfo* cpuTime1 = FF_LIST_GET(FFCpuUsageInfo, cpuTimes1, i); + FFCpuUsageInfo* cpuTime2 = FF_LIST_GET(FFCpuUsageInfo, cpuTimes2, i); + *FF_LIST_ADD(double, *result) = (double) (cpuTime2->inUseAll - cpuTime1->inUseAll) / (double) (cpuTime2->totalAll - cpuTime1->totalAll) * 100; + cpuTime1->inUseAll = cpuTime2->inUseAll; + cpuTime1->totalAll = cpuTime2->totalAll; + } + startTime = ffTimeGetNow(); + return NULL; +} diff --git a/src/detection/cpuusage/cpuusage.h b/src/detection/cpuusage/cpuusage.h new file mode 100644 index 0000000..353b729 --- /dev/null +++ b/src/detection/cpuusage/cpuusage.h @@ -0,0 +1,12 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/cpuusage/option.h" + +typedef struct FFCpuUsageInfo { + uint64_t inUseAll; + uint64_t totalAll; +} FFCpuUsageInfo; +const char* ffGetCpuUsageInfo(FFlist* cpuTimes); + +const char* ffGetCpuUsageResult(FFCPUUsageOptions* options, FFlist* result); // list of double diff --git a/src/detection/cpuusage/cpuusage_apple.c b/src/detection/cpuusage/cpuusage_apple.c new file mode 100644 index 0000000..50a8b15 --- /dev/null +++ b/src/detection/cpuusage/cpuusage_apple.c @@ -0,0 +1,33 @@ +#include "fastfetch.h" +#include "detection/cpuusage/cpuusage.h" + +#include <mach/processor_info.h> +#include <mach/mach_host.h> +#include <mach/vm_map.h> + +const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { + natural_t numCPUs = 0U; + processor_info_array_t cpuInfo; + mach_msg_type_number_t numCpuInfo; + + if (host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &numCPUs, &cpuInfo, &numCpuInfo) != KERN_SUCCESS) { + return "host_processor_info() failed"; + } + if (numCPUs * CPU_STATE_MAX != numCpuInfo) { + return "Unexpected host_processor_info() result"; + } + + for (natural_t i = 0U; i < numCPUs; ++i) { + integer_t inUse = cpuInfo[CPU_STATE_MAX * i + CPU_STATE_USER] + cpuInfo[CPU_STATE_MAX * i + CPU_STATE_SYSTEM] + cpuInfo[CPU_STATE_MAX * i + CPU_STATE_NICE]; + integer_t total = inUse + cpuInfo[CPU_STATE_MAX * i + CPU_STATE_IDLE]; + + FFCpuUsageInfo* info = FF_LIST_ADD(FFCpuUsageInfo, *cpuTimes); + *info = (FFCpuUsageInfo) { + .inUseAll = (uint64_t) inUse, + .totalAll = (uint64_t) total, + }; + } + + vm_deallocate(mach_task_self(), (vm_address_t) cpuInfo, numCpuInfo * sizeof(integer_t)); + return NULL; +} diff --git a/src/detection/cpuusage/cpuusage_bsd.c b/src/detection/cpuusage/cpuusage_bsd.c new file mode 100644 index 0000000..f690d15 --- /dev/null +++ b/src/detection/cpuusage/cpuusage_bsd.c @@ -0,0 +1,58 @@ +#include "detection/cpuusage/cpuusage.h" +#include "common/mallocHelper.h" + +#include <sys/types.h> +#include <sys/sysctl.h> +#include <sys/resource.h> +#include <stdlib.h> + +#if __OpenBSD__ || __NetBSD__ + #include <sys/sched.h> +#endif + +const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { + size_t neededLength = 0; +#if __OpenBSD__ || __NetBSD__ + #ifdef KERN_CPTIME + int ctls[] = { CTL_KERN, KERN_CPTIME }; + #else + int ctls[] = { CTL_KERN, KERN_CP_TIME }; + #endif + if (sysctl(ctls, 2, NULL, &neededLength, NULL, 0) != 0) { + return "sysctl({CTL_KERN, KERN_CPTIME}, 2, NULL) failed"; + } +#else + if (sysctlbyname("kern.cp_times", NULL, &neededLength, NULL, 0) != 0) { + return "sysctlbyname(kern.cp_times, NULL) failed"; + } +#endif + + uint32_t coreCount = (uint32_t) (neededLength / (CPUSTATES * sizeof(uint64_t))); + assert(coreCount > 0); + + FF_AUTO_FREE uint64_t (*cpTimes)[CPUSTATES] = malloc(neededLength); + +#if __OpenBSD__ || __NetBSD__ + if (sysctl(ctls, 2, cpTimes, &neededLength, NULL, 0) != 0) { + return "sysctl({CTL_KERN, KERN_CPTIME}, 2, NULL) failed"; + } +#else + if (sysctlbyname("kern.cp_times", cpTimes, &neededLength, NULL, 0) != 0) { + return "sysctlbyname(kern.cp_times, cpTime) failed"; + } +#endif + + for (uint32_t i = 0; i < coreCount; ++i) { + uint64_t* cpTime = cpTimes[i]; + uint64_t inUse = cpTime[CP_USER] + cpTime[CP_NICE] + cpTime[CP_SYS] + cpTime[CP_INTR]; + uint64_t total = inUse + cpTime[CP_IDLE]; + + FFCpuUsageInfo* info = FF_LIST_ADD(FFCpuUsageInfo, *cpuTimes); + *info = (FFCpuUsageInfo) { + .inUseAll = inUse, + .totalAll = total, + }; + } + + return NULL; +} diff --git a/src/detection/cpuusage/cpuusage_haiku.c b/src/detection/cpuusage/cpuusage_haiku.c new file mode 100644 index 0000000..f4d3c63 --- /dev/null +++ b/src/detection/cpuusage/cpuusage_haiku.c @@ -0,0 +1,27 @@ +#include "fastfetch.h" +#include "detection/cpuusage/cpuusage.h" +#include "common/mallocHelper.h" + +#include <OS.h> + +const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { + system_info sysInfo; + if (get_system_info(&sysInfo) != B_OK) { + return "get_system_info() failed"; + } + + FF_AUTO_FREE cpu_info* cpuInfo = malloc(sizeof(*cpuInfo) * sysInfo.cpu_count); + if (get_cpu_info(0, sysInfo.cpu_count, cpuInfo) != B_OK) { + return "get_cpu_info() failed"; + } + + uint64_t uptime = (uint64_t) system_time(); + + for (uint32_t i = 0; i < sysInfo.cpu_count; ++i) { + FFCpuUsageInfo* info = FF_LIST_ADD(FFCpuUsageInfo, *cpuTimes); + info->inUseAll = (uint64_t) cpuInfo[i].active_time; + info->totalAll = uptime; + } + + return NULL; +} diff --git a/src/detection/cpuusage/cpuusage_linux.c b/src/detection/cpuusage/cpuusage_linux.c new file mode 100644 index 0000000..fd2f43b --- /dev/null +++ b/src/detection/cpuusage/cpuusage_linux.c @@ -0,0 +1,46 @@ +#include "fastfetch.h" +#include "detection/cpuusage/cpuusage.h" +#include "common/io.h" + +#include <stdio.h> +#include <inttypes.h> + +const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { + char buf[PROC_FILE_BUFFSIZ]; + ssize_t nRead = ffReadFileData("/proc/stat", ARRAY_SIZE(buf) - 1, buf); + if (nRead < 0) { +#ifdef __ANDROID__ + return "Accessing \"/proc/stat\" is restricted on Android O+"; +#else + return "ffReadFileData(\"/proc/stat\", ARRAY_SIZE(buf) - 1, buf) failed"; +#endif + } + buf[nRead] = '\0'; + + // Skip first line + char* start = NULL; + if ((start = strchr(buf, '\n')) == NULL) { + return "skip first line failed"; + } + ++start; + + uint64_t user = 0, nice = 0, system = 0, idle = 0, iowait = 0, irq = 0, softirq = 0; + char* token = NULL; + while ((token = strchr(start, '\n'))) { + if (sscanf(start, "cpu%*d%" PRIu64 "%" PRIu64 "%" PRIu64 "%" PRIu64 "%" PRIu64 "%" PRIu64 "%" PRIu64 "%*[^\n]\n", &user, &nice, &system, &idle, &iowait, &irq, &softirq) == 7) { + uint64_t inUse = user + nice + system + irq + softirq; + uint64_t total = inUse + idle + iowait; + + FFCpuUsageInfo* info = FF_LIST_ADD(FFCpuUsageInfo, *cpuTimes); + *info = (FFCpuUsageInfo) { + .inUseAll = inUse, + .totalAll = total, + }; + } else { + break; + } + start = token + 1; + } + + return NULL; +} diff --git a/src/detection/cpuusage/cpuusage_nosupport.c b/src/detection/cpuusage/cpuusage_nosupport.c new file mode 100644 index 0000000..faade7b --- /dev/null +++ b/src/detection/cpuusage/cpuusage_nosupport.c @@ -0,0 +1,6 @@ +#include "fastfetch.h" +#include "detection/cpuusage/cpuusage.h" + +const char* ffGetCpuUsageInfo(FF_A_UNUSED FFlist* cpuTimes) { + return "Not support on this platform"; +} diff --git a/src/detection/cpuusage/cpuusage_sunos.c b/src/detection/cpuusage/cpuusage_sunos.c new file mode 100644 index 0000000..813d74d --- /dev/null +++ b/src/detection/cpuusage/cpuusage_sunos.c @@ -0,0 +1,38 @@ +#include "fastfetch.h" +#include "detection/cpuusage/cpuusage.h" + +#include <kstat.h> +#include <sys/sysinfo.h> + +static inline void kstatFreeWrap(kstat_ctl_t** pkc) { + assert(pkc); + if (*pkc) { + kstat_close(*pkc); + } +} + +const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { + FF_A_CLEANUP(kstatFreeWrap) kstat_ctl_t* kc = kstat_open(); + if (!kc) { + return "kstat_open() failed"; + } + + for (int i = 0;; ++i) { + kstat_t* ks = kstat_lookup(kc, "cpu_stat", i, NULL); + + cpu_stat_t cs; + if (!ks || kstat_read(kc, ks, &cs) < 0) { + break; + } + + uint64_t inUse = cs.cpu_sysinfo.cpu[CPU_USER] + cs.cpu_sysinfo.cpu[CPU_KERNEL]; + uint64_t total = inUse + cs.cpu_sysinfo.cpu[CPU_IDLE] + cs.cpu_sysinfo.cpu[CPU_WAIT]; + + FFCpuUsageInfo* info = FF_LIST_ADD(FFCpuUsageInfo, *cpuTimes); + *info = (FFCpuUsageInfo) { + .inUseAll = inUse, + .totalAll = total, + }; + } + return NULL; +} diff --git a/src/detection/cpuusage/cpuusage_windows.c b/src/detection/cpuusage/cpuusage_windows.c new file mode 100644 index 0000000..c23c28a --- /dev/null +++ b/src/detection/cpuusage/cpuusage_windows.c @@ -0,0 +1,169 @@ +#include "detection/cpuusage/cpuusage.h" +#include "common/mallocHelper.h" +#include "common/debug.h" + +#include <ntstatus.h> +#include <windows.h> +#include <wchar.h> +#include "common/windows/perflib_.h" +#include "common/windows/nt.h" + +static const char* getInfoByNqsi(FFlist* cpuTimes) { + ULONG size = 0; + if (NtQuerySystemInformation(SystemProcessorPerformanceInformation, NULL, 0, &size) != STATUS_INFO_LENGTH_MISMATCH) { + return "NtQuerySystemInformation(SystemProcessorPerformanceInformation, NULL) failed"; + } + + SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION* FF_AUTO_FREE pinfo = (SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION*) malloc(size); + if (!NT_SUCCESS(NtQuerySystemInformation(SystemProcessorPerformanceInformation, pinfo, size, &size))) { + return "NtQuerySystemInformation(SystemProcessorPerformanceInformation, size) failed"; + } + + for (uint32_t i = 0; i < size / sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION); ++i) { + SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION* coreInfo = pinfo + i; + + // KernelTime includes IdleTime and DpcTime. + coreInfo->KernelTime.QuadPart -= coreInfo->IdleTime.QuadPart; + + uint64_t inUse = (uint64_t) (coreInfo->UserTime.QuadPart + coreInfo->KernelTime.QuadPart); + uint64_t total = inUse + (uint64_t) coreInfo->IdleTime.QuadPart; + + FFCpuUsageInfo* info = FF_LIST_ADD(FFCpuUsageInfo, *cpuTimes); + *info = (FFCpuUsageInfo) { + .inUseAll = inUse, + .totalAll = total, + }; + } + + return NULL; +} + +static const char* getInfoByPerflib(FFlist* cpuTimes) { + static HANDLE hQuery = NULL; + + if (hQuery == NULL) { + struct FFPerfQuerySpec { + PERF_COUNTER_IDENTIFIER Identifier; + WCHAR Name[16]; + } querySpec = { + .Identifier = { + // Processor Information GUID + // HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\_V2Providers\{383487a6-3676-4870-a4e7-d45b30c35629}\{b4fc721a-0378-476f-89ba-a5a79f810b36} + .CounterSetGuid = { 0xb4fc721a, 0x0378, 0x476f, { 0x89, 0xba, 0xa5, 0xa7, 0x9f, 0x81, 0x0b, 0x36 } }, + .Size = sizeof(querySpec), + .CounterId = PERF_WILDCARD_COUNTER, // https://learn.microsoft.com/en-us/windows/win32/perfctrs/using-the-perflib-functions-to-consume-counter-data + .InstanceId = PERF_WILDCARD_COUNTER, + }, + .Name = PERF_WILDCARD_INSTANCE, + }; + + if (PerfOpenQueryHandle(NULL, &hQuery) != ERROR_SUCCESS) { + PerfCloseQueryHandle(hQuery); + hQuery = INVALID_HANDLE_VALUE; + return "PerfOpenQueryHandle() failed"; + } + + if (PerfAddCounters(hQuery, &querySpec.Identifier, sizeof(querySpec)) != ERROR_SUCCESS) { + PerfCloseQueryHandle(hQuery); + hQuery = INVALID_HANDLE_VALUE; + return "PerfAddCounters() failed"; + } + + if (querySpec.Identifier.Status != ERROR_SUCCESS) { + PerfCloseQueryHandle(hQuery); + hQuery = INVALID_HANDLE_VALUE; + return "PerfAddCounters() reports invalid identifier"; + } + } + + if (hQuery == INVALID_HANDLE_VALUE) { + return "Init hQuery failed"; + } + + DWORD dataSize = 0; + if (PerfQueryCounterData(hQuery, NULL, 0, &dataSize) != ERROR_NOT_ENOUGH_MEMORY) { + return "PerfQueryCounterData(NULL) failed"; + } + + if (dataSize <= sizeof(PERF_DATA_HEADER) + sizeof(PERF_COUNTER_HEADER)) { + return "instance doesn't exist"; + } + + FF_AUTO_FREE PERF_DATA_HEADER* const pDataHeader = (PERF_DATA_HEADER*) malloc(dataSize); + if (PerfQueryCounterData(hQuery, pDataHeader, dataSize, &dataSize) != ERROR_SUCCESS) { + return "PerfQueryCounterData(pDataHeader) failed"; + } + + PERF_COUNTER_HEADER* pCounterHeader = (PERF_COUNTER_HEADER*) (pDataHeader + 1); + if (pCounterHeader->dwType != PERF_COUNTERSET) { + return "Invalid counter type"; + } + + PERF_MULTI_COUNTERS* pMultiCounters = (PERF_MULTI_COUNTERS*) (pCounterHeader + 1); + if (pMultiCounters->dwCounters == 0) { + return "No CPU counters found"; + } + + PERF_MULTI_INSTANCES* pMultiInstances = (PERF_MULTI_INSTANCES*) ((BYTE*) pMultiCounters + pMultiCounters->dwSize); + if (pMultiInstances->dwInstances == 0) { + return "No CPU instances found"; + } + + PERF_INSTANCE_HEADER* pInstanceHeader = (PERF_INSTANCE_HEADER*) (pMultiInstances + 1); + for (DWORD iInstance = 0; iInstance < pMultiInstances->dwInstances; ++iInstance) { + const wchar_t* instanceName = (const wchar_t*) ((BYTE*) pInstanceHeader + sizeof(*pInstanceHeader)); + + PERF_COUNTER_DATA* pCounterData = (PERF_COUNTER_DATA*) ((BYTE*) pInstanceHeader + pInstanceHeader->Size); + + uint64_t processorUtility = UINT64_MAX, utilityBase = UINT64_MAX; + for (ULONG iCounter = 0; iCounter != pMultiCounters->dwCounters; iCounter++) { + DWORD* pCounterIds = (DWORD*) (pMultiCounters + 1); + // https://learn.microsoft.com/en-us/windows/win32/perfctrs/using-the-perflib-functions-to-consume-counter-data + switch (pCounterIds[iCounter]) { + case 26: // % Processor Utility (#26, Type=PERF_AVERAGE_BULK) + assert(pCounterData->dwDataSize == sizeof(uint64_t)); + processorUtility = *(uint64_t*) (pCounterData + 1); + break; + case 27: // % Utility Base (#27, Type=PERF_AVERAGE_BASE) + assert(pCounterData->dwDataSize == sizeof(uint32_t)); + utilityBase = *(uint32_t*) (pCounterData + 1) * 100LLU; + break; + } + + pCounterData = (PERF_COUNTER_DATA*) ((BYTE*) pCounterData + pCounterData->dwSize); + } + + if (wcschr(instanceName, L'_') == NULL /* ignore `_Total` */) { + if (processorUtility == UINT64_MAX) { + return "Counter \"% Processor Utility\" are not supported"; + } + + FFCpuUsageInfo* info = FF_LIST_ADD(FFCpuUsageInfo, *cpuTimes); + *info = (FFCpuUsageInfo) { + .inUseAll = processorUtility, + .totalAll = utilityBase, + }; + } + + pInstanceHeader = (PERF_INSTANCE_HEADER*) pCounterData; + } + + return NULL; +} + +const char* ffGetCpuUsageInfo(FFlist* cpuTimes) { + const char* error = NULL; + + if (ffIsWindows10OrGreater()) { + error = getInfoByPerflib(cpuTimes); + FF_DEBUG("Get CPU usage info by Perflib: %s", error ?: "success"); + if (!error) { + return NULL; + } + ffListClear(cpuTimes); + } + + error = getInfoByNqsi(cpuTimes); + FF_DEBUG("Get CPU usage info by NtQuerySystemInformation: %s", error ?: "success"); + return error; +} diff --git a/src/detection/cursor/cursor.h b/src/detection/cursor/cursor.h new file mode 100644 index 0000000..34ef86c --- /dev/null +++ b/src/detection/cursor/cursor.h @@ -0,0 +1,12 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/cursor/option.h" + +typedef struct FFCursorResult { + FFstrbuf theme; + FFstrbuf size; + FFstrbuf error; +} FFCursorResult; + +void ffDetectCursor(FFCursorResult* result); diff --git a/src/detection/cursor/cursor_apple.m b/src/detection/cursor/cursor_apple.m new file mode 100644 index 0000000..610bd1e --- /dev/null +++ b/src/detection/cursor/cursor_apple.m @@ -0,0 +1,63 @@ +#include "cursor.h" + +#import <Foundation/Foundation.h> + +static void appendColor(FFstrbuf* str, NSDictionary* dict) +{ + uint32_t r = (uint32_t) (((NSNumber*) dict[@"red"]).doubleValue * 255 + .5); + uint32_t g = (uint32_t) (((NSNumber*) dict[@"green"]).doubleValue * 255 + .5); + uint32_t b = (uint32_t) (((NSNumber*) dict[@"blue"]).doubleValue * 255 + .5); + uint32_t a = (uint32_t) (((NSNumber*) dict[@"alpha"]).doubleValue * 255 + .5); + uint32_t color = (r << 24) | (g << 16) | (b << 8) | a; + + switch (color) + { + case 0x000000FF: ffStrbufAppendS(str, "Black"); return; + case 0x0433FFFF: ffStrbufAppendS(str, "Blue"); return; + case 0xAA7942FF: ffStrbufAppendS(str, "Brown"); return; + case 0x00FDFFFF: ffStrbufAppendS(str, "Cyan"); return; + case 0x00F900FF: ffStrbufAppendS(str, "Green"); return; + case 0xFF40FFFF: ffStrbufAppendS(str, "Magenta"); return; + case 0xFF9300FF: ffStrbufAppendS(str, "Orange"); return; + case 0x942192FF: ffStrbufAppendS(str, "Purple"); return; + case 0xFF2600FF: ffStrbufAppendS(str, "Red"); return; + case 0xFFFB00FF: ffStrbufAppendS(str, "Yellow"); return; + case 0xFFFFFFFF: ffStrbufAppendS(str, "White"); return; + case 0x00000000: ffStrbufAppendS(str, "Transparent"); return; + default: ffStrbufAppendF(str, "#%08X", color); return; + } +} + +void ffDetectCursor(FFCursorResult* result) +{ + NSError* error; + NSString* fileName = [NSString stringWithFormat:@"file://%s/Library/Preferences/com.apple.universalaccess.plist", instance.state.platform.homeDir.chars]; + NSDictionary* dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL URLWithString:fileName] + error:&error]; + if(error) + { + ffStrbufAppendS(&result->error, error.localizedDescription.UTF8String); + return; + } + + NSDictionary* color; + + ffStrbufAppendS(&result->theme, "Fill - "); + if ((color = dict[@"cursorFill"])) + appendColor(&result->theme, color); + else + ffStrbufAppendS(&result->theme, "Black"); + + ffStrbufAppendS(&result->theme, ", Outline - "); + + if ((color = dict[@"cursorOutline"])) + appendColor(&result->theme, color); + else + ffStrbufAppendS(&result->theme, "White"); + + NSNumber* mouseDriverCursorSize = dict[@"mouseDriverCursorSize"]; + if (mouseDriverCursorSize) + ffStrbufAppendF(&result->size, "%d", (int) (mouseDriverCursorSize.doubleValue * 32 + 0.5)); + else + ffStrbufAppendS(&result->size, "32"); +} diff --git a/src/detection/cursor/cursor_linux.c b/src/detection/cursor/cursor_linux.c new file mode 100644 index 0000000..89dabbc --- /dev/null +++ b/src/detection/cursor/cursor_linux.c @@ -0,0 +1,101 @@ +#include "cursor.h" + +#include "detection/gtk_qt/gtk_qt.h" +#include "detection/displayserver/displayserver.h" +#include "common/properties.h" +#include "common/parsing.h" +#include "common/settings.h" +#include "common/strutil.h" + +#include <stdlib.h> + +static bool detectCursorGTK(FFCursorResult* result) { + const FFGTKResult* gtk = ffDetectGTK4(); + + if (gtk->cursor.length == 0) { + gtk = ffDetectGTK3(); + } + + if (gtk->cursor.length == 0) { + gtk = ffDetectGTK2(); + } + + if (gtk->cursor.length == 0) { + return false; + } + + ffStrbufAppend(&result->theme, >k->cursor); + ffStrbufAppend(&result->size, >k->cursorSize); + return true; +} + +static void detectCursorFromConfigFile(const char* relativeFilePath, const char* themeStart, const char* themeDefault, const char* sizeStart, const char* sizeDefault, FFCursorResult* result) { + if (ffParsePropFileConfigValues(relativeFilePath, 2, (FFpropquery[]) { { themeStart, &result->theme }, { sizeStart, &result->size } })) { + if (result->theme.length == 0) { + ffStrbufAppendS(&result->theme, themeDefault); + } + + if (result->size.length == 0) { + ffStrbufAppendS(&result->size, sizeDefault); + } + } + + if (result->theme.length == 0) { + ffStrbufAppendF(&result->error, "Couldn't find cursor in %s", relativeFilePath); + } +} + +static bool detectCursorFromXResources(FFCursorResult* result) { + ffParsePropFileHomeValues(".Xresources", 2, (FFpropquery[]) { { "Xcursor.theme :", &result->theme }, { "Xcursor.size :", &result->size } }); + + return result->theme.length > 0; +} + +static bool detectCursorFromEnv(FFCursorResult* result) { + const char* xcursor_theme = getenv("XCURSOR_THEME"); + + if (!ffStrSet(xcursor_theme)) { + return false; + } + + ffStrbufAppendS(&result->theme, xcursor_theme); + ffStrbufAppendS(&result->size, getenv("XCURSOR_SIZE")); + + return true; +} + +static bool detectCursorHyprcursor(FFCursorResult* result) { + const char* hyprcursor_theme = getenv("HYPRCURSOR_THEME"); + + if (!ffStrSet(hyprcursor_theme)) { + return false; + } + + ffStrbufAppendS(&result->theme, hyprcursor_theme); + ffStrbufAppendS(&result->size, getenv("HYPRCURSOR_SIZE")); + + return true; +} + +void ffDetectCursor(FFCursorResult* result) { + const FFDisplayServerResult* wmde = ffConnectDisplayServer(); + + if (ffStrbufEqualS(&wmde->wmPrettyName, FF_WM_PRETTY_WSLG)) { + ffStrbufAppendS(&result->error, "WSLg uses native windows cursor"); + } else if (ffStrbufIgnCaseEqualS(&wmde->wmProtocolName, FF_WM_PROTOCOL_TTY)) { + ffStrbufAppendS(&result->error, "Cursor isn't supported in TTY"); + } else if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_PLASMA)) { + detectCursorFromConfigFile("kcminputrc", "cursorTheme =", "Breeze", "cursorSize =", "24", result); + } else if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_LXQT)) { + detectCursorFromConfigFile("lxqt/session.conf", "cursor_theme =", "Adwaita", "cursor_size =", "24", result); + } else if (ffStrbufIgnCaseEqualS(&wmde->wmPrettyName, FF_WM_PRETTY_HYPRLAND) && detectCursorHyprcursor(result)) { + return; + } else if ( + !detectCursorGTK(result) && + !detectCursorFromEnv(result) && + !ffParsePropFileHome(".icons/default/index.theme", "Inherits =", &result->theme) && + !detectCursorFromXResources(result) && + !ffParsePropFileData("icons/default/index.theme", "Inherits =", &result->theme)) { + ffStrbufAppendS(&result->error, "Couldn't find cursor"); + } +} diff --git a/src/detection/cursor/cursor_nosupport.c b/src/detection/cursor/cursor_nosupport.c new file mode 100644 index 0000000..6413d92 --- /dev/null +++ b/src/detection/cursor/cursor_nosupport.c @@ -0,0 +1,5 @@ +#include "cursor.h" + +void ffDetectCursor(FF_A_UNUSED FFCursorResult* result) { + ffStrbufInitS(&result->error, "Not supported on this platform"); +} diff --git a/src/detection/cursor/cursor_windows.c b/src/detection/cursor/cursor_windows.c new file mode 100644 index 0000000..a6814ec --- /dev/null +++ b/src/detection/cursor/cursor_windows.c @@ -0,0 +1,17 @@ +#include "cursor.h" + +#include "common/io.h" +#include "common/windows/registry.h" + +void ffDetectCursor(FFCursorResult* result) { + FF_AUTO_CLOSE_FD HANDLE hKey = NULL; + if (ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Control Panel\\Cursors", &hKey, &result->error)) { + if (ffRegReadStrbuf(hKey, NULL, &result->theme, &result->error)) { + uint32_t cursorBaseSize; + if (ffRegReadUint(hKey, L"CursorBaseSize", &cursorBaseSize, NULL)) { + // Not available on Windows 8.1 + ffStrbufAppendUInt(&result->size, cursorBaseSize); + }; + } + } +} diff --git a/src/detection/de/de.h b/src/detection/de/de.h new file mode 100644 index 0000000..67e8c1c --- /dev/null +++ b/src/detection/de/de.h @@ -0,0 +1,6 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/de/option.h" + +const char* ffDetectDEVersion(const FFstrbuf* deName, FFstrbuf* result, FFDEOptions* options); diff --git a/src/detection/de/de_linux.c b/src/detection/de/de_linux.c new file mode 100644 index 0000000..de7a220 --- /dev/null +++ b/src/detection/de/de_linux.c @@ -0,0 +1,284 @@ +#include "de.h" + +#include "common/dbus.h" +#include "common/io.h" +#include "common/library.h" +#include "common/parsing.h" +#include "common/properties.h" +#include "common/processing.h" +#include "common/binary.h" +#include "common/path.h" +#include "detection/displayserver/displayserver.h" + +#include <ctype.h> +#ifdef __FreeBSD__ + #include <paths.h> + #ifndef _PATH_LOCALBASE + #define _PATH_LOCALBASE "/usr/local" + #endif +#elif __OpenBSD__ + #define _PATH_LOCALBASE "/usr/local" +#elif __NetBSD__ + #define _PATH_LOCALBASE "/usr/pkg" +#endif + +static void getKDE(FFstrbuf* result, FF_A_UNUSED FFDEOptions* options) { +#ifdef _PATH_LOCALBASE + ffParsePropFile(_PATH_LOCALBASE "/share/wayland-sessions/plasma.desktop", "X-KDE-PluginInfo-Version =", result); + if (result->length == 0) { + ffParsePropFile(_PATH_LOCALBASE "/share/xsessions/plasmax11.desktop", "X-KDE-PluginInfo-Version =", result); + } +#else + ffParsePropFile(FASTFETCH_TARGET_DIR_USR "/share/wayland-sessions/plasma.desktop", "X-KDE-PluginInfo-Version =", result); + if (result->length == 0) { + ffParsePropFile(FASTFETCH_TARGET_DIR_USR "/share/xsessions/plasmax11.desktop", "X-KDE-PluginInfo-Version =", result); + } +#endif + + if (result->length == 0) { + ffParsePropFileData("xsessions/plasma.desktop", "X-KDE-PluginInfo-Version =", result); + } + if (result->length == 0) { + ffParsePropFileData("xsessions/plasma5.desktop", "X-KDE-PluginInfo-Version =", result); + } + + if (result->length == 0) { + ffParsePropFileData("wayland-sessions/plasmawayland.desktop", "X-KDE-PluginInfo-Version =", result); + } + if (result->length == 0) { + ffParsePropFileData("wayland-sessions/plasmawayland5.desktop", "X-KDE-PluginInfo-Version =", result); + } + + if (result->length == 0) { + if (ffProcessAppendStdOut(result, (char* const[]) { "plasmashell", "--version", NULL }) == NULL) { // plasmashell 5.27.5 + ffStrbufSubstrAfterLastC(result, ' '); + } + } +} + +static const char* getGnomeByDbus(FF_A_UNUSED FFstrbuf* result) { +#ifdef FF_HAVE_DBUS + FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {}; + if (ffDBusLoadData(DBUS_BUS_SESSION, &dbus) != NULL) { + return "ffDBusLoadData() failed"; + } + + ffDBusGetPropertyString(&dbus, "org.gnome.Shell", "/org/gnome/Shell", "org.gnome.Shell", "ShellVersion", result); + return NULL; +#else // FF_HAVE_DBUS + return "ffDBusLoadData() failed: dbus support not compiled in"; +#endif // FF_HAVE_DBUS +} + +static void getGnome(FFstrbuf* result, FF_A_UNUSED FFDEOptions* options) { + getGnomeByDbus(result); + + if (result->length == 0) { + if (ffProcessAppendStdOut(result, (char* const[]) { "gnome-shell", "--version", NULL }) == NULL) { // GNOME Shell 44.1 + ffStrbufSubstrAfterLastC(result, ' '); + } + } +} + +static void getCinnamon(FFstrbuf* result, FF_A_UNUSED FFDEOptions* options) { + ffStrbufSetS(result, getenv("CINNAMON_VERSION")); + + if (result->length == 0) { + ffParsePropFileData("applications/cinnamon.desktop", "X-GNOME-Bugzilla-Version =", result); + } + + if (result->length == 0) { + if (ffProcessAppendStdOut(result, (char* const[]) { "cinnamon", "--version", NULL }) == NULL) { // Cinnamon 6.2.2 + ffStrbufSubstrAfterLastC(result, ' '); + } + } +} + +static void getMate(FFstrbuf* result, FF_A_UNUSED FFDEOptions* options) { + FF_STRBUF_AUTO_DESTROY major = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY minor = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY micro = ffStrbufCreate(); + + ffParsePropFileDataValues("mate-about/mate-version.xml", 3, (FFpropquery[]) { { "<platform>", &major }, { "<minor>", &minor }, { "<micro>", µ } }); + + ffParseSemver(result, &major, &minor, µ); + + if (result->length == 0) { + ffProcessAppendStdOut(result, (char* const[]) { "mate-session", "--version", NULL }); + + ffStrbufSubstrAfterFirstC(result, ' '); + ffStrbufTrim(result, ' '); + } +} + +static const char* getXfce4ByLib(FFstrbuf* result) { +#ifndef FF_DISABLE_DLOPEN + const char* xfce_version_string(void); // from `xfce4/libxfce4util/xfce-misutils.h + FF_LIBRARY_LOAD_MESSAGE(xfce4util, "libxfce4util" FF_LIBRARY_EXTENSION, 7); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(xfce4util, xfce_version_string); + ffStrbufSetS(result, ffxfce_version_string()); + return NULL; +#else + FF_UNUSED(result); + return "dlopen is disabled"; +#endif +} + +static void getXFCE4(FFstrbuf* result, FF_A_UNUSED FFDEOptions* options) { + getXfce4ByLib(result); + + if (result->length == 0) { + // This is somewhat slow + ffProcessAppendStdOut(result, (char* const[]) { "xfce4-session", "--version", NULL }); + + ffStrbufSubstrBeforeFirstC(result, ')'); + ffStrbufSubstrAfterLastC(result, ' '); + ffStrbufTrim(result, ' '); + } +} + +static void getLXQt(FFstrbuf* result, FF_A_UNUSED FFDEOptions* options) { + ffParsePropFileData("gconfig/lxqt.pc", "Version:", result); + + if (result->length == 0) { + ffParsePropFileData("cmake/lxqt/lxqt-config.cmake", "set ( LXQT_VERSION", result); + } + if (result->length == 0) { + ffParsePropFileData("cmake/lxqt/lxqt-config-version.cmake", "set ( PACKAGE_VERSION", result); + } + + if (result->length == 0) { + // This is really, really, really slow. Thank you, LXQt developers + ffProcessAppendStdOut(result, (char* const[]) { "lxqt-session", "-v", NULL }); + + result->length = 0; // don't set '\0' byte + ffParsePropLines(result->chars, "liblxqt", result); + } +} + +static void getBudgie(FFstrbuf* result, FF_A_UNUSED FFDEOptions* options) { + ffParsePropFileData("budgie/budgie-version.xml", "<str>", result); +} + +static void getUnity(FFstrbuf* result, FF_A_UNUSED FFDEOptions* options) { + if (ffParsePropFile("/usr/bin/unity", "parser = OptionParser(version= \"%prog ", result)) { + ffStrbufSubstrBeforeFirstC(result, '"'); + } +} + +static bool extractTdeVersion(const char* line, uint32_t len, void* userdata) { + int count = 0; + sscanf(line, "R%*d.%*d.%*d%n", &count); + if (count == 0) { + return true; + } + + ffStrbufSetNS((FFstrbuf*) userdata, len, line); + return false; +} + +static const char* getTrinity(FFstrbuf* result, FF_A_UNUSED FFDEOptions* options) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + const char* error = ffFindExecutableInPath("tde-config", &path); + if (error) { + return "Failed to find tde-config path"; + } + + ffStrbufSubstrBeforeLastC(&path, '/'); + ffStrbufAppendS(&path, "/../lib/libtdecore.so"); + + if (ffBinaryExtractStrings(path.chars, extractTdeVersion, result, strlen("R0.0.0")) == NULL) { + return NULL; + } + + ffStrbufClear(&path); + if (ffProcessAppendStdOut(&path, (char* const[]) { "tde-config", "--version", NULL }) == NULL) { + ffParsePropLines(path.chars, "TDE: ", result); + return NULL; + } + + return "All methods failed"; +} + +static const char* getCosmic(FFstrbuf* result, FF_A_UNUSED FFDEOptions* options) { + if (ffProcessAppendStdOut(result, (char* const[]) { "cosmic-comp", "--version", NULL }) == NULL) { + // cosmic-comp 0.1.0 (git commit fa88002ba41d2edec25dd7ffdee9719fbb928fc0) + ffStrbufSubstrAfterFirstC(result, ' '); + ffStrbufSubstrBeforeFirstC(result, ' '); + return NULL; + } + + return "All methods failed"; +} + +static const char* getEnlightenmentByDbus(FF_A_UNUSED FFstrbuf* result) { +#ifdef FF_HAVE_DBUS + FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {}; + if (ffDBusLoadData(DBUS_BUS_SESSION, &dbus) != NULL) { + return "ffDBusLoadData() failed"; + } + + DBusMessage* reply = ffDBusGetMethodReply(&dbus, "org.enlightenment.wm.service", "/org/enlightenment/wm/RemoteObject", "org.enlightenment.wm.Core", "Version", NULL, NULL); + if (!reply) { + return "ffDBusGetMethodReply() failed"; + } + + DBusMessageIter rootIterator; + if (!dbus.lib->ffdbus_message_iter_init(reply, &rootIterator)) { + dbus.lib->ffdbus_message_unref(reply); + return "dbus_message_iter_init() failed"; + } + if (!ffDBusGetString(&dbus, &rootIterator, result)) { + dbus.lib->ffdbus_message_unref(reply); + return "ffDBusGetString() failed"; + } + dbus.lib->ffdbus_message_unref(reply); + + return NULL; +#else // FF_HAVE_DBUS + return "ffDBusLoadData() failed: dbus support not compiled in"; +#endif // FF_HAVE_DBUS +} + +static void getEnlightenment(FFstrbuf* result, FF_A_UNUSED FFDEOptions* options) { + getEnlightenmentByDbus(result); + + if (result->length == 0) { + if (ffProcessAppendStdOut(result, (char* const[]) { "enlightenment", "--version", NULL }) == NULL) { // ...\nVersion: 0.27.1\n... + ffStrbufSubstrAfterFirstS(result, "Version: "); + ffStrbufSubstrBeforeFirstC(result, '\n'); + } + } +} +const char* ffDetectDEVersion(const FFstrbuf* deName, FFstrbuf* result, FFDEOptions* options) { + if (!instance.config.general.detectVersion) { + return "Disabled by config"; + } + + if (ffStrbufEqualS(deName, FF_DE_PRETTY_PLASMA)) { + getKDE(result, options); + } else if (ffStrbufEqualS(deName, FF_DE_PRETTY_GNOME)) { + getGnome(result, options); + } else if (ffStrbufEqualS(deName, FF_DE_PRETTY_CINNAMON)) { + getCinnamon(result, options); + } else if (ffStrbufEqualS(deName, FF_DE_PRETTY_XFCE4)) { + getXFCE4(result, options); + } else if (ffStrbufEqualS(deName, FF_DE_PRETTY_MATE)) { + getMate(result, options); + } else if (ffStrbufEqualS(deName, FF_DE_PRETTY_LXQT)) { + getLXQt(result, options); + } else if (ffStrbufEqualS(deName, FF_DE_PRETTY_BUDGIE)) { + getBudgie(result, options); + } else if (ffStrbufEqualS(deName, FF_DE_PRETTY_UNITY)) { + getUnity(result, options); + } else if (ffStrbufEqualS(deName, "trinity")) { + getTrinity(result, options); + } else if (ffStrbufEqualS(deName, "COSMIC")) { + getCosmic(result, options); + } else if (ffStrbufEqualS(deName, FF_DE_PRETTY_ENLIGHTENMENT)) { + getEnlightenment(result, options); + } else { + return "Unsupported DE"; + } + return NULL; +} diff --git a/src/detection/de/de_nosupport.c b/src/detection/de/de_nosupport.c new file mode 100644 index 0000000..d870ae0 --- /dev/null +++ b/src/detection/de/de_nosupport.c @@ -0,0 +1,5 @@ +#include "de.h" + +const char* ffDetectDEVersion(FF_A_UNUSED const FFstrbuf* deName, FF_A_UNUSED FFstrbuf* result, FF_A_UNUSED FFDEOptions* options) { + return "Not supported on this platform"; +} diff --git a/src/detection/disk/disk.c b/src/detection/disk/disk.c new file mode 100644 index 0000000..c86282d --- /dev/null +++ b/src/detection/disk/disk.c @@ -0,0 +1,55 @@ +#include "disk.h" + +static int compareDisks(const FFDisk* disk1, const FFDisk* disk2) { + return ffStrbufComp(&disk1->mountpoint, &disk2->mountpoint); +} + +const char* ffDetectDisks(FFDiskOptions* options, FFlist* disks) { + const char* error = ffDetectDisksImpl(options, disks); + + if (error) { + return error; + } + if (disks->length == 0) { + return NULL; + } + + // We need to sort the disks, so that we can detect, which disk a path resides on + // For example for /boot/efi/bootmgr we need to check /boot/efi before /boot + // Note that we sort alphabetically here for a better ordering when printing the list, + // so the check must be done in reverse order + ffListSort(disks, sizeof(FFDisk), (void*) compareDisks); + FF_LIST_FOR_EACH (FFDisk, disk, *disks) { + if (disk->bytesTotal == 0) { + disk->type |= FF_DISK_VOLUME_TYPE_UNKNOWN_BIT; + } else { + disk->bytesUsed = disk->bytesTotal - (options->calcType == FF_DISK_CALC_TYPE_FREE ? disk->bytesFree : disk->bytesAvailable); + } + } + + return NULL; +} + +#ifndef _WIN32 + #include <fnmatch.h> + +bool ffDiskMatchesFolderPatterns(FFstrbuf* folders, const char* path, char separator) { + uint32_t startIndex = 0; + while (startIndex < folders->length) { + uint32_t sepIndex = ffStrbufNextIndexC(folders, startIndex, separator); + + char savedSep = folders->chars[sepIndex]; // Can be '\0' if at end + folders->chars[sepIndex] = '\0'; + + bool matched = fnmatch(&folders->chars[startIndex], path, 0) == 0; + folders->chars[sepIndex] = savedSep; + + if (matched) { + return true; + } + + startIndex = sepIndex + 1; + } + return false; +} +#endif diff --git a/src/detection/disk/disk.h b/src/detection/disk/disk.h new file mode 100644 index 0000000..57aaf2f --- /dev/null +++ b/src/detection/disk/disk.h @@ -0,0 +1,40 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/disk/option.h" + +#ifdef _WIN32 + #define FF_DISK_FOLDER_SEPARATOR ';' +#else + #define FF_DISK_FOLDER_SEPARATOR ':' +#endif + +typedef struct FFDisk { + FFstrbuf mountFrom; + FFstrbuf mountpoint; + FFstrbuf filesystem; + FFstrbuf name; + FFDiskVolumeType type; + + uint64_t bytesUsed; + uint64_t bytesFree; + uint64_t bytesAvailable; + uint64_t bytesTotal; + + uint32_t filesUsed; + uint32_t filesTotal; + + uint64_t createTime; +} FFDisk; + +/** + * Returns a List of FFDisk, sorted alphabetically by mountpoint. + * If error is not set, disks contains at least one disk. + */ +const char* ffDetectDisks(FFDiskOptions* options, FFlist* disks /* list of FFDisk */); + +const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks); + +#ifndef _WIN32 +bool ffDiskMatchesFolderPatterns(FFstrbuf* folders, const char* path, char separator); +#endif diff --git a/src/detection/disk/disk_bsd.c b/src/detection/disk/disk_bsd.c new file mode 100644 index 0000000..9156d79 --- /dev/null +++ b/src/detection/disk/disk_bsd.c @@ -0,0 +1,214 @@ +#include "disk.h" +#include "common/mallocHelper.h" +#include "common/strutil.h" + +#include <stdalign.h> +#include <sys/mount.h> +#include <sys/stat.h> + +#ifdef __NetBSD__ + #include <sys/types.h> + #include <sys/statvfs.h> + #define statfs statvfs + #define f_flags f_flag + #define f_bsize f_frsize +#endif + +#ifdef __FreeBSD__ + #if __has_include(<libgeom.h>) + #include <libgeom.h> + +static const char* detectFsLabel(struct statfs* fs, FFDisk* disk) { + if (!ffStrStartsWith(fs->f_mntfromname, "/dev/")) { + return "Only block devices are supported"; + } + + // Detect volume label in geom tree + static struct gmesh geomTree; + static struct gclass* cLabels; + if (!cLabels) { + if (geomTree.lg_ident) { + return "Previous geom_gettree() failed"; + } + + if (geom_gettree(&geomTree) < 0) { + geomTree.lg_ident = (void*) (intptr_t) -1; + return "geom_gettree() failed"; + } + + for (cLabels = geomTree.lg_class.lh_first; cLabels && !ffStrEquals(cLabels->lg_name, "LABEL"); cLabels = cLabels->lg_class.le_next); + if (!cLabels) { + return "Class LABEL is not found"; + } + } + + for (struct ggeom* label = cLabels->lg_geom.lh_first; label; label = label->lg_geom.le_next) { + struct gprovider* provider = label->lg_provider.lh_first; + if (!provider || !ffStrEquals(label->lg_name, fs->f_mntfromname + strlen("/dev/"))) { + continue; + } + const char* str = strchr(provider->lg_name, '/'); + ffStrbufSetS(&disk->name, str ? str + 1 : provider->lg_name); + } + + return NULL; +} + #else +static const char* detectFsLabel(FF_A_UNUSED struct statfs* fs, FF_A_UNUSED FFDisk* disk) { + return "Fastfetch was compiled without libgeom support"; +} + #endif + +static void detectFsInfo(struct statfs* fs, FFDisk* disk) { + if (ffStrbufEqualS(&disk->filesystem, "zfs")) { + disk->type = !ffStrbufStartsWithS(&disk->mountFrom, "zroot/") || ffStrbufStartsWithS(&disk->mountFrom, "zroot/ROOT/") + ? FF_DISK_VOLUME_TYPE_REGULAR_BIT + : FF_DISK_VOLUME_TYPE_SUBVOLUME_BIT; + } else if (fs->f_flags & MNT_IGNORE) { + disk->type = FF_DISK_VOLUME_TYPE_HIDDEN_BIT; + } else if (!(fs->f_flags & MNT_LOCAL)) { + disk->type = FF_DISK_VOLUME_TYPE_EXTERNAL_BIT; + } else { + disk->type = FF_DISK_VOLUME_TYPE_REGULAR_BIT; + } + + detectFsLabel(fs, disk); +} +#elif __APPLE__ + #include "common/apple/cf_helpers.h" + + #include <sys/attr.h> + #include <unistd.h> + + #ifndef MAC_OS_X_VERSION_10_15 + #define MNT_REMOVABLE 0x00000200 + #endif + +struct CmnAttrBuf { + uint32_t length; + attrreference_t nameRef; + char nameSpace[NAME_MAX * 3 + 1]; +} FF_A_PACKED; + +void detectFsInfo(struct statfs* fs, FFDisk* disk) { + if (fs->f_flags & MNT_DONTBROWSE) { + disk->type = FF_DISK_VOLUME_TYPE_HIDDEN_BIT; + } else if (fs->f_flags & MNT_REMOVABLE || !(fs->f_flags & MNT_LOCAL)) { + disk->type = FF_DISK_VOLUME_TYPE_EXTERNAL_BIT; + } else { + disk->type = FF_DISK_VOLUME_TYPE_REGULAR_BIT; + } + + alignas(4) struct CmnAttrBuf attrBuf; + if (getattrlist(disk->mountpoint.chars, &(struct attrlist) { + .bitmapcount = ATTR_BIT_MAP_COUNT, + .commonattr = ATTR_CMN_NAME, + }, + &attrBuf, + sizeof(attrBuf), + 0) == 0) { + ffStrbufInitNS(&disk->name, attrBuf.nameRef.attr_length - 1 /* excluding '\0' */, attrBuf.nameSpace); + } +} +#else +static void detectFsInfo(struct statfs* fs, FFDisk* disk) { + #ifdef MNT_IGNORE + if (fs->f_flags & MNT_IGNORE) { + disk->type = FF_DISK_VOLUME_TYPE_HIDDEN_BIT; + } else + #endif + if (!(fs->f_flags & MNT_LOCAL)) { + disk->type = FF_DISK_VOLUME_TYPE_EXTERNAL_BIT; + } else { + disk->type = FF_DISK_VOLUME_TYPE_REGULAR_BIT; + } +} +#endif + +const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { +#ifndef __NetBSD__ + int size = getfsstat(NULL, 0, MNT_WAIT); + if (size <= 0) { + return "getfsstat(NULL, 0, MNT_WAIT) failed"; + } +#else + int size = getvfsstat(NULL, 0, ST_WAIT); + if (size <= 0) { + return "getvfsstat(NULL, 0, ST_WAIT) failed"; + } +#endif + + FF_AUTO_FREE struct statfs* buf = malloc(sizeof(*buf) * (unsigned) size); +#ifndef __NetBSD__ + if (getfsstat(buf, (int) (sizeof(*buf) * (unsigned) size), MNT_NOWAIT) <= 0) { + return "getfsstat(buf, size, MNT_NOWAIT) failed"; + } +#else + if (getvfsstat(buf, sizeof(*buf) * (unsigned) size, ST_NOWAIT) <= 0) { + return "getvfsstat(buf, size, ST_NOWAIT) failed"; + } +#endif + + for (struct statfs* fs = buf; fs < buf + size; ++fs) { + if (__builtin_expect(options->folders.length > 0, 0)) { + if (!ffStrbufSeparatedContainS(&options->folders, fs->f_mntonname, FF_DISK_FOLDER_SEPARATOR)) { + continue; + } + } else if (!ffStrEquals(fs->f_mntonname, "/") && !ffStrStartsWith(fs->f_mntfromname, "/dev/") && !ffStrEquals(fs->f_fstypename, "zfs") && !ffStrEquals(fs->f_fstypename, "fusefs.sshfs")) { + continue; + } + + if (options->hideFolders.length && ffDiskMatchesFolderPatterns(&options->hideFolders, fs->f_mntonname, FF_DISK_FOLDER_SEPARATOR)) { + continue; + } + + if (options->hideFS.length && ffStrbufSeparatedContainS(&options->hideFS, fs->f_fstypename, ':')) { + continue; + } + +#ifdef __FreeBSD__ + // f_bavail and f_ffree are signed on FreeBSD... + if (fs->f_bavail < 0) { + fs->f_bavail = 0; + } + if (fs->f_ffree < 0) { + fs->f_ffree = 0; + } +#endif + + FFDisk* disk = FF_LIST_ADD(FFDisk, *disks); + + disk->bytesTotal = (uint64_t) fs->f_blocks * (uint64_t) fs->f_bsize; + disk->bytesFree = (uint64_t) fs->f_bfree * (uint64_t) fs->f_bsize; + disk->bytesAvailable = (uint64_t) fs->f_bavail * (uint64_t) fs->f_bsize; + disk->bytesUsed = 0; // To be filled in ./disk.c + + disk->filesTotal = (uint32_t) fs->f_files; + disk->filesUsed = (uint32_t) fs->f_files - (uint32_t) fs->f_ffree; + + ffStrbufInitS(&disk->mountFrom, fs->f_mntfromname); + ffStrbufInitS(&disk->mountpoint, fs->f_mntonname); + ffStrbufInitS(&disk->filesystem, fs->f_fstypename); + ffStrbufInit(&disk->name); + disk->type = 0; + disk->createTime = 0; + + detectFsInfo(fs, disk); + + if (fs->f_flags & MNT_RDONLY) { + disk->type |= FF_DISK_VOLUME_TYPE_READONLY_BIT; + } + +#ifdef __OpenBSD__ + #define st_birthtimespec __st_birthtim +#endif +#ifndef __DragonFly__ + struct stat st; + if (stat(fs->f_mntonname, &st) == 0 && st.st_birthtimespec.tv_sec > 0) { + disk->createTime = (uint64_t) (((uint64_t) st.st_birthtimespec.tv_sec * 1000) + ((uint64_t) st.st_birthtimespec.tv_nsec / 1000000)); + } +#endif + } + + return NULL; +} diff --git a/src/detection/disk/disk_haiku.cpp b/src/detection/disk/disk_haiku.cpp new file mode 100644 index 0000000..0987897 --- /dev/null +++ b/src/detection/disk/disk_haiku.cpp @@ -0,0 +1,74 @@ +extern "C" { +#include "disk.h" +#include "common/strutil.h" +} +#include <fs_info.h> +#include <Directory.h> +#include <Path.h> + +const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { + int32 pos = 0; + + for (dev_t dev; (dev = next_dev(&pos)) >= B_OK;) { + fs_info fs; + if (fs_stat_dev(dev, &fs) < 0) { + continue; + } + + node_ref node(fs.dev, fs.root); + BDirectory dir(&node); + BPath path(&dir); + if (path.InitCheck() != B_OK) { + continue; + } + + if (__builtin_expect(options->folders.length, 0)) { + if (!ffStrbufSeparatedContainS(&options->folders, path.Path(), FF_DISK_FOLDER_SEPARATOR)) { + continue; + } + } + + if (options->hideFolders.length && ffDiskMatchesFolderPatterns(&options->hideFolders, path.Path(), FF_DISK_FOLDER_SEPARATOR)) { + continue; + } + + if (options->hideFS.length && ffStrbufSeparatedContainS(&options->hideFS, fs.fsh_name, ':')) { + continue; + } + + FFDisk* disk = FF_LIST_ADD(FFDisk, *disks); + + disk->bytesTotal = (uint64_t) fs.total_blocks * (uint64_t) fs.block_size; + disk->bytesFree = (uint64_t) fs.free_blocks * (uint64_t) fs.block_size; + disk->bytesAvailable = disk->bytesFree; + disk->bytesUsed = 0; // To be filled in ./disk.c + + disk->filesTotal = (uint32_t) fs.total_nodes; + disk->filesUsed = (uint32_t) (fs.total_nodes - fs.free_nodes); + + ffStrbufInitS(&disk->mountFrom, fs.device_name); + ffStrbufInitS(&disk->mountpoint, path.Path()); + ffStrbufInitS(&disk->filesystem, fs.fsh_name); + ffStrbufInitS(&disk->name, fs.volume_name); + disk->type = FF_DISK_VOLUME_TYPE_NONE; + if (!(fs.flags & B_FS_IS_PERSISTENT)) { + disk->type = (FFDiskVolumeType) (disk->type | FF_DISK_VOLUME_TYPE_HIDDEN_BIT); + } + if (fs.flags & B_FS_IS_READONLY) { + disk->type = (FFDiskVolumeType) (disk->type | FF_DISK_VOLUME_TYPE_READONLY_BIT); + } + if (fs.flags & B_FS_IS_REMOVABLE) { + disk->type = (FFDiskVolumeType) (disk->type | FF_DISK_VOLUME_TYPE_EXTERNAL_BIT); + } + if (disk->type == FF_DISK_VOLUME_TYPE_NONE) { + disk->type = FF_DISK_VOLUME_TYPE_REGULAR_BIT; + } + disk->createTime = 0; + + time_t crTime; + if (dir.GetCreationTime(&crTime) == B_OK) { + disk->createTime = (uint64_t) crTime * 1000; + } + } + return NULL; +} diff --git a/src/detection/disk/disk_linux.c b/src/detection/disk/disk_linux.c new file mode 100644 index 0000000..7205317 --- /dev/null +++ b/src/detection/disk/disk_linux.c @@ -0,0 +1,337 @@ +#include "disk.h" + +#include "common/io.h" +#include "common/strutil.h" + +#include <limits.h> +#include <ctype.h> +#include <dirent.h> +#include <mntent.h> +#include <sys/stat.h> +#include <sys/statvfs.h> + +#if defined(STATX_BTIME) && !defined(__ANDROID__) + #include <sys/syscall.h> +#endif + +#ifdef __USE_LARGEFILE64 + #define stat stat64 + #define statvfs statvfs64 + #define dirent dirent64 + #define readdir readdir64 +#endif + +static bool isPhysicalDevice(const struct mntent* device) { +#ifndef __ANDROID__ // On Android, `/dev` is not accessible, so that the following checks always fail + + // Always show the root path + if (ffStrEquals(device->mnt_dir, "/")) { + return true; + } + + if (ffStrEquals(device->mnt_fsname, "none")) { + return false; + } + + // DrvFs is a filesystem plugin to WSL that was designed to support interop between WSL and the Windows filesystem. + if (ffStrEquals(device->mnt_type, "9p")) { + return ffStrContains(device->mnt_opts, "aname=drvfs"); + } + + // ZFS pool + if (ffStrEquals(device->mnt_type, "zfs")) { + return true; + } + + // sshfs + if (ffStrEquals(device->mnt_type, "fuse.sshfs")) { + return true; + } + + // Pseudo filesystems don't have a device in /dev + if (!ffStrStartsWith(device->mnt_fsname, "/dev/")) { + return false; + } + + // #731 + if (ffStrEquals(device->mnt_type, "bcachefs")) { + return true; + } + + if ( + ffStrStartsWith(device->mnt_fsname + 5, "loop") || // Ignore loop devices + ffStrStartsWith(device->mnt_fsname + 5, "ram") || // Ignore ram devices + ffStrStartsWith(device->mnt_fsname + 5, "fd") // Ignore fd devices + ) { + return false; + } + + if (ffStrStartsWith(device->mnt_dir, "/bedrock/")) { // Ignore Bedrock Linux subvolumes + return false; + } + + struct stat deviceStat; + if (stat(device->mnt_fsname, &deviceStat) != 0) { + return false; + } + + // Ignore all devices that are not block devices + if (!S_ISBLK(deviceStat.st_mode)) { + return false; + } + +#else + + // Pseudo filesystems don't have a device in /dev + if (!ffStrStartsWith(device->mnt_fsname, "/dev/")) { + return false; + } + + if ( + ffStrStartsWith(device->mnt_fsname + 5, "loop") || // Ignore loop devices + ffStrStartsWith(device->mnt_fsname + 5, "ram") || // Ignore ram devices + ffStrStartsWith(device->mnt_fsname + 5, "fd") // Ignore fd devices + ) { + return false; + } + + // https://source.android.com/docs/core/ota/apex?hl=zh-cn + if (ffStrStartsWith(device->mnt_dir, "/apex/")) { + return false; + } + +#endif // __ANDROID__ + + return true; +} + +static void detectNameFromPath(FFDisk* disk, const struct stat* deviceStat, FFstrbuf* basePath) { + FF_AUTO_CLOSE_DIR DIR* dir = opendir(basePath->chars); + if (dir == NULL) { + return; + } + + uint32_t basePathLength = basePath->length; + + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + ffStrbufAppendS(basePath, entry->d_name); + + struct stat entryStat; + bool ret = stat(basePath->chars, &entryStat) == 0; + + ffStrbufSubstrBefore(basePath, basePathLength); + + if (!ret || deviceStat->st_ino != entryStat.st_ino) { + continue; + } + + ffStrbufAppendS(&disk->name, entry->d_name); + break; + } +} + +static void detectName(FFDisk* disk) { + struct stat deviceStat; + if (stat(disk->mountFrom.chars, &deviceStat) != 0) { + return; + } + + FF_STRBUF_AUTO_DESTROY basePath = ffStrbufCreate(); + + // Try label first + ffStrbufSetS(&basePath, "/dev/disk/by-label/"); + detectNameFromPath(disk, &deviceStat, &basePath); + + if (disk->name.length == 0) { + // Try partlabel second + ffStrbufSetS(&basePath, "/dev/disk/by-partlabel/"); + detectNameFromPath(disk, &deviceStat, &basePath); + } + + if (disk->name.length == 0) { + return; + } + + ffStrbufDecodeHexEscapeSequences(&disk->name); +} + +#ifdef __ANDROID__ + +static void detectType(FF_A_UNUSED const FFlist* disks, FFDisk* currentDisk, FF_A_UNUSED struct mntent* device) { + if (ffStrbufEqualS(¤tDisk->mountpoint, "/") || ffStrbufEqualS(¤tDisk->mountpoint, "/storage/emulated")) { + currentDisk->type = FF_DISK_VOLUME_TYPE_REGULAR_BIT; + } else if (ffStrbufStartsWithS(¤tDisk->mountpoint, "/mnt/media_rw/")) { + currentDisk->type = FF_DISK_VOLUME_TYPE_EXTERNAL_BIT; + } else { + currentDisk->type = FF_DISK_VOLUME_TYPE_HIDDEN_BIT; + } +} + +#else + +static bool isSubvolume(const FFlist* disks, FFDisk* currentDisk) { + if (ffStrbufEqualS(¤tDisk->mountFrom, "drvfs")) { // WSL Windows drives + return false; + } + + if (ffStrbufEqualS(¤tDisk->filesystem, "zfs")) { + // ZFS subvolumes + uint32_t index = ffStrbufFirstIndexC(¤tDisk->mountFrom, '/'); + if (index == currentDisk->mountFrom.length) { + return false; + } + + FF_STRBUF_AUTO_DESTROY zpoolName = ffStrbufCreateNS(index, currentDisk->mountFrom.chars); + for (uint32_t i = 0; i < disks->length - 1; i++) { + const FFDisk* otherDevice = FF_LIST_GET(FFDisk, *disks, i); + if (ffStrbufEqualS(&otherDevice->filesystem, "zfs") && ffStrbufStartsWith(&otherDevice->mountFrom, &zpoolName)) { + return true; + } + } + + return false; + } else { + // Filter all disks which device was already found. This catches BTRFS subvolumes. + for (uint32_t i = 0; i < disks->length - 1; i++) { + const FFDisk* otherDevice = FF_LIST_GET(FFDisk, *disks, i); + + if (ffStrbufEqual(¤tDisk->mountFrom, &otherDevice->mountFrom)) { + return true; + } + } + } + + return false; +} + +static bool isRemovable(FFDisk* currentDisk) { + if (!ffStrbufStartsWithS(¤tDisk->mountFrom, "/dev/")) { + return false; + } + + char sysBlockPartition[64]; + snprintf(sysBlockPartition, ARRAY_SIZE(sysBlockPartition), "/sys/class/block/%s", currentDisk->mountFrom.chars + strlen("/dev/")); + + char sysBlockVolume[PATH_MAX]; // /sys/devices/pci0000:00/0000:00:14.0/usb4/4-3/4-3:1.0/host0/target0:0:0/0:0:0:0/block/sda/sda1 + if (realpath(sysBlockPartition, sysBlockVolume) == NULL) { + return false; + } + char* lastSlash = strrchr(sysBlockVolume, '/'); + if (lastSlash == NULL) { + return false; + } + strcpy(lastSlash + 1, "removable"); + + char removableChar = '0'; + return ffReadFileData(sysBlockVolume, 1, &removableChar) > 0 && removableChar == '1'; +} + +static void detectType(const FFlist* disks, FFDisk* currentDisk, struct mntent* device) { + if (hasmntopt(device, "x-gvfs-hide") || hasmntopt(device, "hidden")) { + currentDisk->type = FF_DISK_VOLUME_TYPE_HIDDEN_BIT; + } else if (isSubvolume(disks, currentDisk)) { + currentDisk->type = FF_DISK_VOLUME_TYPE_SUBVOLUME_BIT; + } else if (isRemovable(currentDisk)) { + currentDisk->type = FF_DISK_VOLUME_TYPE_EXTERNAL_BIT; + } else { + currentDisk->type = FF_DISK_VOLUME_TYPE_REGULAR_BIT; + } + if (hasmntopt(device, MNTOPT_RO)) { + currentDisk->type |= FF_DISK_VOLUME_TYPE_READONLY_BIT; + } +} + +#endif + +static void detectStats(FFDisk* disk) { + struct statvfs fs; + if (statvfs(disk->mountpoint.chars, &fs) != 0) { + memset(&fs, 0, sizeof(fs)); // Set all values to 0, so our values get initialized to 0 too + } + + disk->bytesTotal = fs.f_blocks * (uint64_t) fs.f_frsize; + disk->bytesFree = fs.f_bfree * (uint64_t) fs.f_frsize; + disk->bytesAvailable = fs.f_bavail * (uint64_t) fs.f_frsize; + disk->bytesUsed = 0; // To be filled in ./disk.c + + if (fs.f_files >= fs.f_ffree) { + disk->filesTotal = (uint32_t) fs.f_files; + disk->filesUsed = (uint32_t) (disk->filesTotal - fs.f_ffree); + } else { + // Windows filesystem in WSL + disk->filesTotal = disk->filesUsed = 0; + } + + disk->createTime = 0; +#ifdef SYS_statx + struct statx stx; + if (syscall(SYS_statx, 0, disk->mountpoint.chars, 0, STATX_BTIME, &stx) == 0 && (stx.stx_mask & STATX_BTIME) && stx.stx_btime.tv_sec > 685065600 /*birth of Linux*/) { + disk->createTime = (uint64_t) ((stx.stx_btime.tv_sec * 1000) + (stx.stx_btime.tv_nsec / 1000000)); + } +#endif + +#ifdef __ANDROID__ // hasmntopt requires a higher Android API level + if (fs.f_flag & ST_RDONLY) { + disk->type |= FF_DISK_VOLUME_TYPE_READONLY_BIT; + } +#endif +} + +const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { + FILE* mountsFile = setmntent("/proc/mounts", "r"); + if (mountsFile == NULL) { + return "setmntent(\"/proc/mounts\", \"r\") == NULL"; + } + + struct mntent* device; + + while ((device = getmntent(mountsFile))) { + if (__builtin_expect(options->folders.length > 0, false)) { + if (!ffStrbufSeparatedContainS(&options->folders, device->mnt_dir, FF_DISK_FOLDER_SEPARATOR)) { + continue; + } + } else if (!isPhysicalDevice(device)) { + continue; + } + + if (options->hideFolders.length && ffDiskMatchesFolderPatterns(&options->hideFolders, device->mnt_dir, FF_DISK_FOLDER_SEPARATOR)) { + continue; + } + + if (options->hideFS.length && ffStrbufSeparatedContainS(&options->hideFS, device->mnt_type, ':')) { + continue; + } + + // We have a valid device, add it to the list + FFDisk* disk = FF_LIST_ADD(FFDisk, *disks); + disk->type = FF_DISK_VOLUME_TYPE_NONE; + + // detect mountFrom + ffStrbufInitS(&disk->mountFrom, device->mnt_fsname); + + // detect mountpoint + ffStrbufInitS(&disk->mountpoint, device->mnt_dir); + + // detect filesystem + ffStrbufInitS(&disk->filesystem, device->mnt_type); + + // detect name + ffStrbufInit(&disk->name); + detectName(disk); // Also detects external devices + + // detect type + detectType(disks, disk, device); + + // Detects stats + detectStats(disk); + } + + endmntent(mountsFile); + + return NULL; +} diff --git a/src/detection/disk/disk_nosupport.c b/src/detection/disk/disk_nosupport.c new file mode 100644 index 0000000..eabddbc --- /dev/null +++ b/src/detection/disk/disk_nosupport.c @@ -0,0 +1,5 @@ +#include "disk.h" + +const char* ffDetectDisksImpl(FF_A_UNUSED FFDiskOptions* options, FF_A_UNUSED FFlist* disks) { + return "Not supported on this platform"; +} diff --git a/src/detection/disk/disk_sunos.c b/src/detection/disk/disk_sunos.c new file mode 100644 index 0000000..3d811e0 --- /dev/null +++ b/src/detection/disk/disk_sunos.c @@ -0,0 +1,145 @@ +#include "disk.h" +#include "common/io.h" +#include "common/strutil.h" + +#include <sys/mntent.h> +#include <sys/stat.h> +#include <sys/statvfs.h> +#include <sys/mount.h> +#include <sys/mnttab.h> + +static bool isPhysicalDevice(const struct mnttab* device) { + // Always show the root path + if (ffStrEquals(device->mnt_mountp, "/")) { + return true; + } + + if (ffStrEquals(device->mnt_special, "none")) { + return false; + } + + // ZFS pool + if (ffStrEquals(device->mnt_fstype, "zfs")) { + return true; + } + + // Pseudo filesystems don't have a device in /dev + if (!ffStrStartsWith(device->mnt_special, "/dev/")) { + return false; + } + + struct stat deviceStat; + if (stat(device->mnt_special, &deviceStat) != 0) { + return false; + } + + // Ignore all devices that are not block devices + if (!S_ISBLK(deviceStat.st_mode)) { + return false; + } + + return true; +} + +static bool isSubvolume(const FFlist* disks, FFDisk* currentDisk) { + if (ffStrbufEqualS(¤tDisk->filesystem, "zfs")) { + // ZFS subvolumes + uint32_t index = ffStrbufFirstIndexC(¤tDisk->mountFrom, '/'); + if (index == currentDisk->mountFrom.length) { + return false; + } + + FF_STRBUF_AUTO_DESTROY zpoolName = ffStrbufCreateNS(index, currentDisk->mountFrom.chars); + for (uint32_t i = 0; i < disks->length - 1; i++) { + const FFDisk* otherDevice = FF_LIST_GET(FFDisk, *disks, i); + if (ffStrbufEqualS(&otherDevice->filesystem, "zfs") && ffStrbufStartsWith(&otherDevice->mountFrom, &zpoolName)) { + return true; + } + } + + return false; + } else { + // Filter all disks which device was already found. This catches BTRFS subvolumes. + for (uint32_t i = 0; i < disks->length - 1; i++) { + const FFDisk* otherDevice = FF_LIST_GET(FFDisk, *disks, i); + + if (ffStrbufEqual(¤tDisk->mountFrom, &otherDevice->mountFrom)) { + return true; + } + } + } + + return false; +} + +static void detectType(const FFlist* disks, FFDisk* currentDisk, struct mnttab* device) { + if (hasmntopt(device, MNTOPT_NOBROWSE)) { + currentDisk->type = FF_DISK_VOLUME_TYPE_HIDDEN_BIT; + } else if (isSubvolume(disks, currentDisk)) { + currentDisk->type = FF_DISK_VOLUME_TYPE_SUBVOLUME_BIT; + } else { + currentDisk->type = FF_DISK_VOLUME_TYPE_REGULAR_BIT; + } + if (hasmntopt(device, MNTOPT_RO)) { + currentDisk->type |= FF_DISK_VOLUME_TYPE_READONLY_BIT; + } +} + +static void detectStats(FFDisk* disk) { + struct statvfs fs; + if (statvfs(disk->mountpoint.chars, &fs) != 0) { + memset(&fs, 0, sizeof(fs)); + } + + disk->bytesTotal = fs.f_blocks * fs.f_frsize; + disk->bytesFree = fs.f_bfree * fs.f_frsize; + disk->bytesAvailable = fs.f_bavail * fs.f_frsize; + disk->bytesUsed = 0; // To be filled in ./disk.c + + disk->filesTotal = (uint32_t) fs.f_files; + disk->filesUsed = (uint32_t) (disk->filesTotal - fs.f_ffree); + + ffStrbufSetS(&disk->name, fs.f_fstr); + ffStrbufTrimRightSpace(&disk->name); + + disk->createTime = 0; +} + +const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { + FF_AUTO_CLOSE_FILE FILE* mountsFile = fopen(MNTTAB, "r"); + if (mountsFile == NULL) { + return "fopen(\"" MNTTAB "\", \"r\") == NULL"; + } + + struct mnttab device; + + while (getmntent(mountsFile, &device) == 0) { + if (__builtin_expect(options->folders.length, 0)) { + if (!ffStrbufSeparatedContainS(&options->folders, device.mnt_mountp, FF_DISK_FOLDER_SEPARATOR)) { + continue; + } + } else if (!isPhysicalDevice(&device)) { + continue; + } + + if (options->hideFolders.length && ffDiskMatchesFolderPatterns(&options->hideFolders, device.mnt_mountp, FF_DISK_FOLDER_SEPARATOR)) { + continue; + } + + if (options->hideFS.length && ffStrbufSeparatedContainS(&options->hideFS, device.mnt_fstype, ':')) { + continue; + } + + // We have a valid device, add it to the list + FFDisk* disk = FF_LIST_ADD(FFDisk, *disks); + disk->type = FF_DISK_VOLUME_TYPE_NONE; + ffStrbufInitS(&disk->mountFrom, device.mnt_special); + ffStrbufInitS(&disk->mountpoint, device.mnt_mountp); + ffStrbufInitS(&disk->filesystem, device.mnt_fstype); + ffStrbufInit(&disk->name); + detectType(disks, disk, &device); + detectStats(disk); + } + + return NULL; +} diff --git a/src/detection/disk/disk_windows.c b/src/detection/disk/disk_windows.c new file mode 100644 index 0000000..0b3c93d --- /dev/null +++ b/src/detection/disk/disk_windows.c @@ -0,0 +1,128 @@ +#include "disk.h" +#include "common/io.h" +#include "common/time.h" +#include "common/windows/unicode.h" +#include "common/windows/nt.h" + +#include <windows.h> +#include <winioctl.h> +#include <ntstatus.h> +#include <stdalign.h> + +const char* ffDetectDisksImpl(FFDiskOptions* options, FFlist* disks) { + PROCESS_DEVICEMAP_INFORMATION_EX info = {}; + ULONG size = 0; + if (!NT_SUCCESS(NtQueryInformationProcess(NtCurrentProcess(), ProcessDeviceMap, &info, sizeof(info), &size))) { + return "NtQueryInformationProcess(ProcessDeviceMap) failed"; + } + + // For cross-platform portability; used by `presets/examples/13.jsonc` + if (options->folders.length == 1 && options->folders.chars[0] == '/') { + options->folders.chars[0] = (char) SharedUserData->NtSystemRoot[0]; + ffStrbufAppendS(&options->folders, ":\\"); + } + + wchar_t mountpointW[] = L"X:\\"; + char mountpointA[] = "X:\\"; + + for (wchar_t i = L'A'; i <= L'Z'; i++) { + if (!(info.Query.DriveMap & (1 << (i - L'A')))) { + continue; + } + mountpointW[0] = i; + mountpointA[0] = (char) i; + + UINT driveType = info.Query.DriveType[i - L'A']; + + if (__builtin_expect((long) options->folders.length, 0)) { + if (!ffStrbufSeparatedContainNS(&options->folders, 3, mountpointA, FF_DISK_FOLDER_SEPARATOR)) { + continue; + } + } else if (driveType == DRIVE_NO_ROOT_DIR) { + continue; + } + + if (options->hideFolders.length && ffStrbufSeparatedContainNS(&options->hideFolders, 3, mountpointA, FF_DISK_FOLDER_SEPARATOR)) { + continue; + } + + FF_AUTO_CLOSE_FD HANDLE handle = CreateFileW(mountpointW, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (handle == INVALID_HANDLE_VALUE) { + continue; + } + + IO_STATUS_BLOCK iosb; + + alignas(FILE_FS_ATTRIBUTE_INFORMATION) uint8_t bufFsAttr[1024]; + FILE_FS_ATTRIBUTE_INFORMATION* fsAttr = NT_SUCCESS(NtQueryVolumeInformationFile(handle, &iosb, bufFsAttr, sizeof(bufFsAttr), FileFsAttributeInformation)) + ? (FILE_FS_ATTRIBUTE_INFORMATION*) bufFsAttr + : NULL; + + FF_STRBUF_AUTO_DESTROY diskFileSystemBuf = ffStrbufCreate(); + if (fsAttr) { + ffStrbufSetNWS(&diskFileSystemBuf, fsAttr->FileSystemNameLength / sizeof(WCHAR), fsAttr->FileSystemName); + if (options->hideFS.length && ffStrbufSeparatedContain(&options->hideFS, &diskFileSystemBuf, ':')) { + continue; + } + } + + FFDisk* disk = FF_LIST_ADD(FFDisk, *disks); + + disk->filesUsed = 0; + disk->filesTotal = 0; + disk->bytesTotal = 0; + disk->bytesFree = 0; + disk->bytesUsed = 0; // To be filled in ./disk.c + disk->bytesAvailable = 0; + disk->createTime = 0; + ffStrbufInit(&disk->filesystem); + ffStrbufInit(&disk->name); + ffStrbufInitNS(&disk->mountpoint, 3, mountpointA); + ffStrbufInit(&disk->mountFrom); + disk->type = driveType == DRIVE_REMOVABLE || driveType == DRIVE_REMOTE || driveType == DRIVE_CDROM + ? FF_DISK_VOLUME_TYPE_EXTERNAL_BIT + : driveType == DRIVE_FIXED + ? FF_DISK_VOLUME_TYPE_REGULAR_BIT + : FF_DISK_VOLUME_TYPE_HIDDEN_BIT; + + { + wchar_t volumeName[MAX_PATH + 1]; + mountpointW[2] = L'\0'; + if (QueryDosDeviceW(mountpointW, volumeName, ARRAY_SIZE(volumeName))) { + ffStrbufSetWS(&disk->mountFrom, volumeName); + } + mountpointW[2] = L'\\'; + } + + alignas(FILE_FS_VOLUME_INFORMATION) uint8_t bufFsVolume[1024]; + FILE_FS_VOLUME_INFORMATION* fsVolume = NT_SUCCESS(NtQueryVolumeInformationFile(handle, &iosb, bufFsVolume, sizeof(bufFsVolume), FileFsVolumeInformation)) + ? (FILE_FS_VOLUME_INFORMATION*) bufFsVolume + : NULL; + + if (fsVolume) { + if (fsVolume->VolumeLabelLength > 0) { + ffStrbufSetNWS(&disk->name, fsVolume->VolumeLabelLength / sizeof(WCHAR), fsVolume->VolumeLabel); + } + if (fsVolume->VolumeCreationTime.QuadPart) { + disk->createTime = ffFileTimeToUnixMs((uint64_t) fsVolume->VolumeCreationTime.QuadPart); + } + } + + if (fsAttr) { + ffStrbufInitMove(&disk->filesystem, &diskFileSystemBuf); + if (fsAttr->FileSystemAttributes & FILE_READ_ONLY_VOLUME) { + disk->type |= FF_DISK_VOLUME_TYPE_READONLY_BIT; + } + } + + FILE_FS_FULL_SIZE_INFORMATION fsFullSize; + if (NT_SUCCESS(NtQueryVolumeInformationFile(handle, &iosb, &fsFullSize, sizeof(fsFullSize), FileFsFullSizeInformation))) { + uint64_t units = fsFullSize.BytesPerSector * fsFullSize.SectorsPerAllocationUnit; + disk->bytesTotal = (uint64_t) fsFullSize.TotalAllocationUnits.QuadPart * units; + disk->bytesFree = (uint64_t) fsFullSize.ActualAvailableAllocationUnits.QuadPart * units; + disk->bytesAvailable = (uint64_t) fsFullSize.CallerAvailableAllocationUnits.QuadPart * units; + } + } + + return NULL; +} diff --git a/src/detection/diskio/diskio.c b/src/detection/diskio/diskio.c new file mode 100644 index 0000000..6ff66be --- /dev/null +++ b/src/detection/diskio/diskio.c @@ -0,0 +1,88 @@ +#include "diskio.h" + +#include "common/time.h" + +const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options); + +static FFlist ioCounters1; +static uint64_t time1; + +void ffPrepareDiskIO(FFDiskIOOptions* options) { + if (options->detectTotal) { + return; + } + + if (time1 != 0) { + return; // Already prepared + } + + ffListInit(&ioCounters1); + ffDiskIOGetIoCounters(&ioCounters1, options); + time1 = ffTimeGetNow(); +} + +const char* ffDetectDiskIO(FFlist* result, FFDiskIOOptions* options) { + const char* error = NULL; + + if (options->detectTotal) { + error = ffDiskIOGetIoCounters(result, options); + if (error) { + return error; + } + return NULL; + } + + if (time1 == 0) { + ffListInit(&ioCounters1); + error = ffDiskIOGetIoCounters(&ioCounters1, options); + if (error) { + return error; + } + time1 = ffTimeGetNow(); + } + + if (ioCounters1.length == 0) { + return "No physical disk found"; + } + + uint64_t time2 = ffTimeGetNow(); + while (time2 - time1 < options->waitTime) { + ffTimeSleep((uint32_t) (options->waitTime - (time2 - time1))); + time2 = ffTimeGetNow(); + } + + error = ffDiskIOGetIoCounters(result, options); + if (error) { + return error; + } + + if (result->length != ioCounters1.length) { + return "Different number of physical disks. Hardware change?"; + } + + for (uint32_t i = 0; i < result->length; ++i) { + FFDiskIOResult* icPrev = FF_LIST_GET(FFDiskIOResult, ioCounters1, i); + FFDiskIOResult* icCurr = FF_LIST_GET(FFDiskIOResult, *result, i); + if (!ffStrbufEqual(&icPrev->devPath, &icCurr->devPath)) { + return "Physical disk device path changed"; + } + + static_assert(sizeof(FFDiskIOResult) - offsetof(FFDiskIOResult, bytesRead) == sizeof(uint64_t) * 4, "Unexpected struct FFDiskIOResult layout"); + for (size_t off = offsetof(FFDiskIOResult, bytesRead); off < sizeof(FFDiskIOResult); off += sizeof(uint64_t)) { + uint64_t* prevValue = (uint64_t*) ((uint8_t*) icPrev + off); + uint64_t* currValue = (uint64_t*) ((uint8_t*) icCurr + off); + uint64_t temp = *currValue; + *currValue -= *prevValue; + *currValue /= (time2 - time1) / 1000 /* seconds */; + + // For next function call + *prevValue = temp; + } + } + + // For next function call + time1 = time2; + // Leak ioCounters1 here + + return NULL; +} diff --git a/src/detection/diskio/diskio.h b/src/detection/diskio/diskio.h new file mode 100644 index 0000000..39c665a --- /dev/null +++ b/src/detection/diskio/diskio.h @@ -0,0 +1,15 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/diskio/option.h" + +typedef struct FFDiskIOResult { + FFstrbuf name; + FFstrbuf devPath; + uint64_t bytesRead; + uint64_t readCount; + uint64_t bytesWritten; + uint64_t writeCount; +} FFDiskIOResult; + +const char* ffDetectDiskIO(FFlist* result, FFDiskIOOptions* options); diff --git a/src/detection/diskio/diskio_apple.c b/src/detection/diskio/diskio_apple.c new file mode 100644 index 0000000..dc28907 --- /dev/null +++ b/src/detection/diskio/diskio_apple.c @@ -0,0 +1,57 @@ +#include "diskio.h" +#include "common/apple/cf_helpers.h" + +#include <IOKit/IOKitLib.h> +#include <IOKit/IOBSD.h> +#include <IOKit/storage/IOMedia.h> +#include <IOKit/storage/IOBlockStorageDriver.h> +#include <IOKit/storage/IOStorageDeviceCharacteristics.h> +#include <IOKit/storage/IOStorageProtocolCharacteristics.h> + +const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { + FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = 0; + if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching(kIOBlockStorageDriverClass), &iterator) != KERN_SUCCESS) { + return "IOServiceGetMatchingServices() failed"; + } + + io_registry_entry_t registryEntry; + while ((registryEntry = IOIteratorNext(iterator)) != IO_OBJECT_NULL) { + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryDriver = registryEntry; + + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryMedia = IO_OBJECT_NULL; + if (IORegistryEntryGetChildEntry(entryDriver, kIOServicePlane, &entryMedia) != KERN_SUCCESS) { + continue; + } + + io_name_t deviceName; + if (IORegistryEntryGetName(entryMedia, deviceName) != KERN_SUCCESS) { + continue; + } + + if (options->namePrefix.length && strncmp(deviceName, options->namePrefix.chars, options->namePrefix.length) != 0) { + continue; + } + + FF_CFTYPE_AUTO_RELEASE CFDictionaryRef statistics = IORegistryEntryCreateCFProperty(entryDriver, CFSTR(kIOBlockStorageDriverStatisticsKey), kCFAllocatorDefault, kNilOptions); + if (!statistics) { + continue; + } + + FFDiskIOResult* device = FF_LIST_ADD(FFDiskIOResult, *result); + ffStrbufInitS(&device->name, deviceName); + ffStrbufInit(&device->devPath); + + ffCfDictGetInt64(statistics, CFSTR(kIOBlockStorageDriverStatisticsBytesReadKey), (int64_t*) &device->bytesRead); + ffCfDictGetInt64(statistics, CFSTR(kIOBlockStorageDriverStatisticsBytesWrittenKey), (int64_t*) &device->bytesWritten); + ffCfDictGetInt64(statistics, CFSTR(kIOBlockStorageDriverStatisticsReadsKey), (int64_t*) &device->readCount); + ffCfDictGetInt64(statistics, CFSTR(kIOBlockStorageDriverStatisticsWritesKey), (int64_t*) &device->writeCount); + + FF_CFTYPE_AUTO_RELEASE CFStringRef bsdName = IORegistryEntryCreateCFProperty(entryMedia, CFSTR(kIOBSDNameKey), kCFAllocatorDefault, kNilOptions); + if (bsdName) { + ffCfStrGetString(bsdName, &device->devPath); + ffStrbufPrependS(&device->devPath, "/dev/"); + } + } + + return NULL; +} diff --git a/src/detection/diskio/diskio_bsd.c b/src/detection/diskio/diskio_bsd.c new file mode 100644 index 0000000..f5eb4b8 --- /dev/null +++ b/src/detection/diskio/diskio_bsd.c @@ -0,0 +1,120 @@ +#include "diskio.h" + +#if __has_include(<libgeom.h>) + + #include "common/strutil.h" + + #include <devstat.h> + #include <memory.h> + #include <fcntl.h> + #include <sys/ioctl.h> + #include <sys/disk.h> + #include <libgeom.h> + +const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { + FF_A_CLEANUP(geom_deletetree) struct gmesh geomTree = {}; + if (geom_gettree(&geomTree) < 0) { + return "geom_gettree() failed"; + } + + if (geom_stats_open() < 0) { + return "geom_stats_open() failed"; + } + + void* snap = geom_stats_snapshot_get(); + if (!snap) { + return "geom_stats_snapshot_get() failed"; + } + + struct devstat* snapIter; + while ((snapIter = geom_stats_snapshot_next(snap)) != NULL) { + if (snapIter->device_type & DEVSTAT_TYPE_PASS) { + continue; + } + struct gident* geomId = geom_lookupid(&geomTree, snapIter->id); + if (geomId == NULL) { + continue; + } + if (geomId->lg_what != ISPROVIDER) { + continue; + } + struct gprovider* provider = (struct gprovider*) geomId->lg_ptr; + if (provider->lg_geom->lg_rank != 1) { + continue; + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + for (struct gconfig* ptr = provider->lg_config.lh_first; ptr; ptr = ptr->lg_config.le_next) { + if (ffStrEquals(ptr->lg_name, "descr")) { + ffStrbufSetS(&name, ptr->lg_val); + } + } + if (name.length == 0) { + ffStrbufSetS(&name, provider->lg_name); + } + + if (options->namePrefix.length && !ffStrbufStartsWith(&name, &options->namePrefix)) { + continue; + } + + FFDiskIOResult* device = FF_LIST_ADD(FFDiskIOResult, *result); + ffStrbufInitF(&device->devPath, "/dev/%s", provider->lg_name); + device->bytesRead = snapIter->bytes[DEVSTAT_READ]; + device->readCount = snapIter->operations[DEVSTAT_READ]; + device->bytesWritten = snapIter->bytes[DEVSTAT_WRITE]; + device->writeCount = snapIter->operations[DEVSTAT_WRITE]; + ffStrbufInitMove(&device->name, &name); + } + + geom_stats_snapshot_free(snap); + geom_stats_close(); + + return NULL; +} + +#else + + #include <devstat.h> + #include <memory.h> + +const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { + if (checkversion() < 0) { + return "checkversion() failed"; + } + + struct statinfo stats = { + .dinfo = (struct devinfo*) calloc(1, sizeof(struct devinfo)), + }; + if (getdevs(&stats) < 0) { + return "getdevs() failed"; + } + + for (int i = 0; i < stats.dinfo->numdevs; i++) { + struct devstat* current = &stats.dinfo->devices[i]; + if (current->device_type & DEVSTAT_TYPE_PASS) { + continue; + } + + char deviceName[128]; + snprintf(deviceName, sizeof(deviceName), "%s%d", current->device_name, current->unit_number); + + if (options->namePrefix.length && strncmp(deviceName, options->namePrefix.chars, options->namePrefix.length) != 0) { + continue; + } + + FFDiskIOResult* device = FF_LIST_ADD(FFDiskIOResult, *result); + ffStrbufInitS(&device->name, deviceName); + ffStrbufInitF(&device->devPath, "/dev/%s", deviceName); + device->bytesRead = current->bytes_read; + device->readCount = current->num_reads; + device->bytesWritten = current->bytes_written; + device->writeCount = current->num_writes; + } + + free(stats.dinfo->mem_ptr); + free(stats.dinfo); + + return NULL; +} + +#endif diff --git a/src/detection/diskio/diskio_linux.c b/src/detection/diskio/diskio_linux.c new file mode 100644 index 0000000..c7ddc55 --- /dev/null +++ b/src/detection/diskio/diskio_linux.c @@ -0,0 +1,99 @@ +#include "diskio.h" +#include "common/io.h" +#include "common/properties.h" +#include "common/strutil.h" + +#include <ctype.h> +#include <limits.h> +#include <inttypes.h> +#include <fcntl.h> + +static const char* parseDiskIOCounters(int dfd, const char* devName, FFlist* result, FFDiskIOOptions* options) { + FF_AUTO_CLOSE_FD int devfd = openat(dfd, "device", O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY); + if (devfd < 0) { + return "virtual device"; + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + + { + if (ffAppendFileBufferRelative(devfd, "vendor", &name)) { + ffStrbufTrimRightSpace(&name); + if (name.length > 0) { + ffStrbufAppendC(&name, ' '); + } + } + + ffAppendFileBufferRelative(devfd, "model", &name); + ffStrbufTrimRightSpace(&name); + + if (name.length == 0) { + ffStrbufSetS(&name, devName); + } else if (ffStrStartsWith(devName, "nvme")) { + int devid, nsid; + if (sscanf(devName, "nvme%dn%d", &devid, &nsid) == 2) { + bool multiNs = nsid > 1; + if (!multiNs) { + char pathSysBlock[16]; + snprintf(pathSysBlock, ARRAY_SIZE(pathSysBlock), "nvme%dn2", devid); + multiNs = faccessat(devfd, pathSysBlock, F_OK, 0) == 0; + } + if (multiNs) { + // In Asahi Linux, there are multiple namespaces for the same NVMe drive. + ffStrbufAppendF(&name, " - %d", nsid); + } + } + } + + if (options->namePrefix.length && !ffStrbufStartsWith(&name, &options->namePrefix)) { + return "ignored"; + } + } + + // I/Os merges sectors ticks ... + uint64_t nRead, sectorRead, nWritten, sectorWritten; + { + char sysBlockStat[PROC_FILE_BUFFSIZ]; + ssize_t fileSize = ffReadFileDataRelative(dfd, "stat", ARRAY_SIZE(sysBlockStat) - 1, sysBlockStat); + if (fileSize <= 0) { + return "failed to read stat file"; + } + sysBlockStat[fileSize] = '\0'; + if (sscanf(sysBlockStat, "%" PRIu64 "%*u%" PRIu64 "%*u%" PRIu64 "%*u%" PRIu64 "%*u", &nRead, §orRead, &nWritten, §orWritten) <= 0) { + return "invalid stat file format"; + } + } + + FFDiskIOResult* device = FF_LIST_ADD(FFDiskIOResult, *result); + ffStrbufInitMove(&device->name, &name); + ffStrbufInitF(&device->devPath, "/dev/%s", devName); + device->bytesRead = sectorRead * 512; + device->bytesWritten = sectorWritten * 512; + device->readCount = nRead; + device->writeCount = nWritten; + + return NULL; +} + +const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { + FF_AUTO_CLOSE_DIR DIR* sysBlockDirp = opendir("/sys/block/"); + if (sysBlockDirp == NULL) { + return "opendir(\"/sys/block/\") == NULL"; + } + + struct dirent* sysBlockEntry; + while ((sysBlockEntry = readdir(sysBlockDirp)) != NULL) { + const char* const devName = sysBlockEntry->d_name; + + if (devName[0] == '.') { + continue; + } + + FF_AUTO_CLOSE_FD int dfd = openat(dirfd(sysBlockDirp), devName, O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY); + if (dfd > 0) { + parseDiskIOCounters(dfd, devName, result, options); + } + } + + return NULL; +} diff --git a/src/detection/diskio/diskio_nbsd.c b/src/detection/diskio/diskio_nbsd.c new file mode 100644 index 0000000..9ca9578 --- /dev/null +++ b/src/detection/diskio/diskio_nbsd.c @@ -0,0 +1,50 @@ +#include "diskio.h" +#include "common/io.h" +#include "common/strutil.h" +#include "common/mallocHelper.h" + +#include <sys/iostat.h> +#include <sys/sysctl.h> + +const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { + int mib[] = { CTL_HW, HW_IOSTATS, sizeof(struct io_sysctl) }; + size_t len; + if (sysctl(mib, ARRAY_SIZE(mib), NULL, &len, NULL, 0) < 0) { + return "sysctl({HW_IOSTATS}, NULL) failed"; + } + uint32_t nDrive = (uint32_t) (len / sizeof(struct io_sysctl)); + + FF_AUTO_FREE struct io_sysctl* stats = malloc(len); + + if (sysctl(mib, ARRAY_SIZE(mib), stats, &len, NULL, 0) < 0) { + return "sysctl({HW_IOSTATS}, stats) failed"; + } + + char path[64] = "/dev/"; + + for (uint32_t i = 0; i < nDrive; ++i) { + struct io_sysctl* st = &stats[i]; + + if (options->namePrefix.length && strncmp(st->name, options->namePrefix.chars, options->namePrefix.length) != 0) { + continue; + } + + // Skip partitions + char* end = ffStrCopy(&path[5], st->name, ARRAY_SIZE(path) - 8); + *end++ = 'c'; + *end = '\0'; + if (!ffPathExists(path, FF_PATHTYPE_ANY)) { + continue; + } + + FFDiskIOResult* device = FF_LIST_ADD(FFDiskIOResult, *result); + ffStrbufInitNS(&device->devPath, (uint32_t) (end - path), path); + ffStrbufInitS(&device->name, st->name); + device->bytesRead = st->rbytes; + device->readCount = st->rxfer; + device->bytesWritten = st->wbytes; + device->writeCount = st->wxfer; + } + + return NULL; +} diff --git a/src/detection/diskio/diskio_nosupport.c b/src/detection/diskio/diskio_nosupport.c new file mode 100644 index 0000000..f5de15d --- /dev/null +++ b/src/detection/diskio/diskio_nosupport.c @@ -0,0 +1,5 @@ +#include "diskio.h" + +const char* ffDiskIOGetIoCounters(FF_A_UNUSED FFlist* result, FF_A_UNUSED FFDiskIOOptions* options) { + return "Not supported on this platform"; +} diff --git a/src/detection/diskio/diskio_obsd.c b/src/detection/diskio/diskio_obsd.c new file mode 100644 index 0000000..8dbc67e --- /dev/null +++ b/src/detection/diskio/diskio_obsd.c @@ -0,0 +1,39 @@ +#include "diskio.h" +#include "common/strutil.h" +#include "common/mallocHelper.h" + +#include <sys/disk.h> +#include <sys/sysctl.h> + +const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { + int mib[] = { CTL_HW, HW_DISKSTATS }; + size_t len; + if (sysctl(mib, ARRAY_SIZE(mib), NULL, &len, NULL, 0) < 0) { + return "sysctl({HW_DISKSTATS}, NULL) failed"; + } + uint32_t nDrive = (uint32_t) (len / sizeof(struct diskstats)); + + FF_AUTO_FREE struct diskstats* stats = malloc(len); + + if (sysctl(mib, ARRAY_SIZE(mib), stats, &len, NULL, 0) < 0) { + return "sysctl({HW_DISKSTATS}, stats) failed"; + } + + for (uint32_t i = 0; i < nDrive; ++i) { + struct diskstats* st = &stats[i]; + + if (options->namePrefix.length && strncmp(st->ds_name, options->namePrefix.chars, options->namePrefix.length) != 0) { + continue; + } + + FFDiskIOResult* device = FF_LIST_ADD(FFDiskIOResult, *result); + ffStrbufInitF(&device->devPath, "/dev/%s", st->ds_name); + ffStrbufInitS(&device->name, st->ds_name); + device->bytesRead = st->ds_rbytes; + device->readCount = st->ds_rxfer; + device->bytesWritten = st->ds_wbytes; + device->writeCount = st->ds_wxfer; + } + + return NULL; +} diff --git a/src/detection/diskio/diskio_sunos.c b/src/detection/diskio/diskio_sunos.c new file mode 100644 index 0000000..7cda420 --- /dev/null +++ b/src/detection/diskio/diskio_sunos.c @@ -0,0 +1,42 @@ +#include "diskio.h" +#include "common/strutil.h" +#include <kstat.h> + +static inline void kstatFreeWrap(kstat_ctl_t** pkc) { + assert(pkc); + if (*pkc) { + kstat_close(*pkc); + } +} + +const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { + FF_A_CLEANUP(kstatFreeWrap) kstat_ctl_t* kc = kstat_open(); + if (!kc) { + return "kstat_open() failed"; + } + + for (kstat_t* ks = kc->kc_chain; ks; ks = ks->ks_next) { + if (ks->ks_type != KSTAT_TYPE_IO || !ffStrEquals(ks->ks_class, "disk")) { + continue; + } + + if (options->namePrefix.length && strncmp(ks->ks_name, options->namePrefix.chars, options->namePrefix.length) != 0) { + continue; + } + + kstat_io_t kio; + if (kstat_read(kc, ks, &kio) < 0) { + continue; + } + + FFDiskIOResult* device = FF_LIST_ADD(FFDiskIOResult, *result); + ffStrbufInit(&device->devPath); // unlike other platforms, `/dev/ks_name` is not available + ffStrbufInitS(&device->name, ks->ks_name); + device->bytesRead = kio.nread; + device->readCount = kio.reads; + device->bytesWritten = kio.nwritten; + device->writeCount = kio.writes; + } + + return NULL; +} diff --git a/src/detection/diskio/diskio_windows.c b/src/detection/diskio/diskio_windows.c new file mode 100644 index 0000000..80ad94e --- /dev/null +++ b/src/detection/diskio/diskio_windows.c @@ -0,0 +1,101 @@ +#include "diskio.h" +#include "common/io.h" +#include "common/windows/unicode.h" + +#include <windows.h> +#include <winioctl.h> + +static bool detectPhysicalDisk(const wchar_t* szDevice, FFlist* result, FFDiskIOOptions* options) { + FF_AUTO_CLOSE_FD HANDLE hDevice = CreateFileW(szDevice, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + if (hDevice == INVALID_HANDLE_VALUE) { + return false; + } + + DWORD retSize; + char sddBuffer[4096]; + if (!DeviceIoControl( + hDevice, + IOCTL_STORAGE_QUERY_PROPERTY, + &(STORAGE_PROPERTY_QUERY) { + .PropertyId = StorageDeviceProperty, + .QueryType = PropertyStandardQuery, + }, + sizeof(STORAGE_PROPERTY_QUERY), + &sddBuffer, + ARRAY_SIZE(sddBuffer), + &retSize, + NULL) || + retSize == 0) { + return true; + } + + FFDiskIOResult* device = FF_LIST_ADD(FFDiskIOResult, *result); + STORAGE_DEVICE_DESCRIPTOR* sdd = (STORAGE_DEVICE_DESCRIPTOR*) sddBuffer; + + ffStrbufInit(&device->name); + if (sdd->VendorIdOffset != 0) { + ffStrbufSetS(&device->name, (const char*) sddBuffer + sdd->VendorIdOffset); + ffStrbufTrim(&device->name, ' '); + } + if (sdd->ProductIdOffset != 0) { + if (device->name.length) { + ffStrbufAppendC(&device->name, ' '); + } + + ffStrbufAppendS(&device->name, (const char*) sddBuffer + sdd->ProductIdOffset); + ffStrbufTrimRight(&device->name, ' '); + } + + if (!device->name.length) { + ffStrbufSetWS(&device->name, szDevice); + } + + if (options->namePrefix.length && !ffStrbufStartsWith(&device->name, &options->namePrefix)) { + ffStrbufDestroy(&device->name); + result->length--; + return true; + } + + DISK_PERFORMANCE dp = {}; + if (DeviceIoControl(hDevice, IOCTL_DISK_PERFORMANCE, NULL, 0, &dp, sizeof(dp), &retSize, NULL)) { + device->bytesRead = (uint64_t) dp.BytesRead.QuadPart; + device->readCount = (uint64_t) dp.ReadCount; + device->bytesWritten = (uint64_t) dp.BytesWritten.QuadPart; + device->writeCount = (uint64_t) dp.WriteCount; + } else { + ffStrbufDestroy(&device->name); + result->length--; + } + + ffStrbufInitWS(&device->devPath, szDevice); + + return true; +} + +const char* ffDiskIOGetIoCounters(FFlist* result, FFDiskIOOptions* options) { + { + wchar_t szPhysicalDrive[32] = L"\\\\.\\PhysicalDrive"; + wchar_t* pNum = szPhysicalDrive + strlen("\\\\.\\PhysicalDrive"); + for (uint32_t idev = 0;; ++idev) { + _ultow(idev, pNum, 10); + + if (!detectPhysicalDisk(szPhysicalDrive, result, options)) { + break; + } + } + } + + { + wchar_t szCdrom[32] = L"\\\\.\\CDROM"; + wchar_t* pNum = szCdrom + strlen("\\\\.\\CDROM"); + for (uint32_t idev = 0;; ++idev) { + _ultow(idev, pNum, 10); + + if (!detectPhysicalDisk(szCdrom, result, options)) { + break; + } + } + } + + return NULL; +} diff --git a/src/detection/displayserver/displayserver.c b/src/detection/displayserver/displayserver.c new file mode 100644 index 0000000..58c8c89 --- /dev/null +++ b/src/detection/displayserver/displayserver.c @@ -0,0 +1,67 @@ +#include "displayserver.h" + +FFDisplayResult* ffdsAppendDisplay( + FFDisplayServerResult* result, + uint32_t width, + uint32_t height, + double refreshRate, + uint32_t dpi, + uint32_t preferredWidth, + uint32_t preferredHeight, + double preferredRefreshRate, + uint32_t rotation, + FFstrbuf* name, + FFDisplayType type, + bool primary, + uint64_t id, + uint32_t physicalWidth, + uint32_t physicalHeight, + const char* platformApi) { + if (width == 0 || height == 0) { + return NULL; + } + + FFDisplayResult* display = FF_LIST_ADD(FFDisplayResult, result->displays); + display->width = width; + display->height = height; + display->refreshRate = refreshRate; + display->dpi = dpi ?: 96; // 0 means unknown + display->preferredWidth = preferredWidth; + display->preferredHeight = preferredHeight; + display->preferredRefreshRate = preferredRefreshRate; + display->rotation = rotation; + ffStrbufInitMove(&display->name, name); + display->type = type; + display->id = id; + display->physicalWidth = physicalWidth; + display->physicalHeight = physicalHeight; + display->primary = primary; + display->platformApi = platformApi; + + display->bitDepth = 0; + display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNKNOWN; + display->manufactureYear = 0; + display->manufactureWeek = 0; + display->serial = 0; + display->drrStatus = FF_DISPLAY_DRR_STATUS_UNKNOWN; + + return display; +} + +void ffConnectDisplayServerImpl(FFDisplayServerResult* ds); + +const FFDisplayServerResult* ffConnectDisplayServer() { + static FFDisplayServerResult result; + static bool initialized = false; + if (!initialized) { + initialized = true; + ffStrbufInit(&result.wmProcessName); + ffStrbufInit(&result.wmPrettyName); + ffStrbufInit(&result.wmProtocolName); + ffStrbufInit(&result.deProcessName); + ffStrbufInit(&result.dePrettyName); + ffListInit(&result.displays); + ffConnectDisplayServerImpl(&result); + } + return &result; +} diff --git a/src/detection/displayserver/displayserver.h b/src/detection/displayserver/displayserver.h new file mode 100644 index 0000000..fd88f37 --- /dev/null +++ b/src/detection/displayserver/displayserver.h @@ -0,0 +1,123 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/display/option.h" + +#define FF_DE_PRETTY_PLASMA "KDE Plasma" +#define FF_DE_PRETTY_GNOME "GNOME" +#define FF_DE_PRETTY_GNOME_CLASSIC "GNOME Classic" +#define FF_DE_PRETTY_XFCE4 "Xfce4" +#define FF_DE_PRETTY_CINNAMON "Cinnamon" +#define FF_DE_PRETTY_MATE "Mate" +#define FF_DE_PRETTY_LXDE "LXDE" +#define FF_DE_PRETTY_LXQT "LXQt" +#define FF_DE_PRETTY_BUDGIE "Budgie" +#define FF_DE_PRETTY_CDE "CDE" +#define FF_DE_PRETTY_UNITY "Unity" +#define FF_DE_PRETTY_UKUI "UKUI" +#define FF_DE_PRETTY_NEBIDE "NebiDE" +#define FF_DE_PRETTY_ENLIGHTENMENT "Enlightenment" + +#define FF_WM_PRETTY_KWIN "KWin" +#define FF_WM_PRETTY_MUTTER "Mutter" +#define FF_WM_PRETTY_MUFFIN "Muffin" +#define FF_WM_PRETTY_MARCO "Marco" +#define FF_WM_PRETTY_XFWM4 "Xfwm4" +#define FF_WM_PRETTY_OPENBOX "Openbox" +#define FF_WM_PRETTY_I3 "i3" +#define FF_WM_PRETTY_HYPRLAND "Hyprland" +#define FF_WM_PRETTY_WAYFIRE "Wayfire" +#define FF_WM_PRETTY_SWAY "Sway" +#define FF_WM_PRETTY_BSPWM "bspwm" +#define FF_WM_PRETTY_DWM "dwm" +#define FF_WM_PRETTY_WESTON "Weston" +#define FF_WM_PRETTY_XMONAD "XMonad" +#define FF_WM_PRETTY_WSLG "WSLg" +#define FF_WM_PRETTY_TINYWM "TinyWM" +#define FF_WM_PRETTY_QTILE "Qtile" +#define FF_WM_PRETTY_HERBSTLUFTWM "herbstluftwm" +#define FF_WM_PRETTY_ICEWM "IceWM" +#define FF_WM_PRETTY_SPECTRWM "spectrwm" +#define FF_WM_PRETTY_DTWM "dtwm" +#define FF_WM_PRETTY_FVWM "fvwm" +#define FF_WM_PRETTY_CTWM "ctwm" +#define FF_WM_PRETTY_COSMIC_COMP "cosmic-comp" +#define FF_WM_PRETTY_RATPOISON "ratpoison" +#define FF_WM_PRETTY_ENLIGHTENMENT "Enlightenment" + +#define FF_WM_PROTOCOL_TTY "TTY" +#define FF_WM_PROTOCOL_X11 "X11" +#define FF_WM_PROTOCOL_WAYLAND "Wayland" +#define FF_WM_PROTOCOL_SURFACEFLINGER "SurfaceFlinger" + +typedef enum FF_A_PACKED FFDisplayType { + FF_DISPLAY_TYPE_UNKNOWN, + FF_DISPLAY_TYPE_BUILTIN, + FF_DISPLAY_TYPE_EXTERNAL, +} FFDisplayType; + +typedef enum FF_A_PACKED FFDisplayHdrStatus { + FF_DISPLAY_HDR_STATUS_UNKNOWN, + FF_DISPLAY_HDR_STATUS_UNSUPPORTED, + FF_DISPLAY_HDR_STATUS_SUPPORTED, + FF_DISPLAY_HDR_STATUS_ENABLED, +} FFDisplayHdrStatus; + +typedef enum FF_A_PACKED FFDisplayVrrStatus { + FF_DISPLAY_DRR_STATUS_UNKNOWN, + FF_DISPLAY_DRR_STATUS_DISABLED, + FF_DISPLAY_DRR_STATUS_ENABLED, +} FFDisplayVrrStatus; + +typedef struct FFDisplayResult { + uint32_t width; // in px + uint32_t height; // in px + double refreshRate; // in Hz + uint32_t dpi; // Base 96 + uint32_t preferredWidth; // in px + uint32_t preferredHeight; // in px + double preferredRefreshRate; // in Hz + FFstrbuf name; + FFDisplayType type; + uint32_t rotation; + uint64_t id; // platform dependent + uint32_t physicalWidth; // in mm + uint32_t physicalHeight; // in mm + bool primary; + const char* platformApi; + uint8_t bitDepth; + FFDisplayHdrStatus hdrStatus; + uint16_t manufactureYear; + uint16_t manufactureWeek; + uint32_t serial; + FFDisplayVrrStatus drrStatus; +} FFDisplayResult; + +typedef struct FFDisplayServerResult { + FFstrbuf wmProcessName; + FFstrbuf wmPrettyName; + FFstrbuf wmProtocolName; + FFstrbuf deProcessName; + FFstrbuf dePrettyName; + FFlist displays; // List of FFDisplayResult +} FFDisplayServerResult; + +const FFDisplayServerResult* ffConnectDisplayServer(); + +FFDisplayResult* ffdsAppendDisplay( + FFDisplayServerResult* result, + uint32_t width, + uint32_t height, + double refreshRate, + uint32_t dpi, + uint32_t preferredWidth, + uint32_t preferredHeight, + double preferredRefreshRate, + uint32_t rotation, + FFstrbuf* name, + FFDisplayType type, + bool primary, + uint64_t id, + uint32_t physicalWidth, + uint32_t physicalHeight, + const char* platformApi); diff --git a/src/detection/displayserver/displayserver_android.c b/src/detection/displayserver/displayserver_android.c new file mode 100644 index 0000000..9b2848a --- /dev/null +++ b/src/detection/displayserver/displayserver_android.c @@ -0,0 +1,209 @@ +#include "displayserver.h" +#include "common/settings.h" +#include "common/processing.h" +#include "linux/displayserver_linux.h" + +#include <math.h> + +static bool checkHdrStatus(FFDisplayResult* display) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + if (ffSettingsGetAndroidProperty("ro.surface_flinger.has_HDR_display", &buffer)) { + if (ffStrbufIgnCaseEqualS(&buffer, "true")) { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_SUPPORTED; + + if (ffSettingsGetAndroidProperty("persist.sys.hdr_mode", &buffer) && + ffStrbufToUInt(&buffer, 0) > 0) { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED; + } + + return true; + } else { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + return true; + } + } + + display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNKNOWN; + return false; +} + +static void detectWithDumpsys(FFDisplayServerResult* ds) { + FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); + if (ffProcessAppendStdOut(&buf, (char*[]) { + "/system/bin/dumpsys", + "display", + NULL, + }) != NULL || + buf.length == 0) { + return; // Only works in `adb shell`, or when rooted + } + + uint32_t index = 0; + while ((index = ffStrbufNextIndexS(&buf, index, "DisplayDeviceInfo")) < buf.length) { + index += strlen("DisplayDeviceInfo"); + uint32_t nextIndex = ffStrbufNextIndexC(&buf, index, '\n'); + buf.chars[nextIndex] = '\0'; + const char* info = buf.chars + index; + + // {"Builtin display": uniqueId="local:4630947134992368259", 1440 x 3200, modeId 2, defaultModeId 1, supportedModes [{id=1, width=1440, height=3200, fps=60.000004, alternativeRefreshRates=[24.000002, 30.000002, 40.0, 120.00001, 120.00001, 120.00001, 120.00001, 120.00001]}, + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(64); + unsigned width = 0, height = 0, modeId = 0; + double refreshRate = 0; + // {"Builtin display": uniqueId="local:4630947134992368259", 1440 x 3200, modeId 2 + int res = sscanf(info, "{\"%63[^\"]\":%*s%u x %u, modeId%u", name.chars, &width, &height, &modeId); + if (res >= 3) { + if (res == 4) { + ++info; // skip first '{' + while ((info = strchr(info, '{'))) { + ++info; + + unsigned id; + double fps; + // id=1, width=1440, height=3200, fps=60.000004, + if (sscanf(info, "id=%u, %*s%*s fps=%lf", &id, &fps) >= 2) { + if (id == modeId) { + refreshRate = fps; + break; + } + } else { + break; + } + } + } + + ffStrbufRecalculateLength(&name); + FFDisplayResult* display = ffdsAppendDisplay(ds, + (uint32_t) width, + (uint32_t) height, + refreshRate, + 0, + 0, + 0, + 0, + 0, + &name, + FF_DISPLAY_TYPE_UNKNOWN, + false, + 0, + 0, + 0, + "dumpsys"); + if (display) { + display->hdrStatus = checkHdrStatus(display); + } + } + + index = nextIndex + 1; + } +} + +static bool detectWithGetprop(FFDisplayServerResult* ds) { + // Only for MiUI + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + if (ffSettingsGetAndroidProperty("persist.sys.miui_resolution", &buffer) && + ffStrbufContainC(&buffer, ',')) { + // 1440,3200,560 => width,height,densityDpi + uint32_t width = (uint32_t) ffStrbufToUInt(&buffer, 0); + ffStrbufSubstrAfterFirstC(&buffer, ','); + uint32_t height = (uint32_t) ffStrbufToUInt(&buffer, 0); + ffStrbufSubstrAfterFirstC(&buffer, ','); + uint32_t dpi = (uint32_t) ffStrbufToUInt(&buffer, 0) * 96 / 160; + FFDisplayResult* display = ffdsAppendDisplay(ds, + width, + height, + 0, + dpi, + 0, + 0, + 0, + 0, + NULL, + FF_DISPLAY_TYPE_BUILTIN, + false, + 0, + 0, + 0, + "getprop"); + if (display) { + display->hdrStatus = checkHdrStatus(display); + } + return !!display; + } + + return false; +} + +static bool detectDE(FFDisplayServerResult* ds) { + if (ffSettingsGetAndroidProperty("ro.vivo.os.build.display.id", &ds->dePrettyName)) // OriginOS 6 + { + ffStrbufAppendC(&ds->dePrettyName, ' '); + ffSettingsGetAndroidProperty("ro.vivo.product.version", &ds->dePrettyName); // PD2505D_xxx + return true; + } + if (ffSettingsGetAndroidProperty("ro.build.version.magic", &ds->dePrettyName) || + ffSettingsGetAndroidProperty("ro.build.version.emui", &ds->dePrettyName)) { + ffStrbufReplaceAllC(&ds->dePrettyName, '_', ' '); + return true; + } + if (ffSettingsGetAndroidProperty("ro.mi.os.version.name", &ds->dePrettyName)) { + // MiUI like + ffStrbufClear(&ds->dePrettyName); + ffSettingsGetAndroidProperty("ro.build.version.incremental", &ds->dePrettyName); // Detail version number + if (ffStrbufStartsWithS(&ds->dePrettyName, "OS")) { + ds->dePrettyName.chars[0] = 'S'; + ds->dePrettyName.chars[1] = ' '; + ffStrbufPrependS(&ds->dePrettyName, "HyperO"); + } else if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) { + ds->dePrettyName.chars[0] = ' '; + ffStrbufPrependS(&ds->dePrettyName, "MiUI"); + } else { + ffStrbufSetStatic(&ds->dePrettyName, "MiUI"); + } + return true; + } + if (ffSettingsGetAndroidProperty("ro.build.version.oplusrom", &ds->dePrettyName)) { + if (ffStrbufStartsWithS(&ds->dePrettyName, "V")) { + ffStrbufSubstrAfter(&ds->dePrettyName, 0); + } + ffStrbufPrependS(&ds->dePrettyName, "ColorOS"); + return true; + } + if (ffSettingsGetAndroidProperty("ro.oxygen.version", &ds->dePrettyName)) { + ffStrbufPrependS(&ds->dePrettyName, "OxygenOS"); + return true; + } + if (ffSettingsGetAndroidProperty("ro.build.display.id", &ds->dePrettyName)) { + if (ffStrbufStartsWithS(&ds->dePrettyName, "RedMagicOS")) { + ffStrbufInsertNC(&ds->dePrettyName, strlen("RedMagicOS"), 1, ' '); + } + + // Google Pixel uses native Android + return true; + } + + return false; +} + +void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) { + const char* error = ffdsConnectXcbRandr(ds); + if (error) { + error = ffdsConnectXrandr(ds); + } + if (!error) { + ffdsDetectWMDE(ds); + return; + } + + // https://source.android.com/docs/core/graphics/surfaceflinger-windowmanager + ffStrbufSetStatic(&ds->wmProcessName, "system_server"); + ffStrbufSetStatic(&ds->wmPrettyName, "WindowManager"); // A system service managed by system_server + ffStrbufSetStatic(&ds->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER); + + if (!detectWithGetprop(ds)) { + detectWithDumpsys(ds); + } + + detectDE(ds); +} diff --git a/src/detection/displayserver/displayserver_apple.c b/src/detection/displayserver/displayserver_apple.c new file mode 100644 index 0000000..39ba604 --- /dev/null +++ b/src/detection/displayserver/displayserver_apple.c @@ -0,0 +1,197 @@ +#include "displayserver.h" +#include "common/apple/cf_helpers.h" +#include "common/strutil.h" +#include "common/edidHelper.h" +#include "detection/os/os.h" + +#include <stdlib.h> +#include <string.h> +#include <assert.h> +#include <CoreGraphics/CGDirectDisplay.h> +#include <CoreVideo/CVDisplayLink.h> + +#ifdef MAC_OS_X_VERSION_10_15 +extern Boolean CoreDisplay_Display_SupportsHDRMode(CGDirectDisplayID display) FF_A_WEAK_IMPORT; +extern Boolean CoreDisplay_Display_IsHDRModeEnabled(CGDirectDisplayID display) FF_A_WEAK_IMPORT; +extern CFDictionaryRef CoreDisplay_DisplayCreateInfoDictionary(CGDirectDisplayID display) FF_A_WEAK_IMPORT; +#else + #include <IOKit/graphics/IOGraphicsLib.h> +#endif + +static void detectDisplays(FFDisplayServerResult* ds) { + CGDirectDisplayID screens[128]; + uint32_t screenCount; + if (CGGetOnlineDisplayList(ARRAY_SIZE(screens), screens, &screenCount) != kCGErrorSuccess) { + return; + } + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + for (uint32_t i = 0; i < screenCount; i++) { + CGDirectDisplayID screen = screens[i]; + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(screen); + if (mode) { + // https://github.com/glfw/glfw/commit/aab08712dd8142b642e2042e7b7ba563acd07a45 + double refreshRate = CGDisplayModeGetRefreshRate(mode); + + if (refreshRate == 0) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + CVDisplayLinkRef link; + if (CVDisplayLinkCreateWithCGDisplay(screen, &link) == kCVReturnSuccess) { + const CVTime time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link); + if (!(time.flags & kCVTimeIsIndefinite)) { + refreshRate = time.timeScale / (double) time.timeValue; // 59.97... + } + CVDisplayLinkRelease(link); + } +#pragma clang diagnostic pop + } + + ffStrbufClear(&buffer); + CFDictionaryRef FF_CFTYPE_AUTO_RELEASE displayInfo = NULL; +#ifdef MAC_OS_X_VERSION_10_15 + if (CoreDisplay_DisplayCreateInfoDictionary) { + displayInfo = CoreDisplay_DisplayCreateInfoDictionary(screen); + } +#else + { + io_service_t servicePort = CGDisplayIOServicePort(screen); + displayInfo = IODisplayCreateInfoDictionary(servicePort, kIODisplayOnlyPreferredName); + } +#endif + + uint32_t physicalWidth = 0, physicalHeight = 0; + uint32_t preferredWidth = 0, preferredHeight = 0; + double preferredRefreshRate = 0; + + if (displayInfo) { + CFDictionaryRef productNames; + if (ffCfDictGetDict(displayInfo, CFSTR(kDisplayProductName), &productNames) == NULL) { + ffCfDictGetString(productNames, CFSTR("en_US"), &buffer); + } + + // CGDisplayScreenSize reports invalid result for external displays on old Intel MacBook Pro + CFDataRef edidRef = (CFDataRef) CFDictionaryGetValue(displayInfo, CFSTR(kIODisplayEDIDKey)); + if (edidRef && CFGetTypeID(edidRef) == CFDataGetTypeID()) { + const uint8_t* edidData = CFDataGetBytePtr(edidRef); + uint32_t edidLength = (uint32_t) CFDataGetLength(edidRef); + if (edidLength >= 128) { + ffEdidGetPhysicalSize(edidData, &physicalWidth, &physicalHeight); + } + } + + if (!physicalWidth || !physicalHeight) { + if (ffCfDictGetInt(displayInfo, CFSTR(kDisplayHorizontalImageSize), (int*) &physicalWidth) == NULL) { + ffCfDictGetInt(displayInfo, CFSTR(kDisplayVerticalImageSize), (int*) &physicalHeight); + } + } + + ffCfDictGetInt(displayInfo, CFSTR("kCGDisplayPixelWidth"), (int*) &preferredWidth); + ffCfDictGetInt(displayInfo, CFSTR("kCGDisplayPixelHeight"), (int*) &preferredHeight); + if (preferredWidth && preferredHeight) { + FF_CFTYPE_AUTO_RELEASE CFArrayRef allModes = CGDisplayCopyAllDisplayModes(screen, NULL); + if (allModes) { + for (CFIndex i = 0, count = CFArrayGetCount(allModes); i < count; i++) { + CGDisplayModeRef modeInfo = (CGDisplayModeRef) CFArrayGetValueAtIndex(allModes, i); + if (CGDisplayModeGetPixelWidth(modeInfo) == preferredWidth && CGDisplayModeGetPixelHeight(modeInfo) == preferredHeight) { + double rr = CGDisplayModeGetRefreshRate(modeInfo); + if (rr > preferredRefreshRate) { + preferredRefreshRate = rr; + } + break; + } + } + } + } + } + + if ((!physicalWidth || !physicalHeight) && CGDisplayPrimaryDisplay(screen) == screen) // #1406 + { + CGSize size = CGDisplayScreenSize(screen); + physicalWidth = (uint32_t) (size.width + 0.5); + physicalHeight = (uint32_t) (size.height + 0.5); + } + + uint32_t pixelWidth = (uint32_t) CGDisplayModeGetPixelWidth(mode); + uint32_t pixelHeight = (uint32_t) CGDisplayModeGetPixelHeight(mode); + + FFDisplayResult* display = ffdsAppendDisplay(ds, + pixelWidth, + pixelHeight, + refreshRate, + pixelHeight * 96 / (uint32_t) CGDisplayModeGetHeight(mode), + preferredWidth, + preferredHeight, + preferredRefreshRate, + (uint32_t) CGDisplayRotation(screen), + &buffer, + CGDisplayIsBuiltin(screen) ? FF_DISPLAY_TYPE_BUILTIN : FF_DISPLAY_TYPE_EXTERNAL, + CGDisplayIsMain(screen), + (uint64_t) screen, + physicalWidth, + physicalHeight, + "CoreGraphics"); + if (display) { +#ifndef MAC_OS_X_VERSION_10_11 + FF_CFTYPE_AUTO_RELEASE CFStringRef pe = CGDisplayModeCopyPixelEncoding(mode); + if (pe) { + display->bitDepth = (uint8_t) (CFStringGetLength(pe) - CFStringFind(pe, CFSTR("B"), 0).location); + } +#else + // https://stackoverflow.com/a/33519316/9976392 + // Also shitty, but better than parsing `CFCopyDescription(mode)` + CFDictionaryRef dict = (CFDictionaryRef) * ((int64_t*) mode + 2); + if (CFGetTypeID(dict) == CFDictionaryGetTypeID()) { + int32_t bitDepth; + ffCfDictGetInt(dict, kCGDisplayBitsPerSample, &bitDepth); + display->bitDepth = (uint8_t) bitDepth; + } +#endif + + if (display->type == FF_DISPLAY_TYPE_BUILTIN && displayInfo) { + display->hdrStatus = CFDictionaryContainsKey(displayInfo, CFSTR("ReferencePeakHDRLuminance")) + ? FF_DISPLAY_HDR_STATUS_SUPPORTED + : FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + } +#ifdef MAC_OS_X_VERSION_10_15 + else if (CoreDisplay_Display_SupportsHDRMode) { + if (CoreDisplay_Display_SupportsHDRMode(screen)) { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_SUPPORTED; + if (CoreDisplay_Display_IsHDRModeEnabled && CoreDisplay_Display_IsHDRModeEnabled(screen)) { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED; + } + } else { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + } + } +#endif + + display->serial = CGDisplaySerialNumber(screen); + + if (displayInfo) { + int value; + if (ffCfDictGetInt(displayInfo, CFSTR(kDisplayYearOfManufacture), &value) == NULL) { + display->manufactureYear = (uint16_t) value; + } + if (ffCfDictGetInt(displayInfo, CFSTR(kDisplayWeekOfManufacture), &value) == NULL) { + display->manufactureWeek = (uint16_t) value; + } + } + } + CGDisplayModeRelease(mode); + } + CGDisplayRelease(screen); + } +} + +void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) { + { + FF_CFTYPE_AUTO_RELEASE CFMachPortRef port = CGWindowServerCreateServerPort(); + if (port) { + ffStrbufSetStatic(&ds->wmProcessName, "WindowServer"); + ffStrbufSetStatic(&ds->wmPrettyName, "Quartz Compositor"); + } + } + + detectDisplays(ds); +} diff --git a/src/detection/displayserver/displayserver_haiku.cpp b/src/detection/displayserver/displayserver_haiku.cpp new file mode 100644 index 0000000..c1ddaf6 --- /dev/null +++ b/src/detection/displayserver/displayserver_haiku.cpp @@ -0,0 +1,71 @@ +extern "C" { +#include "displayserver.h" +} + +#include <math.h> + +#include <Application.h> +#include <Screen.h> + +extern "C" void ffConnectDisplayServerImpl(FFDisplayServerResult* ds); + +static void detectDisplays(FFDisplayServerResult* ds) { + // We need a valid be_app to query the app_server here. + BApplication app("application/x-vnd.fastfetch-cli-fastfetch"); + BScreen s{}; // default screen is the main one + bool main = true; + + do { + if (!s.IsValid()) { + continue; + } + + display_mode mode; + if (s.GetMode(&mode) != B_OK) { + continue; + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateA(128); + monitor_info monitor; + // WARNING: This is experimental new Haiku API + status_t err = s.GetMonitorInfo(&monitor); + if (err == B_OK) { + ffStrbufSetF(&name, "%s %s", monitor.vendor, monitor.name); + ffStrbufTrimRightSpace(&name); + } + + uint32_t width = (uint32_t) s.Frame().Width() + 1; + uint32_t height = (uint32_t) (uint32_t) s.Frame().Height() + 1; + FFDisplayResult* res = ffdsAppendDisplay(ds, + width, + height, + (double) mode.timing.pixel_clock * 1000 / (mode.timing.v_total * mode.timing.h_total), + 0, + 0, + 0, + 0, + 0, + &name, + FF_DISPLAY_TYPE_UNKNOWN, + main, + (uint64_t) s.ID().id, + 0, + 0, + "BScreen"); + if (err == B_OK) { + res->manufactureWeek = monitor.produced.week; + res->manufactureYear = monitor.produced.year; + } + main = false; + } while (s.SetToNext() == B_OK); + + return; +} + +void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) { + ffStrbufSetStatic(&ds->wmProcessName, "app_server"); + ffStrbufSetStatic(&ds->wmPrettyName, "Application Server"); + ffStrbufSetStatic(&ds->dePrettyName, "Application Kit"); + + detectDisplays(ds); +} diff --git a/src/detection/displayserver/displayserver_windows.c b/src/detection/displayserver/displayserver_windows.c new file mode 100644 index 0000000..f92c144 --- /dev/null +++ b/src/detection/displayserver/displayserver_windows.c @@ -0,0 +1,219 @@ +#include "displayserver.h" +#include "common/edidHelper.h" +#include "common/windows/registry.h" +#include "common/windows/unicode.h" + +#include <windows.h> +#include <shellscalingapi.h> + +static inline void freeArgBuffer(FFArgBuffer* buffer) { + if (buffer->data) { + free(buffer->data); + } + buffer->data = NULL; + buffer->length = 0; +} +#define FF_AUTO_FREE_ARG_BUFFER FF_A_CLEANUP(freeArgBuffer) + +// http://undoc.airesoft.co.uk/user32.dll/IsThreadDesktopComposited.php +BOOL WINAPI IsThreadDesktopComposited(); +BOOL WINAPI GetDpiForMonitorInternal(HMONITOR hmonitor, MONITOR_DPI_TYPE dpiType, UINT* dpiX, UINT* dpiY); + +static void detectDisplays(FFDisplayServerResult* ds) { + DISPLAYCONFIG_PATH_INFO paths[128]; + uint32_t pathCount = ARRAY_SIZE(paths); + DISPLAYCONFIG_MODE_INFO modes[256]; + uint32_t modeCount = ARRAY_SIZE(modes); + + if (QueryDisplayConfig( + QDC_ONLY_ACTIVE_PATHS, + &pathCount, + paths, + &modeCount, + modes, + NULL) == ERROR_SUCCESS) { + for (uint32_t i = 0; i < pathCount; ++i) { + const DISPLAYCONFIG_PATH_INFO* path = &paths[i]; + const DISPLAYCONFIG_SOURCE_MODE* sourceMode = &modes[path->sourceInfo.modeInfoIdx].sourceMode; + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + uint32_t physicalWidth = 0, physicalHeight = 0; + + DISPLAYCONFIG_TARGET_DEVICE_NAME targetName = { + .header = { + .type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME, + .size = sizeof(targetName), + .adapterId = path->targetInfo.adapterId, + .id = path->targetInfo.id, + }, + }; + FF_AUTO_FREE_ARG_BUFFER FFArgBuffer edid = {}; + if (DisplayConfigGetDeviceInfo(&targetName.header) == ERROR_SUCCESS) { + wchar_t regPath[256] = L"SYSTEM\\CurrentControlSet\\Enum"; + wchar_t* pRegPath = regPath + strlen("SYSTEM\\CurrentControlSet\\Enum"); + wchar_t* pDevPath = targetName.monitorDevicePath + strlen("\\\\?"); + while (*pDevPath && *pDevPath != L'{') { + if (*pDevPath == L'#') { + *pRegPath = L'\\'; + } else { + *pRegPath = *pDevPath; + } + ++pRegPath; + ++pDevPath; + assert(pRegPath < regPath + ARRAY_SIZE(regPath) + strlen("Device Parameters")); + } + wcscpy(pRegPath, L"Device Parameters"); + + FF_AUTO_CLOSE_FD HANDLE hKey = NULL; + if (ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, regPath, &hKey, NULL) && + ffRegReadData(hKey, L"EDID", &edid, NULL) && + ffEdidIsValid(edid.data, edid.length)) { + ffEdidGetName(edid.data, &name); + ffEdidGetPhysicalSize(edid.data, &physicalWidth, &physicalHeight); + } else { + edid.length = 0; + if (targetName.flags.friendlyNameFromEdid) { + ffStrbufSetWS(&name, targetName.monitorFriendlyDeviceName); + } else { + ffStrbufSetWS(&name, targetName.monitorDevicePath); + ffStrbufSubstrAfterFirstC(&name, '#'); + ffStrbufSubstrBeforeFirstC(&name, '#'); + } + } + } + + uint32_t width = sourceMode->width; + uint32_t height = sourceMode->height; + uint32_t rotation; + switch (path->targetInfo.rotation) { + case DISPLAYCONFIG_ROTATION_ROTATE90: + rotation = 90; + break; + case DISPLAYCONFIG_ROTATION_ROTATE180: + rotation = 180; + break; + case DISPLAYCONFIG_ROTATION_ROTATE270: + rotation = 270; + break; + default: + rotation = 0; + break; + } + + DISPLAYCONFIG_TARGET_PREFERRED_MODE preferredMode = { + .header = { + .type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_PREFERRED_MODE, + .size = sizeof(preferredMode), + .adapterId = path->targetInfo.adapterId, + .id = path->targetInfo.id, + } + }; + double preferredRefreshRate = 0; + if (DisplayConfigGetDeviceInfo(&preferredMode.header) == ERROR_SUCCESS) { + DISPLAYCONFIG_RATIONAL freq = preferredMode.targetMode.targetVideoSignalInfo.vSyncFreq; + preferredRefreshRate = freq.Numerator / (double) freq.Denominator; + } + + uint32_t systemDpi = 0; + HMONITOR hMonitor = MonitorFromPoint(*(POINT*) &sourceMode->position, MONITOR_DEFAULTTONULL); + if (hMonitor) { + UINT ignored; + GetDpiForMonitorInternal(hMonitor, MDT_EFFECTIVE_DPI, &systemDpi, &ignored); + } + + if (systemDpi == 0) { + HDC hdc = GetDC(NULL); + systemDpi = (uint32_t) GetDeviceCaps(hdc, LOGPIXELSX); + if (systemDpi == 0) { + systemDpi = 96; + } + ReleaseDC(NULL, hdc); + } + + if (path->targetInfo.rotation == DISPLAYCONFIG_ROTATION_ROTATE90 || + path->targetInfo.rotation == DISPLAYCONFIG_ROTATION_ROTATE270) { + uint32_t temp = width; + width = height; + height = temp; + } + + FFDisplayResult* display = ffdsAppendDisplay(ds, + width, + height, + path->targetInfo.refreshRate.Numerator / (double) path->targetInfo.refreshRate.Denominator, + systemDpi, + preferredMode.width, + preferredMode.height, + preferredRefreshRate, + rotation, + &name, + path->targetInfo.outputTechnology == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_OTHER ? FF_DISPLAY_TYPE_UNKNOWN : path->targetInfo.outputTechnology == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL || path->targetInfo.outputTechnology == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EMBEDDED || path->targetInfo.outputTechnology == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EMBEDDED ? FF_DISPLAY_TYPE_BUILTIN + : FF_DISPLAY_TYPE_EXTERNAL, + sourceMode->position.x == 0 && sourceMode->position.y == 0, + (uintptr_t) hMonitor, + physicalWidth, + physicalHeight, + "GDI"); + + if (display) { + DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2 advColorInfo2 = { + .header = { + .type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2, + .size = sizeof(advColorInfo2), + .adapterId = path->targetInfo.adapterId, + .id = path->targetInfo.id, + } + }; + if (DisplayConfigGetDeviceInfo(&advColorInfo2.header) == ERROR_SUCCESS) { + if (advColorInfo2.highDynamicRangeUserEnabled) { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED; + } else if (advColorInfo2.highDynamicRangeSupported) { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_SUPPORTED; + } else { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + } + display->bitDepth = (uint8_t) advColorInfo2.bitsPerColorChannel; + } else { + DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO advColorInfo = { + .header = { + .type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO, + .size = sizeof(advColorInfo), + .adapterId = path->targetInfo.adapterId, + .id = path->targetInfo.id, + } + }; + if (DisplayConfigGetDeviceInfo(&advColorInfo.header) == ERROR_SUCCESS) { + if (advColorInfo.advancedColorEnabled) { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED; + } else if (advColorInfo.advancedColorSupported) { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_SUPPORTED; + } else { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + } + display->bitDepth = (uint8_t) advColorInfo.bitsPerColorChannel; + } else { + display->hdrStatus = FF_DISPLAY_HDR_STATUS_UNKNOWN; + } + } + if (edid.length > 0) { + ffEdidGetSerialAndManufactureDate(edid.data, &display->serial, &display->manufactureYear, &display->manufactureWeek); + } + display->drrStatus = path->flags & DISPLAYCONFIG_PATH_BOOST_REFRESH_RATE ? FF_DISPLAY_DRR_STATUS_ENABLED : FF_DISPLAY_DRR_STATUS_DISABLED; + } + } + } +} + +void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) { + if (IsThreadDesktopComposited()) { + ffStrbufSetStatic(&ds->wmProcessName, "dwm.exe"); + ffStrbufSetStatic(&ds->wmPrettyName, "Desktop Window Manager"); + } else { + // `explorer.exe` only provides a subset of WM functions, as well as the taskbar and desktop icons. + // While a window itself is drawn by kernel (GDI). Killing `explorer.exe` won't affect how windows are displayed generally. + ffStrbufSetStatic(&ds->wmProcessName, "explorer.exe"); + ffStrbufSetStatic(&ds->wmPrettyName, "Internal"); + } + + detectDisplays(ds); +} diff --git a/src/detection/displayserver/linux/common.c b/src/detection/displayserver/linux/common.c new file mode 100644 index 0000000..305766d --- /dev/null +++ b/src/detection/displayserver/linux/common.c @@ -0,0 +1,16 @@ +#include "displayserver_linux.h" +#include "common/strutil.h" + +FFDisplayType ffdsGetDisplayType(const char* name) { + if (ffStrStartsWith(name, "eDP-") || ffStrStartsWith(name, "LVDS-")) { + return FF_DISPLAY_TYPE_BUILTIN; + } else if (ffStrStartsWith(name, "HDMI-") || + ffStrStartsWith(name, "DP-") || + ffStrStartsWith(name, "DisplayPort-") || + ffStrStartsWith(name, "DVI-") || + ffStrStartsWith(name, "VGA-")) { + return FF_DISPLAY_TYPE_EXTERNAL; + } + + return FF_DISPLAY_TYPE_UNKNOWN; +} diff --git a/src/detection/displayserver/linux/displayserver_linux.c b/src/detection/displayserver/linux/displayserver_linux.c new file mode 100644 index 0000000..70d2d40 --- /dev/null +++ b/src/detection/displayserver/linux/displayserver_linux.c @@ -0,0 +1,91 @@ +#include "displayserver_linux.h" +#include "common/io.h" +#include "common/strutil.h" + +#ifdef __FreeBSD__ + #include "common/settings.h" +#endif + +static void getWMProtocolNameFromEnv(FFDisplayServerResult* result) { + const char* env = getenv("XDG_SESSION_TYPE"); + if (env) { + if (ffStrEqualsIgnCase(env, "wayland")) { + ffStrbufSetS(&result->wmProtocolName, FF_WM_PROTOCOL_WAYLAND); + } else if (ffStrEqualsIgnCase(env, "x11") || ffStrEqualsIgnCase(env, "xorg")) { + ffStrbufSetS(&result->wmProtocolName, FF_WM_PROTOCOL_X11); + } else if (ffStrEqualsIgnCase(env, "tty")) { + ffStrbufSetS(&result->wmProtocolName, FF_WM_PROTOCOL_TTY); + } else { + ffStrbufSetS(&result->wmProtocolName, env); + } + + return; + } + + if (getenv("WAYLAND_DISPLAY") != NULL || getenv("WAYLAND_SOCKET") != NULL) { + ffStrbufSetStatic(&result->wmProtocolName, FF_WM_PROTOCOL_WAYLAND); + return; + } + + if (getenv("DISPLAY") != NULL) // XWayland also set this + { + ffStrbufSetStatic(&result->wmProtocolName, FF_WM_PROTOCOL_X11); + return; + } + + env = getenv("TERM"); + if (ffStrSet(env) && ffStrEqualsIgnCase(env, "linux")) { + ffStrbufSetStatic(&result->wmProtocolName, FF_WM_PROTOCOL_TTY); + return; + } +} + +void ffConnectDisplayServerImpl(FFDisplayServerResult* ds) { + if (instance.config.general.dsForceDrm == FF_DS_FORCE_DRM_TYPE_FALSE) { + // We try wayland as our preferred display server, as it supports the most features. + // This method can't detect the name of our WM / DE + ffdsConnectWayland(ds); + + // Try the x11 libs, from most feature rich to least. + // We use the display list to detect if a connection is needed. + // They respect wmProtocolName, and only detect display if it is set. + if (ds->displays.length == 0) { + ffdsConnectXcbRandr(ds); + } + + if (ds->displays.length == 0) { + ffdsConnectXrandr(ds); + } + } + + // This display detection method is display server independent. + // Use it if all connections failed + if (ds->displays.length == 0) { + ffdsConnectDrm(ds); + } + +#ifdef __FreeBSD__ + if (ds->displays.length == 0) { + FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); + if (ffSettingsGetFreeBSDKenv("screen.width", &buf)) { + uint32_t width = (uint32_t) ffStrbufToUInt(&buf, 0); + if (width) { + ffStrbufClear(&buf); + if (ffSettingsGetFreeBSDKenv("screen.height", &buf)) { + uint32_t height = (uint32_t) ffStrbufToUInt(&buf, 0); + ffdsAppendDisplay(ds, width, height, 0, 0, 0, 0, 0, 0, NULL, FF_DISPLAY_TYPE_UNKNOWN, false, 0, 0, 0, "kenv"); + } + } + } + } +#endif + + if (ds->wmProtocolName.length == 0) { + getWMProtocolNameFromEnv(ds); + } + + if (!ffStrbufEqualS(&ds->wmProtocolName, FF_WM_PROTOCOL_TTY)) { + // This fills in missing information about WM / DE by using env vars and iterating processes + ffdsDetectWMDE(ds); + } +} diff --git a/src/detection/displayserver/linux/displayserver_linux.h b/src/detection/displayserver/linux/displayserver_linux.h new file mode 100644 index 0000000..19ae4e7 --- /dev/null +++ b/src/detection/displayserver/linux/displayserver_linux.h @@ -0,0 +1,13 @@ +#pragma once + +#include "detection/displayserver/displayserver.h" + +const char* ffdsConnectWayland(FFDisplayServerResult* result); + +const char* ffdsConnectXcbRandr(FFDisplayServerResult* result); +const char* ffdsConnectXrandr(FFDisplayServerResult* result); +const char* ffdsConnectDrm(FFDisplayServerResult* result); + +void ffdsDetectWMDE(FFDisplayServerResult* result); + +FFDisplayType ffdsGetDisplayType(const char* drmConnectorName); diff --git a/src/detection/displayserver/linux/drm.c b/src/detection/displayserver/linux/drm.c new file mode 100644 index 0000000..43c6afc --- /dev/null +++ b/src/detection/displayserver/linux/drm.c @@ -0,0 +1,437 @@ +#include "displayserver_linux.h" +#include "common/io.h" +#include "common/edidHelper.h" +#include "common/strutil.h" + +#ifdef __linux__ + #include <dirent.h> + +static const char* drmParseSysfs(FFDisplayServerResult* result) { + const char* drmDirPath = "/sys/class/drm/"; + + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(drmDirPath); + if (dirp == NULL) { + return "opendir(drmDirPath) failed"; + } + + FF_STRBUF_AUTO_DESTROY drmDir = ffStrbufCreateA(64); + ffStrbufAppendS(&drmDir, drmDirPath); + + uint32_t drmDirLength = drmDir.length; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + ffStrbufAppendS(&drmDir, entry->d_name); + uint32_t drmDirWithDnameLength = drmDir.length; + + char buf; + ffStrbufAppendS(&drmDir, "/enabled"); + if (ffReadFileData(drmDir.chars, sizeof(buf), &buf) <= 0 || buf != 'e') { + /* read failed or enabled != "enabled" */ + ffStrbufSubstrBefore(&drmDir, drmDirWithDnameLength); + ffStrbufAppendS(&drmDir, "/status"); + buf = 'd'; + ffReadFileData(drmDir.chars, sizeof(buf), &buf); + if (buf != 'c') { + /* read failed or status != "connected" */ + ffStrbufSubstrBefore(&drmDir, drmDirLength); + continue; + } + } + + unsigned width = 0, height = 0, physicalWidth = 0, physicalHeight = 0; + double refreshRate = 0; + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + + ffStrbufSubstrBefore(&drmDir, drmDirWithDnameLength); + ffStrbufAppendS(&drmDir, "/edid"); + + const char* plainName = entry->d_name; + if (ffStrStartsWith(plainName, "card")) { + const char* tmp = strchr(plainName + strlen("card"), '-'); + if (tmp) { + plainName = tmp + 1; + } + } + + uint8_t edidData[512]; + ssize_t edidLength = ffReadFileData(drmDir.chars, ARRAY_SIZE(edidData), edidData); + if (edidLength <= 0 || edidLength % 128 != 0) { + edidLength = 0; + ffStrbufSubstrBefore(&drmDir, drmDirWithDnameLength); + ffStrbufAppendS(&drmDir, "/modes"); + + char modes[32]; + if (ffReadFileData(drmDir.chars, ARRAY_SIZE(modes), modes) >= 3) { + sscanf(modes, "%ux%u", &width, &height); + ffStrbufAppendS(&name, plainName); + } + } else { + ffEdidGetName(edidData, &name); + ffEdidGetPreferredResolutionAndRefreshRate(edidData, &width, &height, &refreshRate); + ffEdidGetPhysicalSize(edidData, &physicalWidth, &physicalHeight); + } + + FFDisplayResult* item = ffdsAppendDisplay( + result, + width, + height, + refreshRate, + 0, + 0, + 0, + 0, + 0, + &name, + ffdsGetDisplayType(plainName), + false, + 0, + physicalWidth, + physicalHeight, + "sysfs-drm"); + if (item && edidLength) { + item->hdrStatus = ffEdidGetHdrCompatible(edidData, (uint32_t) edidLength) ? FF_DISPLAY_HDR_STATUS_SUPPORTED : FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + ffEdidGetSerialAndManufactureDate(edidData, &item->serial, &item->manufactureYear, &item->manufactureWeek); + } + + ffStrbufSubstrBefore(&drmDir, drmDirLength); + } + + return NULL; +} +#endif + +#ifdef FF_HAVE_DRM + + #include "common/library.h" + + #include <xf86drm.h> + #include <xf86drmMode.h> + #include <fcntl.h> + +// https://gitlab.freedesktop.org/mesa/drm/-/blob/main/xf86drmMode.c#L1785 +// It's not supported on Ubuntu 20.04 +static inline const char* drmType2Name(uint32_t connector_type) { + /* Keep the strings in sync with the kernel's drm_connector_enum_list in + * drm_connector.c. */ + switch (connector_type) { + case DRM_MODE_CONNECTOR_Unknown: + return "Unknown"; + case DRM_MODE_CONNECTOR_VGA: + return "VGA"; + case DRM_MODE_CONNECTOR_DVII: + return "DVI-I"; + case DRM_MODE_CONNECTOR_DVID: + return "DVI-D"; + case DRM_MODE_CONNECTOR_DVIA: + return "DVI-A"; + case DRM_MODE_CONNECTOR_Composite: + return "Composite"; + case DRM_MODE_CONNECTOR_SVIDEO: + return "SVIDEO"; + case DRM_MODE_CONNECTOR_LVDS: + return "LVDS"; + case DRM_MODE_CONNECTOR_Component: + return "Component"; + case DRM_MODE_CONNECTOR_9PinDIN: + return "DIN"; + case DRM_MODE_CONNECTOR_DisplayPort: + return "DP"; + case DRM_MODE_CONNECTOR_HDMIA: + return "HDMI-A"; + case DRM_MODE_CONNECTOR_HDMIB: + return "HDMI-B"; + case DRM_MODE_CONNECTOR_TV: + return "TV"; + case DRM_MODE_CONNECTOR_eDP: + return "eDP"; + case DRM_MODE_CONNECTOR_VIRTUAL: + return "Virtual"; + case DRM_MODE_CONNECTOR_DSI: + return "DSI"; + case DRM_MODE_CONNECTOR_DPI: + return "DPI"; + case DRM_MODE_CONNECTOR_WRITEBACK: + return "Writeback"; + case 19 /*DRM_MODE_CONNECTOR_SPI*/: + return "SPI"; + case 20 /*DRM_MODE_CONNECTOR_USB*/: + return "USB"; + default: + return "Unsupported"; + } +} + +FF_A_UNUSED static const char* drmGetEdidByConnId(uint32_t connId, uint8_t* edidData, ssize_t* edidLength) { + const char* drmDirPath = "/sys/class/drm/"; + + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(drmDirPath); + if (dirp == NULL) { + return "opendir(drmDirPath) failed"; + } + + FF_STRBUF_AUTO_DESTROY drmDir = ffStrbufCreateA(64); + ffStrbufAppendS(&drmDir, drmDirPath); + + uint32_t drmDirLength = drmDir.length; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + ffStrbufAppendS(&drmDir, entry->d_name); + uint32_t drmDirWithDnameLength = drmDir.length; + + char connectorId[16] = {}; + + ffStrbufAppendS(&drmDir, "/connector_id"); + ffReadFileData(drmDir.chars, ARRAY_SIZE(connectorId), connectorId); + if (strtoul(connectorId, NULL, 10) != connId) { + ffStrbufSubstrBefore(&drmDir, drmDirLength); + continue; + } + + ffStrbufSubstrBefore(&drmDir, drmDirWithDnameLength); + ffStrbufAppendS(&drmDir, "/edid"); + *edidLength = ffReadFileData(drmDir.chars, (uint32_t) *edidLength, edidData); + return NULL; + } + + return "Failed to match connector ID"; +} + +static const char* drmConnectLibdrm(FFDisplayServerResult* result) { + FF_LIBRARY_LOAD_MESSAGE(libdrm, "libdrm" FF_LIBRARY_EXTENSION, 2) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmGetDevices) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeGetResources) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeGetConnectorCurrent) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeGetCrtc) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeGetEncoder) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeGetFB) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeGetProperty) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeGetPropertyBlob) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeFreeResources) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeFreeCrtc) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeFreeConnector) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeFreeEncoder) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeFreeFB) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeFreeProperty) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmModeFreePropertyBlob) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmFreeDevices) + + drmDevice* devices[64]; + int nDevices = ffdrmGetDevices(devices, ARRAY_SIZE(devices)); + if (nDevices <= 0) { + return "drmGetDevices() failed"; + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + + for (int iDev = 0; iDev < nDevices; ++iDev) { + drmDevice* dev = devices[iDev]; + + if (!(dev->available_nodes & (1 << DRM_NODE_PRIMARY))) { + continue; + } + + const char* path = dev->nodes[DRM_NODE_PRIMARY]; + + #if __linux__ + ffStrbufSetF(&name, "/sys/class/drm/%s/device/power/runtime_status", strrchr(path, '/') + 1); + + char buffer[8] = ""; + if (ffReadFileData(name.chars, strlen("suspend"), buffer) > 0 && ffStrStartsWith(buffer, "suspend")) { + continue; + } + #endif + + FF_AUTO_CLOSE_FD int primaryFd = open(path, O_RDWR | O_CLOEXEC); + if (primaryFd < 0) { + continue; + } + + drmModeRes* res = ffdrmModeGetResources(primaryFd); + if (!res) { + continue; + } + + for (int iConn = 0; iConn < res->count_connectors; ++iConn) { + drmModeConnector* conn = ffdrmModeGetConnectorCurrent(primaryFd, res->connectors[iConn]); + if (!conn) { + continue; + } + + if (conn->connection != DRM_MODE_DISCONNECTED) { + drmModeEncoder* encoder = ffdrmModeGetEncoder(primaryFd, conn->encoder_id); + uint32_t width = 0, height = 0, refreshRate = 0; + uint8_t bitDepth = 0; + + if (encoder) { + drmModeCrtc* crtc = ffdrmModeGetCrtc(primaryFd, encoder->crtc_id); + if (crtc) { + width = crtc->mode.hdisplay; + height = crtc->mode.vdisplay; + refreshRate = crtc->mode.vrefresh; + if (refreshRate == 0) { + // There are weird cases that we can't get the refresh rate from the CRTC but from the modes + for (int iMode = 0; iMode < conn->count_modes; ++iMode) { + drmModeModeInfo* mode = &conn->modes[iMode]; + if (mode->clock == crtc->mode.clock && mode->htotal == crtc->mode.htotal) { + refreshRate = mode->vrefresh; + break; + } + } + } + + drmModeFBPtr fb = ffdrmModeGetFB(primaryFd, crtc->buffer_id); + if (fb) { + bitDepth = (uint8_t) (fb->depth / 3); + ffdrmModeFreeFB(fb); + } + + ffdrmModeFreeCrtc(crtc); + } + + ffdrmModeFreeEncoder(encoder); + } + + uint32_t preferredWidth = 0, preferredHeight = 0, preferredRefreshRate = 0; + + for (int iMode = 0; iMode < conn->count_modes; ++iMode) { + drmModeModeInfo* mode = &conn->modes[iMode]; + + if (mode->type & DRM_MODE_TYPE_PREFERRED) { + preferredWidth = mode->hdisplay; + preferredHeight = mode->vdisplay; + preferredRefreshRate = mode->vrefresh; + break; + } + } + + // NVIDIA DRM driver seems incomplete and conn->encoder_id == 0 + // Assume preferred resolution is used as what we do in drmParseSys + if (width == 0 || height == 0) { + width = preferredWidth; + height = preferredHeight; + refreshRate = preferredRefreshRate; + } + + ffStrbufClear(&name); + uint16_t myear = 0, mweak = 0; + uint32_t serial = 0; + FFDisplayHdrStatus hdrStatus = FF_DISPLAY_HDR_STATUS_UNKNOWN; + + for (int iProp = 0; iProp < conn->count_props; ++iProp) { + drmModePropertyRes* prop = ffdrmModeGetProperty(primaryFd, conn->props[iProp]); + if (!prop) { + continue; + } + + uint32_t type = prop->flags & (DRM_MODE_PROP_LEGACY_TYPE | DRM_MODE_PROP_EXTENDED_TYPE); + if (type == DRM_MODE_PROP_BLOB && ffStrEquals(prop->name, "EDID")) { + drmModePropertyBlobPtr blob = NULL; + + if (prop->count_blobs > 0 && prop->blob_ids != NULL) { + blob = ffdrmModeGetPropertyBlob(primaryFd, prop->blob_ids[0]); + } else { + blob = ffdrmModeGetPropertyBlob(primaryFd, (uint32_t) conn->prop_values[iProp]); + } + + if (blob) { + if (blob->length >= 128) { + ffEdidGetName(blob->data, &name); + hdrStatus = ffEdidGetHdrCompatible(blob->data, blob->length) ? FF_DISPLAY_HDR_STATUS_SUPPORTED : FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + ffEdidGetSerialAndManufactureDate(blob->data, &serial, &myear, &mweak); + } + ffdrmModeFreePropertyBlob(blob); + } + break; + } + ffdrmModeFreeProperty(prop); + } + + #if __linux__ + if (name.length == 0) { + uint8_t edidData[512]; + ssize_t edidLength = 0; + drmGetEdidByConnId(conn->connector_id, edidData, &edidLength); + if (edidLength > 0 && edidLength % 128 == 0) { + ffEdidGetName(edidData, &name); + hdrStatus = ffEdidGetHdrCompatible(edidData, (uint32_t) edidLength) ? FF_DISPLAY_HDR_STATUS_SUPPORTED : FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + ffEdidGetSerialAndManufactureDate(edidData, &serial, &myear, &mweak); + } + } + #endif + + if (name.length == 0) { + const char* connectorTypeName = drmType2Name(conn->connector_type); + if (connectorTypeName == NULL) { + connectorTypeName = "Unknown"; + } + ffStrbufSetF(&name, "%s-%d", connectorTypeName, iConn + 1); + } + + FFDisplayResult* item = ffdsAppendDisplay(result, + width, + height, + refreshRate, + 0, + preferredWidth, + preferredHeight, + preferredRefreshRate, + 0, + &name, + conn->connector_type == DRM_MODE_CONNECTOR_eDP || conn->connector_type == DRM_MODE_CONNECTOR_LVDS + ? FF_DISPLAY_TYPE_BUILTIN + : conn->connector_type == DRM_MODE_CONNECTOR_HDMIA || conn->connector_type == DRM_MODE_CONNECTOR_HDMIB || conn->connector_type == DRM_MODE_CONNECTOR_DisplayPort + ? FF_DISPLAY_TYPE_EXTERNAL + : FF_DISPLAY_TYPE_UNKNOWN, + false, + conn->connector_id, + conn->mmWidth, + conn->mmHeight, + "libdrm"); + + if (item) { + item->hdrStatus = hdrStatus; + item->serial = serial; + item->manufactureYear = myear; + item->manufactureWeek = mweak; + item->bitDepth = bitDepth; + } + } + + ffdrmModeFreeConnector(conn); + } + + ffdrmModeFreeResources(res); + } + + ffdrmFreeDevices(devices, nDevices); + + return NULL; +} + +#endif + +const char* ffdsConnectDrm(FF_A_UNUSED FFDisplayServerResult* result) { +#ifdef FF_HAVE_DRM + if (instance.config.general.dsForceDrm != FF_DS_FORCE_DRM_TYPE_SYSFS_ONLY) { + if (drmConnectLibdrm(result) == NULL) { + return NULL; + } + } +#endif + +#ifdef __linux__ + return drmParseSysfs(result); +#endif + + return "fastfetch was compiled without drm support"; +} diff --git a/src/detection/displayserver/linux/wayland/global-output.c b/src/detection/displayserver/linux/wayland/global-output.c new file mode 100644 index 0000000..51fb425 --- /dev/null +++ b/src/detection/displayserver/linux/wayland/global-output.c @@ -0,0 +1,173 @@ +#ifdef FF_HAVE_WAYLAND + + #include "wayland.h" + #include "common/strutil.h" + #include "xdg-output-unstable-v1-client-protocol.h" + +static void waylandOutputModeListener(void* data, FF_A_UNUSED struct wl_output* output, uint32_t flags, int32_t width, int32_t height, int32_t refreshRate) { + WaylandDisplay* display = data; + + if (flags & WL_OUTPUT_MODE_CURRENT) { + display->width = width; + display->height = height; + display->refreshRate = refreshRate; + } + if (flags & WL_OUTPUT_MODE_PREFERRED) { + display->preferredWidth = width; + display->preferredHeight = height; + display->preferredRefreshRate = refreshRate; + } +} + +static void waylandOutputScaleListener(void* data, FF_A_UNUSED struct wl_output* output, int32_t scale) { + WaylandDisplay* display = data; + display->dpi = 96 * (uint32_t) scale; +} + +static void waylandOutputGeometryListener(void* data, + FF_A_UNUSED struct wl_output* output, + FF_A_UNUSED int32_t x, + FF_A_UNUSED int32_t y, + int32_t physical_width, + int32_t physical_height, + FF_A_UNUSED int32_t subpixel, + FF_A_UNUSED const char* make, + FF_A_UNUSED const char* model, + int32_t transform) { + WaylandDisplay* display = data; + display->physicalWidth = physical_width; + display->physicalHeight = physical_height; + display->transform = (enum wl_output_transform) transform; +} + +static void handleXdgLogicalSize(void* data, FF_A_UNUSED struct zxdg_output_v1* _, int32_t width, FF_A_UNUSED int32_t height) { + WaylandDisplay* display = data; + // Seems the values are only useful when ractional scale is enabled + if (width < display->width) { + display->dpi = (uint32_t) (display->width * 96 / width); + } +} + +// Dirty hack for #477 +// The order of these callbacks MUST follow `struct wl_output_listener` +static void* outputListener[] = { + waylandOutputGeometryListener, // geometry + waylandOutputModeListener, // mode + stubListener, // done + waylandOutputScaleListener, // scale + ffWaylandOutputNameListener, // name + ffWaylandOutputDescriptionListener, // description +}; +static_assert( + sizeof(outputListener) >= sizeof(struct wl_output_listener), + "sizeof(outputListener) is too small. Please report it to fastfetch github issue"); + +static struct zxdg_output_v1_listener zxdgOutputListener = { + .logical_position = (void*) stubListener, + .logical_size = handleXdgLogicalSize, + .done = (void*) stubListener, + .name = (void*) ffWaylandOutputNameListener, + .description = (void*) ffWaylandOutputDescriptionListener, +}; + +const char* ffWaylandHandleGlobalOutput(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version) { + const char* api = "wayland-global"; + uint32_t bindVersion = min(version, WL_OUTPUT_DESCRIPTION_SINCE_VERSION); + struct wl_proxy* output = wldata->ffwl_proxy_marshal_constructor_versioned((struct wl_proxy*) registry, WL_REGISTRY_BIND, wldata->ffwl_output_interface, bindVersion, name, wldata->ffwl_output_interface->name, bindVersion, NULL); + if (output == NULL) { + return "Failed to create wl_output"; + } + + WaylandDisplay display = { + .parent = wldata, + .transform = WL_OUTPUT_TRANSFORM_NORMAL, + .type = FF_DISPLAY_TYPE_UNKNOWN, + .name = ffStrbufCreate(), + .description = ffStrbufCreate(), + .edidName = ffStrbufCreate(), + }; + + if (wldata->ffwl_proxy_add_listener(output, (void (**)(void)) &outputListener, &display) < 0) { + wldata->ffwl_proxy_destroy(output); + return "Failed to add listener to wl_output"; + } + if (wldata->ffwl_display_roundtrip(wldata->display) < 0) { + wldata->ffwl_proxy_destroy(output); + return "Failed to roundtrip wl_output"; + } + + if (wldata->zxdgOutputManager) { + uint32_t bindVersion = min(version, ZXDG_OUTPUT_V1_DESCRIPTION_SINCE_VERSION); + struct wl_proxy* zxdgOutput = wldata->ffwl_proxy_marshal_constructor_versioned(wldata->zxdgOutputManager, ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT, &zxdg_output_v1_interface, bindVersion, NULL, output); + + if (zxdgOutput) { + wldata->ffwl_proxy_add_listener(zxdgOutput, (void (**)(void)) &zxdgOutputListener, &display); + wldata->ffwl_display_roundtrip(wldata->display); + wldata->ffwl_proxy_destroy(zxdgOutput); + api = "wayland-global-zxdg"; + } + } + + wldata->ffwl_proxy_destroy(output); + + if (display.width <= 0 || display.height <= 0) { + return "Failed to get display information from wl_output"; + } + + uint32_t rotation = ffWaylandHandleRotation(&display); + + FFDisplayResult* item = ffdsAppendDisplay(wldata->result, + (uint32_t) display.width, + (uint32_t) display.height, + display.refreshRate / 1000.0, + display.dpi, + (uint32_t) display.preferredWidth, + (uint32_t) display.preferredHeight, + display.preferredRefreshRate / 1000.0, + rotation, + display.edidName.length + ? &display.edidName + // Try ignoring `eDP-1-unknown`, where `unknown` is localized + : display.description.length && !ffStrbufContain(&display.description, &display.name) + ? &display.description + : &display.name, + display.type, + false, + display.id, + (uint32_t) display.physicalWidth, + (uint32_t) display.physicalHeight, + api); + if (item) { + if (display.hdrSupported) { + item->hdrStatus = FF_DISPLAY_HDR_STATUS_SUPPORTED; + } else if (display.hdrInfoAvailable) { + item->hdrStatus = FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + } else { + item->hdrStatus = FF_DISPLAY_HDR_STATUS_UNKNOWN; + } + + item->manufactureYear = display.myear; + item->manufactureWeek = display.mweek; + item->serial = display.serial; + } + + ffStrbufDestroy(&display.description); + ffStrbufDestroy(&display.name); + ffStrbufDestroy(&display.edidName); + + return NULL; +} + +const char* ffWaylandHandleZxdgOutput(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version) { + uint32_t bindVersion = min(version, ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT_SINCE_VERSION); + struct wl_proxy* manager = wldata->ffwl_proxy_marshal_constructor_versioned((struct wl_proxy*) registry, WL_REGISTRY_BIND, &zxdg_output_manager_v1_interface, bindVersion, name, zxdg_output_manager_v1_interface.name, bindVersion, NULL); + if (manager == NULL) { + return "Failed to create zxdg_output_manager_v1"; + } + + wldata->zxdgOutputManager = manager; + + return NULL; +} + +#endif diff --git a/src/detection/displayserver/linux/wayland/kde-output-device-v2-client-protocol.h b/src/detection/displayserver/linux/wayland/kde-output-device-v2-client-protocol.h new file mode 100644 index 0000000..44ab8ab --- /dev/null +++ b/src/detection/displayserver/linux/wayland/kde-output-device-v2-client-protocol.h @@ -0,0 +1,1384 @@ +/* Generated by wayland-scanner 1.24.0 */ + +#ifndef KDE_OUTPUT_DEVICE_V2_CLIENT_PROTOCOL_H +#define KDE_OUTPUT_DEVICE_V2_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include <wayland-client.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_kde_output_device_v2 The kde_output_device_v2 protocol + * @section page_ifaces_kde_output_device_v2 Interfaces + * - @subpage page_iface_kde_output_device_registry_v2 - output devices + * - @subpage page_iface_kde_output_device_v2 - output configuration representation + * - @subpage page_iface_kde_output_device_mode_v2 - output mode + * @section page_copyright_kde_output_device_v2 Copyright + * <pre> + * + * SPDX-FileCopyrightText: 2008-2011 Kristian Høgsberg + * SPDX-FileCopyrightText: 2010-2011 Intel Corporation + * SPDX-FileCopyrightText: 2012-2013 Collabora, Ltd. + * SPDX-FileCopyrightText: 2015 Sebastian Kügler <sebas@kde.org> + * SPDX-FileCopyrightText: 2021 Méven Car <meven.car@enioka.com> + * + * SPDX-License-Identifier: MIT-CMU + * </pre> + */ +struct kde_output_device_mode_v2; +struct kde_output_device_registry_v2; +struct kde_output_device_v2; + +#ifndef KDE_OUTPUT_DEVICE_REGISTRY_V2_INTERFACE + #define KDE_OUTPUT_DEVICE_REGISTRY_V2_INTERFACE +/** + * @page page_iface_kde_output_device_registry_v2 kde_output_device_registry_v2 + * @section page_iface_kde_output_device_registry_v2_desc Description + * + * This interface can be used to list output devices. + * + * If this global is bound with a version less than 21, the unsupported_version + * protocol error will be posted. + * @section page_iface_kde_output_device_registry_v2_api API + * See @ref iface_kde_output_device_registry_v2. + */ +/** + * @defgroup iface_kde_output_device_registry_v2 The kde_output_device_registry_v2 interface + * + * This interface can be used to list output devices. + * + * If this global is bound with a version less than 21, the unsupported_version + * protocol error will be posted. + */ +extern const struct wl_interface kde_output_device_registry_v2_interface; +#endif +#ifndef KDE_OUTPUT_DEVICE_V2_INTERFACE + #define KDE_OUTPUT_DEVICE_V2_INTERFACE +/** + * @page page_iface_kde_output_device_v2 kde_output_device_v2 + * @section page_iface_kde_output_device_v2_desc Description + * + * An output device describes a display device available to the compositor. + * output_device is similar to wl_output, but focuses on output + * configuration management. + * + * A client can query all global output_device objects to enlist all + * available display devices, even those that may currently not be + * represented by the compositor as a wl_output. + * + * The client sends configuration changes to the server through the + * outputconfiguration interface, and the server applies the configuration + * changes to the hardware and signals changes to the output devices + * accordingly. + * + * This object is published as global during start up for every available + * display devices, or when one later becomes available, for example by + * being hotplugged via a physical connector. + * + * Warning! The protocol described in this file is a desktop environment + * implementation detail. Regular clients must not use this protocol. + * Backward incompatible changes may be added without bumping the major + * version of the extension. + * @section page_iface_kde_output_device_v2_api API + * See @ref iface_kde_output_device_v2. + */ +/** + * @defgroup iface_kde_output_device_v2 The kde_output_device_v2 interface + * + * An output device describes a display device available to the compositor. + * output_device is similar to wl_output, but focuses on output + * configuration management. + * + * A client can query all global output_device objects to enlist all + * available display devices, even those that may currently not be + * represented by the compositor as a wl_output. + * + * The client sends configuration changes to the server through the + * outputconfiguration interface, and the server applies the configuration + * changes to the hardware and signals changes to the output devices + * accordingly. + * + * This object is published as global during start up for every available + * display devices, or when one later becomes available, for example by + * being hotplugged via a physical connector. + * + * Warning! The protocol described in this file is a desktop environment + * implementation detail. Regular clients must not use this protocol. + * Backward incompatible changes may be added without bumping the major + * version of the extension. + */ +extern const struct wl_interface kde_output_device_v2_interface; +#endif +#ifndef KDE_OUTPUT_DEVICE_MODE_V2_INTERFACE + #define KDE_OUTPUT_DEVICE_MODE_V2_INTERFACE +/** + * @page page_iface_kde_output_device_mode_v2 kde_output_device_mode_v2 + * @section page_iface_kde_output_device_mode_v2_desc Description + * + * This object describes an output mode. + * + * Some heads don't support output modes, in which case modes won't be + * advertised. + * + * Properties sent via this interface are applied atomically via the + * kde_output_device.done event. No guarantees are made regarding the order + * in which properties are sent. + * @section page_iface_kde_output_device_mode_v2_api API + * See @ref iface_kde_output_device_mode_v2. + */ +/** + * @defgroup iface_kde_output_device_mode_v2 The kde_output_device_mode_v2 interface + * + * This object describes an output mode. + * + * Some heads don't support output modes, in which case modes won't be + * advertised. + * + * Properties sent via this interface are applied atomically via the + * kde_output_device.done event. No guarantees are made regarding the order + * in which properties are sent. + */ +extern const struct wl_interface kde_output_device_mode_v2_interface; +#endif + +#ifndef KDE_OUTPUT_DEVICE_REGISTRY_V2_ERROR_ENUM + #define KDE_OUTPUT_DEVICE_REGISTRY_V2_ERROR_ENUM +/** + * @ingroup iface_kde_output_device_registry_v2 + * kde_output_device_registry_v2 error values + * + * These errors can be emitted in response to some requests. + */ +enum kde_output_device_registry_v2_error { + /** + * the registry was bound with an unsupported version + */ + KDE_OUTPUT_DEVICE_REGISTRY_V2_ERROR_UNSUPPORTED_VERSION = 0, +}; +#endif /* KDE_OUTPUT_DEVICE_REGISTRY_V2_ERROR_ENUM */ + +/** + * @ingroup iface_kde_output_device_registry_v2 + * @struct kde_output_device_registry_v2_listener + */ +struct kde_output_device_registry_v2_listener { + /** + * no new output announcements + * + * This event is sent in response to the stop request. The + * compositor will immediately destroy the object after sending + * this event. + * @since 21 + */ + void (*finished)(void* data, + struct kde_output_device_registry_v2* kde_output_device_registry_v2); + /** + * new available output + * + * This event is sent when a new output is connected or after + * binding this global to list all available outputs. + * @since 21 + */ + void (*output)(void* data, + struct kde_output_device_registry_v2* kde_output_device_registry_v2, + struct kde_output_device_v2* output); +}; + +/** + * @ingroup iface_kde_output_device_registry_v2 + */ +static inline int +kde_output_device_registry_v2_add_listener(struct kde_output_device_registry_v2* kde_output_device_registry_v2, + const struct kde_output_device_registry_v2_listener* listener, + void* data) { + return wl_proxy_add_listener((struct wl_proxy*) kde_output_device_registry_v2, + (void (**)(void)) listener, + data); +} + +#define KDE_OUTPUT_DEVICE_REGISTRY_V2_STOP 0 + +/** + * @ingroup iface_kde_output_device_registry_v2 + */ +#define KDE_OUTPUT_DEVICE_REGISTRY_V2_FINISHED_SINCE_VERSION 21 +/** + * @ingroup iface_kde_output_device_registry_v2 + */ +#define KDE_OUTPUT_DEVICE_REGISTRY_V2_OUTPUT_SINCE_VERSION 21 + +/** + * @ingroup iface_kde_output_device_registry_v2 + */ +#define KDE_OUTPUT_DEVICE_REGISTRY_V2_STOP_SINCE_VERSION 21 + +/** @ingroup iface_kde_output_device_registry_v2 */ +static inline void +kde_output_device_registry_v2_set_user_data(struct kde_output_device_registry_v2* kde_output_device_registry_v2, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*) kde_output_device_registry_v2, user_data); +} + +/** @ingroup iface_kde_output_device_registry_v2 */ +static inline void* +kde_output_device_registry_v2_get_user_data(struct kde_output_device_registry_v2* kde_output_device_registry_v2) { + return wl_proxy_get_user_data((struct wl_proxy*) kde_output_device_registry_v2); +} + +static inline uint32_t +kde_output_device_registry_v2_get_version(struct kde_output_device_registry_v2* kde_output_device_registry_v2) { + return wl_proxy_get_version((struct wl_proxy*) kde_output_device_registry_v2); +} + +/** @ingroup iface_kde_output_device_registry_v2 */ +static inline void +kde_output_device_registry_v2_destroy(struct kde_output_device_registry_v2* kde_output_device_registry_v2) { + wl_proxy_destroy((struct wl_proxy*) kde_output_device_registry_v2); +} + +/** + * @ingroup iface_kde_output_device_registry_v2 + * + * This request indicates that the client no longer wants to receive new + * output announcements. The compositor will send the + * kde_output_device_registry_v2.finished event in response to this request. + * The compositor may still send new output announcements after calling this + * request until the kde_output_device_registry_v2.finished event is sent. + */ +// static inline void +// kde_output_device_registry_v2_stop(struct kde_output_device_registry_v2* kde_output_device_registry_v2) { +// wl_proxy_marshal_flags((struct wl_proxy*) kde_output_device_registry_v2, +// KDE_OUTPUT_DEVICE_REGISTRY_V2_STOP, +// NULL, +// wl_proxy_get_version((struct wl_proxy*) kde_output_device_registry_v2), +// 0); +// } + +#ifndef KDE_OUTPUT_DEVICE_V2_SUBPIXEL_ENUM + #define KDE_OUTPUT_DEVICE_V2_SUBPIXEL_ENUM +/** + * @ingroup iface_kde_output_device_v2 + * subpixel geometry information + * + * This enumeration describes how the physical pixels on an output are + * laid out. + */ +enum kde_output_device_v2_subpixel { + KDE_OUTPUT_DEVICE_V2_SUBPIXEL_UNKNOWN = 0, + KDE_OUTPUT_DEVICE_V2_SUBPIXEL_NONE = 1, + KDE_OUTPUT_DEVICE_V2_SUBPIXEL_HORIZONTAL_RGB = 2, + KDE_OUTPUT_DEVICE_V2_SUBPIXEL_HORIZONTAL_BGR = 3, + KDE_OUTPUT_DEVICE_V2_SUBPIXEL_VERTICAL_RGB = 4, + KDE_OUTPUT_DEVICE_V2_SUBPIXEL_VERTICAL_BGR = 5, +}; +#endif /* KDE_OUTPUT_DEVICE_V2_SUBPIXEL_ENUM */ + +#ifndef KDE_OUTPUT_DEVICE_V2_TRANSFORM_ENUM + #define KDE_OUTPUT_DEVICE_V2_TRANSFORM_ENUM +/** + * @ingroup iface_kde_output_device_v2 + * transform from framebuffer to output + * + * This describes the transform, that a compositor will apply to a + * surface to compensate for the rotation or mirroring of an + * output device. + * + * The flipped values correspond to an initial flip around a + * vertical axis followed by rotation. + * + * The purpose is mainly to allow clients to render accordingly and + * tell the compositor, so that for fullscreen surfaces, the + * compositor is still able to scan out directly client surfaces. + */ +enum kde_output_device_v2_transform { + KDE_OUTPUT_DEVICE_V2_TRANSFORM_NORMAL = 0, + KDE_OUTPUT_DEVICE_V2_TRANSFORM_90 = 1, + KDE_OUTPUT_DEVICE_V2_TRANSFORM_180 = 2, + KDE_OUTPUT_DEVICE_V2_TRANSFORM_270 = 3, + KDE_OUTPUT_DEVICE_V2_TRANSFORM_FLIPPED = 4, + KDE_OUTPUT_DEVICE_V2_TRANSFORM_FLIPPED_90 = 5, + KDE_OUTPUT_DEVICE_V2_TRANSFORM_FLIPPED_180 = 6, + KDE_OUTPUT_DEVICE_V2_TRANSFORM_FLIPPED_270 = 7, +}; +#endif /* KDE_OUTPUT_DEVICE_V2_TRANSFORM_ENUM */ + +#ifndef KDE_OUTPUT_DEVICE_V2_CAPABILITY_ENUM + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_ENUM +/** + * @ingroup iface_kde_output_device_v2 + * describes capabilities of the outputdevice + * + * Describes what capabilities this device has. + */ +enum kde_output_device_v2_capability { + /** + * if this output_device can use overscan + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_OVERSCAN = 0x1, + /** + * if this outputdevice supports variable refresh rate + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_VRR = 0x2, + /** + * if setting the rgb range is possible + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_RGB_RANGE = 0x4, + /** + * if this outputdevice supports high dynamic range + * @since 3 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_HIGH_DYNAMIC_RANGE = 0x8, + /** + * if this outputdevice supports a wide color gamut + * @since 3 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_WIDE_COLOR_GAMUT = 0x10, + /** + * if this outputdevice supports autorotation + * @since 4 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_AUTO_ROTATE = 0x20, + /** + * if this outputdevice supports icc profiles + * @since 5 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_ICC_PROFILE = 0x40, + /** + * if this outputdevice supports the brightness setting + * @since 9 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_BRIGHTNESS = 0x80, + /** + * if this outputdevice supports the built-in color profile + * @since 12 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_BUILT_IN_COLOR = 0x100, + /** + * if this outputdevice supports DDC/CI + * @since 14 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_DDC_CI = 0x200, + /** + * if this outputdevice supports setting max bpc + * @since 15 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_MAX_BITS_PER_COLOR = 0x400, + /** + * if this outputdevice supports EDR + * @since 16 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_EDR = 0x800, + /** + * if this outputdevice supports the sharpness setting + * @since 17 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_SHARPNESS = 0x1000, + /** + * if this outputdevice supports custom modes + * @since 18 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_CUSTOM_MODES = 0x2000, + /** + * @since 19 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_AUTO_BRIGHTNESS = 0x4000, + /** + * if this outputdevice supports HDR ICC profiles + * @since 22 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_HDR_ICC_PROFILE = 0x8000, + /** + * if this outputdevice supports the abm level setting + * @since 23 + */ + KDE_OUTPUT_DEVICE_V2_CAPABILITY_ABM_LEVEL = 0x10000, +}; + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_HIGH_DYNAMIC_RANGE_SINCE_VERSION 3 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_WIDE_COLOR_GAMUT_SINCE_VERSION 3 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_AUTO_ROTATE_SINCE_VERSION 4 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_ICC_PROFILE_SINCE_VERSION 5 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_BRIGHTNESS_SINCE_VERSION 9 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_BUILT_IN_COLOR_SINCE_VERSION 12 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_DDC_CI_SINCE_VERSION 14 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_MAX_BITS_PER_COLOR_SINCE_VERSION 15 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_EDR_SINCE_VERSION 16 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_SHARPNESS_SINCE_VERSION 17 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_CUSTOM_MODES_SINCE_VERSION 18 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_AUTO_BRIGHTNESS_SINCE_VERSION 19 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_HDR_ICC_PROFILE_SINCE_VERSION 22 + /** + * @ingroup iface_kde_output_device_v2 + */ + #define KDE_OUTPUT_DEVICE_V2_CAPABILITY_ABM_LEVEL_SINCE_VERSION 23 +#endif /* KDE_OUTPUT_DEVICE_V2_CAPABILITY_ENUM */ + +#ifndef KDE_OUTPUT_DEVICE_V2_VRR_POLICY_ENUM + #define KDE_OUTPUT_DEVICE_V2_VRR_POLICY_ENUM +/** + * @ingroup iface_kde_output_device_v2 + * describes vrr policy + * + * Describes when the compositor may employ variable refresh rate + */ +enum kde_output_device_v2_vrr_policy { + KDE_OUTPUT_DEVICE_V2_VRR_POLICY_NEVER = 0, + KDE_OUTPUT_DEVICE_V2_VRR_POLICY_ALWAYS = 1, + KDE_OUTPUT_DEVICE_V2_VRR_POLICY_AUTOMATIC = 2, +}; +#endif /* KDE_OUTPUT_DEVICE_V2_VRR_POLICY_ENUM */ + +#ifndef KDE_OUTPUT_DEVICE_V2_RGB_RANGE_ENUM + #define KDE_OUTPUT_DEVICE_V2_RGB_RANGE_ENUM +/** + * @ingroup iface_kde_output_device_v2 + * describes RGB range policy + * + * Whether full or limited color range should be used + */ +enum kde_output_device_v2_rgb_range { + KDE_OUTPUT_DEVICE_V2_RGB_RANGE_AUTOMATIC = 0, + KDE_OUTPUT_DEVICE_V2_RGB_RANGE_FULL = 1, + KDE_OUTPUT_DEVICE_V2_RGB_RANGE_LIMITED = 2, +}; +#endif /* KDE_OUTPUT_DEVICE_V2_RGB_RANGE_ENUM */ + +#ifndef KDE_OUTPUT_DEVICE_V2_AUTO_ROTATE_POLICY_ENUM + #define KDE_OUTPUT_DEVICE_V2_AUTO_ROTATE_POLICY_ENUM +/** + * @ingroup iface_kde_output_device_v2 + * describes when auto rotate should be used + */ +enum kde_output_device_v2_auto_rotate_policy { + KDE_OUTPUT_DEVICE_V2_AUTO_ROTATE_POLICY_NEVER = 0, + KDE_OUTPUT_DEVICE_V2_AUTO_ROTATE_POLICY_IN_TABLET_MODE = 1, + KDE_OUTPUT_DEVICE_V2_AUTO_ROTATE_POLICY_ALWAYS = 2, +}; +#endif /* KDE_OUTPUT_DEVICE_V2_AUTO_ROTATE_POLICY_ENUM */ + +#ifndef KDE_OUTPUT_DEVICE_V2_COLOR_PROFILE_SOURCE_ENUM + #define KDE_OUTPUT_DEVICE_V2_COLOR_PROFILE_SOURCE_ENUM +/** + * @ingroup iface_kde_output_device_v2 + * which source the compositor should use for the color profile on an output + */ +enum kde_output_device_v2_color_profile_source { + KDE_OUTPUT_DEVICE_V2_COLOR_PROFILE_SOURCE_SRGB = 0, + KDE_OUTPUT_DEVICE_V2_COLOR_PROFILE_SOURCE_ICC = 1, + KDE_OUTPUT_DEVICE_V2_COLOR_PROFILE_SOURCE_EDID = 2, +}; +#endif /* KDE_OUTPUT_DEVICE_V2_COLOR_PROFILE_SOURCE_ENUM */ + +#ifndef KDE_OUTPUT_DEVICE_V2_COLOR_POWER_TRADEOFF_ENUM + #define KDE_OUTPUT_DEVICE_V2_COLOR_POWER_TRADEOFF_ENUM +/** + * @ingroup iface_kde_output_device_v2 + * tradeoff between power and accuracy + * + * The compositor can do a lot of things that trade between + * performance, power and color accuracy. This setting describes + * a high level preference from the user about in which direction + * that tradeoff should be made. + */ +enum kde_output_device_v2_color_power_tradeoff { + /** + * prefer efficiency and performance + */ + KDE_OUTPUT_DEVICE_V2_COLOR_POWER_TRADEOFF_EFFICIENCY = 0, + /** + * prefer accuracy + */ + KDE_OUTPUT_DEVICE_V2_COLOR_POWER_TRADEOFF_ACCURACY = 1, +}; +#endif /* KDE_OUTPUT_DEVICE_V2_COLOR_POWER_TRADEOFF_ENUM */ + +#ifndef KDE_OUTPUT_DEVICE_V2_EDR_POLICY_ENUM + #define KDE_OUTPUT_DEVICE_V2_EDR_POLICY_ENUM +/** + * @ingroup iface_kde_output_device_v2 + * when the compositor may make use of EDR + */ +enum kde_output_device_v2_edr_policy { + KDE_OUTPUT_DEVICE_V2_EDR_POLICY_NEVER = 0, + KDE_OUTPUT_DEVICE_V2_EDR_POLICY_ALWAYS = 1, +}; +#endif /* KDE_OUTPUT_DEVICE_V2_EDR_POLICY_ENUM */ + +/** + * @ingroup iface_kde_output_device_v2 + * @struct kde_output_device_v2_listener + */ +struct kde_output_device_v2_listener { + /** + * geometric properties of the output + * + * The geometry event describes geometric properties of the + * output. The event is sent when binding to the output object and + * whenever any of the properties change. + * @param x x position within the global compositor space + * @param y y position within the global compositor space + * @param physical_width width in millimeters of the output + * @param physical_height height in millimeters of the output + * @param subpixel subpixel orientation of the output + * @param make textual description of the manufacturer + * @param model textual description of the model + * @param transform transform that maps framebuffer to output + */ + void (*geometry)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + int32_t x, + int32_t y, + int32_t physical_width, + int32_t physical_height, + int32_t subpixel, + const char* make, + const char* model, + int32_t transform); + /** + * current mode + * + * This event describes the mode currently in use for this head. + * It is only sent if the output is enabled. + */ + void (*current_mode)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + struct kde_output_device_mode_v2* mode); + /** + * advertise available output modes and current one + * + * The mode event describes an available mode for the output. + * + * When the client binds to the output_device object, the server + * sends this event once for every available mode the output_device + * can be operated by. + * + * There will always be at least one event sent out on initial + * binding, which represents the current mode. + * + * Later if an output changes, its mode event is sent again for the + * eventual added modes and lastly the current mode. In other + * words, the current mode is always represented by the latest + * event sent with the current flag set. + * + * The size of a mode is given in physical hardware units of the + * output device. This is not necessarily the same as the output + * size in the global compositor space. For instance, the output + * may be scaled, as described in kde_output_device_v2.scale, or + * transformed, as described in kde_output_device_v2.transform. + */ + void (*mode)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + struct kde_output_device_mode_v2* mode); + /** + * sent all information about output + * + * This event is sent after all other properties have been sent + * on binding to the output object as well as after any other + * output property change have been applied later on. This allows + * to see changes to the output properties as atomic, even if + * multiple events successively announce them. + */ + void (*done)(void* data, + struct kde_output_device_v2* kde_output_device_v2); + /** + * output scaling properties + * + * This event contains scaling geometry information that is not + * in the geometry event. It may be sent after binding the output + * object or if the output scale changes later. If it is not sent, + * the client should assume a scale of 1. + * + * A scale larger than 1 means that the compositor will + * automatically scale surface buffers by this amount when + * rendering. This is used for high resolution displays where + * applications rendering at the native resolution would be too + * small to be legible. + * + * It is intended that scaling aware clients track the current + * output of a surface, and if it is on a scaled output it should + * use wl_surface.set_buffer_scale with the scale of the output. + * That way the compositor can avoid scaling the surface, and the + * client can supply a higher detail image. + * @param factor scaling factor of output + */ + void (*scale)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + wl_fixed_t factor); + /** + * advertise EDID data for the output + * + * The edid event encapsulates the EDID data for the + * outputdevice. + * + * The event is sent when binding to the output object. The EDID + * data may be empty, in which case this event is sent anyway. If + * the EDID information is empty, you can fall back to the name et + * al. properties of the outputdevice. + * @param raw base64-encoded EDID string + */ + void (*edid)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + const char* raw); + /** + * output is enabled or disabled + * + * The enabled event notifies whether this output is currently + * enabled and used for displaying content by the server. The event + * is sent when binding to the output object and whenever later on + * an output changes its state by becoming enabled or disabled. + * @param enabled output enabled state + */ + void (*enabled)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + int32_t enabled); + /** + * A unique id for this outputdevice + * + * The uuid can be used to identify the output. It's controlled + * by the server entirely. The server should make sure the uuid is + * persistent across restarts. An empty uuid is considered invalid. + * @param uuid output devices ID + */ + void (*uuid)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + const char* uuid); + /** + * Serial Number + * + * Serial ID of the monitor, sent on startup before the first + * done event. + * @param serialNumber textual representation of serial number + */ + void (*serial_number)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + const char* serialNumber); + /** + * EISA ID + * + * EISA ID of the monitor, sent on startup before the first done + * event. + * @param eisaId textual representation of EISA identifier + */ + void (*eisa_id)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + const char* eisaId); + /** + * capability flags + * + * What capabilities this device has, sent on startup before the + * first done event. + */ + void (*capabilities)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t flags); + /** + * overscan + * + * Overscan value of the monitor in percent, sent on startup + * before the first done event. + * @param overscan amount of overscan of the monitor + */ + void (*overscan)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t overscan); + /** + * Variable Refresh Rate Policy + * + * What policy the compositor will employ regarding its use of + * variable refresh rate. + */ + void (*vrr_policy)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t vrr_policy); + /** + * RGB range + * + * What rgb range the compositor is using for this output + */ + void (*rgb_range)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t rgb_range); + /** + * Output's name + * + * Name of the output, it's useful to cross-reference to an + * zxdg_output_v1 and ultimately QScreen + * @since 2 + */ + void (*name)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + const char* name); + /** + * if HDR is enabled + * + * Whether or not high dynamic range is enabled for this output + * @param hdr_enabled 1 if enabled, 0 if disabled + * @since 3 + */ + void (*high_dynamic_range)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t hdr_enabled); + /** + * the brightness of sdr if hdr is enabled + * + * If high dynamic range is used, this value defines the + * brightness in nits for content that's in standard dynamic range + * format. Note that while the value is in nits, that doesn't + * necessarily translate to the same brightness on the screen. + * @since 3 + */ + void (*sdr_brightness)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t sdr_brightness); + /** + * if WCG is enabled + * + * Whether or not the use of a wide color gamut is enabled for + * this output + * @param wcg_enabled 1 if enabled, 0 if disabled + * @since 3 + */ + void (*wide_color_gamut)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t wcg_enabled); + /** + * describes when auto rotate is used + * + * + * @since 4 + */ + void (*auto_rotate_policy)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t policy); + /** + * describes the path to the ICC profile used in SDR mode + * + * + * @since 5 + */ + void (*icc_profile_path)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + const char* profile_path); + /** + * metadata about the screen's brightness limits + * + * + * @param max_peak_brightness in nits + * @param max_frame_average_brightness in nits + * @param min_brightness in 0.0001 nits + * @since 6 + */ + void (*brightness_metadata)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t max_peak_brightness, + uint32_t max_frame_average_brightness, + uint32_t min_brightness); + /** + * overrides for the screen's brightness limits + * + * + * @param max_peak_brightness -1 for no override, positive values are the brightness in nits + * @param max_average_brightness -1 for no override, positive values are the brightness in nits + * @param min_brightness -1 for no override, positive values are the brightness in 0.0001 nits + * @since 6 + */ + void (*brightness_overrides)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + int32_t max_peak_brightness, + int32_t max_average_brightness, + int32_t min_brightness); + /** + * describes which gamut is assumed for sRGB applications + * + * This can be used to provide the colors users assume sRGB + * applications should have based on the default experience on many + * modern sRGB screens. + * @param gamut_wideness 0 means rec.709 primaries, 10000 means native primaries + * @since 6 + */ + void (*sdr_gamut_wideness)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t gamut_wideness); + /** + * describes which source the compositor uses for the color profile on an output in SDR mode + * + * + * @since 7 + */ + void (*color_profile_source)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t source); + /** + * brightness multiplier + * + * This is the brightness modifier of the output. It doesn't + * specify any absolute values, but is merely a multiplier on top + * of other brightness values, like sdr_brightness and + * brightness_metadata. 0 is the minimum brightness (not completely + * dark) and 10000 is the maximum brightness. This is currently + * only supported / meaningful while HDR is active. + * @param brightness brightness in 0-10000 + * @since 8 + */ + void (*brightness)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t brightness); + /** + * the preferred color/power tradeoff + * + * + * @since 10 + */ + void (*color_power_tradeoff)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t preference); + /** + * dimming multiplier + * + * This is the dimming multiplier of the output. This is similar + * to the brightness setting, except it's meant to be a temporary + * setting only, not persistent and may be implemented differently + * depending on the display. 0 is the minimum dimming factor (not + * completely dark) and 10000 means the output is not dimmed. + * @param multiplier multiplier in 0-10000 + * @since 11 + */ + void (*dimming)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t multiplier); + /** + * source output for mirroring + * + * + * @param source uuid of the source output + * @since 13 + */ + void (*replication_source)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + const char* source); + /** + * if DDC/CI should be used to control brightness etc. + * + * If the ddc_ci capability is present, this determines if + * settings such as brightness, contrast or others should be set + * using DDC/CI. + * @param allowed 1 if allowed, 0 if disabled + * @since 14 + */ + void (*ddc_ci_allowed)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t allowed); + /** + * override max bpc + * + * This limits the amount of bits per color that are sent to the + * display. + * @param max_bpc 0 for the default / automatic + * @since 15 + */ + void (*max_bits_per_color)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t max_bpc); + /** + * range of max bits per color value + * + * + * @param min_value the minimum supported by the driver + * @param max_value the maximum supported by the driver + * @since 15 + */ + void (*max_bits_per_color_range)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t min_value, + uint32_t max_value); + /** + * if and to what value automatic max bpc is limited + * + * + * @param max_bpc_limit which value automatic bpc gets limited to. 0 if not limited + * @since 15 + */ + void (*automatic_max_bits_per_color_limit)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t max_bpc_limit); + /** + * when the compositor may apply EDR + * + * When EDR is enabled, the compositor may increase the backlight + * beyond the user-specified setting, in order to present HDR + * content on displays without native HDR support. This will + * usually result in better visuals, but also increases battery + * usage. + * @since 16 + */ + void (*edr_policy)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t policy); + /** + * sharpness strength + * + * This is the sharpness modifier of the output. 0 is sharpness + * disabled and 10000 is the maximum sharpness + * @param sharpness sharpness in 0-10000 + * @since 17 + */ + void (*sharpness)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t sharpness); + /** + * output priority + * + * Describes the position of the output in the output order list, + * with lower values being earlier in the list. There's no specific + * value the list has to start at, this value is only used in + * sorting outputs. + * + * Note that the output order protocol is not sufficient for this, + * as an output may not be in the output order if it's disabled or + * mirroring another screen. + * @param priority priority + * @since 18 + */ + void (*priority)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t priority); + /** + * whether or not automatic brightness is enabled + * + * + * @param enabled 1 for enabled, 0 for disabled + * @since 20 + */ + void (*auto_brightness)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t enabled); + /** + * the output has been removed + * + * This event is sent when the output device is disconnected and + * no new updates will be sent. The client should call the + * kde_output_device_v2.release request after receiving this event. + * @since 21 + */ + void (*removed)(void* data, + struct kde_output_device_v2* kde_output_device_v2); + /** + * describes the path to the ICC profile used in HDR mode + * + * + * @since 22 + */ + void (*hdr_icc_profile_path)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + const char* profile_path); + /** + * describes which source the compositor uses for the color profile on an output in HDR mode + * + * + * @since 22 + */ + void (*hdr_color_profile_source)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t source); + /** + * allowed level of adaptive backlight modulation + * + * Adaptive backlight modulation is a feature that reduces the + * backlight and increases contrast of colors on the screen to + * improve power usage. + * @param level 0 is off, 4 is the maximum level + * @since 23 + */ + void (*abm_level)(void* data, + struct kde_output_device_v2* kde_output_device_v2, + uint32_t level); +}; + +/** + * @ingroup iface_kde_output_device_v2 + */ +static inline int +kde_output_device_v2_add_listener(struct kde_output_device_v2* kde_output_device_v2, + const struct kde_output_device_v2_listener* listener, + void* data) { + return wl_proxy_add_listener((struct wl_proxy*) kde_output_device_v2, + (void (**)(void)) listener, + data); +} + +#define KDE_OUTPUT_DEVICE_V2_RELEASE 0 + +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_GEOMETRY_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_CURRENT_MODE_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_MODE_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_DONE_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_SCALE_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_EDID_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_ENABLED_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_UUID_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_SERIAL_NUMBER_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_EISA_ID_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_CAPABILITIES_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_OVERSCAN_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_VRR_POLICY_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_RGB_RANGE_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_NAME_SINCE_VERSION 2 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_HIGH_DYNAMIC_RANGE_SINCE_VERSION 3 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_SDR_BRIGHTNESS_SINCE_VERSION 3 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_WIDE_COLOR_GAMUT_SINCE_VERSION 3 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_AUTO_ROTATE_POLICY_SINCE_VERSION 4 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_ICC_PROFILE_PATH_SINCE_VERSION 5 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_BRIGHTNESS_METADATA_SINCE_VERSION 6 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_BRIGHTNESS_OVERRIDES_SINCE_VERSION 6 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_SDR_GAMUT_WIDENESS_SINCE_VERSION 6 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_COLOR_PROFILE_SOURCE_SINCE_VERSION 7 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_BRIGHTNESS_SINCE_VERSION 8 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_COLOR_POWER_TRADEOFF_SINCE_VERSION 10 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_DIMMING_SINCE_VERSION 11 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_REPLICATION_SOURCE_SINCE_VERSION 13 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_DDC_CI_ALLOWED_SINCE_VERSION 14 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_MAX_BITS_PER_COLOR_SINCE_VERSION 15 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_MAX_BITS_PER_COLOR_RANGE_SINCE_VERSION 15 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_AUTOMATIC_MAX_BITS_PER_COLOR_LIMIT_SINCE_VERSION 15 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_EDR_POLICY_SINCE_VERSION 16 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_SHARPNESS_SINCE_VERSION 17 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_PRIORITY_SINCE_VERSION 18 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_AUTO_BRIGHTNESS_SINCE_VERSION 20 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_REMOVED_SINCE_VERSION 21 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_HDR_ICC_PROFILE_PATH_SINCE_VERSION 22 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_HDR_COLOR_PROFILE_SOURCE_SINCE_VERSION 22 +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_ABM_LEVEL_SINCE_VERSION 23 + +/** + * @ingroup iface_kde_output_device_v2 + */ +#define KDE_OUTPUT_DEVICE_V2_RELEASE_SINCE_VERSION 21 + +/** @ingroup iface_kde_output_device_v2 */ +static inline void +kde_output_device_v2_set_user_data(struct kde_output_device_v2* kde_output_device_v2, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*) kde_output_device_v2, user_data); +} + +/** @ingroup iface_kde_output_device_v2 */ +static inline void* +kde_output_device_v2_get_user_data(struct kde_output_device_v2* kde_output_device_v2) { + return wl_proxy_get_user_data((struct wl_proxy*) kde_output_device_v2); +} + +static inline uint32_t +kde_output_device_v2_get_version(struct kde_output_device_v2* kde_output_device_v2) { + return wl_proxy_get_version((struct wl_proxy*) kde_output_device_v2); +} + +/** @ingroup iface_kde_output_device_v2 */ +static inline void +kde_output_device_v2_destroy(struct kde_output_device_v2* kde_output_device_v2) { + wl_proxy_destroy((struct wl_proxy*) kde_output_device_v2); +} + +/** + * @ingroup iface_kde_output_device_v2 + * + * This notifies the compositor that the client no longer wishes to use + * the kde_output_device_v2 object. + */ +// static inline void +// kde_output_device_v2_release(struct kde_output_device_v2* kde_output_device_v2) { +// wl_proxy_marshal_flags((struct wl_proxy*) kde_output_device_v2, +// KDE_OUTPUT_DEVICE_V2_RELEASE, +// NULL, +// wl_proxy_get_version((struct wl_proxy*) kde_output_device_v2), +// WL_MARSHAL_FLAG_DESTROY); +// } + +#ifndef KDE_OUTPUT_DEVICE_MODE_V2_FLAGS_ENUM + #define KDE_OUTPUT_DEVICE_MODE_V2_FLAGS_ENUM +/** + * @ingroup iface_kde_output_device_mode_v2 + * mode flags + */ +enum kde_output_device_mode_v2_flags { + KDE_OUTPUT_DEVICE_MODE_V2_FLAGS_CUSTOM = 0x1, + KDE_OUTPUT_DEVICE_MODE_V2_FLAGS_REDUCED_BLANKING = 0x2, +}; +#endif /* KDE_OUTPUT_DEVICE_MODE_V2_FLAGS_ENUM */ + +/** + * @ingroup iface_kde_output_device_mode_v2 + * @struct kde_output_device_mode_v2_listener + */ +struct kde_output_device_mode_v2_listener { + /** + * mode size + * + * This event describes the mode size. The size is given in + * physical hardware units of the output device. This is not + * necessarily the same as the output size in the global compositor + * space. For instance, the output may be scaled or transformed. + * @param width width of the mode in hardware units + * @param height height of the mode in hardware units + */ + void (*size)(void* data, + struct kde_output_device_mode_v2* kde_output_device_mode_v2, + int32_t width, + int32_t height); + /** + * mode refresh rate + * + * This event describes the mode's fixed vertical refresh rate. + * It is only sent if the mode has a fixed refresh rate. + * @param refresh vertical refresh rate in mHz + */ + void (*refresh)(void* data, + struct kde_output_device_mode_v2* kde_output_device_mode_v2, + int32_t refresh); + /** + * mode is preferred + * + * This event advertises this mode as preferred. + */ + void (*preferred)(void* data, + struct kde_output_device_mode_v2* kde_output_device_mode_v2); + /** + * the mode has been destroyed + * + * The compositor will destroy the object immediately after + * sending this event, so it will become invalid and the client + * should release any resources associated with it. + */ + void (*removed)(void* data, + struct kde_output_device_mode_v2* kde_output_device_mode_v2); + /** + * mode flags + * + * This event describes the mode's flags. + * @since 19 + */ + void (*flags)(void* data, + struct kde_output_device_mode_v2* kde_output_device_mode_v2, + uint32_t flags); +}; + +/** + * @ingroup iface_kde_output_device_mode_v2 + */ +static inline int +kde_output_device_mode_v2_add_listener(struct kde_output_device_mode_v2* kde_output_device_mode_v2, + const struct kde_output_device_mode_v2_listener* listener, + void* data) { + return wl_proxy_add_listener((struct wl_proxy*) kde_output_device_mode_v2, + (void (**)(void)) listener, + data); +} + +/** + * @ingroup iface_kde_output_device_mode_v2 + */ +#define KDE_OUTPUT_DEVICE_MODE_V2_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_mode_v2 + */ +#define KDE_OUTPUT_DEVICE_MODE_V2_REFRESH_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_mode_v2 + */ +#define KDE_OUTPUT_DEVICE_MODE_V2_PREFERRED_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_mode_v2 + */ +#define KDE_OUTPUT_DEVICE_MODE_V2_REMOVED_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_device_mode_v2 + */ +#define KDE_OUTPUT_DEVICE_MODE_V2_FLAGS_SINCE_VERSION 19 + +/** @ingroup iface_kde_output_device_mode_v2 */ +static inline void +kde_output_device_mode_v2_set_user_data(struct kde_output_device_mode_v2* kde_output_device_mode_v2, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*) kde_output_device_mode_v2, user_data); +} + +/** @ingroup iface_kde_output_device_mode_v2 */ +static inline void* +kde_output_device_mode_v2_get_user_data(struct kde_output_device_mode_v2* kde_output_device_mode_v2) { + return wl_proxy_get_user_data((struct wl_proxy*) kde_output_device_mode_v2); +} + +static inline uint32_t +kde_output_device_mode_v2_get_version(struct kde_output_device_mode_v2* kde_output_device_mode_v2) { + return wl_proxy_get_version((struct wl_proxy*) kde_output_device_mode_v2); +} + +/** @ingroup iface_kde_output_device_mode_v2 */ +static inline void +kde_output_device_mode_v2_destroy(struct kde_output_device_mode_v2* kde_output_device_mode_v2) { + wl_proxy_destroy((struct wl_proxy*) kde_output_device_mode_v2); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/detection/displayserver/linux/wayland/kde-output-device-v2-protocol.c b/src/detection/displayserver/linux/wayland/kde-output-device-v2-protocol.c new file mode 100644 index 0000000..9dd7556 --- /dev/null +++ b/src/detection/displayserver/linux/wayland/kde-output-device-v2-protocol.c @@ -0,0 +1,128 @@ +#ifdef FF_HAVE_WAYLAND + +/* Generated by wayland-scanner 1.24.0 */ + +/* + * SPDX-FileCopyrightText: 2008-2011 Kristian Høgsberg + * SPDX-FileCopyrightText: 2010-2011 Intel Corporation + * SPDX-FileCopyrightText: 2012-2013 Collabora, Ltd. + * SPDX-FileCopyrightText: 2015 Sebastian Kügler <sebas@kde.org> + * SPDX-FileCopyrightText: 2021 Méven Car <meven.car@enioka.com> + * + * SPDX-License-Identifier: MIT-CMU + */ + + #include <stdbool.h> + #include <stdlib.h> + #include <stdint.h> + #include <wayland-util.h> + +extern const struct wl_interface kde_output_device_mode_v2_interface; +extern const struct wl_interface kde_output_device_v2_interface; + +static const struct wl_interface* kde_output_device_v2_types[] = { + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + &kde_output_device_v2_interface, + &kde_output_device_mode_v2_interface, + &kde_output_device_mode_v2_interface, +}; + +static const struct wl_message kde_output_device_registry_v2_requests[] = { + { "stop", "21", kde_output_device_v2_types + 0 }, +}; + +static const struct wl_message kde_output_device_registry_v2_events[] = { + { "finished", "21", kde_output_device_v2_types + 0 }, + { "output", "21n", kde_output_device_v2_types + 8 }, +}; + +WL_EXPORT const struct wl_interface kde_output_device_registry_v2_interface = { + "kde_output_device_registry_v2", + 23, + 1, + kde_output_device_registry_v2_requests, + 2, + kde_output_device_registry_v2_events, +}; + +static const struct wl_message kde_output_device_v2_requests[] = { + { "release", "21", kde_output_device_v2_types + 0 }, +}; + +static const struct wl_message kde_output_device_v2_events[] = { + { "geometry", "iiiiissi", kde_output_device_v2_types + 0 }, + { "current_mode", "o", kde_output_device_v2_types + 9 }, + { "mode", "n", kde_output_device_v2_types + 10 }, + { "done", "", kde_output_device_v2_types + 0 }, + { "scale", "f", kde_output_device_v2_types + 0 }, + { "edid", "s", kde_output_device_v2_types + 0 }, + { "enabled", "i", kde_output_device_v2_types + 0 }, + { "uuid", "s", kde_output_device_v2_types + 0 }, + { "serial_number", "s", kde_output_device_v2_types + 0 }, + { "eisa_id", "s", kde_output_device_v2_types + 0 }, + { "capabilities", "u", kde_output_device_v2_types + 0 }, + { "overscan", "u", kde_output_device_v2_types + 0 }, + { "vrr_policy", "u", kde_output_device_v2_types + 0 }, + { "rgb_range", "u", kde_output_device_v2_types + 0 }, + { "name", "2s", kde_output_device_v2_types + 0 }, + { "high_dynamic_range", "3u", kde_output_device_v2_types + 0 }, + { "sdr_brightness", "3u", kde_output_device_v2_types + 0 }, + { "wide_color_gamut", "3u", kde_output_device_v2_types + 0 }, + { "auto_rotate_policy", "4u", kde_output_device_v2_types + 0 }, + { "icc_profile_path", "5s", kde_output_device_v2_types + 0 }, + { "brightness_metadata", "6uuu", kde_output_device_v2_types + 0 }, + { "brightness_overrides", "6iii", kde_output_device_v2_types + 0 }, + { "sdr_gamut_wideness", "6u", kde_output_device_v2_types + 0 }, + { "color_profile_source", "7u", kde_output_device_v2_types + 0 }, + { "brightness", "8u", kde_output_device_v2_types + 0 }, + { "color_power_tradeoff", "10u", kde_output_device_v2_types + 0 }, + { "dimming", "11u", kde_output_device_v2_types + 0 }, + { "replication_source", "13s", kde_output_device_v2_types + 0 }, + { "ddc_ci_allowed", "14u", kde_output_device_v2_types + 0 }, + { "max_bits_per_color", "15u", kde_output_device_v2_types + 0 }, + { "max_bits_per_color_range", "15uu", kde_output_device_v2_types + 0 }, + { "automatic_max_bits_per_color_limit", "15u", kde_output_device_v2_types + 0 }, + { "edr_policy", "16u", kde_output_device_v2_types + 0 }, + { "sharpness", "17u", kde_output_device_v2_types + 0 }, + { "priority", "18u", kde_output_device_v2_types + 0 }, + { "auto_brightness", "20u", kde_output_device_v2_types + 0 }, + { "removed", "21", kde_output_device_v2_types + 0 }, + { "hdr_icc_profile_path", "22s", kde_output_device_v2_types + 0 }, + { "hdr_color_profile_source", "22u", kde_output_device_v2_types + 0 }, + { "abm_level", "23u", kde_output_device_v2_types + 0 }, +}; + +WL_EXPORT const struct wl_interface kde_output_device_v2_interface = { + "kde_output_device_v2", + 23, + 1, + kde_output_device_v2_requests, + 40, + kde_output_device_v2_events, +}; + +static const struct wl_message kde_output_device_mode_v2_events[] = { + { "size", "ii", kde_output_device_v2_types + 0 }, + { "refresh", "i", kde_output_device_v2_types + 0 }, + { "preferred", "", kde_output_device_v2_types + 0 }, + { "removed", "", kde_output_device_v2_types + 0 }, + { "flags", "19u", kde_output_device_v2_types + 0 }, +}; + +WL_EXPORT const struct wl_interface kde_output_device_mode_v2_interface = { + "kde_output_device_mode_v2", + 22, + 0, + NULL, + 5, + kde_output_device_mode_v2_events, +}; + +#endif diff --git a/src/detection/displayserver/linux/wayland/kde-output-order-v1-client-protocol.h b/src/detection/displayserver/linux/wayland/kde-output-order-v1-client-protocol.h new file mode 100644 index 0000000..b9928c3 --- /dev/null +++ b/src/detection/displayserver/linux/wayland/kde-output-order-v1-client-protocol.h @@ -0,0 +1,134 @@ +/* Generated by wayland-scanner 1.22.0 */ + +#ifndef KDE_OUTPUT_ORDER_V1_CLIENT_PROTOCOL_H +#define KDE_OUTPUT_ORDER_V1_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_kde_output_order_v1 The kde_output_order_v1 protocol + * @section page_ifaces_kde_output_order_v1 Interfaces + * - @subpage page_iface_kde_output_order_v1 - announce order of outputs + * @section page_copyright_kde_output_order_v1 Copyright + * <pre> + * + * SPDX-FileCopyrightText: 2022 Xaver Hugl <xaver.hugl@gmail.com> + * + * SPDX-License-Identifier: MIT-CMU + * </pre> + */ +struct kde_output_order_v1; + +#ifndef KDE_OUTPUT_ORDER_V1_INTERFACE + #define KDE_OUTPUT_ORDER_V1_INTERFACE +/** + * @page page_iface_kde_output_order_v1 kde_output_order_v1 + * @section page_iface_kde_output_order_v1_desc Description + * + * Announce the order in which desktop environment components should be placed on outputs. + * The compositor will send the list of outputs when the global is bound and whenever there is a change. + * @section page_iface_kde_output_order_v1_api API + * See @ref iface_kde_output_order_v1. + */ +/** + * @defgroup iface_kde_output_order_v1 The kde_output_order_v1 interface + * + * Announce the order in which desktop environment components should be placed on outputs. + * The compositor will send the list of outputs when the global is bound and whenever there is a change. + */ +extern const struct wl_interface kde_output_order_v1_interface; +#endif + +/** + * @ingroup iface_kde_output_order_v1 + * @struct kde_output_order_v1_listener + */ +struct kde_output_order_v1_listener { + /** + * output name + * + * Specifies the output identified by their wl_output.name. + * @param output_name the name of the output + */ + void (*output)(void* data, + struct kde_output_order_v1* kde_output_order_v1, + const char* output_name); + /** + * done + * + * Specifies that the output list is complete. On the next output + * event, a new list begins. + */ + void (*done)(void* data, + struct kde_output_order_v1* kde_output_order_v1); +}; + +/** + * @ingroup iface_kde_output_order_v1 + */ +static inline int +kde_output_order_v1_add_listener(struct kde_output_order_v1* kde_output_order_v1, + const struct kde_output_order_v1_listener* listener, + void* data) { + return wl_proxy_add_listener((struct wl_proxy*) kde_output_order_v1, + (void (**)(void)) listener, + data); +} + +#define KDE_OUTPUT_ORDER_V1_DESTROY 0 + +/** + * @ingroup iface_kde_output_order_v1 + */ +#define KDE_OUTPUT_ORDER_V1_OUTPUT_SINCE_VERSION 1 +/** + * @ingroup iface_kde_output_order_v1 + */ +#define KDE_OUTPUT_ORDER_V1_DONE_SINCE_VERSION 1 + +/** + * @ingroup iface_kde_output_order_v1 + */ +#define KDE_OUTPUT_ORDER_V1_DESTROY_SINCE_VERSION 1 + +// /** @ingroup iface_kde_output_order_v1 */ +// static inline void +// kde_output_order_v1_set_user_data(struct kde_output_order_v1 *kde_output_order_v1, void *user_data) +// { +// wl_proxy_set_user_data((struct wl_proxy *) kde_output_order_v1, user_data); +// } + +// /** @ingroup iface_kde_output_order_v1 */ +// static inline void * +// kde_output_order_v1_get_user_data(struct kde_output_order_v1 *kde_output_order_v1) +// { +// return wl_proxy_get_user_data((struct wl_proxy *) kde_output_order_v1); +// } + +// static inline uint32_t +// kde_output_order_v1_get_version(struct kde_output_order_v1 *kde_output_order_v1) +// { +// return wl_proxy_get_version((struct wl_proxy *) kde_output_order_v1); +// } + +// /** +// * @ingroup iface_kde_output_order_v1 +// */ +// static inline void +// kde_output_order_v1_destroy(struct kde_output_order_v1 *kde_output_order_v1) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) kde_output_order_v1, +// KDE_OUTPUT_ORDER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) kde_output_order_v1), WL_MARSHAL_FLAG_DESTROY); +// } + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/detection/displayserver/linux/wayland/kde-output-order-v1-protocol.c b/src/detection/displayserver/linux/wayland/kde-output-order-v1-protocol.c new file mode 100644 index 0000000..00db32a --- /dev/null +++ b/src/detection/displayserver/linux/wayland/kde-output-order-v1-protocol.c @@ -0,0 +1,37 @@ +#ifdef FF_HAVE_WAYLAND + +/* Generated by wayland-scanner 1.22.0 */ + +/* + * SPDX-FileCopyrightText: 2022 Xaver Hugl <xaver.hugl@gmail.com> + * + * SPDX-License-Identifier: MIT-CMU + */ + + #include <stdlib.h> + #include <stdint.h> + #include "wayland-util.h" + +static const struct wl_interface* kde_output_order_v1_types[] = { + NULL, +}; + +static const struct wl_message kde_output_order_v1_requests[] = { + { "destroy", "", kde_output_order_v1_types + 0 }, +}; + +static const struct wl_message kde_output_order_v1_events[] = { + { "output", "s", kde_output_order_v1_types + 0 }, + { "done", "", kde_output_order_v1_types + 0 }, +}; + +WL_EXPORT const struct wl_interface kde_output_order_v1_interface = { + "kde_output_order_v1", + 1, + 1, + kde_output_order_v1_requests, + 2, + kde_output_order_v1_events, +}; + +#endif diff --git a/src/detection/displayserver/linux/wayland/kde-output.c b/src/detection/displayserver/linux/wayland/kde-output.c new file mode 100644 index 0000000..90d5770 --- /dev/null +++ b/src/detection/displayserver/linux/wayland/kde-output.c @@ -0,0 +1,300 @@ +#ifdef FF_HAVE_WAYLAND + + #include "wayland.h" + #include "kde-output-device-v2-client-protocol.h" + #include "kde-output-order-v1-client-protocol.h" + #include "common/edidHelper.h" + #include "common/base64.h" + +typedef struct WaylandKdeMode { + int32_t width; + int32_t height; + int32_t refreshRate; + bool preferred; + struct kde_output_device_mode_v2* pMode; +} WaylandKdeMode; + +static void waylandKdeModeSizeListener(void* data, FF_A_UNUSED struct kde_output_device_mode_v2* _, int32_t width, int32_t height) { + WaylandKdeMode* mode = (WaylandKdeMode*) data; + mode->width = width; + mode->height = height; +} + +static void waylandKdeModeRefreshListener(void* data, FF_A_UNUSED struct kde_output_device_mode_v2* _, int32_t rate) { + WaylandKdeMode* mode = (WaylandKdeMode*) data; + mode->refreshRate = rate; +} + +static void waylandKdeModePreferredListener(void* data, FF_A_UNUSED struct kde_output_device_mode_v2* _) { + WaylandKdeMode* mode = (WaylandKdeMode*) data; + mode->preferred = true; +} + +static const struct kde_output_device_mode_v2_listener modeListener = { + .size = waylandKdeModeSizeListener, + .refresh = waylandKdeModeRefreshListener, + .preferred = waylandKdeModePreferredListener, + .removed = (void*) stubListener, + .flags = (void*) stubListener, +}; + +static void waylandKdeModeListener(void* data, FF_A_UNUSED struct kde_output_device_v2* _, struct kde_output_device_mode_v2* mode) { + WaylandDisplay* wldata = (WaylandDisplay*) data; + if (!wldata->internal) { + return; + } + + WaylandKdeMode* newMode = FF_LIST_ADD(WaylandKdeMode, *(FFlist*) wldata->internal); + *newMode = (WaylandKdeMode) { .pMode = mode }; + + // Strangely, the listener is called only in this function, but not in `waylandKdeCurrentModeListener` + wldata->parent->ffwl_proxy_add_listener((struct wl_proxy*) mode, (void (**)(void)) &modeListener, newMode); +} + +static void waylandKdeCurrentModeListener(void* data, FF_A_UNUSED struct kde_output_device_v2* _, struct kde_output_device_mode_v2* mode) { + // waylandKdeModeListener is always run before this + WaylandDisplay* wldata = (WaylandDisplay*) data; + if (!wldata->internal) { + return; + } + + int set = 0; + FF_LIST_FOR_EACH (WaylandKdeMode, m, *(FFlist*) wldata->internal) { + if (m->pMode == mode) { + wldata->width = m->width; + wldata->height = m->height; + wldata->refreshRate = m->refreshRate; + if (++set == 2) { + break; + } + } + if (m->preferred) { + wldata->preferredWidth = m->width; + wldata->preferredHeight = m->height; + wldata->preferredRefreshRate = m->refreshRate; + if (++set == 2) { + break; + } + } + } +} + +static void waylandKdeScaleListener(void* data, FF_A_UNUSED struct kde_output_device_v2* _, wl_fixed_t scale) { + WaylandDisplay* wldata = (WaylandDisplay*) data; + wldata->dpi = (uint32_t) scale * 3 / 8; // wl_fixed_to_double(scale) * 96; +} + +static void waylandKdeEdidListener(void* data, FF_A_UNUSED struct kde_output_device_v2* _, const char* raw) { + if (!*raw) { + return; + } + WaylandDisplay* wldata = (WaylandDisplay*) data; + FF_STRBUF_AUTO_DESTROY b64 = ffStrbufCreateStatic(raw); + FF_STRBUF_AUTO_DESTROY edid = ffBase64DecodeStrbuf(&b64); + if (edid.length < 128) { + return; + } + ffEdidGetName((const uint8_t*) edid.chars, &wldata->edidName); + wldata->hdrSupported = ffEdidGetHdrCompatible((const uint8_t*) edid.chars, edid.length); + ffEdidGetSerialAndManufactureDate((const uint8_t*) edid.chars, &wldata->serial, &wldata->myear, &wldata->mweek); + wldata->hdrInfoAvailable = true; +} + +static void waylandKdeEnabledListener(void* data, FF_A_UNUSED struct kde_output_device_v2* _, int32_t enabled) { + WaylandDisplay* wldata = (WaylandDisplay*) data; + if (!enabled) { + wldata->internal = NULL; + } +} + +static void waylandKdeGeometryListener(void* data, + FF_A_UNUSED struct kde_output_device_v2* kde_output_device_v2, + FF_A_UNUSED int32_t x, + FF_A_UNUSED int32_t y, + int32_t physical_width, + int32_t physical_height, + FF_A_UNUSED int32_t subpixel, + FF_A_UNUSED const char* make, + FF_A_UNUSED const char* model, + int32_t transform) { + WaylandDisplay* display = data; + display->physicalWidth = physical_width; + display->physicalHeight = physical_height; + display->transform = (enum wl_output_transform) transform; +} + +static void waylandKdeNameListener(void* data, FF_A_UNUSED struct kde_output_device_v2* kde_output_device_v2, const char* name) { + WaylandDisplay* display = data; + display->type = ffdsGetDisplayType(name); + // As display->id is used as an internal identifier, we don't need it to be NUL terminated + strncpy((char*) &display->id, name, sizeof(display->id)); + + ffStrbufAppendS(&display->name, name); +} + +static void waylandKdeHdrListener(void* data, FF_A_UNUSED struct kde_output_device_v2* kde_output_device_v2, uint32_t hdr_enabled) { + WaylandDisplay* display = data; + display->hdrEnabled = !!hdr_enabled; +} + +static void waylandKdeMaxBitsPerColorListener(void* data, FF_A_UNUSED struct kde_output_device_v2* kde_output_device_v2, uint32_t max_bpc) { + WaylandDisplay* display = data; + display->bitDepth = (uint8_t) max_bpc; +} + +static struct kde_output_device_v2_listener outputListener = { + .geometry = waylandKdeGeometryListener, + .current_mode = waylandKdeCurrentModeListener, + .mode = waylandKdeModeListener, + .done = (void*) stubListener, + .scale = waylandKdeScaleListener, + .edid = waylandKdeEdidListener, + .enabled = waylandKdeEnabledListener, + .uuid = (void*) stubListener, + .serial_number = (void*) stubListener, + .eisa_id = (void*) stubListener, + .capabilities = (void*) stubListener, + .overscan = (void*) stubListener, + .vrr_policy = (void*) stubListener, + .rgb_range = (void*) stubListener, + .name = waylandKdeNameListener, + .high_dynamic_range = waylandKdeHdrListener, + .sdr_brightness = (void*) stubListener, + .wide_color_gamut = (void*) stubListener, + .auto_rotate_policy = (void*) stubListener, + .icc_profile_path = (void*) stubListener, + .brightness_metadata = (void*) stubListener, + .brightness_overrides = (void*) stubListener, + .sdr_gamut_wideness = (void*) stubListener, + .color_profile_source = (void*) stubListener, + .brightness = (void*) stubListener, + .color_power_tradeoff = (void*) stubListener, + .dimming = (void*) stubListener, + .replication_source = (void*) stubListener, + .ddc_ci_allowed = (void*) stubListener, + .max_bits_per_color = (void*) waylandKdeMaxBitsPerColorListener, + .max_bits_per_color_range = (void*) stubListener, + .automatic_max_bits_per_color_limit = (void*) stubListener, + .edr_policy = (void*) stubListener, +}; + +const char* ffWaylandHandleKdeOutput(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version) { + uint32_t bindVersion = min(version, KDE_OUTPUT_DEVICE_V2_MAX_BITS_PER_COLOR_SINCE_VERSION); + struct wl_proxy* output = wldata->ffwl_proxy_marshal_constructor_versioned((struct wl_proxy*) registry, WL_REGISTRY_BIND, &kde_output_device_v2_interface, bindVersion, name, kde_output_device_v2_interface.name, bindVersion, NULL); + if (output == NULL) { + return "Failed to create kde_output_device_v2"; + } + + FF_LIST_AUTO_DESTROY modes = ffListCreate(); + WaylandDisplay display = { + .parent = wldata, + .transform = WL_OUTPUT_TRANSFORM_NORMAL, + .type = FF_DISPLAY_TYPE_UNKNOWN, + .name = ffStrbufCreate(), + .description = ffStrbufCreate(), + .edidName = ffStrbufCreate(), + .internal = &modes, + }; + + if (wldata->ffwl_proxy_add_listener(output, (void (**)(void)) &outputListener, &display) < 0) { + wldata->ffwl_proxy_destroy(output); + return "Failed to add listener to kde_output_device_v2"; + } + + if (wldata->ffwl_display_roundtrip(wldata->display) < 0) { + wldata->ffwl_proxy_destroy(output); + return "Failed to roundtrip kde_output_device_v2"; + } + // Destroy any mode proxies that were created during the listeners. + // wl proxies created for modes are not automatically freed by destroying + // the parent output proxy, so destroy them explicitly to avoid leaks. + FF_LIST_FOR_EACH (WaylandKdeMode, m, modes) { + if (m->pMode) { + wldata->ffwl_proxy_destroy((struct wl_proxy*) m->pMode); + m->pMode = NULL; + } + } + + wldata->ffwl_proxy_destroy(output); + + if (display.width <= 0 || display.height <= 0 || !display.internal) { + return "Failed to get display information from kde_output_device_v2"; + } + + uint32_t rotation = ffWaylandHandleRotation(&display); + + FFDisplayResult* item = ffdsAppendDisplay(wldata->result, + (uint32_t) display.width, + (uint32_t) display.height, + display.refreshRate / 1000.0, + display.dpi, + (uint32_t) display.preferredWidth, + (uint32_t) display.preferredHeight, + display.preferredRefreshRate / 1000.0, + rotation, + display.edidName.length + ? &display.edidName + : &display.name, + display.type, + false, + display.id, + (uint32_t) display.physicalWidth, + (uint32_t) display.physicalHeight, + "wayland-kde"); + if (item) { + if (display.hdrEnabled) { + item->hdrStatus = FF_DISPLAY_HDR_STATUS_ENABLED; + } else if (display.hdrSupported) { + item->hdrStatus = FF_DISPLAY_HDR_STATUS_SUPPORTED; + } else if (display.hdrInfoAvailable) { + item->hdrStatus = FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + } else { + item->hdrStatus = FF_DISPLAY_HDR_STATUS_UNKNOWN; + } + + item->manufactureYear = display.myear; + item->manufactureWeek = display.mweek; + item->serial = display.serial; + item->bitDepth = display.bitDepth; + } + + ffStrbufDestroy(&display.description); + ffStrbufDestroy(&display.name); + ffStrbufDestroy(&display.edidName); + + return NULL; +} + +static void waylandKdeOutputOrderListener(void* data, FF_A_UNUSED struct kde_output_order_v1* _, const char* output_name) { + uint64_t* id = (uint64_t*) data; + if (*id == 0) { + *id = ffWaylandGenerateIdFromName(output_name); + } +} + +static const struct kde_output_order_v1_listener orderListener = { + .output = waylandKdeOutputOrderListener, + .done = (void*) stubListener, +}; + +const char* ffWaylandHandleKdeOutputOrder(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version) { + uint32_t bindVersion = min(version, KDE_OUTPUT_ORDER_V1_OUTPUT_SINCE_VERSION); + struct wl_proxy* output = wldata->ffwl_proxy_marshal_constructor_versioned((struct wl_proxy*) registry, WL_REGISTRY_BIND, &kde_output_order_v1_interface, bindVersion, name, kde_output_order_v1_interface.name, bindVersion, NULL); + if (output == NULL) { + return "Failed to create kde_output_order_v1"; + } + + if (wldata->ffwl_proxy_add_listener(output, (void (**)(void)) &orderListener, &wldata->primaryDisplayId) < 0) { + wldata->ffwl_proxy_destroy(output); + return "Failed to add listener to kde_output_order_v1"; + } + if (wldata->ffwl_display_roundtrip(wldata->display) < 0) { + wldata->ffwl_proxy_destroy(output); + return "Failed to roundtrip kde_output_order_v1"; + } + wldata->ffwl_proxy_destroy(output); + + return NULL; +} + +#endif diff --git a/src/detection/displayserver/linux/wayland/wayland.c b/src/detection/displayserver/linux/wayland/wayland.c new file mode 100644 index 0000000..064c239 --- /dev/null +++ b/src/detection/displayserver/linux/wayland/wayland.c @@ -0,0 +1,340 @@ +#include "../displayserver_linux.h" +#include "common/io.h" +#include "common/edidHelper.h" +#include "common/strutil.h" + +#include <stdlib.h> +#include <string.h> + +#ifdef FF_HAVE_WAYLAND + + #include <sys/socket.h> + + #include "common/properties.h" + + #include "wayland.h" + #include "wlr-output-management-unstable-v1-client-protocol.h" + #include "kde-output-device-v2-client-protocol.h" + #include "kde-output-order-v1-client-protocol.h" + #include "xdg-output-unstable-v1-client-protocol.h" + + #if __FreeBSD__ + #include <sys/un.h> + #include <sys/ucred.h> + #include <sys/sysctl.h> + #endif + +static bool waylandDetectWM(int fd, FFDisplayServerResult* result) { + #if __linux__ || __GNU__ || (__FreeBSD__ && !__DragonFly__) + + #if __linux__ || __GNU__ + struct ucred ucred = {}; + socklen_t len = sizeof(ucred); + if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &ucred, &len) == -1 || ucred.pid <= 0) { + return false; + } + + FF_STRBUF_AUTO_DESTROY procPath = ffStrbufCreate(); + ffStrbufAppendF(&procPath, "/proc/%d/cmdline", ucred.pid); // We check the cmdline for the process name, because it is not trimmed. + if (!ffReadFileBuffer(procPath.chars, &result->wmProcessName)) { + return false; + } + #else + struct xucred ucred = {}; + socklen_t len = sizeof(ucred); + if (getsockopt(fd, AF_UNSPEC, LOCAL_PEERCRED, &ucred, &len) == -1 || ucred.cr_pid <= 0) { + return false; + } + + size_t size = 4096; + ffStrbufEnsureFixedLengthFree(&result->wmProcessName, (uint32_t) size); + + if (sysctl((int[]) { CTL_KERN, KERN_PROC, KERN_PROC_ARGS, ucred.cr_pid }, 4, result->wmProcessName.chars, &size, NULL, 0) != 0) { + return false; + } + result->wmProcessName.length = (uint32_t) size - 1; + #endif + + // #1135: wl-restart is a special case + const char* filename = strrchr(result->wmProcessName.chars, '/'); + if (filename) { + filename++; + } else { + filename = result->wmProcessName.chars; + } + + if (ffStrEquals(filename, "wl-restart")) { + ffStrbufSubstrAfterLastC(&result->wmProcessName, '\0'); + } + + ffStrbufSubstrBeforeFirstC(&result->wmProcessName, '\0'); // Trim the arguments + ffStrbufSubstrAfterLastC(&result->wmProcessName, '/'); // Trim the path + + return true; + + #else + FF_UNUSED(fd, result); + return false; + #endif +} + +static void waylandGlobalAddListener(void* data, struct wl_registry* registry, uint32_t name, const char* interface, uint32_t version) { + WaylandData* wldata = data; + + if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE || wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_GLOBAL) && ffStrEquals(interface, wldata->ffwl_output_interface->name)) { + wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_GLOBAL; + if (ffWaylandHandleGlobalOutput(wldata, registry, name, version) != NULL) { + wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_NONE; + } + } else if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE || wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_ZWLR) && ffStrEquals(interface, zwlr_output_manager_v1_interface.name)) { + wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_ZWLR; + if (ffWaylandHandleZwlrOutput(wldata, registry, name, version) != NULL) { + wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_NONE; + } + } else if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE || wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_KDE) && ffStrEquals(interface, kde_output_device_v2_interface.name)) { + wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_KDE; + if (ffWaylandHandleKdeOutput(wldata, registry, name, version) != NULL) { + wldata->protocolType = FF_WAYLAND_PROTOCOL_TYPE_NONE; + } + } else if (ffStrEquals(interface, kde_output_order_v1_interface.name)) { + ffWaylandHandleKdeOutputOrder(wldata, registry, name, version); + } else if ((wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_GLOBAL || wldata->protocolType == FF_WAYLAND_PROTOCOL_TYPE_NONE) && ffStrEquals(interface, zxdg_output_manager_v1_interface.name)) { + ffWaylandHandleZxdgOutput(wldata, registry, name, version); + } +} + +static FF_A_UNUSED bool matchDrmConnector(const char* connName, WaylandDisplay* wldata) { + // https://wayland.freedesktop.org/docs/html/apa.html#protocol-spec-wl_output-event-name + // The doc says that "do not assume that the name is a reflection of an underlying DRM connector, X11 connection, etc." + // However I can't find a better method to get the edid data + const char* drmDirPath = "/sys/class/drm/"; + + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(drmDirPath); + if (dirp == NULL) { + return false; + } + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + const char* plainName = entry->d_name; + if (ffStrStartsWith(plainName, "card")) { + const char* tmp = strchr(plainName + strlen("card"), '-'); + if (tmp) { + plainName = tmp + 1; + } + } + if (ffStrEquals(plainName, connName)) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateF("%s%s/edid", drmDirPath, entry->d_name); + + uint8_t edidData[512]; + ssize_t edidLength = ffReadFileData(path.chars, ARRAY_SIZE(edidData), edidData); + if (edidLength > 0 && edidLength % 128 == 0) { + ffEdidGetName(edidData, &wldata->edidName); + ffEdidGetHdrCompatible(edidData, (uint32_t) edidLength); + ffEdidGetSerialAndManufactureDate(edidData, &wldata->serial, &wldata->myear, &wldata->mweek); + wldata->hdrInfoAvailable = true; + return true; + } + break; + } + } + return false; +} + +void ffWaylandOutputNameListener(void* data, FF_A_UNUSED void* output, const char* name) { + WaylandDisplay* display = data; + if (display->id) { + return; + } + + display->type = ffdsGetDisplayType(name); + #if __linux__ + if (!display->edidName.length) { + matchDrmConnector(name, display); + } + #endif + display->id = ffWaylandGenerateIdFromName(name); + ffStrbufAppendS(&display->name, name); +} + +void ffWaylandOutputDescriptionListener(void* data, FF_A_UNUSED void* output, const char* description) { + WaylandDisplay* display = data; + if (display->description.length) { + return; + } + + while (*description == ' ') { + ++description; + } + if (!ffStrEquals(description, "Unknown Display") && !ffStrContains(description, "(null)")) { + ffStrbufAppendS(&display->description, description); + } +} + +uint32_t ffWaylandHandleRotation(WaylandDisplay* display) { + uint32_t rotation; + switch (display->transform) { + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + case WL_OUTPUT_TRANSFORM_90: + rotation = 90; + break; + case WL_OUTPUT_TRANSFORM_FLIPPED_180: + case WL_OUTPUT_TRANSFORM_180: + rotation = 180; + break; + case WL_OUTPUT_TRANSFORM_FLIPPED_270: + case WL_OUTPUT_TRANSFORM_270: + rotation = 270; + break; + default: + rotation = 0; + break; + } + + switch (rotation) { + case 90: + case 270: { + int32_t temp = display->width; + display->width = display->height; + display->height = temp; + + temp = display->physicalWidth; + display->physicalWidth = display->physicalHeight; + display->physicalHeight = temp; + break; + } + default: + break; + } + return rotation; +} + +const char* ffdsConnectWayland(FFDisplayServerResult* result) { + if (getenv("XDG_RUNTIME_DIR") == NULL) { + return "Wayland requires $XDG_RUNTIME_DIR being set"; + } + + FF_LIBRARY_LOAD_MESSAGE(wayland, "libwayland-client" FF_LIBRARY_EXTENSION, 1) + + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_display_connect) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_display_get_fd) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_proxy_marshal_constructor) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_display_disconnect) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wayland, wl_registry_interface) + + WaylandData data = {}; + + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_proxy_marshal_constructor_versioned) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_proxy_add_listener) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_proxy_destroy) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_display_roundtrip) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(wayland, data, wl_output_interface) + + data.display = ffwl_display_connect(NULL); + if (data.display == NULL) { + return "wl_display_connect returned NULL"; + } + + waylandDetectWM(ffwl_display_get_fd(data.display), result); + + struct wl_proxy* registry = ffwl_proxy_marshal_constructor((struct wl_proxy*) data.display, WL_DISPLAY_GET_REGISTRY, ffwl_registry_interface, NULL); + if (registry == NULL) { + ffwl_display_disconnect(data.display); + return "wl_display_get_registry returned NULL"; + } + + data.result = result; + + struct wl_registry_listener registry_listener = { + .global = waylandGlobalAddListener, + .global_remove = (void*) stubListener + }; + + data.ffwl_proxy_add_listener(registry, (void (**)(void)) ®istry_listener, &data); + data.ffwl_display_roundtrip(data.display); + + if (data.zxdgOutputManager) { + data.ffwl_proxy_destroy(data.zxdgOutputManager); + } + + data.ffwl_proxy_destroy(registry); + ffwl_display_disconnect(data.display); + + if (data.primaryDisplayId == 0 && result->wmProcessName.length > 0) { + const char* fileName = ffStrbufEqualS(&result->wmProcessName, "gnome-shell") + ? "monitors.xml" + : ffStrbufEqualS(&result->wmProcessName, "cinnamon") + ? "cinnamon-monitors.xml" + : NULL; + if (fileName) { + FF_STRBUF_AUTO_DESTROY monitorsXml = ffStrbufCreate(); + FF_LIST_FOR_EACH (FFstrbuf, basePath, instance.state.platform.configDirs) { + char path[1024]; + snprintf(path, ARRAY_SIZE(path), "%s%s", basePath->chars, fileName); + if (ffReadFileBuffer(path, &monitorsXml)) { + break; + } + } + if (monitorsXml.length) { + // <monitors version="2"> + // <configuration> + // <logicalmonitor> + // <x>0</x> + // <y>0</y> + // <scale>1.7489879131317139</scale> + // <primary>yes</primary> + // <monitor> + // <monitorspec> + // <connector>Virtual-1</connector> + // <vendor>unknown</vendor> + // <product>unknown</product> + // <serial>unknown</serial> + // </monitorspec> + // <mode> + // <width>3456</width> + // <height>2160</height> + // <rate>60.000068664550781</rate> + // </mode> + // </monitor> + // </logicalmonitor> + // </configuration> + // </monitors> + uint32_t start = ffStrbufFirstIndexS(&monitorsXml, "<primary>yes</primary>"); + if (start < monitorsXml.length) { + start = ffStrbufNextIndexS(&monitorsXml, start, "<connector>"); + if (start < monitorsXml.length) { + uint32_t end = ffStrbufNextIndexS(&monitorsXml, start, "</connector>"); + if (end < monitorsXml.length) { + ffStrbufSubstrBefore(&monitorsXml, end); + const char* name = monitorsXml.chars + start + strlen("<connector>"); + data.primaryDisplayId = ffWaylandGenerateIdFromName(name); + } + } + } + } + } + } + + if (data.primaryDisplayId) { + FF_LIST_FOR_EACH (FFDisplayResult, d, data.result->displays) { + if (d->id == data.primaryDisplayId) { + d->primary = true; + break; + } + } + } + + // We successfully connected to wayland and detected the display. + // So we can set set the session type to wayland. + // This is used as an indicator that we are running wayland by the x11 backends. + ffStrbufSetStatic(&result->wmProtocolName, FF_WM_PROTOCOL_WAYLAND); + return NULL; +} + +#else + +const char* ffdsConnectWayland(FF_A_UNUSED FFDisplayServerResult* result) { + return "Fastfetch was compiled without Wayland support"; +} + +#endif diff --git a/src/detection/displayserver/linux/wayland/wayland.h b/src/detection/displayserver/linux/wayland/wayland.h new file mode 100644 index 0000000..b103ab9 --- /dev/null +++ b/src/detection/displayserver/linux/wayland/wayland.h @@ -0,0 +1,89 @@ +#pragma once + +#ifdef FF_HAVE_WAYLAND + + #include "common/library.h" + #include "common/strutil.h" + + #include <wayland-client.h> + + #include "../displayserver_linux.h" + +static inline uint32_t min(uint32_t a, uint32_t b) { + return a < b ? a : b; +} + +typedef enum FF_A_PACKED WaylandProtocolType { + FF_WAYLAND_PROTOCOL_TYPE_NONE, + FF_WAYLAND_PROTOCOL_TYPE_GLOBAL, + FF_WAYLAND_PROTOCOL_TYPE_ZWLR, + FF_WAYLAND_PROTOCOL_TYPE_KDE, +} WaylandProtocolType; + +typedef struct WaylandData { + FFDisplayServerResult* result; + FF_LIBRARY_SYMBOL(wl_proxy_marshal_constructor_versioned) + FF_LIBRARY_SYMBOL(wl_proxy_add_listener) + FF_LIBRARY_SYMBOL(wl_proxy_destroy) + FF_LIBRARY_SYMBOL(wl_display_roundtrip) + struct wl_display* display; + const struct wl_interface* ffwl_output_interface; + WaylandProtocolType protocolType; + uint64_t primaryDisplayId; + struct wl_proxy* zxdgOutputManager; +} WaylandData; + +typedef struct WaylandDisplay { + WaylandData* parent; + void* internal; + int32_t width; + int32_t height; + int32_t refreshRate; + int32_t preferredWidth; + int32_t preferredHeight; + int32_t preferredRefreshRate; + int32_t physicalWidth; + int32_t physicalHeight; + uint32_t dpi; + enum wl_output_transform transform; + FFDisplayType type; + FFstrbuf name; + FFstrbuf description; + FFstrbuf edidName; + uint64_t id; + bool hdrInfoAvailable; + bool hdrSupported; + bool hdrEnabled; + uint16_t myear; + uint16_t mweek; + uint32_t serial; + uint8_t bitDepth; +} WaylandDisplay; + +inline static void stubListener(void* data, ...) { + (void) data; +} + +inline static uint64_t ffWaylandGenerateIdFromName(const char* name) { + uint64_t id = 0; + size_t len = strlen(name); + if (len > sizeof(id)) { + memcpy(&id, name + (len - sizeof(id)), sizeof(id)); // copy the last 8 bytes + } else if (len > 0) { + memcpy(&id, name, len); + } + return id; +} + +void ffWaylandOutputNameListener(void* data, FF_A_UNUSED void* output, const char* name); +void ffWaylandOutputDescriptionListener(void* data, FF_A_UNUSED void* output, const char* description); +// Modifies content of display. Don't call this function when calling ffdsAppendDisplay +uint32_t ffWaylandHandleRotation(WaylandDisplay* display); + +const char* ffWaylandHandleGlobalOutput(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version); +const char* ffWaylandHandleZwlrOutput(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version); +const char* ffWaylandHandleKdeOutput(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version); +const char* ffWaylandHandleKdeOutputOrder(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version); +const char* ffWaylandHandleZxdgOutput(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version); + +#endif diff --git a/src/detection/displayserver/linux/wayland/wlr-output-management-unstable-v1-client-protocol.h b/src/detection/displayserver/linux/wayland/wlr-output-management-unstable-v1-client-protocol.h new file mode 100644 index 0000000..2b0584d --- /dev/null +++ b/src/detection/displayserver/linux/wayland/wlr-output-management-unstable-v1-client-protocol.h @@ -0,0 +1,1292 @@ +/* Generated by wayland-scanner 1.24.0 */ + +#ifndef WLR_OUTPUT_MANAGEMENT_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define WLR_OUTPUT_MANAGEMENT_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include <wayland-client.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_wlr_output_management_unstable_v1 The wlr_output_management_unstable_v1 protocol + * protocol to configure output devices + * + * @section page_desc_wlr_output_management_unstable_v1 Description + * + * This protocol exposes interfaces to obtain and modify output device + * configuration. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible changes + * may be added together with the corresponding interface version bump. + * Backward incompatible changes are done by bumping the version number in + * the protocol and interface names and resetting the interface version. + * Once the protocol is to be declared stable, the 'z' prefix and the + * version number in the protocol and interface names are removed and the + * interface version number is reset. + * + * @section page_ifaces_wlr_output_management_unstable_v1 Interfaces + * - @subpage page_iface_zwlr_output_manager_v1 - output device configuration manager + * - @subpage page_iface_zwlr_output_head_v1 - output device + * - @subpage page_iface_zwlr_output_mode_v1 - output mode + * - @subpage page_iface_zwlr_output_configuration_v1 - output configuration + * - @subpage page_iface_zwlr_output_configuration_head_v1 - head configuration + * @section page_copyright_wlr_output_management_unstable_v1 Copyright + * <pre> + * + * Copyright © 2019 Purism SPC + * + * Permission to use, copy, modify, distribute, and sell this + * software and its documentation for any purpose is hereby granted + * without fee, provided that the above copyright notice appear in + * all copies and that both that copyright notice and this permission + * notice appear in supporting documentation, and that the name of + * the copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + * THIS SOFTWARE. + * </pre> + */ +struct zwlr_output_configuration_head_v1; +struct zwlr_output_configuration_v1; +struct zwlr_output_head_v1; +struct zwlr_output_manager_v1; +struct zwlr_output_mode_v1; + +#ifndef ZWLR_OUTPUT_MANAGER_V1_INTERFACE + #define ZWLR_OUTPUT_MANAGER_V1_INTERFACE +/** + * @page page_iface_zwlr_output_manager_v1 zwlr_output_manager_v1 + * @section page_iface_zwlr_output_manager_v1_desc Description + * + * This interface is a manager that allows reading and writing the current + * output device configuration. + * + * Output devices that display pixels (e.g. a physical monitor or a virtual + * output in a window) are represented as heads. Heads cannot be created nor + * destroyed by the client, but they can be enabled or disabled and their + * properties can be changed. Each head may have one or more available modes. + * + * Whenever a head appears (e.g. a monitor is plugged in), it will be + * advertised via the head event. Immediately after the output manager is + * bound, all current heads are advertised. + * + * Whenever a head's properties change, the relevant wlr_output_head events + * will be sent. Not all head properties will be sent: only properties that + * have changed need to. + * + * Whenever a head disappears (e.g. a monitor is unplugged), a + * wlr_output_head.finished event will be sent. + * + * After one or more heads appear, change or disappear, the done event will + * be sent. It carries a serial which can be used in a create_configuration + * request to update heads properties. + * + * The information obtained from this protocol should only be used for output + * configuration purposes. This protocol is not designed to be a generic + * output property advertisement protocol for regular clients. Instead, + * protocols such as xdg-output should be used. + * @section page_iface_zwlr_output_manager_v1_api API + * See @ref iface_zwlr_output_manager_v1. + */ +/** + * @defgroup iface_zwlr_output_manager_v1 The zwlr_output_manager_v1 interface + * + * This interface is a manager that allows reading and writing the current + * output device configuration. + * + * Output devices that display pixels (e.g. a physical monitor or a virtual + * output in a window) are represented as heads. Heads cannot be created nor + * destroyed by the client, but they can be enabled or disabled and their + * properties can be changed. Each head may have one or more available modes. + * + * Whenever a head appears (e.g. a monitor is plugged in), it will be + * advertised via the head event. Immediately after the output manager is + * bound, all current heads are advertised. + * + * Whenever a head's properties change, the relevant wlr_output_head events + * will be sent. Not all head properties will be sent: only properties that + * have changed need to. + * + * Whenever a head disappears (e.g. a monitor is unplugged), a + * wlr_output_head.finished event will be sent. + * + * After one or more heads appear, change or disappear, the done event will + * be sent. It carries a serial which can be used in a create_configuration + * request to update heads properties. + * + * The information obtained from this protocol should only be used for output + * configuration purposes. This protocol is not designed to be a generic + * output property advertisement protocol for regular clients. Instead, + * protocols such as xdg-output should be used. + */ +extern const struct wl_interface zwlr_output_manager_v1_interface; +#endif +#ifndef ZWLR_OUTPUT_HEAD_V1_INTERFACE + #define ZWLR_OUTPUT_HEAD_V1_INTERFACE +/** + * @page page_iface_zwlr_output_head_v1 zwlr_output_head_v1 + * @section page_iface_zwlr_output_head_v1_desc Description + * + * A head is an output device. The difference between a wl_output object and + * a head is that heads are advertised even if they are turned off. A head + * object only advertises properties and cannot be used directly to change + * them. + * + * A head has some read-only properties: modes, name, description and + * physical_size. These cannot be changed by clients. + * + * Other properties can be updated via a wlr_output_configuration object. + * + * Properties sent via this interface are applied atomically via the + * wlr_output_manager.done event. No guarantees are made regarding the order + * in which properties are sent. + * @section page_iface_zwlr_output_head_v1_api API + * See @ref iface_zwlr_output_head_v1. + */ +/** + * @defgroup iface_zwlr_output_head_v1 The zwlr_output_head_v1 interface + * + * A head is an output device. The difference between a wl_output object and + * a head is that heads are advertised even if they are turned off. A head + * object only advertises properties and cannot be used directly to change + * them. + * + * A head has some read-only properties: modes, name, description and + * physical_size. These cannot be changed by clients. + * + * Other properties can be updated via a wlr_output_configuration object. + * + * Properties sent via this interface are applied atomically via the + * wlr_output_manager.done event. No guarantees are made regarding the order + * in which properties are sent. + */ +extern const struct wl_interface zwlr_output_head_v1_interface; +#endif +#ifndef ZWLR_OUTPUT_MODE_V1_INTERFACE + #define ZWLR_OUTPUT_MODE_V1_INTERFACE +/** + * @page page_iface_zwlr_output_mode_v1 zwlr_output_mode_v1 + * @section page_iface_zwlr_output_mode_v1_desc Description + * + * This object describes an output mode. + * + * Some heads don't support output modes, in which case modes won't be + * advertised. + * + * Properties sent via this interface are applied atomically via the + * wlr_output_manager.done event. No guarantees are made regarding the order + * in which properties are sent. + * @section page_iface_zwlr_output_mode_v1_api API + * See @ref iface_zwlr_output_mode_v1. + */ +/** + * @defgroup iface_zwlr_output_mode_v1 The zwlr_output_mode_v1 interface + * + * This object describes an output mode. + * + * Some heads don't support output modes, in which case modes won't be + * advertised. + * + * Properties sent via this interface are applied atomically via the + * wlr_output_manager.done event. No guarantees are made regarding the order + * in which properties are sent. + */ +extern const struct wl_interface zwlr_output_mode_v1_interface; +#endif +#ifndef ZWLR_OUTPUT_CONFIGURATION_V1_INTERFACE + #define ZWLR_OUTPUT_CONFIGURATION_V1_INTERFACE +/** + * @page page_iface_zwlr_output_configuration_v1 zwlr_output_configuration_v1 + * @section page_iface_zwlr_output_configuration_v1_desc Description + * + * This object is used by the client to describe a full output configuration. + * + * First, the client needs to setup the output configuration. Each head can + * be either enabled (and configured) or disabled. It is a protocol error to + * send two enable_head or disable_head requests with the same head. It is a + * protocol error to omit a head in a configuration. + * + * Then, the client can apply or test the configuration. The compositor will + * then reply with a succeeded, failed or cancelled event. Finally the client + * should destroy the configuration object. + * @section page_iface_zwlr_output_configuration_v1_api API + * See @ref iface_zwlr_output_configuration_v1. + */ +/** + * @defgroup iface_zwlr_output_configuration_v1 The zwlr_output_configuration_v1 interface + * + * This object is used by the client to describe a full output configuration. + * + * First, the client needs to setup the output configuration. Each head can + * be either enabled (and configured) or disabled. It is a protocol error to + * send two enable_head or disable_head requests with the same head. It is a + * protocol error to omit a head in a configuration. + * + * Then, the client can apply or test the configuration. The compositor will + * then reply with a succeeded, failed or cancelled event. Finally the client + * should destroy the configuration object. + */ +extern const struct wl_interface zwlr_output_configuration_v1_interface; +#endif +#ifndef ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_INTERFACE + #define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_INTERFACE +/** + * @page page_iface_zwlr_output_configuration_head_v1 zwlr_output_configuration_head_v1 + * @section page_iface_zwlr_output_configuration_head_v1_desc Description + * + * This object is used by the client to update a single head's configuration. + * + * It is a protocol error to set the same property twice. + * @section page_iface_zwlr_output_configuration_head_v1_api API + * See @ref iface_zwlr_output_configuration_head_v1. + */ +/** + * @defgroup iface_zwlr_output_configuration_head_v1 The zwlr_output_configuration_head_v1 interface + * + * This object is used by the client to update a single head's configuration. + * + * It is a protocol error to set the same property twice. + */ +extern const struct wl_interface zwlr_output_configuration_head_v1_interface; +#endif + +/** + * @ingroup iface_zwlr_output_manager_v1 + * @struct zwlr_output_manager_v1_listener + */ +struct zwlr_output_manager_v1_listener { + /** + * introduce a new head + * + * This event introduces a new head. This happens whenever a new + * head appears (e.g. a monitor is plugged in) or after the output + * manager is bound. + */ + void (*head)(void* data, + struct zwlr_output_manager_v1* zwlr_output_manager_v1, + struct zwlr_output_head_v1* head); + /** + * sent all information about current configuration + * + * This event is sent after all information has been sent after + * binding to the output manager object and after any subsequent + * changes. This applies to child head and mode objects as well. In + * other words, this event is sent whenever a head or mode is + * created or destroyed and whenever one of their properties has + * been changed. Not all state is re-sent each time the current + * configuration changes: only the actual changes are sent. + * + * This allows changes to the output configuration to be seen as + * atomic, even if they happen via multiple events. + * + * A serial is sent to be used in a future create_configuration + * request. + * @param serial current configuration serial + */ + void (*done)(void* data, + struct zwlr_output_manager_v1* zwlr_output_manager_v1, + uint32_t serial); + /** + * the compositor has finished with the manager + * + * This event indicates that the compositor is done sending + * manager events. The compositor will destroy the object + * immediately after sending this event, so it will become invalid + * and the client should release any resources associated with it. + */ + void (*finished)(void* data, + struct zwlr_output_manager_v1* zwlr_output_manager_v1); +}; + +/** + * @ingroup iface_zwlr_output_manager_v1 + */ +static inline int +zwlr_output_manager_v1_add_listener(struct zwlr_output_manager_v1* zwlr_output_manager_v1, + const struct zwlr_output_manager_v1_listener* listener, + void* data) { + return wl_proxy_add_listener((struct wl_proxy*) zwlr_output_manager_v1, + (void (**)(void)) listener, + data); +} + +#define ZWLR_OUTPUT_MANAGER_V1_CREATE_CONFIGURATION 0 +#define ZWLR_OUTPUT_MANAGER_V1_STOP 1 + +/** + * @ingroup iface_zwlr_output_manager_v1 + */ +#define ZWLR_OUTPUT_MANAGER_V1_HEAD_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_manager_v1 + */ +#define ZWLR_OUTPUT_MANAGER_V1_DONE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_manager_v1 + */ +#define ZWLR_OUTPUT_MANAGER_V1_FINISHED_SINCE_VERSION 1 + +/** + * @ingroup iface_zwlr_output_manager_v1 + */ +#define ZWLR_OUTPUT_MANAGER_V1_CREATE_CONFIGURATION_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_manager_v1 + */ +#define ZWLR_OUTPUT_MANAGER_V1_STOP_SINCE_VERSION 1 + +/** @ingroup iface_zwlr_output_manager_v1 */ +static inline void +zwlr_output_manager_v1_set_user_data(struct zwlr_output_manager_v1* zwlr_output_manager_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*) zwlr_output_manager_v1, user_data); +} + +/** @ingroup iface_zwlr_output_manager_v1 */ +static inline void* +zwlr_output_manager_v1_get_user_data(struct zwlr_output_manager_v1* zwlr_output_manager_v1) { + return wl_proxy_get_user_data((struct wl_proxy*) zwlr_output_manager_v1); +} + +static inline uint32_t +zwlr_output_manager_v1_get_version(struct zwlr_output_manager_v1* zwlr_output_manager_v1) { + return wl_proxy_get_version((struct wl_proxy*) zwlr_output_manager_v1); +} + +/** @ingroup iface_zwlr_output_manager_v1 */ +static inline void +zwlr_output_manager_v1_destroy(struct zwlr_output_manager_v1* zwlr_output_manager_v1) { + wl_proxy_destroy((struct wl_proxy*) zwlr_output_manager_v1); +} + +// /** +// * @ingroup iface_zwlr_output_manager_v1 +// * +// * Create a new output configuration object. This allows to update head +// * properties. +// */ +// static inline struct zwlr_output_configuration_v1 * +// zwlr_output_manager_v1_create_configuration(struct zwlr_output_manager_v1 *zwlr_output_manager_v1, uint32_t serial) +// { +// struct wl_proxy *id; + +// id = wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_manager_v1, +// ZWLR_OUTPUT_MANAGER_V1_CREATE_CONFIGURATION, &zwlr_output_configuration_v1_interface, wl_proxy_get_version((struct wl_proxy *) zwlr_output_manager_v1), 0, NULL, serial); + +// return (struct zwlr_output_configuration_v1 *) id; +// } + +// /** +// * @ingroup iface_zwlr_output_manager_v1 +// * +// * Indicates the client no longer wishes to receive events for output +// * configuration changes. However the compositor may emit further events, +// * until the finished event is emitted. +// * +// * The client must not send any more requests after this one. +// */ +// static inline void +// zwlr_output_manager_v1_stop(struct zwlr_output_manager_v1 *zwlr_output_manager_v1) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_manager_v1, +// ZWLR_OUTPUT_MANAGER_V1_STOP, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_manager_v1), 0); +// } + +#ifndef ZWLR_OUTPUT_HEAD_V1_ADAPTIVE_SYNC_STATE_ENUM + #define ZWLR_OUTPUT_HEAD_V1_ADAPTIVE_SYNC_STATE_ENUM +enum zwlr_output_head_v1_adaptive_sync_state { + /** + * adaptive sync is disabled + */ + ZWLR_OUTPUT_HEAD_V1_ADAPTIVE_SYNC_STATE_DISABLED = 0, + /** + * adaptive sync is enabled + */ + ZWLR_OUTPUT_HEAD_V1_ADAPTIVE_SYNC_STATE_ENABLED = 1, +}; +#endif /* ZWLR_OUTPUT_HEAD_V1_ADAPTIVE_SYNC_STATE_ENUM */ + +/** + * @ingroup iface_zwlr_output_head_v1 + * @struct zwlr_output_head_v1_listener + */ +struct zwlr_output_head_v1_listener { + /** + * head name + * + * This event describes the head name. + * + * The naming convention is compositor defined, but limited to + * alphanumeric characters and dashes (-). Each name is unique + * among all wlr_output_head objects, but if a wlr_output_head + * object is destroyed the same name may be reused later. The names + * will also remain consistent across sessions with the same + * hardware and software configuration. + * + * Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. + * However, do not assume that the name is a reflection of an + * underlying DRM connector, X11 connection, etc. + * + * If this head matches a wl_output, the wl_output.name event must + * report the same name. + * + * The name event is sent after a wlr_output_head object is + * created. This event is only sent once per object, and the name + * does not change over the lifetime of the wlr_output_head object. + */ + void (*name)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + const char* name); + /** + * head description + * + * This event describes a human-readable description of the head. + * + * The description is a UTF-8 string with no convention defined for + * its contents. Examples might include 'Foocorp 11" Display' or + * 'Virtual X11 output via :1'. However, do not assume that the + * name is a reflection of the make, model, serial of the + * underlying DRM connector or the display name of the underlying + * X11 connection, etc. + * + * If this head matches a wl_output, the wl_output.description + * event must report the same name. + * + * The description event is sent after a wlr_output_head object is + * created. This event is only sent once per object, and the + * description does not change over the lifetime of the + * wlr_output_head object. + */ + void (*description)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + const char* description); + /** + * head physical size + * + * This event describes the physical size of the head. This event + * is only sent if the head has a physical size (e.g. is not a + * projector or a virtual device). + * + * The physical size event is sent after a wlr_output_head object + * is created. This event is only sent once per object, and the + * physical size does not change over the lifetime of the + * wlr_output_head object. + * @param width width in millimeters of the output + * @param height height in millimeters of the output + */ + void (*physical_size)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + int32_t width, + int32_t height); + /** + * introduce a mode + * + * This event introduces a mode for this head. It is sent once + * per supported mode. + */ + void (*mode)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + struct zwlr_output_mode_v1* mode); + /** + * head is enabled or disabled + * + * This event describes whether the head is enabled. A disabled + * head is not mapped to a region of the global compositor space. + * + * When a head is disabled, some properties (current_mode, + * position, transform and scale) are irrelevant. + * @param enabled zero if disabled, non-zero if enabled + */ + void (*enabled)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + int32_t enabled); + /** + * current mode + * + * This event describes the mode currently in use for this head. + * It is only sent if the output is enabled. + */ + void (*current_mode)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + struct zwlr_output_mode_v1* mode); + /** + * current position + * + * This events describes the position of the head in the global + * compositor space. It is only sent if the output is enabled. + * @param x x position within the global compositor space + * @param y y position within the global compositor space + */ + void (*position)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + int32_t x, + int32_t y); + /** + * current transformation + * + * This event describes the transformation currently applied to + * the head. It is only sent if the output is enabled. + */ + void (*transform)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + int32_t transform); + /** + * current scale + * + * This events describes the scale of the head in the global + * compositor space. It is only sent if the output is enabled. + */ + void (*scale)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + wl_fixed_t scale); + /** + * the head has disappeared + * + * This event indicates that the head is no longer available. The + * head object becomes inert. Clients should send a destroy request + * and release any resources associated with it. + */ + void (*finished)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1); + /** + * head manufacturer + * + * This event describes the manufacturer of the head. + * + * Together with the model and serial_number events the purpose is + * to allow clients to recognize heads from previous sessions and + * for example load head-specific configurations back. + * + * It is not guaranteed this event will be ever sent. A reason for + * that can be that the compositor does not have information about + * the make of the head or the definition of a make is not sensible + * in the current setup, for example in a virtual session. Clients + * can still try to identify the head by available information from + * other events but should be aware that there is an increased risk + * of false positives. + * + * If sent, the make event is sent after a wlr_output_head object + * is created and only sent once per object. The make does not + * change over the lifetime of the wlr_output_head object. + * + * It is not recommended to display the make string in UI to users. + * For that the string provided by the description event should be + * preferred. + * @since 2 + */ + void (*make)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + const char* make); + /** + * head model + * + * This event describes the model of the head. + * + * Together with the make and serial_number events the purpose is + * to allow clients to recognize heads from previous sessions and + * for example load head-specific configurations back. + * + * It is not guaranteed this event will be ever sent. A reason for + * that can be that the compositor does not have information about + * the model of the head or the definition of a model is not + * sensible in the current setup, for example in a virtual session. + * Clients can still try to identify the head by available + * information from other events but should be aware that there is + * an increased risk of false positives. + * + * If sent, the model event is sent after a wlr_output_head object + * is created and only sent once per object. The model does not + * change over the lifetime of the wlr_output_head object. + * + * It is not recommended to display the model string in UI to + * users. For that the string provided by the description event + * should be preferred. + * @since 2 + */ + void (*model)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + const char* model); + /** + * head serial number + * + * This event describes the serial number of the head. + * + * Together with the make and model events the purpose is to allow + * clients to recognize heads from previous sessions and for + * example load head- specific configurations back. + * + * It is not guaranteed this event will be ever sent. A reason for + * that can be that the compositor does not have information about + * the serial number of the head or the definition of a serial + * number is not sensible in the current setup. Clients can still + * try to identify the head by available information from other + * events but should be aware that there is an increased risk of + * false positives. + * + * If sent, the serial number event is sent after a wlr_output_head + * object is created and only sent once per object. The serial + * number does not change over the lifetime of the wlr_output_head + * object. + * + * It is not recommended to display the serial_number string in UI + * to users. For that the string provided by the description event + * should be preferred. + * @since 2 + */ + void (*serial_number)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + const char* serial_number); + /** + * current adaptive sync state + * + * This event describes whether adaptive sync is currently + * enabled for the head or not. Adaptive sync is also known as + * Variable Refresh Rate or VRR. + * @since 4 + */ + void (*adaptive_sync)(void* data, + struct zwlr_output_head_v1* zwlr_output_head_v1, + uint32_t state); +}; + +/** + * @ingroup iface_zwlr_output_head_v1 + */ +static inline int +zwlr_output_head_v1_add_listener(struct zwlr_output_head_v1* zwlr_output_head_v1, + const struct zwlr_output_head_v1_listener* listener, + void* data) { + return wl_proxy_add_listener((struct wl_proxy*) zwlr_output_head_v1, + (void (**)(void)) listener, + data); +} + +#define ZWLR_OUTPUT_HEAD_V1_RELEASE 0 + +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_NAME_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_DESCRIPTION_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_PHYSICAL_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_MODE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_ENABLED_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_CURRENT_MODE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_POSITION_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_TRANSFORM_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_SCALE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_FINISHED_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_MAKE_SINCE_VERSION 2 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_MODEL_SINCE_VERSION 2 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_SERIAL_NUMBER_SINCE_VERSION 2 +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_ADAPTIVE_SYNC_SINCE_VERSION 4 + +/** + * @ingroup iface_zwlr_output_head_v1 + */ +#define ZWLR_OUTPUT_HEAD_V1_RELEASE_SINCE_VERSION 3 + +/** @ingroup iface_zwlr_output_head_v1 */ +static inline void +zwlr_output_head_v1_set_user_data(struct zwlr_output_head_v1* zwlr_output_head_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*) zwlr_output_head_v1, user_data); +} + +/** @ingroup iface_zwlr_output_head_v1 */ +static inline void* +zwlr_output_head_v1_get_user_data(struct zwlr_output_head_v1* zwlr_output_head_v1) { + return wl_proxy_get_user_data((struct wl_proxy*) zwlr_output_head_v1); +} + +static inline uint32_t +zwlr_output_head_v1_get_version(struct zwlr_output_head_v1* zwlr_output_head_v1) { + return wl_proxy_get_version((struct wl_proxy*) zwlr_output_head_v1); +} + +/** @ingroup iface_zwlr_output_head_v1 */ +static inline void +zwlr_output_head_v1_destroy(struct zwlr_output_head_v1* zwlr_output_head_v1) { + wl_proxy_destroy((struct wl_proxy*) zwlr_output_head_v1); +} + +// /** +// * @ingroup iface_zwlr_output_head_v1 +// * +// * This request indicates that the client will no longer use this head +// * object. +// */ +// static inline void +// zwlr_output_head_v1_release(struct zwlr_output_head_v1 *zwlr_output_head_v1) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_head_v1, +// ZWLR_OUTPUT_HEAD_V1_RELEASE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_head_v1), WL_MARSHAL_FLAG_DESTROY); +// } + +/** + * @ingroup iface_zwlr_output_mode_v1 + * @struct zwlr_output_mode_v1_listener + */ +struct zwlr_output_mode_v1_listener { + /** + * mode size + * + * This event describes the mode size. The size is given in + * physical hardware units of the output device. This is not + * necessarily the same as the output size in the global compositor + * space. For instance, the output may be scaled or transformed. + * @param width width of the mode in hardware units + * @param height height of the mode in hardware units + */ + void (*size)(void* data, + struct zwlr_output_mode_v1* zwlr_output_mode_v1, + int32_t width, + int32_t height); + /** + * mode refresh rate + * + * This event describes the mode's fixed vertical refresh rate. + * It is only sent if the mode has a fixed refresh rate. + * @param refresh vertical refresh rate in mHz + */ + void (*refresh)(void* data, + struct zwlr_output_mode_v1* zwlr_output_mode_v1, + int32_t refresh); + /** + * mode is preferred + * + * This event advertises this mode as preferred. + */ + void (*preferred)(void* data, + struct zwlr_output_mode_v1* zwlr_output_mode_v1); + /** + * the mode has disappeared + * + * This event indicates that the mode is no longer available. The + * mode object becomes inert. Clients should send a destroy request + * and release any resources associated with it. + */ + void (*finished)(void* data, + struct zwlr_output_mode_v1* zwlr_output_mode_v1); +}; + +/** + * @ingroup iface_zwlr_output_mode_v1 + */ +static inline int +zwlr_output_mode_v1_add_listener(struct zwlr_output_mode_v1* zwlr_output_mode_v1, + const struct zwlr_output_mode_v1_listener* listener, + void* data) { + return wl_proxy_add_listener((struct wl_proxy*) zwlr_output_mode_v1, + (void (**)(void)) listener, + data); +} + +#define ZWLR_OUTPUT_MODE_V1_RELEASE 0 + +/** + * @ingroup iface_zwlr_output_mode_v1 + */ +#define ZWLR_OUTPUT_MODE_V1_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_mode_v1 + */ +#define ZWLR_OUTPUT_MODE_V1_REFRESH_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_mode_v1 + */ +#define ZWLR_OUTPUT_MODE_V1_PREFERRED_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_mode_v1 + */ +#define ZWLR_OUTPUT_MODE_V1_FINISHED_SINCE_VERSION 1 + +/** + * @ingroup iface_zwlr_output_mode_v1 + */ +#define ZWLR_OUTPUT_MODE_V1_RELEASE_SINCE_VERSION 3 + +/** @ingroup iface_zwlr_output_mode_v1 */ +static inline void +zwlr_output_mode_v1_set_user_data(struct zwlr_output_mode_v1* zwlr_output_mode_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*) zwlr_output_mode_v1, user_data); +} + +/** @ingroup iface_zwlr_output_mode_v1 */ +static inline void* +zwlr_output_mode_v1_get_user_data(struct zwlr_output_mode_v1* zwlr_output_mode_v1) { + return wl_proxy_get_user_data((struct wl_proxy*) zwlr_output_mode_v1); +} + +static inline uint32_t +zwlr_output_mode_v1_get_version(struct zwlr_output_mode_v1* zwlr_output_mode_v1) { + return wl_proxy_get_version((struct wl_proxy*) zwlr_output_mode_v1); +} + +/** @ingroup iface_zwlr_output_mode_v1 */ +static inline void +zwlr_output_mode_v1_destroy(struct zwlr_output_mode_v1* zwlr_output_mode_v1) { + wl_proxy_destroy((struct wl_proxy*) zwlr_output_mode_v1); +} + +// /** +// * @ingroup iface_zwlr_output_mode_v1 +// * +// * This request indicates that the client will no longer use this mode +// * object. +// */ +// static inline void +// zwlr_output_mode_v1_release(struct zwlr_output_mode_v1 *zwlr_output_mode_v1) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_mode_v1, +// ZWLR_OUTPUT_MODE_V1_RELEASE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_mode_v1), WL_MARSHAL_FLAG_DESTROY); +// } + +#ifndef ZWLR_OUTPUT_CONFIGURATION_V1_ERROR_ENUM + #define ZWLR_OUTPUT_CONFIGURATION_V1_ERROR_ENUM +enum zwlr_output_configuration_v1_error { + /** + * head has been configured twice + */ + ZWLR_OUTPUT_CONFIGURATION_V1_ERROR_ALREADY_CONFIGURED_HEAD = 1, + /** + * head has not been configured + */ + ZWLR_OUTPUT_CONFIGURATION_V1_ERROR_UNCONFIGURED_HEAD = 2, + /** + * request sent after configuration has been applied or tested + */ + ZWLR_OUTPUT_CONFIGURATION_V1_ERROR_ALREADY_USED = 3, +}; +#endif /* ZWLR_OUTPUT_CONFIGURATION_V1_ERROR_ENUM */ + +/** + * @ingroup iface_zwlr_output_configuration_v1 + * @struct zwlr_output_configuration_v1_listener + */ +struct zwlr_output_configuration_v1_listener { + /** + * configuration changes succeeded + * + * Sent after the compositor has successfully applied the changes + * or tested them. + * + * Upon receiving this event, the client should destroy this + * object. + * + * If the current configuration has changed, events to describe the + * changes will be sent followed by a wlr_output_manager.done + * event. + */ + void (*succeeded)(void* data, + struct zwlr_output_configuration_v1* zwlr_output_configuration_v1); + /** + * configuration changes failed + * + * Sent if the compositor rejects the changes or failed to apply + * them. The compositor should revert any changes made by the apply + * request that triggered this event. + * + * Upon receiving this event, the client should destroy this + * object. + */ + void (*failed)(void* data, + struct zwlr_output_configuration_v1* zwlr_output_configuration_v1); + /** + * configuration has been cancelled + * + * Sent if the compositor cancels the configuration because the + * state of an output changed and the client has outdated + * information (e.g. after an output has been hotplugged). + * + * The client can create a new configuration with a newer serial + * and try again. + * + * Upon receiving this event, the client should destroy this + * object. + */ + void (*cancelled)(void* data, + struct zwlr_output_configuration_v1* zwlr_output_configuration_v1); +}; + +/** + * @ingroup iface_zwlr_output_configuration_v1 + */ +static inline int +zwlr_output_configuration_v1_add_listener(struct zwlr_output_configuration_v1* zwlr_output_configuration_v1, + const struct zwlr_output_configuration_v1_listener* listener, + void* data) { + return wl_proxy_add_listener((struct wl_proxy*) zwlr_output_configuration_v1, + (void (**)(void)) listener, + data); +} + +#define ZWLR_OUTPUT_CONFIGURATION_V1_ENABLE_HEAD 0 +#define ZWLR_OUTPUT_CONFIGURATION_V1_DISABLE_HEAD 1 +#define ZWLR_OUTPUT_CONFIGURATION_V1_APPLY 2 +#define ZWLR_OUTPUT_CONFIGURATION_V1_TEST 3 +#define ZWLR_OUTPUT_CONFIGURATION_V1_DESTROY 4 + +/** + * @ingroup iface_zwlr_output_configuration_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_V1_SUCCEEDED_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_configuration_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_V1_FAILED_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_configuration_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_V1_CANCELLED_SINCE_VERSION 1 + +/** + * @ingroup iface_zwlr_output_configuration_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_V1_ENABLE_HEAD_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_configuration_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_V1_DISABLE_HEAD_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_configuration_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_V1_APPLY_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_configuration_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_V1_TEST_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_configuration_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_V1_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_zwlr_output_configuration_v1 */ +static inline void +zwlr_output_configuration_v1_set_user_data(struct zwlr_output_configuration_v1* zwlr_output_configuration_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*) zwlr_output_configuration_v1, user_data); +} + +/** @ingroup iface_zwlr_output_configuration_v1 */ +static inline void* +zwlr_output_configuration_v1_get_user_data(struct zwlr_output_configuration_v1* zwlr_output_configuration_v1) { + return wl_proxy_get_user_data((struct wl_proxy*) zwlr_output_configuration_v1); +} + +static inline uint32_t +zwlr_output_configuration_v1_get_version(struct zwlr_output_configuration_v1* zwlr_output_configuration_v1) { + return wl_proxy_get_version((struct wl_proxy*) zwlr_output_configuration_v1); +} + +// /** +// * @ingroup iface_zwlr_output_configuration_v1 +// * +// * Enable a head. This request creates a head configuration object that can +// * be used to change the head's properties. +// */ +// static inline struct zwlr_output_configuration_head_v1 * +// zwlr_output_configuration_v1_enable_head(struct zwlr_output_configuration_v1 *zwlr_output_configuration_v1, struct zwlr_output_head_v1 *head) +// { +// struct wl_proxy *id; + +// id = wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_configuration_v1, +// ZWLR_OUTPUT_CONFIGURATION_V1_ENABLE_HEAD, &zwlr_output_configuration_head_v1_interface, wl_proxy_get_version((struct wl_proxy *) zwlr_output_configuration_v1), 0, NULL, head); + +// return (struct zwlr_output_configuration_head_v1 *) id; +// } + +// /** +// * @ingroup iface_zwlr_output_configuration_v1 +// * +// * Disable a head. +// */ +// static inline void +// zwlr_output_configuration_v1_disable_head(struct zwlr_output_configuration_v1 *zwlr_output_configuration_v1, struct zwlr_output_head_v1 *head) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_configuration_v1, +// ZWLR_OUTPUT_CONFIGURATION_V1_DISABLE_HEAD, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_configuration_v1), 0, head); +// } + +// /** +// * @ingroup iface_zwlr_output_configuration_v1 +// * +// * Apply the new output configuration. +// * +// * In case the configuration is successfully applied, there is no guarantee +// * that the new output state matches completely the requested +// * configuration. For instance, a compositor might round the scale if it +// * doesn't support fractional scaling. +// * +// * After this request has been sent, the compositor must respond with an +// * succeeded, failed or cancelled event. Sending a request that isn't the +// * destructor is a protocol error. +// */ +// static inline void +// zwlr_output_configuration_v1_apply(struct zwlr_output_configuration_v1 *zwlr_output_configuration_v1) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_configuration_v1, +// ZWLR_OUTPUT_CONFIGURATION_V1_APPLY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_configuration_v1), 0); +// } + +// /** +// * @ingroup iface_zwlr_output_configuration_v1 +// * +// * Test the new output configuration. The configuration won't be applied, +// * but will only be validated. +// * +// * Even if the compositor succeeds to test a configuration, applying it may +// * fail. +// * +// * After this request has been sent, the compositor must respond with an +// * succeeded, failed or cancelled event. Sending a request that isn't the +// * destructor is a protocol error. +// */ +// static inline void +// zwlr_output_configuration_v1_test(struct zwlr_output_configuration_v1 *zwlr_output_configuration_v1) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_configuration_v1, +// ZWLR_OUTPUT_CONFIGURATION_V1_TEST, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_configuration_v1), 0); +// } + +// /** +// * @ingroup iface_zwlr_output_configuration_v1 +// * +// * Using this request a client can tell the compositor that it is not going +// * to use the configuration object anymore. Any changes to the outputs +// * that have not been applied will be discarded. +// * +// * This request also destroys wlr_output_configuration_head objects created +// * via this object. +// */ +// static inline void +// zwlr_output_configuration_v1_destroy(struct zwlr_output_configuration_v1 *zwlr_output_configuration_v1) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_configuration_v1, +// ZWLR_OUTPUT_CONFIGURATION_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_configuration_v1), WL_MARSHAL_FLAG_DESTROY); +// } + +#ifndef ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_ERROR_ENUM + #define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_ERROR_ENUM +enum zwlr_output_configuration_head_v1_error { + /** + * property has already been set + */ + ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_ERROR_ALREADY_SET = 1, + /** + * mode doesn't belong to head + */ + ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_ERROR_INVALID_MODE = 2, + /** + * mode is invalid + */ + ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_ERROR_INVALID_CUSTOM_MODE = 3, + /** + * transform value outside enum + */ + ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_ERROR_INVALID_TRANSFORM = 4, + /** + * scale negative or zero + */ + ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_ERROR_INVALID_SCALE = 5, + /** + * invalid enum value used in the set_adaptive_sync request + * @since 4 + */ + ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_ERROR_INVALID_ADAPTIVE_SYNC_STATE = 6, +}; + /** + * @ingroup iface_zwlr_output_configuration_head_v1 + */ + #define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_ERROR_INVALID_ADAPTIVE_SYNC_STATE_SINCE_VERSION 4 +#endif /* ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_ERROR_ENUM */ + +#define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_MODE 0 +#define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_CUSTOM_MODE 1 +#define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_POSITION 2 +#define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_TRANSFORM 3 +#define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_SCALE 4 +#define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_ADAPTIVE_SYNC 5 + +/** + * @ingroup iface_zwlr_output_configuration_head_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_MODE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_configuration_head_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_CUSTOM_MODE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_configuration_head_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_POSITION_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_configuration_head_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_TRANSFORM_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_configuration_head_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_SCALE_SINCE_VERSION 1 +/** + * @ingroup iface_zwlr_output_configuration_head_v1 + */ +#define ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_ADAPTIVE_SYNC_SINCE_VERSION 4 + +/** @ingroup iface_zwlr_output_configuration_head_v1 */ +static inline void +zwlr_output_configuration_head_v1_set_user_data(struct zwlr_output_configuration_head_v1* zwlr_output_configuration_head_v1, void* user_data) { + wl_proxy_set_user_data((struct wl_proxy*) zwlr_output_configuration_head_v1, user_data); +} + +/** @ingroup iface_zwlr_output_configuration_head_v1 */ +static inline void* +zwlr_output_configuration_head_v1_get_user_data(struct zwlr_output_configuration_head_v1* zwlr_output_configuration_head_v1) { + return wl_proxy_get_user_data((struct wl_proxy*) zwlr_output_configuration_head_v1); +} + +static inline uint32_t +zwlr_output_configuration_head_v1_get_version(struct zwlr_output_configuration_head_v1* zwlr_output_configuration_head_v1) { + return wl_proxy_get_version((struct wl_proxy*) zwlr_output_configuration_head_v1); +} + +/** @ingroup iface_zwlr_output_configuration_head_v1 */ +static inline void +zwlr_output_configuration_head_v1_destroy(struct zwlr_output_configuration_head_v1* zwlr_output_configuration_head_v1) { + wl_proxy_destroy((struct wl_proxy*) zwlr_output_configuration_head_v1); +} + +// /** +// * @ingroup iface_zwlr_output_configuration_head_v1 +// * +// * This request sets the head's mode. +// */ +// static inline void +// zwlr_output_configuration_head_v1_set_mode(struct zwlr_output_configuration_head_v1 *zwlr_output_configuration_head_v1, struct zwlr_output_mode_v1 *mode) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_configuration_head_v1, +// ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_MODE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_configuration_head_v1), 0, mode); +// } + +// /** +// * @ingroup iface_zwlr_output_configuration_head_v1 +// * +// * This request assigns a custom mode to the head. The size is given in +// * physical hardware units of the output device. If set to zero, the +// * refresh rate is unspecified. +// * +// * It is a protocol error to set both a mode and a custom mode. +// */ +// static inline void +// zwlr_output_configuration_head_v1_set_custom_mode(struct zwlr_output_configuration_head_v1 *zwlr_output_configuration_head_v1, int32_t width, int32_t height, int32_t refresh) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_configuration_head_v1, +// ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_CUSTOM_MODE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_configuration_head_v1), 0, width, height, refresh); +// } + +// /** +// * @ingroup iface_zwlr_output_configuration_head_v1 +// * +// * This request sets the head's position in the global compositor space. +// */ +// static inline void +// zwlr_output_configuration_head_v1_set_position(struct zwlr_output_configuration_head_v1 *zwlr_output_configuration_head_v1, int32_t x, int32_t y) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_configuration_head_v1, +// ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_POSITION, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_configuration_head_v1), 0, x, y); +// } + +// /** +// * @ingroup iface_zwlr_output_configuration_head_v1 +// * +// * This request sets the head's transform. +// */ +// static inline void +// zwlr_output_configuration_head_v1_set_transform(struct zwlr_output_configuration_head_v1 *zwlr_output_configuration_head_v1, int32_t transform) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_configuration_head_v1, +// ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_TRANSFORM, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_configuration_head_v1), 0, transform); +// } + +// /** +// * @ingroup iface_zwlr_output_configuration_head_v1 +// * +// * This request sets the head's scale. +// */ +// static inline void +// zwlr_output_configuration_head_v1_set_scale(struct zwlr_output_configuration_head_v1 *zwlr_output_configuration_head_v1, wl_fixed_t scale) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_configuration_head_v1, +// ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_SCALE, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_configuration_head_v1), 0, scale); +// } + +// /** +// * @ingroup iface_zwlr_output_configuration_head_v1 +// * +// * This request enables/disables adaptive sync. Adaptive sync is also +// * known as Variable Refresh Rate or VRR. +// */ +// static inline void +// zwlr_output_configuration_head_v1_set_adaptive_sync(struct zwlr_output_configuration_head_v1 *zwlr_output_configuration_head_v1, uint32_t state) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zwlr_output_configuration_head_v1, +// ZWLR_OUTPUT_CONFIGURATION_HEAD_V1_SET_ADAPTIVE_SYNC, NULL, wl_proxy_get_version((struct wl_proxy *) zwlr_output_configuration_head_v1), 0, state); +// } + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/detection/displayserver/linux/wayland/wlr-output-management-unstable-v1-protocol.c b/src/detection/displayserver/linux/wayland/wlr-output-management-unstable-v1-protocol.c new file mode 100644 index 0000000..a231fd4 --- /dev/null +++ b/src/detection/displayserver/linux/wayland/wlr-output-management-unstable-v1-protocol.c @@ -0,0 +1,176 @@ +#ifdef FF_HAVE_WAYLAND + +/* Generated by wayland-scanner 1.24.0 */ + +/* + * Copyright © 2019 Purism SPC + * + * Permission to use, copy, modify, distribute, and sell this + * software and its documentation for any purpose is hereby granted + * without fee, provided that the above copyright notice appear in + * all copies and that both that copyright notice and this permission + * notice appear in supporting documentation, and that the name of + * the copyright holders not be used in advertising or publicity + * pertaining to distribution of the software without specific, + * written prior permission. The copyright holders make no + * representations about the suitability of this software for any + * purpose. It is provided "as is" without express or implied + * warranty. + * + * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + * THIS SOFTWARE. + */ + + #include <stdbool.h> + #include <stdlib.h> + #include <stdint.h> + #include <wayland-util.h> + + #ifndef __has_attribute + #define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ + #endif + + #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) + #define WL_PRIVATE __attribute__((visibility("hidden"))) + #else + #define WL_PRIVATE + #endif + +extern const struct wl_interface zwlr_output_configuration_head_v1_interface; +extern const struct wl_interface zwlr_output_configuration_v1_interface; +extern const struct wl_interface zwlr_output_head_v1_interface; +extern const struct wl_interface zwlr_output_mode_v1_interface; + +static const struct wl_interface* wlr_output_management_unstable_v1_types[] = { + NULL, + NULL, + NULL, + &zwlr_output_configuration_v1_interface, + NULL, + &zwlr_output_head_v1_interface, + &zwlr_output_mode_v1_interface, + &zwlr_output_mode_v1_interface, + &zwlr_output_configuration_head_v1_interface, + &zwlr_output_head_v1_interface, + &zwlr_output_head_v1_interface, + &zwlr_output_mode_v1_interface, +}; + +static const struct wl_message zwlr_output_manager_v1_requests[] = { + { "create_configuration", "nu", wlr_output_management_unstable_v1_types + 3 }, + { "stop", "", wlr_output_management_unstable_v1_types + 0 }, +}; + +static const struct wl_message zwlr_output_manager_v1_events[] = { + { "head", "n", wlr_output_management_unstable_v1_types + 5 }, + { "done", "u", wlr_output_management_unstable_v1_types + 0 }, + { "finished", "", wlr_output_management_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwlr_output_manager_v1_interface = { + "zwlr_output_manager_v1", + 4, + 2, + zwlr_output_manager_v1_requests, + 3, + zwlr_output_manager_v1_events, +}; + +static const struct wl_message zwlr_output_head_v1_requests[] = { + { "release", "3", wlr_output_management_unstable_v1_types + 0 }, +}; + +static const struct wl_message zwlr_output_head_v1_events[] = { + { "name", "s", wlr_output_management_unstable_v1_types + 0 }, + { "description", "s", wlr_output_management_unstable_v1_types + 0 }, + { "physical_size", "ii", wlr_output_management_unstable_v1_types + 0 }, + { "mode", "n", wlr_output_management_unstable_v1_types + 6 }, + { "enabled", "i", wlr_output_management_unstable_v1_types + 0 }, + { "current_mode", "o", wlr_output_management_unstable_v1_types + 7 }, + { "position", "ii", wlr_output_management_unstable_v1_types + 0 }, + { "transform", "i", wlr_output_management_unstable_v1_types + 0 }, + { "scale", "f", wlr_output_management_unstable_v1_types + 0 }, + { "finished", "", wlr_output_management_unstable_v1_types + 0 }, + { "make", "2s", wlr_output_management_unstable_v1_types + 0 }, + { "model", "2s", wlr_output_management_unstable_v1_types + 0 }, + { "serial_number", "2s", wlr_output_management_unstable_v1_types + 0 }, + { "adaptive_sync", "4u", wlr_output_management_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwlr_output_head_v1_interface = { + "zwlr_output_head_v1", + 4, + 1, + zwlr_output_head_v1_requests, + 14, + zwlr_output_head_v1_events, +}; + +static const struct wl_message zwlr_output_mode_v1_requests[] = { + { "release", "3", wlr_output_management_unstable_v1_types + 0 }, +}; + +static const struct wl_message zwlr_output_mode_v1_events[] = { + { "size", "ii", wlr_output_management_unstable_v1_types + 0 }, + { "refresh", "i", wlr_output_management_unstable_v1_types + 0 }, + { "preferred", "", wlr_output_management_unstable_v1_types + 0 }, + { "finished", "", wlr_output_management_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwlr_output_mode_v1_interface = { + "zwlr_output_mode_v1", + 3, + 1, + zwlr_output_mode_v1_requests, + 4, + zwlr_output_mode_v1_events, +}; + +static const struct wl_message zwlr_output_configuration_v1_requests[] = { + { "enable_head", "no", wlr_output_management_unstable_v1_types + 8 }, + { "disable_head", "o", wlr_output_management_unstable_v1_types + 10 }, + { "apply", "", wlr_output_management_unstable_v1_types + 0 }, + { "test", "", wlr_output_management_unstable_v1_types + 0 }, + { "destroy", "", wlr_output_management_unstable_v1_types + 0 }, +}; + +static const struct wl_message zwlr_output_configuration_v1_events[] = { + { "succeeded", "", wlr_output_management_unstable_v1_types + 0 }, + { "failed", "", wlr_output_management_unstable_v1_types + 0 }, + { "cancelled", "", wlr_output_management_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwlr_output_configuration_v1_interface = { + "zwlr_output_configuration_v1", + 4, + 5, + zwlr_output_configuration_v1_requests, + 3, + zwlr_output_configuration_v1_events, +}; + +static const struct wl_message zwlr_output_configuration_head_v1_requests[] = { + { "set_mode", "o", wlr_output_management_unstable_v1_types + 11 }, + { "set_custom_mode", "iii", wlr_output_management_unstable_v1_types + 0 }, + { "set_position", "ii", wlr_output_management_unstable_v1_types + 0 }, + { "set_transform", "i", wlr_output_management_unstable_v1_types + 0 }, + { "set_scale", "f", wlr_output_management_unstable_v1_types + 0 }, + { "set_adaptive_sync", "4u", wlr_output_management_unstable_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface zwlr_output_configuration_head_v1_interface = { + "zwlr_output_configuration_head_v1", + 4, + 6, + zwlr_output_configuration_head_v1_requests, + 0, + NULL, +}; + +#endif diff --git a/src/detection/displayserver/linux/wayland/xdg-output-unstable-v1-client-protocol.h b/src/detection/displayserver/linux/wayland/xdg-output-unstable-v1-client-protocol.h new file mode 100644 index 0000000..51784c6 --- /dev/null +++ b/src/detection/displayserver/linux/wayland/xdg-output-unstable-v1-client-protocol.h @@ -0,0 +1,414 @@ +/* Generated by wayland-scanner 1.22.0 */ + +#ifndef XDG_OUTPUT_UNSTABLE_V1_CLIENT_PROTOCOL_H +#define XDG_OUTPUT_UNSTABLE_V1_CLIENT_PROTOCOL_H + +#include <stdint.h> +#include <stddef.h> +#include <wayland-client.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_xdg_output_unstable_v1 The xdg_output_unstable_v1 protocol + * Protocol to describe output regions + * + * @section page_desc_xdg_output_unstable_v1 Description + * + * This protocol aims at describing outputs in a way which is more in line + * with the concept of an output on desktop oriented systems. + * + * Some information are more specific to the concept of an output for + * a desktop oriented system and may not make sense in other applications, + * such as IVI systems for example. + * + * Typically, the global compositor space on a desktop system is made of + * a contiguous or overlapping set of rectangular regions. + * + * The logical_position and logical_size events defined in this protocol + * might provide information identical to their counterparts already + * available from wl_output, in which case the information provided by this + * protocol should be preferred to their equivalent in wl_output. The goal is + * to move the desktop specific concepts (such as output location within the + * global compositor space, etc.) out of the core wl_output protocol. + * + * Warning! The protocol described in this file is experimental and + * backward incompatible changes may be made. Backward compatible + * changes may be added together with the corresponding interface + * version bump. + * Backward incompatible changes are done by bumping the version + * number in the protocol and interface names and resetting the + * interface version. Once the protocol is to be declared stable, + * the 'z' prefix and the version number in the protocol and + * interface names are removed and the interface version number is + * reset. + * + * @section page_ifaces_xdg_output_unstable_v1 Interfaces + * - @subpage page_iface_zxdg_output_manager_v1 - manage xdg_output objects + * - @subpage page_iface_zxdg_output_v1 - compositor logical output region + * @section page_copyright_xdg_output_unstable_v1 Copyright + * <pre> + * + * Copyright © 2017 Red Hat Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * </pre> + */ +struct wl_output; +struct zxdg_output_manager_v1; +struct zxdg_output_v1; + +#ifndef ZXDG_OUTPUT_MANAGER_V1_INTERFACE + #define ZXDG_OUTPUT_MANAGER_V1_INTERFACE +/** + * @page page_iface_zxdg_output_manager_v1 zxdg_output_manager_v1 + * @section page_iface_zxdg_output_manager_v1_desc Description + * + * A global factory interface for xdg_output objects. + * @section page_iface_zxdg_output_manager_v1_api API + * See @ref iface_zxdg_output_manager_v1. + */ +/** + * @defgroup iface_zxdg_output_manager_v1 The zxdg_output_manager_v1 interface + * + * A global factory interface for xdg_output objects. + */ +extern const struct wl_interface zxdg_output_manager_v1_interface; +#endif +#ifndef ZXDG_OUTPUT_V1_INTERFACE + #define ZXDG_OUTPUT_V1_INTERFACE +/** + * @page page_iface_zxdg_output_v1 zxdg_output_v1 + * @section page_iface_zxdg_output_v1_desc Description + * + * An xdg_output describes part of the compositor geometry. + * + * This typically corresponds to a monitor that displays part of the + * compositor space. + * + * For objects version 3 onwards, after all xdg_output properties have been + * sent (when the object is created and when properties are updated), a + * wl_output.done event is sent. This allows changes to the output + * properties to be seen as atomic, even if they happen via multiple events. + * @section page_iface_zxdg_output_v1_api API + * See @ref iface_zxdg_output_v1. + */ +/** + * @defgroup iface_zxdg_output_v1 The zxdg_output_v1 interface + * + * An xdg_output describes part of the compositor geometry. + * + * This typically corresponds to a monitor that displays part of the + * compositor space. + * + * For objects version 3 onwards, after all xdg_output properties have been + * sent (when the object is created and when properties are updated), a + * wl_output.done event is sent. This allows changes to the output + * properties to be seen as atomic, even if they happen via multiple events. + */ +extern const struct wl_interface zxdg_output_v1_interface; +#endif + +#define ZXDG_OUTPUT_MANAGER_V1_DESTROY 0 +#define ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT 1 + +/** + * @ingroup iface_zxdg_output_manager_v1 + */ +#define ZXDG_OUTPUT_MANAGER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_output_manager_v1 + */ +#define ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT_SINCE_VERSION 1 + +// /** @ingroup iface_zxdg_output_manager_v1 */ +// static inline void +// zxdg_output_manager_v1_set_user_data(struct zxdg_output_manager_v1 *zxdg_output_manager_v1, void *user_data) +// { +// wl_proxy_set_user_data((struct wl_proxy *) zxdg_output_manager_v1, user_data); +// } + +// /** @ingroup iface_zxdg_output_manager_v1 */ +// static inline void * +// zxdg_output_manager_v1_get_user_data(struct zxdg_output_manager_v1 *zxdg_output_manager_v1) +// { +// return wl_proxy_get_user_data((struct wl_proxy *) zxdg_output_manager_v1); +// } + +// static inline uint32_t +// zxdg_output_manager_v1_get_version(struct zxdg_output_manager_v1 *zxdg_output_manager_v1) +// { +// return wl_proxy_get_version((struct wl_proxy *) zxdg_output_manager_v1); +// } + +// /** +// * @ingroup iface_zxdg_output_manager_v1 +// * +// * Using this request a client can tell the server that it is not +// * going to use the xdg_output_manager object anymore. +// * +// * Any objects already created through this instance are not affected. +// */ +// static inline void +// zxdg_output_manager_v1_destroy(struct zxdg_output_manager_v1 *zxdg_output_manager_v1) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zxdg_output_manager_v1, +// ZXDG_OUTPUT_MANAGER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_output_manager_v1), WL_MARSHAL_FLAG_DESTROY); +// } + +// /** +// * @ingroup iface_zxdg_output_manager_v1 +// * +// * This creates a new xdg_output object for the given wl_output. +// */ +// static inline struct zxdg_output_v1 * +// zxdg_output_manager_v1_get_xdg_output(struct zxdg_output_manager_v1 *zxdg_output_manager_v1, struct wl_output *output) +// { +// struct wl_proxy *id; + +// id = wl_proxy_marshal_flags((struct wl_proxy *) zxdg_output_manager_v1, +// ZXDG_OUTPUT_MANAGER_V1_GET_XDG_OUTPUT, &zxdg_output_v1_interface, wl_proxy_get_version((struct wl_proxy *) zxdg_output_manager_v1), 0, NULL, output); + +// return (struct zxdg_output_v1 *) id; +// } + +/** + * @ingroup iface_zxdg_output_v1 + * @struct zxdg_output_v1_listener + */ +struct zxdg_output_v1_listener { + /** + * position of the output within the global compositor space + * + * The position event describes the location of the wl_output + * within the global compositor space. + * + * The logical_position event is sent after creating an xdg_output + * (see xdg_output_manager.get_xdg_output) and whenever the + * location of the output changes within the global compositor + * space. + * @param x x position within the global compositor space + * @param y y position within the global compositor space + */ + void (*logical_position)(void* data, + struct zxdg_output_v1* zxdg_output_v1, + int32_t x, + int32_t y); + /** + * size of the output in the global compositor space + * + * The logical_size event describes the size of the output in the + * global compositor space. + * + * Most regular Wayland clients should not pay attention to the + * logical size and would rather rely on xdg_shell interfaces. + * + * Some clients such as Xwayland, however, need this to configure + * their surfaces in the global compositor space as the compositor + * may apply a different scale from what is advertised by the + * output scaling property (to achieve fractional scaling, for + * example). + * + * For example, for a wl_output mode 3840×2160 and a scale factor + * 2: + * + * - A compositor not scaling the monitor viewport in its + * compositing space will advertise a logical size of 3840×2160, + * + * - A compositor scaling the monitor viewport with scale factor 2 + * will advertise a logical size of 1920×1080, + * + * - A compositor scaling the monitor viewport using a fractional + * scale of 1.5 will advertise a logical size of 2560×1440. + * + * For example, for a wl_output mode 1920×1080 and a 90 degree + * rotation, the compositor will advertise a logical size of + * 1080x1920. + * + * The logical_size event is sent after creating an xdg_output (see + * xdg_output_manager.get_xdg_output) and whenever the logical size + * of the output changes, either as a result of a change in the + * applied scale or because of a change in the corresponding output + * mode(see wl_output.mode) or transform (see wl_output.transform). + * @param width width in global compositor space + * @param height height in global compositor space + */ + void (*logical_size)(void* data, + struct zxdg_output_v1* zxdg_output_v1, + int32_t width, + int32_t height); + /** + * all information about the output have been sent + * + * This event is sent after all other properties of an xdg_output + * have been sent. + * + * This allows changes to the xdg_output properties to be seen as + * atomic, even if they happen via multiple events. + * + * For objects version 3 onwards, this event is deprecated. + * Compositors are not required to send it anymore and must send + * wl_output.done instead. + */ + void (*done)(void* data, + struct zxdg_output_v1* zxdg_output_v1); + /** + * name of this output + * + * Many compositors will assign names to their outputs, show them + * to the user, allow them to be configured by name, etc. The + * client may wish to know this name as well to offer the user + * similar behaviors. + * + * The naming convention is compositor defined, but limited to + * alphanumeric characters and dashes (-). Each name is unique + * among all wl_output globals, but if a wl_output global is + * destroyed the same name may be reused later. The names will also + * remain consistent across sessions with the same hardware and + * software configuration. + * + * Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. + * However, do not assume that the name is a reflection of an + * underlying DRM connector, X11 connection, etc. + * + * The name event is sent after creating an xdg_output (see + * xdg_output_manager.get_xdg_output). This event is only sent once + * per xdg_output, and the name does not change over the lifetime + * of the wl_output global. + * + * This event is deprecated, instead clients should use + * wl_output.name. Compositors must still support this event. + * @param name output name + * @since 2 + */ + void (*name)(void* data, + struct zxdg_output_v1* zxdg_output_v1, + const char* name); + /** + * human-readable description of this output + * + * Many compositors can produce human-readable descriptions of + * their outputs. The client may wish to know this description as + * well, to communicate the user for various purposes. + * + * The description is a UTF-8 string with no convention defined for + * its contents. Examples might include 'Foocorp 11" Display' or + * 'Virtual X11 output via :1'. + * + * The description event is sent after creating an xdg_output (see + * xdg_output_manager.get_xdg_output) and whenever the description + * changes. The description is optional, and may not be sent at + * all. + * + * For objects of version 2 and lower, this event is only sent once + * per xdg_output, and the description does not change over the + * lifetime of the wl_output global. + * + * This event is deprecated, instead clients should use + * wl_output.description. Compositors must still support this + * event. + * @param description output description + * @since 2 + */ + void (*description)(void* data, + struct zxdg_output_v1* zxdg_output_v1, + const char* description); +}; + +/** + * @ingroup iface_zxdg_output_v1 + */ +static inline int +zxdg_output_v1_add_listener(struct zxdg_output_v1* zxdg_output_v1, + const struct zxdg_output_v1_listener* listener, + void* data) { + return wl_proxy_add_listener((struct wl_proxy*) zxdg_output_v1, + (void (**)(void)) listener, + data); +} + +#define ZXDG_OUTPUT_V1_DESTROY 0 + +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_LOGICAL_POSITION_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_LOGICAL_SIZE_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_DONE_SINCE_VERSION 1 +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_NAME_SINCE_VERSION 2 +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_DESCRIPTION_SINCE_VERSION 2 + +/** + * @ingroup iface_zxdg_output_v1 + */ +#define ZXDG_OUTPUT_V1_DESTROY_SINCE_VERSION 1 + +// /** @ingroup iface_zxdg_output_v1 */ +// static inline void +// zxdg_output_v1_set_user_data(struct zxdg_output_v1 *zxdg_output_v1, void *user_data) +// { +// wl_proxy_set_user_data((struct wl_proxy *) zxdg_output_v1, user_data); +// } + +// /** @ingroup iface_zxdg_output_v1 */ +// static inline void * +// zxdg_output_v1_get_user_data(struct zxdg_output_v1 *zxdg_output_v1) +// { +// return wl_proxy_get_user_data((struct wl_proxy *) zxdg_output_v1); +// } + +// static inline uint32_t +// zxdg_output_v1_get_version(struct zxdg_output_v1 *zxdg_output_v1) +// { +// return wl_proxy_get_version((struct wl_proxy *) zxdg_output_v1); +// } + +// /** +// * @ingroup iface_zxdg_output_v1 +// * +// * Using this request a client can tell the server that it is not +// * going to use the xdg_output object anymore. +// */ +// static inline void +// zxdg_output_v1_destroy(struct zxdg_output_v1 *zxdg_output_v1) +// { +// wl_proxy_marshal_flags((struct wl_proxy *) zxdg_output_v1, +// ZXDG_OUTPUT_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) zxdg_output_v1), WL_MARSHAL_FLAG_DESTROY); +// } + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/detection/displayserver/linux/wayland/xdg-output-unstable-v1-protocol.c b/src/detection/displayserver/linux/wayland/xdg-output-unstable-v1-protocol.c new file mode 100644 index 0000000..76f6257 --- /dev/null +++ b/src/detection/displayserver/linux/wayland/xdg-output-unstable-v1-protocol.c @@ -0,0 +1,76 @@ +#ifdef FF_HAVE_WAYLAND +/* Generated by wayland-scanner 1.22.0 */ + +/* + * Copyright © 2017 Red Hat Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + + #include <stdlib.h> + #include <stdint.h> + #include <wayland-util.h> + +extern const struct wl_interface wl_output_interface; +extern const struct wl_interface zxdg_output_v1_interface; + +static const struct wl_interface* xdg_output_unstable_v1_types[] = { + NULL, + NULL, + &zxdg_output_v1_interface, + NULL, // &wl_output_interface, +}; + +static const struct wl_message zxdg_output_manager_v1_requests[] = { + { "destroy", "", xdg_output_unstable_v1_types + 0 }, + { "get_xdg_output", "no", xdg_output_unstable_v1_types + 2 }, +}; + +WL_EXPORT const struct wl_interface zxdg_output_manager_v1_interface = { + "zxdg_output_manager_v1", + 3, + 2, + zxdg_output_manager_v1_requests, + 0, + NULL, +}; + +static const struct wl_message zxdg_output_v1_requests[] = { + { "destroy", "", xdg_output_unstable_v1_types + 0 }, +}; + +static const struct wl_message zxdg_output_v1_events[] = { + { "logical_position", "ii", xdg_output_unstable_v1_types + 0 }, + { "logical_size", "ii", xdg_output_unstable_v1_types + 0 }, + { "done", "", xdg_output_unstable_v1_types + 0 }, + { "name", "2s", xdg_output_unstable_v1_types + 0 }, + { "description", "2s", xdg_output_unstable_v1_types + 0 }, +}; + +WL_EXPORT const struct wl_interface zxdg_output_v1_interface = { + "zxdg_output_v1", + 3, + 1, + zxdg_output_v1_requests, + 5, + zxdg_output_v1_events, +}; + +#endif diff --git a/src/detection/displayserver/linux/wayland/zwlr-output.c b/src/detection/displayserver/linux/wayland/zwlr-output.c new file mode 100644 index 0000000..632bd19 --- /dev/null +++ b/src/detection/displayserver/linux/wayland/zwlr-output.c @@ -0,0 +1,212 @@ +#ifdef FF_HAVE_WAYLAND + + #include "wayland.h" + #include "wlr-output-management-unstable-v1-client-protocol.h" + +static void waylandZwlrTransformListener(void* data, FF_A_UNUSED struct zwlr_output_head_v1* zwlr_output_head_v1, int32_t transform) { + WaylandDisplay* wldata = (WaylandDisplay*) data; + wldata->transform = (enum wl_output_transform) transform; +} + +static void waylandZwlrScaleListener(void* data, FF_A_UNUSED struct zwlr_output_head_v1* zwlr_output_head_v1, wl_fixed_t scale) { + WaylandDisplay* wldata = (WaylandDisplay*) data; + wldata->dpi = (uint32_t) scale * 3 / 8; // wl_fixed_to_double(scale) * 96; +} + +typedef struct WaylandZwlrMode { + int32_t width; + int32_t height; + int32_t refreshRate; + bool preferred; + struct zwlr_output_mode_v1* pMode; +} WaylandZwlrMode; + +static void waylandZwlrModeSizeListener(void* data, FF_A_UNUSED struct zwlr_output_mode_v1* zwlr_output_mode_v1, int32_t width, int32_t height) { + WaylandZwlrMode* mode = (WaylandZwlrMode*) data; + mode->width = width; + mode->height = height; +} + +static void waylandZwlrModeRefreshListener(void* data, FF_A_UNUSED struct zwlr_output_mode_v1* zwlr_output_mode_v1, int32_t rate) { + WaylandZwlrMode* mode = (WaylandZwlrMode*) data; + mode->refreshRate = rate; +} + +static void waylandZwlrModePreferredListener(void* data, FF_A_UNUSED struct zwlr_output_mode_v1* zwlr_output_mode_v1) { + WaylandZwlrMode* mode = (WaylandZwlrMode*) data; + mode->preferred = true; +} + +static const struct zwlr_output_mode_v1_listener modeListener = { + .size = waylandZwlrModeSizeListener, + .refresh = waylandZwlrModeRefreshListener, + .preferred = waylandZwlrModePreferredListener, + .finished = (void*) stubListener, +}; + +static void waylandZwlrModeListener(void* data, FF_A_UNUSED struct zwlr_output_head_v1* zwlr_output_head_v1, struct zwlr_output_mode_v1* mode) { + WaylandDisplay* wldata = (WaylandDisplay*) data; + if (!wldata->internal) { + return; + } + + WaylandZwlrMode* newMode = FF_LIST_ADD(WaylandZwlrMode, *(FFlist*) wldata->internal); + *newMode = (WaylandZwlrMode) { .pMode = mode }; + + // Strangely, the listener is called only in this function, but not in `waylandZwlrCurrentModeListener` + wldata->parent->ffwl_proxy_add_listener((struct wl_proxy*) mode, (void (**)(void)) &modeListener, newMode); +} + +static void waylandZwlrCurrentModeListener(void* data, FF_A_UNUSED struct zwlr_output_head_v1* zwlr_output_head_v1, struct zwlr_output_mode_v1* mode) { + // waylandZwlrModeListener is always run before this + WaylandDisplay* wldata = (WaylandDisplay*) data; + if (!wldata->internal) { + return; + } + + int set = 0; + FF_LIST_FOR_EACH (WaylandZwlrMode, m, *(FFlist*) wldata->internal) { + if (m->pMode == mode) { + wldata->width = m->width; + wldata->height = m->height; + wldata->refreshRate = m->refreshRate; + if (++set == 2) { + break; + } + } + if (m->preferred) { + wldata->preferredWidth = m->width; + wldata->preferredHeight = m->height; + wldata->preferredRefreshRate = m->refreshRate; + if (++set == 2) { + break; + } + } + } +} + +static void waylandZwlrPhysicalSizeListener(void* data, FF_A_UNUSED struct zwlr_output_head_v1* zwlr_output_head_v1, int32_t width, int32_t height) { + WaylandDisplay* wldata = (WaylandDisplay*) data; + wldata->physicalWidth = width; + wldata->physicalHeight = height; +} + +static void waylandZwlrEnabledListener(void* data, FF_A_UNUSED struct zwlr_output_head_v1* zwlr_output_head_v1, bool enabled) { + WaylandDisplay* wldata = (WaylandDisplay*) data; + if (!enabled) { + wldata->internal = NULL; + } +} + +static const struct zwlr_output_head_v1_listener headListener = { + .name = (void*) ffWaylandOutputNameListener, + .description = (void*) ffWaylandOutputDescriptionListener, + .physical_size = waylandZwlrPhysicalSizeListener, + .mode = waylandZwlrModeListener, + .enabled = (void*) waylandZwlrEnabledListener, + .current_mode = waylandZwlrCurrentModeListener, + .position = (void*) stubListener, + .transform = waylandZwlrTransformListener, + .scale = waylandZwlrScaleListener, + .finished = (void*) stubListener, + .make = (void*) stubListener, + .model = (void*) stubListener, + .serial_number = (void*) stubListener, + .adaptive_sync = (void*) stubListener, +}; + +static void waylandHandleZwlrHead(void* data, FF_A_UNUSED struct zwlr_output_manager_v1* zwlr_output_manager_v1, struct zwlr_output_head_v1* head) { + WaylandData* wldata = data; + + FF_LIST_AUTO_DESTROY modes = ffListCreate(); + WaylandDisplay display = { + .parent = wldata, + .transform = WL_OUTPUT_TRANSFORM_NORMAL, + .type = FF_DISPLAY_TYPE_UNKNOWN, + .name = ffStrbufCreate(), + .description = ffStrbufCreate(), + .edidName = ffStrbufCreate(), + .internal = &modes, + }; + + wldata->ffwl_proxy_add_listener((struct wl_proxy*) head, (void (**)(void)) &headListener, &display); + wldata->ffwl_display_roundtrip(wldata->display); + + if (display.width <= 0 || display.height <= 0 || !display.internal) { + return; + } + + uint32_t rotation = ffWaylandHandleRotation(&display); + + FFDisplayResult* item = ffdsAppendDisplay(wldata->result, + (uint32_t) display.width, + (uint32_t) display.height, + display.refreshRate / 1000.0, + (uint32_t) display.dpi, + (uint32_t) display.preferredWidth, + (uint32_t) display.preferredHeight, + display.preferredRefreshRate / 1000.0, + rotation, + display.edidName.length + ? &display.edidName + : display.description.length && !ffStrbufContain(&display.description, &display.name) + ? &display.description + : &display.name, + display.type, + false, + display.id, + (uint32_t) display.physicalWidth, + (uint32_t) display.physicalHeight, + "wayland-zwlr"); + if (item) { + if (display.hdrSupported) { + item->hdrStatus = FF_DISPLAY_HDR_STATUS_SUPPORTED; + } else if (display.hdrInfoAvailable) { + item->hdrStatus = FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + } else { + item->hdrStatus = FF_DISPLAY_HDR_STATUS_UNKNOWN; + } + + item->manufactureYear = display.myear; + item->manufactureWeek = display.mweek; + item->serial = display.serial; + } + + ffStrbufDestroy(&display.description); + ffStrbufDestroy(&display.name); + ffStrbufDestroy(&display.edidName); + + // These must be released manually + FF_LIST_FOR_EACH (WaylandZwlrMode, m, modes) { + wldata->ffwl_proxy_destroy((void*) m->pMode); + } + wldata->ffwl_proxy_destroy((void*) head); +} + +static const struct zwlr_output_manager_v1_listener outputListener = { + .head = waylandHandleZwlrHead, + .done = (void*) stubListener, + .finished = (void*) stubListener, +}; + +const char* ffWaylandHandleZwlrOutput(WaylandData* wldata, struct wl_registry* registry, uint32_t name, uint32_t version) { + uint32_t bindVersion = min(version, ZWLR_OUTPUT_MANAGER_V1_HEAD_SINCE_VERSION); + struct wl_proxy* output = wldata->ffwl_proxy_marshal_constructor_versioned((struct wl_proxy*) registry, WL_REGISTRY_BIND, &zwlr_output_manager_v1_interface, bindVersion, name, zwlr_output_manager_v1_interface.name, bindVersion, NULL); + if (output == NULL) { + return "Failed to bind zwlr_output_manager_v1"; + } + + if (wldata->ffwl_proxy_add_listener(output, (void (**)(void)) &outputListener, wldata) < 0) { + wldata->ffwl_proxy_destroy(output); + return "Failed to add listener to zwlr_output_manager_v1"; + } + if (wldata->ffwl_display_roundtrip(wldata->display) < 0) { + wldata->ffwl_proxy_destroy(output); + return "Failed to roundtrip display"; + } + wldata->ffwl_proxy_destroy(output); + + return NULL; +} + +#endif diff --git a/src/detection/displayserver/linux/wmde.c b/src/detection/displayserver/linux/wmde.c new file mode 100644 index 0000000..f0321c8 --- /dev/null +++ b/src/detection/displayserver/linux/wmde.c @@ -0,0 +1,523 @@ +#include "displayserver_linux.h" +#include "common/io.h" +#include "common/properties.h" +#include "common/strutil.h" +#include "common/mallocHelper.h" + +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#if __FreeBSD__ + #include <sys/sysctl.h> + #include <sys/types.h> + #include <sys/user.h> +#elif __OpenBSD__ + #include <sys/param.h> + #include <sys/sysctl.h> + #include <kvm.h> +#elif __sun + #include <procfs.h> +#elif __NetBSD__ + #include <sys/types.h> + #include <sys/sysctl.h> +#endif + +static const char* parseEnv(void) { + const char* env; + + env = getenv("XDG_CURRENT_DESKTOP"); + if (ffStrSet(env)) { + return env; + } + + env = getenv("XDG_SESSION_DESKTOP"); + if (ffStrSet(env)) { + return env; + } + + env = getenv("CURRENT_DESKTOP"); + if (ffStrSet(env)) { + return env; + } + + env = getenv("SESSION_DESKTOP"); + if (ffStrSet(env)) { + return env; + } + + env = getenv("DESKTOP_SESSION"); + if (ffStrSet(env)) { + return env; + } + + if (getenv("KDE_FULL_SESSION") != NULL || getenv("KDE_SESSION_UID") != NULL || getenv("KDE_SESSION_VERSION") != NULL) { + return "KDE"; + } + + if (getenv("GNOME_DESKTOP_SESSION_ID") != NULL) { + return "GNOME"; + } + + if (getenv("MATE_DESKTOP_SESSION_ID") != NULL) { + return "Mate"; + } + + if (getenv("TDE_FULL_SESSION") != NULL) { + return "Trinity"; + } + + if (getenv("HYPRLAND_CMD") != NULL) { + return "Hyprland"; + } + + if (getenv("SWAYSOCK") != NULL) { + return "Sway"; + } + +#if __linux__ && !__ANDROID__ + if ( + getenv("WAYLAND_DISPLAY") != NULL && + ffPathExists("/mnt/wslg/", FF_PATHTYPE_DIRECTORY)) { + return "WSLg"; + } +#endif + + return NULL; +} + +static void applyPrettyNameIfWM(FFDisplayServerResult* result, const char* name) { + if (!ffStrSet(name)) { + return; + } + + if ( + ffStrEqualsIgnCase(name, "kwin") || + ffStrStartsWithIgnCase(name, "kwin_") || + ffStrEndsWithIgnCase(name, "-kwin_wayland") || + ffStrEndsWithIgnCase(name, "-kwin_x11")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_KWIN); + } else if ( + ffStrEqualsIgnCase(name, "gnome-shell") || + ffStrEqualsIgnCase(name, "gnome shell") || + ffStrEqualsIgnCase(name, "gnome-session-binary") || + ffStrEqualsIgnCase(name, "Mutter")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_MUTTER); + } else if ( + ffStrEqualsIgnCase(name, "cinnamon") || + ffStrStartsWithIgnCase(name, "cinnamon-") || + ffStrEqualsIgnCase(name, "Muffin") || + ffStrEqualsIgnCase(name, "Mutter (Muffin)")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_MUFFIN); + } else if (ffStrEqualsIgnCase(name, "sway")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_SWAY); + } else if (ffStrEqualsIgnCase(name, "weston")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_WESTON); + } else if (ffStrEqualsIgnCase(name, "wayfire")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_WAYFIRE); + } else if (ffStrEqualsIgnCase(name, "openbox")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_OPENBOX); + } else if (ffStrEqualsIgnCase(name, "xfwm4")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_XFWM4); + } else if (ffStrEqualsIgnCase(name, "Marco") || + ffStrEqualsIgnCase(name, "Metacity (Marco)")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_MARCO); + } else if (ffStrEqualsIgnCase(name, "xmonad")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_XMONAD); + } else if (ffStrEqualsIgnCase(name, "WSLg")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_WSLG); + } else if (ffStrEqualsIgnCase(name, "dwm")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_DWM); + } else if (ffStrEqualsIgnCase(name, "bspwm")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_BSPWM); + } else if (ffStrEqualsIgnCase(name, "tinywm")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_TINYWM); + } else if (ffStrEqualsIgnCase(name, "qtile")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_QTILE); + } else if (ffStrEqualsIgnCase(name, "herbstluftwm")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_HERBSTLUFTWM); + } else if (ffStrEqualsIgnCase(name, "icewm")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_ICEWM); + } else if (ffStrEqualsIgnCase(name, "dtwm")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_DTWM); + } else if (ffStrEqualsIgnCase(name, "fvwm")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_FVWM); + } else if (ffStrEqualsIgnCase(name, "ctwm")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_CTWM); + } else if (ffStrEqualsIgnCase(name, "hyprland")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_HYPRLAND); + } else if (ffStrEqualsIgnCase(name, "ratpoison")) { + ffStrbufSetS(&result->wmPrettyName, FF_WM_PRETTY_RATPOISON); + } +} + +static void applyNameIfWM(FFDisplayServerResult* result, const char* processName) { + applyPrettyNameIfWM(result, processName); + if (result->wmPrettyName.length > 0) { + ffStrbufSetS(&result->wmProcessName, processName); + } +} + +static void applyBetterWM(FFDisplayServerResult* result, const char* processName) { + if (!ffStrSet(processName)) { + return; + } + + ffStrbufSetS(&result->wmProcessName, processName); + + // If it is a known wm, this will set the pretty name + applyPrettyNameIfWM(result, processName); + + // If it isn't a known wm, set the pretty name to the process name + if (result->wmPrettyName.length == 0) { + ffStrbufAppend(&result->wmPrettyName, &result->wmProcessName); + } +} + +static void applyPrettyNameIfDE(FFDisplayServerResult* result, const char* name) { + if (!ffStrSet(name)) { + return; + } + + else if ( + ffStrEqualsIgnCase(name, "KDE") || + ffStrEqualsIgnCase(name, "plasma") || + ffStrEqualsIgnCase(name, "plasmashell") || + ffStrEqualsIgnCase(name, "plasmawayland")) { + ffStrbufSetStatic(&result->deProcessName, "plasmashell"); + ffStrbufSetStatic(&result->dePrettyName, FF_DE_PRETTY_PLASMA); + applyBetterWM(result, getenv("KDEWM")); + } + + else if ( + ffStrEqualsIgnCase(name, "GNOME") || + ffStrEqualsIgnCase(name, "ubuntu:GNOME") || + ffStrEqualsIgnCase(name, "ubuntu") || + ffStrEqualsIgnCase(name, "gnome-shell")) { + ffStrbufSetStatic(&result->deProcessName, "gnome-shell"); + const char* sessionMode = getenv("GNOME_SHELL_SESSION_MODE"); + if (sessionMode && ffStrEquals(sessionMode, "classic")) { + ffStrbufSetStatic(&result->dePrettyName, FF_DE_PRETTY_GNOME_CLASSIC); + } else { + ffStrbufSetStatic(&result->dePrettyName, FF_DE_PRETTY_GNOME); + } + } + + else if ( + ffStrEqualsIgnCase(name, "X-Cinnamon") || + ffStrEqualsIgnCase(name, "Cinnamon")) { + ffStrbufSetS(&result->deProcessName, "cinnamon"); + ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_CINNAMON); + } + + else if ( + ffStrEqualsIgnCase(name, "XFCE") || + ffStrEqualsIgnCase(name, "X-XFCE") || + ffStrEqualsIgnCase(name, "XFCE4") || + ffStrEqualsIgnCase(name, "X-XFCE4") || + ffStrEqualsIgnCase(name, "xfce4-session")) { + ffStrbufSetS(&result->deProcessName, "xfce4-session"); + ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_XFCE4); + } + + else if ( + ffStrEqualsIgnCase(name, "MATE") || + ffStrEqualsIgnCase(name, "X-MATE") || + ffStrEqualsIgnCase(name, "mate-session")) { + ffStrbufSetS(&result->deProcessName, "mate-session"); + ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_MATE); + } + + else if ( + ffStrEqualsIgnCase(name, "LXQt") || + ffStrEqualsIgnCase(name, "X-LXQt") || + ffStrEqualsIgnCase(name, "lxqt-session")) { + ffStrbufSetS(&result->deProcessName, "lxqt-session"); + ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_LXQT); + if (result->wmProcessName.length == 0) { + FF_STRBUF_AUTO_DESTROY wmProcessNameBuffer = ffStrbufCreate(); + ffParsePropFileConfig("lxqt/session.conf", "window_manager =", &wmProcessNameBuffer); + applyBetterWM(result, wmProcessNameBuffer.chars); + } + } + + else if ( + ffStrEqualsIgnCase(name, "Budgie") || + ffStrEqualsIgnCase(name, "X-Budgie") || + ffStrEqualsIgnCase(name, "budgie-desktop") || + ffStrEqualsIgnCase(name, "Budgie:GNOME")) { + ffStrbufSetS(&result->deProcessName, "budgie-desktop"); + ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_BUDGIE); + } + + else if ( + ffStrEqualsIgnCase(name, "dtsession")) { + ffStrbufSetS(&result->deProcessName, "dtsession"); + ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_CDE); + } + + else if ( + ffStrEqualsIgnCase(name, "ukui-session")) { + ffStrbufSetS(&result->deProcessName, "ukui-session"); + ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_UKUI); + } + + else if ( + ffStrStartsWithIgnCase(name, "Unity:Unity")) { + ffStrbufSetS(&result->deProcessName, "unity-session"); + ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_UNITY); + } + + else if (ffStrEqualsIgnCase(name, "Enlightenment")) { + ffStrbufSetS(&result->deProcessName, "enlightenment_start"); + ffStrbufSetS(&result->dePrettyName, FF_DE_PRETTY_ENLIGHTENMENT); + } +} + +static const char* getFromProcesses(FFDisplayServerResult* result) { + uint32_t userId = instance.state.platform.uid; + +#if __FreeBSD__ + #ifdef __DragonFly__ + #define ki_comm kp_comm + #endif + + int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_UID, (int) userId }; + size_t length = 0; + + if (sysctl(request, ARRAY_SIZE(request), NULL, &length, NULL, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, NULL) failed"; + } + + FF_AUTO_FREE struct kinfo_proc* procs = (struct kinfo_proc*) malloc(length); + if (sysctl(request, ARRAY_SIZE(request), procs, &length, NULL, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_UID}, procs) failed"; + } + + length /= sizeof(*procs); + + for (struct kinfo_proc* proc = procs; proc < procs + length; ++proc) { + if (result->dePrettyName.length == 0) { + applyPrettyNameIfDE(result, proc->ki_comm); + } + + if (result->wmPrettyName.length == 0) { + applyNameIfWM(result, proc->ki_comm); + } + + if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) { + break; + } + } +#elif __OpenBSD__ + kvm_t* kd = kvm_open(NULL, NULL, NULL, KVM_NO_FILES, NULL); + int count = 0; + const struct kinfo_proc* proc = kvm_getprocs(kd, KERN_PROC_UID, (int) userId, sizeof(*proc), &count); + if (proc) { + for (int i = 0; i < count; ++i) { + if (result->dePrettyName.length == 0) { + applyPrettyNameIfDE(result, proc[i].p_comm); + } + + if (result->wmPrettyName.length == 0) { + applyNameIfWM(result, proc[i].p_comm); + } + + if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) { + break; + } + } + } + kvm_close(kd); +#elif __sun + FF_AUTO_CLOSE_DIR DIR* procdir = opendir("/proc"); + if (procdir == NULL) { + return "opendir(\"/proc\") failed"; + } + + FF_STRBUF_AUTO_DESTROY procPath = ffStrbufCreateA(64); + ffStrbufAppendS(&procPath, "/proc/"); + + uint32_t procPathLength = procPath.length; + + struct dirent* dirent; + while ((dirent = readdir(procdir)) != NULL) { + if (!ffCharIsDigit(dirent->d_name[0])) { + continue; + } + + ffStrbufAppendS(&procPath, dirent->d_name); + ffStrbufAppendS(&procPath, "/psinfo"); + psinfo_t proc; + if (ffReadFileData(procPath.chars, sizeof(proc), &proc) == sizeof(proc)) { + ffStrbufSubstrBefore(&procPath, procPathLength); + + if (proc.pr_uid != userId) { + continue; + } + + if (result->dePrettyName.length == 0) { + applyPrettyNameIfDE(result, proc.pr_fname); + } + + if (result->wmPrettyName.length == 0) { + applyNameIfWM(result, proc.pr_fname); + } + + if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) { + break; + } + } + } +#elif __linux__ || __GNU__ + FF_AUTO_CLOSE_DIR DIR* procdir = opendir("/proc"); + if (procdir == NULL) { + return "opendir(\"/proc\") failed"; + } + + FF_STRBUF_AUTO_DESTROY procPath = ffStrbufCreateA(64); + ffStrbufAppendS(&procPath, "/proc/"); + + uint32_t procPathLength = procPath.length; + + FF_STRBUF_AUTO_DESTROY loginuid = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY processName = ffStrbufCreateA(256); // Some processes have large command lines (looking at you chrome) + + struct dirent* dirent; + while ((dirent = readdir(procdir)) != NULL) { + // Match only folders starting with a number (the pid folders) + if (dirent->d_type != DT_DIR || !ffCharIsDigit(dirent->d_name[0])) { + continue; + } + + ffStrbufAppendS(&procPath, dirent->d_name); + uint32_t procFolderPathLength = procPath.length; + + // Don't check for processes not owned by the current user. + ffStrbufAppendS(&procPath, "/loginuid"); + ffReadFileBuffer(procPath.chars, &loginuid); + if (ffStrbufToUInt(&loginuid, (uint64_t) -1) != userId) { + ffStrbufSubstrBefore(&procPath, procPathLength); + continue; + } + + ffStrbufSubstrBefore(&procPath, procFolderPathLength); + + // We check the cmdline for the process name, because it is not trimmed. + ffStrbufAppendS(&procPath, "/cmdline"); + ffReadFileBuffer(procPath.chars, &processName); + ffStrbufTrimRightSpace(&processName); + ffStrbufSubstrBeforeFirstC(&processName, '\0'); // Trim the arguments + ffStrbufSubstrAfterLastC(&processName, '/'); + + ffStrbufSubstrBefore(&procPath, procPathLength); + + if (result->dePrettyName.length == 0) { + applyPrettyNameIfDE(result, processName.chars); + } + + if (result->wmPrettyName.length == 0) { + applyNameIfWM(result, processName.chars); + } + + if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) { + break; + } + } +#elif __NetBSD__ + int request[] = { CTL_KERN, KERN_PROC2, KERN_PROC_UID, (int) userId, sizeof(struct kinfo_proc2), INT_MAX }; + + size_t size = 0; + if (sysctl(request, ARRAY_SIZE(request), NULL, &size, NULL, 0) != 0) { + return "sysctl(KERN_PROC_UID, NULL) failed"; + } + + FF_AUTO_FREE struct kinfo_proc2* procs = malloc(size); + + if (sysctl(request, ARRAY_SIZE(request), procs, &size, NULL, 0) != 0) { + return "sysctl(KERN_PROC_UID, procs) failed"; + } + + for (struct kinfo_proc2* proc = procs; proc < procs + (size / sizeof(struct kinfo_proc2)); proc++) { + if (result->dePrettyName.length == 0) { + applyPrettyNameIfDE(result, proc->p_comm); + } + + if (result->wmPrettyName.length == 0) { + applyNameIfWM(result, proc->p_comm); + } + + if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) { + break; + } + } +#endif + + return NULL; +} + +void ffdsDetectWMDE(FFDisplayServerResult* result) { +#if __ANDROID__ + if (ffStrbufIgnCaseEqualS(&result->wmProtocolName, FF_WM_PROTOCOL_SURFACEFLINGER)) { + return; // Only supported when connected to X11 + } +#endif + + const char* env = parseEnv(); + + if (result->wmProcessName.length > 0) { + // If we found the processName via display server, use it. + // This will set the pretty name if it is a known WM, otherwise the prettyName to the processName + applyPrettyNameIfWM(result, result->wmProcessName.chars); + if (result->wmPrettyName.length == 0) { + ffStrbufSet(&result->wmPrettyName, &result->wmProcessName); + } + } else { + // if env is a known WM, use it + applyNameIfWM(result, env); + } + + // Connecting to a display server only gives WM results, not DE results. + // If we find it in the environment, use that. + applyPrettyNameIfDE(result, env); + + // If WM was found by connection to the sever, and DE in the environment, we can return + // This way we never call getFromProcDir(), which has slow initialization time + if (result->dePrettyName.length > 0 && result->wmPrettyName.length > 0) { + return; + } + + // Get missing WM / DE from processes. + getFromProcesses(result); + + // Return if both wm and de are set, or if env doesn't contain anything + if ( + (result->wmPrettyName.length > 0 && result->dePrettyName.length > 0) || + !ffStrSet(env)) { + return; + } + + // If nothing is set, use env as WM + else if (result->wmPrettyName.length == 0 && result->dePrettyName.length == 0) { + ffStrbufSetS(&result->wmProcessName, env); + ffStrbufSetS(&result->wmPrettyName, env); + } + + // If only WM is not set, and DE doesn't equal env, use env as WM + else if ( + result->wmPrettyName.length == 0 && + ffStrbufIgnCaseCompS(&result->deProcessName, env) != 0 && + ffStrbufIgnCaseCompS(&result->dePrettyName, env) != 0) { + ffStrbufSetS(&result->wmProcessName, env); + ffStrbufSetS(&result->wmPrettyName, env); + } + + // If only DE is not set, and WM doesn't equal env, use env as DE + else if ( + result->dePrettyName.length == 0 && + ffStrbufIgnCaseCompS(&result->wmProcessName, env) != 0 && + ffStrbufIgnCaseCompS(&result->wmPrettyName, env) != 0) { + ffStrbufSetS(&result->deProcessName, env); + ffStrbufSetS(&result->dePrettyName, env); + } +} diff --git a/src/detection/displayserver/linux/xcb.c b/src/detection/displayserver/linux/xcb.c new file mode 100644 index 0000000..8620d71 --- /dev/null +++ b/src/detection/displayserver/linux/xcb.c @@ -0,0 +1,441 @@ +#include "displayserver_linux.h" + +#ifdef FF_HAVE_XCB_RANDR + + #include "common/library.h" + #include "common/properties.h" + #include "common/edidHelper.h" + #include "common/mallocHelper.h" + #include "common/strutil.h" + + #include <stdlib.h> + #include <string.h> + #include <xcb/randr.h> + #include <xcb/xcb.h> + +typedef struct XcbRandrData { + FF_LIBRARY_SYMBOL(xcb_randr_get_screen_resources_current) + FF_LIBRARY_SYMBOL(xcb_randr_get_screen_resources_current_reply) + FF_LIBRARY_SYMBOL(xcb_randr_get_screen_resources_current_modes_iterator) + FF_LIBRARY_SYMBOL(xcb_randr_mode_info_next) + FF_LIBRARY_SYMBOL(xcb_randr_get_monitors) + FF_LIBRARY_SYMBOL(xcb_randr_get_monitors_reply) + FF_LIBRARY_SYMBOL(xcb_randr_get_monitors_monitors_iterator) + FF_LIBRARY_SYMBOL(xcb_randr_monitor_info_next) + FF_LIBRARY_SYMBOL(xcb_randr_monitor_info_outputs_length) + FF_LIBRARY_SYMBOL(xcb_randr_monitor_info_outputs) + FF_LIBRARY_SYMBOL(xcb_randr_output_next) + FF_LIBRARY_SYMBOL(xcb_randr_get_output_info) + FF_LIBRARY_SYMBOL(xcb_randr_get_output_info_reply) + FF_LIBRARY_SYMBOL(xcb_randr_get_crtc_info) + FF_LIBRARY_SYMBOL(xcb_randr_get_crtc_info_reply) + FF_LIBRARY_SYMBOL(xcb_randr_get_output_property) + FF_LIBRARY_SYMBOL(xcb_randr_get_output_property_reply) + FF_LIBRARY_SYMBOL(xcb_randr_get_output_property_data) + FF_LIBRARY_SYMBOL(xcb_randr_get_output_property_data_length) + + FF_LIBRARY_SYMBOL(xcb_intern_atom) + FF_LIBRARY_SYMBOL(xcb_intern_atom_reply) + FF_LIBRARY_SYMBOL(xcb_get_property) + FF_LIBRARY_SYMBOL(xcb_get_property_reply) + FF_LIBRARY_SYMBOL(xcb_get_property_value) + FF_LIBRARY_SYMBOL(xcb_get_property_value_length) + FF_LIBRARY_SYMBOL(xcb_get_atom_name) + FF_LIBRARY_SYMBOL(xcb_get_atom_name_name) + FF_LIBRARY_SYMBOL(xcb_get_atom_name_name_length) + FF_LIBRARY_SYMBOL(xcb_get_atom_name_reply) + FF_LIBRARY_SYMBOL(xcb_get_setup) + FF_LIBRARY_SYMBOL(xcb_setup_vendor) + FF_LIBRARY_SYMBOL(xcb_setup_vendor_length) + + // init once + xcb_connection_t* connection; + FFDisplayServerResult* result; +} XcbRandrData; + +static void* xcbGetProperty(XcbRandrData* data, xcb_window_t window, const char* request) { + xcb_intern_atom_cookie_t requestAtomCookie = data->ffxcb_intern_atom(data->connection, true, (uint16_t) strlen(request), request); + FF_AUTO_FREE xcb_intern_atom_reply_t* requestAtomReply = data->ffxcb_intern_atom_reply(data->connection, requestAtomCookie, NULL); + if (requestAtomReply == NULL) { + return NULL; + } + + xcb_get_property_cookie_t propertyCookie = data->ffxcb_get_property(data->connection, false, window, requestAtomReply->atom, XCB_ATOM_ANY, 0, 8 * 1024); + FF_AUTO_FREE xcb_get_property_reply_t* propertyReply = data->ffxcb_get_property_reply(data->connection, propertyCookie, NULL); + if (propertyReply == NULL) { + return NULL; + } + + int length = data->ffxcb_get_property_value_length(propertyReply); + if (length <= 0) { + return NULL; + } + + // Why are xcb property strings not null terminated??? + void* replyValue = malloc((size_t) length + 1); + memcpy(replyValue, data->ffxcb_get_property_value(propertyReply), (size_t) length); + ((char*) replyValue)[length] = '\0'; + + return replyValue; +} + +static xcb_randr_get_output_property_reply_t* xcbRandrGetProperty(XcbRandrData* data, xcb_randr_output_t output, const char* name) { + xcb_intern_atom_cookie_t requestAtomCookie = data->ffxcb_intern_atom(data->connection, true, (uint16_t) strlen(name), name); + FF_AUTO_FREE xcb_intern_atom_reply_t* requestAtomReply = data->ffxcb_intern_atom_reply(data->connection, requestAtomCookie, NULL); + + if (requestAtomReply) { + xcb_randr_get_output_property_cookie_t outputPropertyCookie = data->ffxcb_randr_get_output_property(data->connection, output, requestAtomReply->atom, XCB_GET_PROPERTY_TYPE_ANY, 0, 100, false, false); + return data->ffxcb_randr_get_output_property_reply(data->connection, outputPropertyCookie, NULL); + } + return NULL; +} + +static void xcbDetectWMfromEWMH(XcbRandrData* data, xcb_window_t rootWindow, FFDisplayServerResult* result) { + if (result->wmProcessName.length > 0 || ffStrbufEqualS(&result->wmProtocolName, FF_WM_PROTOCOL_WAYLAND)) { + return; + } + + FF_AUTO_FREE xcb_window_t* wmWindow = (xcb_window_t*) xcbGetProperty(data, rootWindow, "_NET_SUPPORTING_WM_CHECK"); + if (wmWindow == NULL) { + return; + } + + FF_AUTO_FREE char* wmName = (char*) xcbGetProperty(data, *wmWindow, "WM_NAME"); + if (!ffStrSet(wmName)) { + wmName = (char*) xcbGetProperty(data, *wmWindow, "_NET_WM_NAME"); + } + + if (!ffStrSet(wmName)) { + return; + } + + ffStrbufSetS(&result->wmProcessName, wmName); +} + +static void xcbFetchServerVendor(XcbRandrData* data, FFDisplayServerResult* result) { + const xcb_setup_t* setup = data->ffxcb_get_setup(data->connection); + + int length = data->ffxcb_setup_vendor_length(setup); + if (length <= 0) { + return; + } + + FF_STRBUF_AUTO_DESTROY serverVendor = ffStrbufCreateNS((uint32_t) length, data->ffxcb_setup_vendor(setup)); + + if (!ffStrbufEqualS(&serverVendor, "The X.Org Foundation")) // Original + { + ffStrbufDestroy(&result->wmProtocolName); + ffStrbufInitMove(&result->wmProtocolName, &serverVendor); + } +} + +static bool xcbRandrHandleOutput(XcbRandrData* data, xcb_randr_output_t output, FFstrbuf* name, bool primary, FFDisplayType displayType, struct xcb_randr_get_screen_resources_current_reply_t* screenResources, uint8_t bitDepth, uint32_t dpi) { + xcb_randr_get_output_info_cookie_t outputInfoCookie = data->ffxcb_randr_get_output_info(data->connection, output, XCB_CURRENT_TIME); + FF_AUTO_FREE xcb_randr_get_output_info_reply_t* outputInfoReply = data->ffxcb_randr_get_output_info_reply(data->connection, outputInfoCookie, NULL); + if (outputInfoReply == NULL) { + return false; + } + + FF_AUTO_FREE xcb_randr_get_output_property_reply_t* edidReply = xcbRandrGetProperty(data, output, "EDID"); + uint8_t* edidData = NULL; + uint32_t edidLength = 0; + if (edidReply) { + int len = data->ffxcb_randr_get_output_property_data_length(edidReply); + if (len >= 128) { + edidData = data->ffxcb_randr_get_output_property_data(edidReply); + edidLength = (uint32_t) len; + } + } + + if (edidData) { + ffStrbufClear(name); + ffEdidGetName(edidData, name); + } + + bool randrEmulation = false; + FF_AUTO_FREE xcb_randr_get_output_property_reply_t* randrEmulationReply = xcbRandrGetProperty(data, output, "RANDR Emulation"); + if (randrEmulationReply) { + int len = data->ffxcb_randr_get_output_property_data_length(randrEmulationReply); + if (len >= 1) { + randrEmulation = !!data->ffxcb_randr_get_output_property_data(randrEmulationReply)[0]; + } + } + + xcb_randr_get_crtc_info_cookie_t crtcInfoCookie = data->ffxcb_randr_get_crtc_info(data->connection, outputInfoReply->crtc, XCB_CURRENT_TIME); + FF_AUTO_FREE xcb_randr_get_crtc_info_reply_t* crtcInfoReply = data->ffxcb_randr_get_crtc_info_reply(data->connection, crtcInfoCookie, NULL); + if (crtcInfoReply == NULL) { + return false; + } + + uint32_t rotation; + switch (crtcInfoReply->rotation) { + case XCB_RANDR_ROTATION_ROTATE_90: + rotation = 90; + break; + case XCB_RANDR_ROTATION_ROTATE_180: + rotation = 180; + break; + case XCB_RANDR_ROTATION_ROTATE_270: + rotation = 270; + break; + default: + rotation = 0; + break; + } + + xcb_randr_mode_info_t* currentMode = NULL; + xcb_randr_mode_info_t* preferredMode = NULL; + + if (screenResources) { + xcb_randr_mode_info_iterator_t modesIterator = data->ffxcb_randr_get_screen_resources_current_modes_iterator(screenResources); + + if (outputInfoReply->num_preferred > 0) { + preferredMode = modesIterator.data; + } + + while (modesIterator.rem > 0) { + if (modesIterator.data->id == crtcInfoReply->mode) { + currentMode = modesIterator.data; + break; + } + + data->ffxcb_randr_mode_info_next(&modesIterator); + } + } + + FFDisplayResult* item = ffdsAppendDisplay( + data->result, + (uint32_t) (currentMode ? currentMode->width : crtcInfoReply->width), + (uint32_t) (currentMode ? currentMode->height : crtcInfoReply->height), + currentMode ? (double) currentMode->dot_clock / (double) ((uint32_t) currentMode->htotal * currentMode->vtotal) : 0, + dpi, + preferredMode ? (uint32_t) preferredMode->width : 0, + preferredMode ? (uint32_t) preferredMode->height : 0, + preferredMode ? (double) preferredMode->dot_clock / (double) ((uint32_t) preferredMode->htotal * preferredMode->vtotal) : 0, + rotation, + name, + displayType, + primary, + 0, + (uint32_t) outputInfoReply->mm_width, + (uint32_t) outputInfoReply->mm_height, + randrEmulation + ? (currentMode ? "xcb-randr-emu-mode" : "xcb-randr-emu-crtc") + : (currentMode ? "xcb-randr-mode" : "xcb-randr-crtc") + + ); + if (item) { + if (edidData && edidLength >= 128) { + item->hdrStatus = ffEdidGetHdrCompatible(edidData, (uint32_t) edidLength) ? FF_DISPLAY_HDR_STATUS_SUPPORTED : FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + ffEdidGetSerialAndManufactureDate(edidData, &item->serial, &item->manufactureYear, &item->manufactureWeek); + } + item->bitDepth = bitDepth; + if ((rotation == 90 || rotation == 180) && !randrEmulation) { + // In XWayland mode, width / height has been swapped out of box + uint32_t tmp = item->width; + item->width = item->height; + item->height = tmp; + } + } + + return !!item; +} + +static bool xcbRandrHandleMonitor(XcbRandrData* data, xcb_randr_monitor_info_t* monitor, struct xcb_randr_get_screen_resources_current_reply_t* screenResources, uint8_t bitDepth, uint32_t dpi) { + // for some reasons, we have to construct this our self + xcb_randr_output_iterator_t outputIterator = { + .index = 0, + .data = data->ffxcb_randr_monitor_info_outputs(monitor), + .rem = data->ffxcb_randr_monitor_info_outputs_length(monitor) + }; + + FF_AUTO_FREE xcb_get_atom_name_reply_t* nameReply = data->ffxcb_get_atom_name_reply( + data->connection, + data->ffxcb_get_atom_name(data->connection, monitor->name), + NULL); + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateNS( + (uint32_t) data->ffxcb_get_atom_name_name_length(nameReply), + data->ffxcb_get_atom_name_name(nameReply)); + const FFDisplayType displayType = ffdsGetDisplayType(name.chars); + + bool foundOutput = false; + + while (outputIterator.rem > 0) { + if (xcbRandrHandleOutput(data, *outputIterator.data, &name, monitor->primary, displayType, screenResources, bitDepth, dpi)) { + foundOutput = true; + } + data->ffxcb_randr_output_next(&outputIterator); + } + + if (foundOutput) { + return true; + } + + FFDisplayResult* display = ffdsAppendDisplay( + data->result, + (uint32_t) monitor->width, + (uint32_t) monitor->height, + 0, + dpi, + 0, + 0, + 0, + 0, + &name, + displayType, + !!monitor->primary, + 0, + (uint32_t) monitor->width_in_millimeters, + (uint32_t) monitor->height_in_millimeters, + "xcb-randr-monitor"); + if (display) { + display->bitDepth = bitDepth; + } + return !!display; +} + +static bool xcbRandrHandleMonitors(XcbRandrData* data, xcb_screen_t* screen) { + xcb_randr_get_monitors_cookie_t monitorsCookie = data->ffxcb_randr_get_monitors(data->connection, screen->root, true); + FF_AUTO_FREE xcb_randr_get_monitors_reply_t* monitorsReply = data->ffxcb_randr_get_monitors_reply(data->connection, monitorsCookie, NULL); + if (monitorsReply == NULL) { + return false; + } + + // Init screen resources. They are used to iterate over all modes. xcbRandrHandleMode checks for " == NULL", to fail as late as possible. + xcb_randr_get_screen_resources_current_cookie_t screenResourcesCookie = data->ffxcb_randr_get_screen_resources_current(data->connection, screen->root); + FF_AUTO_FREE struct xcb_randr_get_screen_resources_current_reply_t* screenResources = data->ffxcb_randr_get_screen_resources_current_reply(data->connection, screenResourcesCookie, NULL); + + uint32_t dpi = 0; + FF_AUTO_FREE const char* resourceManager = xcbGetProperty(data, screen->root, "RESOURCE_MANAGER"); + if (resourceManager) { + FF_STRBUF_AUTO_DESTROY dpiStr = ffStrbufCreate(); + if (ffParsePropLines(resourceManager, "Xft.dpi:", &dpiStr)) { + dpi = (uint32_t) ffStrbufToUInt(&dpiStr, 96); + } + } + uint8_t bitDepth = (uint8_t) (screen->root_depth / 3); + + xcb_randr_monitor_info_iterator_t monitorInfoIterator = data->ffxcb_randr_get_monitors_monitors_iterator(monitorsReply); + + bool foundMonitor = false; + + while (monitorInfoIterator.rem > 0) { + if (xcbRandrHandleMonitor(data, monitorInfoIterator.data, screenResources, bitDepth, dpi)) { + foundMonitor = true; + } + data->ffxcb_randr_monitor_info_next(&monitorInfoIterator); + } + + return foundMonitor; +} + +static void xcbRandrHandleScreen(XcbRandrData* data, xcb_screen_t* screen) { + // With all the initialisation done, start the detection + if (xcbRandrHandleMonitors(data, screen)) { + return; + } + + // If detetction failed, fallback to screen = monitor, like in the libxcb.so implementation + ffdsAppendDisplay( + data->result, + (uint32_t) screen->width_in_pixels, + (uint32_t) screen->height_in_pixels, + 0, + 0, + 0, + 0, + 0, + 0, + NULL, + FF_DISPLAY_TYPE_UNKNOWN, + false, + (uint64_t) screen->root, + (uint32_t) screen->width_in_millimeters, + (uint32_t) screen->height_in_millimeters, + "xcb-randr-screen"); +} + +const char* ffdsConnectXcbRandr(FFDisplayServerResult* result) { + FF_LIBRARY_LOAD_MESSAGE(xcbRandr, "libxcb-randr" FF_LIBRARY_EXTENSION, 1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(xcbRandr, xcb_connect) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(xcbRandr, xcb_connection_has_error) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(xcbRandr, xcb_get_setup) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(xcbRandr, xcb_setup_roots_iterator) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(xcbRandr, xcb_screen_next) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(xcbRandr, xcb_disconnect) + + XcbRandrData data; + + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_intern_atom) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_intern_atom_reply) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_get_property) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_get_property_reply) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_get_property_value) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_get_property_value_length) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_get_atom_name) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_get_atom_name_name) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_get_atom_name_name_length) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_get_atom_name_reply) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_get_setup) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_setup_vendor) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_setup_vendor_length) + + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_screen_resources_current) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_screen_resources_current_reply) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_screen_resources_current_modes_iterator) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_mode_info_next) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_monitors) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_monitors_reply) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_monitors_monitors_iterator) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_monitor_info_next) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_monitor_info_outputs_length) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_monitor_info_outputs) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_output_next) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_output_info) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_output_info_reply) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_output_property) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_output_property_reply) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_output_property_data) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_output_property_data_length) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_crtc_info) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xcbRandr, data, xcb_randr_get_crtc_info_reply) + + data.connection = ffxcb_connect(NULL, NULL); + if (ffxcb_connection_has_error(data.connection) > 0) { + ffxcb_disconnect(data.connection); + return "xcb_connect() failed"; + } + + data.result = result; + + xcb_screen_iterator_t iterator = ffxcb_setup_roots_iterator(ffxcb_get_setup(data.connection)); + + if (iterator.rem > 0) { + xcbDetectWMfromEWMH(&data, iterator.data->root, result); + xcbFetchServerVendor(&data, result); + } + + while (iterator.rem > 0) { + xcbRandrHandleScreen(&data, iterator.data); + ffxcb_screen_next(&iterator); + } + + ffxcb_disconnect(data.connection); + + // If wayland hasn't set this, connection failed for it. So we are running only a X Server, not XWayland. + if (result->wmProtocolName.length == 0) { + ffStrbufSetS(&result->wmProtocolName, FF_WM_PROTOCOL_X11); + } + + return NULL; +} + +#else + +const char* ffdsConnectXcbRandr(FFDisplayServerResult* result) { + // Do nothing. There are other implementations coming + FF_UNUSED(result) + return "Fastfetch was compiled without libxcb-randr support"; +} + +#endif diff --git a/src/detection/displayserver/linux/xlib.c b/src/detection/displayserver/linux/xlib.c new file mode 100644 index 0000000..2beffcc --- /dev/null +++ b/src/detection/displayserver/linux/xlib.c @@ -0,0 +1,365 @@ +#include "displayserver_linux.h" + +#ifdef FF_HAVE_XRANDR + + #include "common/library.h" + #include "common/properties.h" + #include "common/edidHelper.h" + #include "common/strutil.h" + + #include <X11/extensions/Xrandr.h> + #include <X11/Xlib.h> + +typedef struct XrandrData { + FF_LIBRARY_SYMBOL(XInternAtom) + FF_LIBRARY_SYMBOL(XGetAtomName) + FF_LIBRARY_SYMBOL(XGetWindowProperty) + FF_LIBRARY_SYMBOL(XServerVendor) + FF_LIBRARY_SYMBOL(XFree) + FF_LIBRARY_SYMBOL(XRRGetMonitors) + FF_LIBRARY_SYMBOL(XRRGetScreenResourcesCurrent) + FF_LIBRARY_SYMBOL(XRRGetOutputInfo) + FF_LIBRARY_SYMBOL(XRRGetOutputProperty) + FF_LIBRARY_SYMBOL(XRRGetCrtcInfo) + FF_LIBRARY_SYMBOL(XRRFreeCrtcInfo) + FF_LIBRARY_SYMBOL(XRRFreeOutputInfo) + FF_LIBRARY_SYMBOL(XRRFreeScreenResources) + FF_LIBRARY_SYMBOL(XRRFreeMonitors) + + // Init once + Display* display; + FFDisplayServerResult* result; +} XrandrData; + +static unsigned char* x11GetProperty(XrandrData* data, Display* display, Window window, const char* request) { + Atom requestAtom = data->ffXInternAtom(display, request, False); + if (requestAtom == None) { + return NULL; + } + + Atom actualType; + unsigned long unused; + unsigned char* result = NULL; + + if (data->ffXGetWindowProperty(display, window, requestAtom, 0, 64, False, AnyPropertyType, &actualType, (int*) &unused, &unused, &unused, &result) != Success) { + return NULL; + } + + return result; +} + +static uint8_t* xrandrGetProperty(XrandrData* data, RROutput output, const char* name, uint32_t* bufSize) { + unsigned long size = 0; + uint8_t* result = NULL; + Atom atomEdid = data->ffXInternAtom(data->display, name, true); + if (atomEdid != None) { + int actual_format = 0; + unsigned long bytes_after = 0; + Atom actual_type = None; + if (data->ffXRRGetOutputProperty(data->display, output, atomEdid, 0, 100, false, false, AnyPropertyType, &actual_type, &actual_format, &size, &bytes_after, &result) == Success) { + if (size == 0) { + data->ffXFree(result); + } else { + if (bufSize) { + *bufSize = (uint32_t) size; + } + return result; + } + } + } + + return NULL; +} + +static void x11DetectWMFromEWMH(XrandrData* data, FFDisplayServerResult* result) { + if (result->wmProcessName.length > 0 || ffStrbufEqualS(&result->wmProtocolName, FF_WM_PROTOCOL_WAYLAND)) { + return; + } + + Window* wmWindow = (Window*) x11GetProperty(data, data->display, DefaultRootWindow(data->display), "_NET_SUPPORTING_WM_CHECK"); + if (wmWindow == NULL) { + return; + } + + char* wmName = (char*) x11GetProperty(data, data->display, *wmWindow, "WM_NAME"); + if (!ffStrSet(wmName)) { + wmName = (char*) x11GetProperty(data, data->display, *wmWindow, "_NET_WM_NAME"); + } + + if (ffStrSet(wmName)) { + ffStrbufSetS(&result->wmProcessName, wmName); + } + + data->ffXFree(wmName); + data->ffXFree(wmWindow); +} + +static void x11FetchServerVendor(XrandrData* data, FFDisplayServerResult* result) { + const char* serverVendor = data->ffXServerVendor(data->display); + if (serverVendor && !ffStrEquals(serverVendor, "The X.Org Foundation")) { + ffStrbufSetS(&result->wmProtocolName, serverVendor); + } +} + +static bool xrandrHandleCrtc(XrandrData* data, XRROutputInfo* output, FFstrbuf* name, bool primary, FFDisplayType displayType, uint8_t* edidData, uint32_t edidLength, XRRScreenResources* screenResources, uint8_t bitDepth, uint32_t dpi, bool randrEmulation) { + // We do the check here, because we want the best fallback display if this call failed + if (screenResources == NULL) { + return false; + } + + XRRCrtcInfo* crtcInfo = data->ffXRRGetCrtcInfo(data->display, screenResources, output->crtc); + if (crtcInfo == NULL) { + return false; + } + + uint32_t rotation; + switch (crtcInfo->rotation) { + case RR_Rotate_90: + rotation = 90; + break; + case RR_Rotate_180: + rotation = 180; + break; + case RR_Rotate_270: + rotation = 270; + break; + default: + rotation = 0; + break; + } + + XRRModeInfo* currentMode = NULL; + for (int i = 0; i < screenResources->nmode; i++) { + if (screenResources->modes[i].id == crtcInfo->mode) { + currentMode = &screenResources->modes[i]; + break; + } + } + + XRRModeInfo* preferredMode = output->npreferred > 0 ? &screenResources->modes[0] : NULL; + + FFDisplayResult* item = ffdsAppendDisplay( + data->result, + (uint32_t) (currentMode ? currentMode->width : crtcInfo->width), + (uint32_t) (currentMode ? currentMode->height : crtcInfo->height), + currentMode ? (double) currentMode->dotClock / (double) ((uint32_t) currentMode->hTotal * currentMode->vTotal) : 0, + dpi, + preferredMode ? (uint32_t) preferredMode->width : 0, + preferredMode ? (uint32_t) preferredMode->height : 0, + preferredMode ? (double) preferredMode->dotClock / (double) ((uint32_t) preferredMode->hTotal * preferredMode->vTotal) : 0, + rotation, + name, + displayType, + primary, + 0, + (uint32_t) output->mm_width, + (uint32_t) output->mm_height, + randrEmulation + ? (currentMode ? "xlib-randr-emu-mode" : "xlib-randr-emu-crtc") + : (currentMode ? "xlib-randr-mode" : "xlib-randr-crtc")); + + if (item) { + if (edidLength) { + item->hdrStatus = ffEdidGetHdrCompatible(edidData, edidLength) ? FF_DISPLAY_HDR_STATUS_SUPPORTED : FF_DISPLAY_HDR_STATUS_UNSUPPORTED; + ffEdidGetSerialAndManufactureDate(edidData, &item->serial, &item->manufactureYear, &item->manufactureWeek); + } + item->bitDepth = bitDepth; + if ((rotation == 90 || rotation == 180) && !randrEmulation) { + // In XWayland mode, width / height has been swapped out of box + uint32_t tmp = item->width; + item->width = item->height; + item->height = tmp; + } + } + + data->ffXRRFreeCrtcInfo(crtcInfo); + return !!item; +} + +static bool xrandrHandleOutput(XrandrData* data, RROutput output, FFstrbuf* name, bool primary, FFDisplayType displayType, XRRScreenResources* screenResources, uint8_t bitDepth, uint32_t dpi) { + XRROutputInfo* outputInfo = data->ffXRRGetOutputInfo(data->display, screenResources, output); + if (outputInfo == NULL) { + return false; + } + + uint32_t edidLength = 0; + uint8_t* edidData = xrandrGetProperty(data, output, RR_PROPERTY_RANDR_EDID, &edidLength); + + if (edidLength >= 128) { + ffStrbufClear(name); + ffEdidGetName(edidData, name); + } else { + edidLength = 0; + } + + uint8_t* randrEmulation = xrandrGetProperty(data, output, "RANDR Emulation", NULL); + + bool res = xrandrHandleCrtc(data, outputInfo, name, primary, displayType, edidData, edidLength, screenResources, bitDepth, dpi, randrEmulation ? !!randrEmulation[0] : false); + + if (edidData) { + data->ffXFree(edidData); + } + if (randrEmulation) { + data->ffXFree(randrEmulation); + } + data->ffXRRFreeOutputInfo(outputInfo); + + return res; +} + +static bool xrandrHandleMonitor(XrandrData* data, XRRMonitorInfo* monitorInfo, XRRScreenResources* screenResources, uint8_t bitDepth, uint32_t dpi) { + bool foundOutput = false; + char* xname = data->ffXGetAtomName(data->display, monitorInfo->name); + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateS(xname); + data->ffXFree(xname); + FFDisplayType displayType = ffdsGetDisplayType(name.chars); + for (int i = 0; i < monitorInfo->noutput; i++) { + if (xrandrHandleOutput(data, monitorInfo->outputs[i], &name, monitorInfo->primary, displayType, screenResources, bitDepth, dpi)) { + foundOutput = true; + } + } + + if (foundOutput) { + return true; + } + + FFDisplayResult* display = ffdsAppendDisplay( + data->result, + (uint32_t) monitorInfo->width, + (uint32_t) monitorInfo->height, + 0, + dpi, + 0, + 0, + 0, + 0, + &name, + displayType, + !!monitorInfo->primary, + 0, + (uint32_t) monitorInfo->mwidth, + (uint32_t) monitorInfo->mheight, + "xlib-randr-monitor"); + if (display) { + display->bitDepth = bitDepth; + } + return !!display; +} + +static bool xrandrHandleMonitors(XrandrData* data, Screen* screen) { + int numberOfMonitors; + XRRMonitorInfo* monitorInfos = data->ffXRRGetMonitors(data->display, RootWindowOfScreen(screen), True, &numberOfMonitors); + if (monitorInfos == NULL) { + return false; + } + + XRRScreenResources* screenResources = data->ffXRRGetScreenResourcesCurrent(data->display, RootWindowOfScreen(screen)); + + uint32_t dpi = 1; + char* resourceManager = (char*) x11GetProperty(data, data->display, screen->root, "RESOURCE_MANAGER"); + if (resourceManager) { + FF_STRBUF_AUTO_DESTROY dpiStr = ffStrbufCreate(); + if (ffParsePropLines(resourceManager, "Xft.dpi:", &dpiStr)) { + dpi = (uint32_t) ffStrbufToUInt(&dpiStr, 96); + } + data->ffXFree(resourceManager); + } + uint8_t bitDepth = (uint8_t) (screen->root_depth / 3); + + bool foundAMonitor = false; + + for (int i = 0; i < numberOfMonitors; i++) { + if (xrandrHandleMonitor(data, &monitorInfos[i], screenResources, bitDepth, dpi)) { + foundAMonitor = true; + } + } + + data->ffXRRFreeMonitors(monitorInfos); + data->ffXRRFreeScreenResources(screenResources); + + return foundAMonitor; +} + +static void xrandrHandleScreen(XrandrData* data, Screen* screen) { + if (xrandrHandleMonitors(data, screen)) { + return; + } + + // Fallback to screen + ffdsAppendDisplay( + data->result, + (uint32_t) WidthOfScreen(screen), + (uint32_t) HeightOfScreen(screen), + 0, + 0, + 0, + 0, + 0, + 0, + NULL, + FF_DISPLAY_TYPE_UNKNOWN, + false, + RootWindowOfScreen(screen), + (uint32_t) WidthMMOfScreen(screen), + (uint32_t) HeightMMOfScreen(screen), + "xlib-randr-screen"); +} + +const char* ffdsConnectXrandr(FFDisplayServerResult* result) { + FF_LIBRARY_LOAD_MESSAGE(xrandr, "libXrandr" FF_LIBRARY_EXTENSION, 3) + + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(xrandr, XOpenDisplay) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(xrandr, XCloseDisplay) + + XrandrData data; + + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XInternAtom); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XGetAtomName); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XGetWindowProperty); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XServerVendor); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XFree); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XRRGetMonitors); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XRRGetScreenResourcesCurrent); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XRRGetOutputInfo); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XRRGetOutputProperty); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XRRGetCrtcInfo); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XRRFreeCrtcInfo); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XRRFreeOutputInfo); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XRRFreeScreenResources); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(xrandr, data, XRRFreeMonitors); + + data.display = ffXOpenDisplay(NULL); + if (data.display == NULL) { + return "XOpenDisplay() failed"; + } + + if (ScreenCount(data.display) > 0) { + x11DetectWMFromEWMH(&data, result); + x11FetchServerVendor(&data, result); + } + + data.result = result; + + for (int i = 0; i < ScreenCount(data.display); i++) { + xrandrHandleScreen(&data, ScreenOfDisplay(data.display, i)); + } + + ffXCloseDisplay(data.display); + + // If wayland hasn't set this, connection failed for it. So we are running only a X Server, not XWayland. + if (result->wmProtocolName.length == 0) { + ffStrbufSetS(&result->wmProtocolName, FF_WM_PROTOCOL_X11); + } + + return NULL; +} + +#else + +const char* ffdsConnectXrandr(FFDisplayServerResult* result) { + // Do nothing here. There are more x11 implementations to come. + FF_UNUSED(result); + return "Fastfetch was compiled without libXrandr support"; +} + +#endif // FF_HAVE_XRANDR diff --git a/src/detection/dns/dns.h b/src/detection/dns/dns.h new file mode 100644 index 0000000..3653f62 --- /dev/null +++ b/src/detection/dns/dns.h @@ -0,0 +1,6 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/dns/option.h" + +const char* ffDetectDNS(FFDNSOptions* options, FFlist* results /* list of FFstrbuf */); diff --git a/src/detection/dns/dns_apple.c b/src/detection/dns/dns_apple.c new file mode 100644 index 0000000..e490ffe --- /dev/null +++ b/src/detection/dns/dns_apple.c @@ -0,0 +1,106 @@ +#include "detection/dns/dns.h" + +#include "common/io.h" +#include "common/mallocHelper.h" +#include "common/strutil.h" +#include "common/apple/cf_helpers.h" +#include "common/debug.h" + +#include <SystemConfiguration/SystemConfiguration.h> + +static const char* detectDnsFromConf(const char* path, FFDNSOptions* options, FFlist* results) { + FF_DEBUG("Attempting to read DNS config from %s", path); + + FF_AUTO_CLOSE_FILE FILE* file = fopen(path, "r"); + if (!file) { + FF_DEBUG("Failed to open %s: %s", path, strerror(errno)); + return "fopen(path, r) failed"; + } + + if (results->length > 0) { + FF_DEBUG("Clearing existing DNS entries (%u entries)", results->length); + FF_LIST_FOR_EACH (FFstrbuf, item, *results) { + ffStrbufDestroy(item); + } + ffListClear(results); + } + + FF_AUTO_FREE char* line = NULL; + size_t len = 0; + + while (getline(&line, &len, file) != -1) { + if (ffStrStartsWith(line, "nameserver")) { + char* nameserver = line + strlen("nameserver"); + while (*nameserver == ' ' || *nameserver == '\t') { + nameserver++; + } + if (*nameserver == '\0') { + continue; + } + + char* comment = strchr(nameserver, '#'); + if (comment) { + *comment = '\0'; + } + + if ((ffStrContainsC(nameserver, ':') && !(options->showType & FF_DNS_TYPE_IPV6_BIT)) || + (ffStrContainsC(nameserver, '.') && !(options->showType & FF_DNS_TYPE_IPV4_BIT))) { + continue; + } + + FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results); + ffStrbufInitS(item, nameserver); + ffStrbufTrimRightSpace(item); + FF_DEBUG("Found DNS server: %s", item->chars); + } + } + + FF_DEBUG("Found %u DNS servers in %s", results->length, path); + return NULL; +} + +const char* ffDetectDNS(FFDNSOptions* options, FFlist* results) { + // Handle macOS-specific DNS configurations + FF_DEBUG("Using SystemConfiguration framework for macOS"); + + // Create a reference to the dynamic store + FF_CFTYPE_AUTO_RELEASE SCDynamicStoreRef store = SCDynamicStoreCreate(NULL, CFSTR("fastfetch"), NULL, NULL); + if (store) { + // Get the network global IPv4 and IPv6 configuration + FF_CFTYPE_AUTO_RELEASE CFStringRef key = SCDynamicStoreKeyCreateNetworkGlobalEntity(NULL, kSCDynamicStoreDomainState, kSCEntNetDNS); + if (key) { + FF_CFTYPE_AUTO_RELEASE CFDictionaryRef dict = SCDynamicStoreCopyValue(store, key); + if (dict) { + // Get the DNS server addresses array + CFArrayRef dnsServers = CFDictionaryGetValue(dict, kSCPropNetDNSServerAddresses); + + if (dnsServers) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + for (CFIndex i = 0; i < CFArrayGetCount(dnsServers); i++) { + if (ffCfStrGetString(CFArrayGetValueAtIndex(dnsServers, i), &buffer) == NULL) { + // Check if the address matches our filter + if ((ffStrbufContainC(&buffer, ':') && !(options->showType & FF_DNS_TYPE_IPV6_BIT)) || + (ffStrbufContainC(&buffer, '.') && !(options->showType & FF_DNS_TYPE_IPV4_BIT))) { + continue; + } + + // Add to results + FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results); + ffStrbufInitMove(item, &buffer); + FF_DEBUG("Found DNS server on macOS: %s", item->chars); + } + } + } + } + } + } + + // If we didn't find any servers, try resolv.conf as fallback + if (results->length > 0) { + return NULL; + } + + FF_DEBUG("No DNS servers found via SystemConfiguration, trying resolv.conf"); + // Try standard resolv.conf location on macOS as a fallback + return detectDnsFromConf("/var/run/resolv.conf", options, results); +} diff --git a/src/detection/dns/dns_linux.c b/src/detection/dns/dns_linux.c new file mode 100644 index 0000000..41e008c --- /dev/null +++ b/src/detection/dns/dns_linux.c @@ -0,0 +1,143 @@ +#include "detection/dns/dns.h" + +#include "common/io.h" +#include "common/mallocHelper.h" +#include "common/strutil.h" +#include "common/debug.h" + +#ifdef __HAIKU__ + #define RESOLV_CONF "/system/settings/network/resolv.conf" +#else + #define RESOLV_CONF "/etc/resolv.conf" +#endif + +static const char* detectDnsFromConf(const char* path, FFDNSOptions* options, FFlist* results) { + FF_DEBUG("Attempting to read DNS config from %s", path); + + FF_AUTO_CLOSE_FILE FILE* file = fopen(path, "r"); + if (!file) { + FF_DEBUG("Failed to open %s: %s", path, strerror(errno)); + return "fopen(path, r) failed"; + } + + if (results->length > 0) { + FF_DEBUG("Clearing existing DNS entries (%u entries)", results->length); + FF_LIST_FOR_EACH (FFstrbuf, item, *results) { + ffStrbufDestroy(item); + } + ffListClear(results); + } + + FF_AUTO_FREE char* line = NULL; + size_t len = 0; + + while (getline(&line, &len, file) != -1) { + if (ffStrStartsWith(line, "nameserver")) { + char* nameserver = line + strlen("nameserver"); + while (*nameserver == ' ' || *nameserver == '\t') { + nameserver++; + } + if (*nameserver == '\0') { + continue; + } + + char* comment = strchr(nameserver, '#'); + if (comment) { + *comment = '\0'; + } + + if ((ffStrContainsC(nameserver, ':') && !(options->showType & FF_DNS_TYPE_IPV6_BIT)) || + (ffStrContainsC(nameserver, '.') && !(options->showType & FF_DNS_TYPE_IPV4_BIT))) { + continue; + } + + FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results); + ffStrbufInitS(item, nameserver); + ffStrbufTrimRightSpace(item); + FF_DEBUG("Found DNS server: %s", item->chars); + } + } + + FF_DEBUG("Found %u DNS servers in %s", results->length, path); + return NULL; +} + +const char* ffDetectDNS(FFDNSOptions* options, FFlist* results) { + FF_DEBUG("Starting DNS detection"); + + const char* error = detectDnsFromConf(FASTFETCH_TARGET_DIR_ROOT RESOLV_CONF, options, results); + if (error != NULL) { + FF_DEBUG("Error detecting DNS: %s", error); + return error; + } + +#if __linux__ && !__ANDROID__ + // Handle different DNS management services + if (results->length == 1) { + const FFstrbuf* firstEntry = FF_LIST_FIRST(FFstrbuf, *results); + + if (ffStrbufEqualS(firstEntry, "127.0.0.53")) { + FF_DEBUG("Detected systemd-resolved (127.0.0.53), checking actual DNS servers"); + // Managed by systemd-resolved + if (detectDnsFromConf("/run/systemd/resolve/resolv.conf", options, results) == NULL) { + return NULL; + } + } else if (ffStrbufEqualS(firstEntry, "127.0.0.1")) { + FF_DEBUG("Detected possible NetworkManager (127.0.0.1), checking actual DNS servers"); + // Managed by NetworkManager + if (detectDnsFromConf("/var/run/NetworkManager/resolv.conf", options, results) == NULL) { + return NULL; + } + } + } + + // Check other possible DNS configuration files + if (results->length == 0) { + FF_DEBUG("No DNS servers found, trying alternative config files"); + + // Try resolvconf + FF_DEBUG("Trying resolvconf configuration"); + if (detectDnsFromConf(FASTFETCH_TARGET_DIR_ROOT "/run/resolvconf/resolv.conf", options, results) == NULL && results->length > 0) { + return NULL; + } + + // Try dnsmasq + FF_DEBUG("Trying dnsmasq configuration"); + if (detectDnsFromConf(FASTFETCH_TARGET_DIR_ROOT "/var/run/dnsmasq/resolv.conf", options, results) == NULL && results->length > 0) { + return NULL; + } + + // Try openresolv + FF_DEBUG("Trying openresolv configuration"); + if (detectDnsFromConf(FASTFETCH_TARGET_DIR_ROOT "/etc/resolv.conf.openresolv", options, results) == NULL && results->length > 0) { + return NULL; + } + } +#elif defined(__FreeBSD__) || defined(__DragonFly__) || defined(__NetBSD__) || defined(__OpenBSD__) + // Handle BSD-specific DNS configurations + if (results->length == 0) { + FF_DEBUG("No DNS servers found, trying BSD-specific config files"); + + // FreeBSD and other BSDs may use resolvconf service + FF_DEBUG("Trying BSD resolvconf configuration"); + if (detectDnsFromConf(FASTFETCH_TARGET_DIR_ROOT "/var/run/resolvconf/resolv.conf", options, results) == NULL && results->length > 0) { + return NULL; + } + + // Some BSDs store DNS configuration here + FF_DEBUG("Trying BSD nameserver configuration"); + if (detectDnsFromConf(FASTFETCH_TARGET_DIR_ROOT "/var/run/nameserver", options, results) == NULL && results->length > 0) { + return NULL; + } + + // Try common BSD paths + FF_DEBUG("Trying BSD common paths"); + if (detectDnsFromConf(FASTFETCH_TARGET_DIR_ROOT "/etc/nameserver", options, results) == NULL && results->length > 0) { + return NULL; + } + } +#endif + + FF_DEBUG("DNS detection completed with %u servers found", results->length); + return NULL; +} diff --git a/src/detection/dns/dns_windows.c b/src/detection/dns/dns_windows.c new file mode 100644 index 0000000..b28eefb --- /dev/null +++ b/src/detection/dns/dns_windows.c @@ -0,0 +1,63 @@ +#include "detection/dns/dns.h" +#include "common/netif.h" +#include "common/mallocHelper.h" + +#include <ws2tcpip.h> +#include <iphlpapi.h> + +const char* ffDetectDNS(FFDNSOptions* options, FFlist* results) { + IP_ADAPTER_ADDRESSES* FF_AUTO_FREE adapter_addresses = NULL; + + // Multiple attempts in case interfaces change while + // we are in the middle of querying them. + DWORD adapter_addresses_buffer_size = 0; + for (int attempts = 0;; ++attempts) { + if (adapter_addresses_buffer_size) { + adapter_addresses = (IP_ADAPTER_ADDRESSES*) realloc(adapter_addresses, adapter_addresses_buffer_size); + assert(adapter_addresses); + } + + DWORD error = GetAdaptersAddresses( + options->showType & FF_DNS_TYPE_IPV4_BIT + ? options->showType & FF_DNS_TYPE_IPV6_BIT ? AF_UNSPEC : AF_INET + : AF_INET6, + GAA_FLAG_SKIP_UNICAST | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_FRIENDLY_NAME, + NULL, + adapter_addresses, + &adapter_addresses_buffer_size); + + if (error == ERROR_SUCCESS) { + break; + } else if (ERROR_BUFFER_OVERFLOW == error && attempts < 4) { + continue; + } else { + return "GetAdaptersAddresses() failed"; + } + } + + uint32_t defaultRouteIfIndex = ffNetifGetDefaultRouteV4()->ifIndex; + // Iterate through all of the adapters + for (IP_ADAPTER_ADDRESSES* adapter = adapter_addresses; adapter; adapter = adapter->Next) { + if (adapter->IfIndex != defaultRouteIfIndex) { + continue; + } + if (adapter->OperStatus != IfOperStatusUp) { + continue; + } + + for (IP_ADAPTER_DNS_SERVER_ADDRESS_XP* ifa = adapter->FirstDnsServerAddress; ifa; ifa = ifa->Next) { + FFstrbuf* item = FF_LIST_ADD(FFstrbuf, *results); + if (ifa->Address.lpSockaddr->sa_family == AF_INET) { + SOCKADDR_IN* ipv4 = (SOCKADDR_IN*) ifa->Address.lpSockaddr; + ffStrbufInitA(item, INET_ADDRSTRLEN); + item->length = (uint32_t) (RtlIpv4AddressToStringA(&ipv4->sin_addr, item->chars) - item->chars); + } else if (ifa->Address.lpSockaddr->sa_family == AF_INET6) { + SOCKADDR_IN6* ipv6 = (SOCKADDR_IN6*) ifa->Address.lpSockaddr; + ffStrbufInitA(item, INET6_ADDRSTRLEN); + item->length = (uint32_t) (RtlIpv6AddressToStringA(&ipv6->sin6_addr, item->chars) - item->chars); + } + } + break; + } + return NULL; +} diff --git a/src/detection/editor/editor.c b/src/detection/editor/editor.c new file mode 100644 index 0000000..b84f279 --- /dev/null +++ b/src/detection/editor/editor.c @@ -0,0 +1,160 @@ +#include "editor.h" +#include "common/processing.h" +#include "common/library.h" +#include "common/strutil.h" +#include "common/path.h" +#include "common/binary.h" + +#include <stdlib.h> + +static bool extractNvimVersionFromBinary(const char* str, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffStrStartsWith(str, "NVIM v")) { + return true; + } + ffStrbufSetS((FFstrbuf*) userdata, str + strlen("NVIM v")); + return false; +} + +static bool extractVimVersionFromBinary(const char* str, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffStrStartsWith(str, "VIM - Vi IMproved ")) { + return true; + } + ffStrbufSetS((FFstrbuf*) userdata, str + strlen("VIM - Vi IMproved ")); + ffStrbufSubstrBeforeFirstC(userdata, ' '); + return false; +} + +static bool extractNanoVersionFromBinary(const char* str, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffStrStartsWith(str, "GNU nano ")) { + return true; + } + ffStrbufSetS((FFstrbuf*) userdata, str + strlen("GNU nano ")); + return false; +} + +const char* ffDetectEditor(FFEditorResult* result) { + ffStrbufSetS(&result->name, getenv("VISUAL")); + if (result->name.length) { + result->type = "Visual"; + } else { + ffStrbufSetS(&result->name, getenv("EDITOR")); + if (result->name.length) { + result->type = "Editor"; + } else { + return "$VISUAL or $EDITOR not set"; + } + } + + if (ffIsAbsolutePath(result->name.chars)) { + ffStrbufSet(&result->path, &result->name); + } else { + const char* error = ffFindExecutableInPath(result->name.chars, &result->path); + if (error) { + return NULL; + } + } + + { + char buf[PATH_MAX + 1]; + if (!realpath(result->path.chars, buf)) { + return NULL; + } + + // WIN32: Should we handle scoop shim exe here? + +#ifdef __linux__ + if (!ffStrEndsWith(buf, "/snap")) +#endif + ffStrbufSetS(&result->path, buf); + } + + { + uint32_t index = ffStrbufLastIndexC(&result->path, +#ifndef _WIN32 + '/' +#else + '\\' +#endif + ); + if (index == result->path.length) { + return NULL; + } + ffStrbufSetS(&result->exe, &result->path.chars[index + 1]); + if (!result->exe.length) { + return NULL; + } + +#ifdef _WIN32 + if (ffStrbufEndsWithS(&result->exe, ".exe")) { + ffStrbufSubstrBefore(&result->exe, result->exe.length - 4); + } +#endif + } + + if (!instance.config.general.detectVersion) { + return NULL; + } + + if (ffStrbufEqualS(&result->exe, "nvim")) { + ffBinaryExtractStrings(result->path.chars, extractNvimVersionFromBinary, &result->version, (uint32_t) strlen("NVIM v0.0.0")); + } else if (ffStrbufEqualS(&result->exe, "vim") || ffStrbufStartsWithS(&result->exe, "vim.")) { + ffBinaryExtractStrings(result->path.chars, extractVimVersionFromBinary, &result->version, (uint32_t) strlen("VIM - Vi IMproved 0.0")); + } else if (ffStrbufEqualS(&result->exe, "nano")) { + ffBinaryExtractStrings(result->path.chars, extractNanoVersionFromBinary, &result->version, (uint32_t) strlen("GNU nano 0.0")); + } + + if (result->version.length > 0) { + return NULL; + } + + const char* param = NULL; + if ( + ffStrbufEqualS(&result->exe, "nano") || + ffStrbufEqualS(&result->exe, "vim") || + ffStrbufStartsWithS(&result->exe, "vim.") || // vim.basic/vim.tiny + ffStrbufEqualS(&result->exe, "nvim") || + ffStrbufEqualS(&result->exe, "micro") || + ffStrbufEqualS(&result->exe, "emacs") || + ffStrbufStartsWithS(&result->exe, "emacs-") || // emacs-29.3 + ffStrbufEqualS(&result->exe, "hx") || + ffStrbufEqualS(&result->exe, "code") || + ffStrbufEqualS(&result->exe, "pluma") || + ffStrbufEqualS(&result->exe, "sublime_text") || + ffStrbufEqualS(&result->exe, "zeditor")) { + param = "--version"; + } else if ( + ffStrbufEqualS(&result->exe, "kak") || + ffStrbufEqualS(&result->exe, "pico")) { + param = "-version"; + } else if ( + ffStrbufEqualS(&result->exe, "ne")) { + param = "-h"; + } else { + return NULL; + } + + ffProcessAppendStdOut(&result->version, (char* const[]) { + result->path.chars, + (char*) param, + NULL, + }); + + if (result->version.length == 0) { + return NULL; + } + + ffStrbufSubstrBeforeFirstC(&result->version, '\n'); + const char* versionStart = strpbrk(result->version.chars, "0123456789"); + if (versionStart != NULL) { + const char* versionEnd = strpbrk(versionStart, " \t\v\f\r"); + if (versionEnd != NULL) { + ffStrbufSubstrBefore(&result->version, (uint32_t) (versionEnd - result->version.chars)); + } + + if (versionStart != result->version.chars) { + ffStrbufSubstrAfter(&result->version, (uint32_t) (versionStart - result->version.chars - 1)); + } + } + + return NULL; +} diff --git a/src/detection/editor/editor.h b/src/detection/editor/editor.h new file mode 100644 index 0000000..ae315c0 --- /dev/null +++ b/src/detection/editor/editor.h @@ -0,0 +1,14 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/editor/option.h" + +typedef struct FFEditorResult { + const char* type; + FFstrbuf name; + FFstrbuf exe; + FFstrbuf path; + FFstrbuf version; +} FFEditorResult; + +const char* ffDetectEditor(FFEditorResult* result); diff --git a/src/detection/font/font.c b/src/detection/font/font.c new file mode 100644 index 0000000..c651f29 --- /dev/null +++ b/src/detection/font/font.c @@ -0,0 +1,19 @@ +#include "font.h" + +const char* ffDetectFontImpl(FFFontResult* font); + +const char* ffDetectFont(FFFontResult* font) { + const char* error = ffDetectFontImpl(font); + + if (error) { + return error; + } + + for (uint32_t i = 0; i < FF_DETECT_FONT_NUM_FONTS; ++i) { + if (font->fonts[i].length > 0) { + return NULL; + } + } + + return "No fonts found"; +} diff --git a/src/detection/font/font.h b/src/detection/font/font.h new file mode 100644 index 0000000..af04c6d --- /dev/null +++ b/src/detection/font/font.h @@ -0,0 +1,20 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/font/option.h" + +enum { FF_DETECT_FONT_NUM_FONTS = 4 }; + +typedef struct FFFontResult { + /** + * Linux / BSD: Qt, GTK2, GTK3, GTK4 + * MacOS: System, User, System Mono, User Mono + * Windows: Caption, Menu, Message, Status + * Haiku: Plain, Menu, Bold, Mono + * Other: Unset, Unset, Unset, Unset + */ + FFstrbuf fonts[FF_DETECT_FONT_NUM_FONTS]; + FFstrbuf display; +} FFFontResult; + +const char* ffDetectFont(FFFontResult* font); diff --git a/src/detection/font/font_apple.m b/src/detection/font/font_apple.m new file mode 100644 index 0000000..e8aff3c --- /dev/null +++ b/src/detection/font/font_apple.m @@ -0,0 +1,37 @@ +#include "common/font.h" +#include "common/io.h" +#include "font.h" + +#import <AppKit/NSFont.h> + +static void generateString(FFFontResult* font) +{ + if(font->fonts[0].length > 0) + { + ffStrbufAppend(&font->display, &font->fonts[0]); + ffStrbufAppendS(&font->display, " [System]"); + if(font->fonts[1].length > 0) + ffStrbufAppendS(&font->display, ", "); + } + + if(font->fonts[1].length > 0) + { + ffStrbufAppend(&font->display, &font->fonts[1]); + ffStrbufAppendS(&font->display, " [User]"); + } +} + +const char* ffDetectFontImpl(FFFontResult* result) +{ + ffStrbufAppendS(&result->fonts[0], [NSFont systemFontOfSize:12].familyName.UTF8String); + ffStrbufAppendS(&result->fonts[1], [NSFont userFontOfSize:12].familyName.UTF8String); + #ifdef MAC_OS_X_VERSION_10_15 + ffStrbufAppendS(&result->fonts[2], [NSFont monospacedSystemFontOfSize:12 weight:400].familyName.UTF8String); + #else + ffStrbufAppendS(&result->fonts[2], ""); + #endif + ffStrbufAppendS(&result->fonts[3], [NSFont userFixedPitchFontOfSize:12].familyName.UTF8String); + generateString(result); + + return NULL; +} diff --git a/src/detection/font/font_haiku.cpp b/src/detection/font/font_haiku.cpp new file mode 100644 index 0000000..dc29dd0 --- /dev/null +++ b/src/detection/font/font_haiku.cpp @@ -0,0 +1,54 @@ +extern "C" { +#include "font.h" +} + +#include <Application.h> +#include <Font.h> +#include <Menu.h> + +extern "C" { +const char* ffDetectFontImpl(FFFontResult* result); +} + +static void generateString(FFFontResult* font) { + const char* types[] = { "Plain", "Menu", "Bold", "Mono" }; + for (uint32_t i = 0; i < ARRAY_SIZE(types); ++i) { + if (i == 0 || !ffStrbufEqual(&font->fonts[i - 1], &font->fonts[i])) { + if (i > 0) { + ffStrbufAppendS(&font->display, "], "); + } + ffStrbufAppendF(&font->display, "%s [%s", font->fonts[i].chars, types[i]); + } else { + ffStrbufAppendS(&font->display, " / "); + ffStrbufAppendS(&font->display, types[i]); + } + } + ffStrbufAppendC(&font->display, ']'); +} + +const char* ffDetectFontImpl(FFFontResult* result) { + struct menu_info menuInfo; + const BFont* f; + // We need a valid be_app to query the app_server here. + BApplication app("application/x-vnd.fastfetch-cli-fastfetch"); + + if ((f = be_plain_font) != NULL) { + f->GetFamilyAndStyle(&menuInfo.f_family, &menuInfo.f_style); + ffStrbufAppendF(&result->fonts[0], "%s %s (%dpt)", menuInfo.f_family, menuInfo.f_style, (int) f->Size()); + } + if (get_menu_info(&menuInfo) == B_OK) { + ffStrbufAppendF(&result->fonts[1], "%s %s (%dpt)", menuInfo.f_family, menuInfo.f_style, (int) menuInfo.font_size); + } + if ((f = be_bold_font) != NULL) { + f->GetFamilyAndStyle(&menuInfo.f_family, &menuInfo.f_style); + ffStrbufAppendF(&result->fonts[2], "%s %s (%dpt)", menuInfo.f_family, menuInfo.f_style, (int) f->Size()); + } + if ((f = be_fixed_font) != NULL) { + f->GetFamilyAndStyle(&menuInfo.f_family, &menuInfo.f_style); + ffStrbufAppendF(&result->fonts[3], "%s %s (%dpt)", menuInfo.f_family, menuInfo.f_style, (int) f->Size()); + } + + generateString(result); + + return NULL; +} diff --git a/src/detection/font/font_linux.c b/src/detection/font/font_linux.c new file mode 100644 index 0000000..5e82b68 --- /dev/null +++ b/src/detection/font/font_linux.c @@ -0,0 +1,53 @@ +#include "common/font.h" +#include "common/parsing.h" +#include "detection/displayserver/displayserver.h" +#include "detection/gtk_qt/gtk_qt.h" +#include "font.h" + +static void generateString(FFFontResult* font) { + if (font->fonts[0].length > 0) { + ffStrbufAppend(&font->display, &font->fonts[0]); + ffStrbufAppendS(&font->display, " [Qt]"); + + for (uint8_t i = 1; i < ARRAY_SIZE(font->fonts); i++) { + if (font->fonts[i].length > 0) { + ffStrbufAppendS(&font->display, ", "); + break; + } + } + } + + ffParseGTK(&font->display, &font->fonts[1], &font->fonts[2], &font->fonts[3]); +} + +const char* ffDetectFontImpl(FFFontResult* result) { + const FFDisplayServerResult* wmde = ffConnectDisplayServer(); + + if (ffStrbufIgnCaseEqualS(&wmde->wmProtocolName, FF_WM_PROTOCOL_TTY)) { + return "Font isn't supported in TTY"; + } + + FFfont qt; + ffFontInitQt(&qt, ffDetectQt()->font.chars); + ffStrbufAppend(&result->fonts[0], &qt.pretty); + ffFontDestroy(&qt); + + FFfont gtk2; + ffFontInitPango(>k2, ffDetectGTK2()->font.chars); + ffStrbufAppend(&result->fonts[1], >k2.pretty); + ffFontDestroy(>k2); + + FFfont gtk3; + ffFontInitPango(>k3, ffDetectGTK3()->font.chars); + ffStrbufAppend(&result->fonts[2], >k3.pretty); + ffFontDestroy(>k3); + + FFfont gtk4; + ffFontInitPango(>k4, ffDetectGTK4()->font.chars); + ffStrbufAppend(&result->fonts[3], >k4.pretty); + ffFontDestroy(>k4); + + generateString(result); + + return NULL; +} diff --git a/src/detection/font/font_nosupport.c b/src/detection/font/font_nosupport.c new file mode 100644 index 0000000..26f9ec6 --- /dev/null +++ b/src/detection/font/font_nosupport.c @@ -0,0 +1,7 @@ +#include "fastfetch.h" +#include "font.h" + +const char* ffDetectFontImpl(FF_A_UNUSED FFFontResult* result) { + FF_UNUSED(result); + return "Not supported on this platform"; +} diff --git a/src/detection/font/font_windows.c b/src/detection/font/font_windows.c new file mode 100644 index 0000000..4fc28d3 --- /dev/null +++ b/src/detection/font/font_windows.c @@ -0,0 +1,63 @@ +#include "font.h" +#include "common/windows/unicode.h" +#include "common/windows/registry.h" + +#include <windows.h> + +static void generateString(FFFontResult* font) { + const char* types[] = { "Caption", "Menu", "Message", "Status" }; + for (uint32_t i = 0; i < ARRAY_SIZE(types); ++i) { + if (i == 0 || !ffStrbufEqual(&font->fonts[i - 1], &font->fonts[i])) { + if (i > 0) { + ffStrbufAppendS(&font->display, "], "); + } + ffStrbufAppendF(&font->display, "%s [%s", font->fonts[i].chars, types[i]); + } else { + ffStrbufAppendS(&font->display, " / "); + ffStrbufAppendS(&font->display, types[i]); + } + } + ffStrbufAppendC(&font->display, ']'); +} + +const char* ffDetectFontImpl(FFFontResult* result) { + FF_AUTO_CLOSE_FD HANDLE hKey = NULL; + if (!ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Control Panel\\Desktop\\WindowMetrics", &hKey, NULL)) { + return "ffRegOpenKeyForRead(HKEY_CURRENT_USER\\Control Panel\\Desktop\\WindowMetrics) failed"; + } + + LOGFONTW fonts[4]; + FFArgBuffer fontBuffers[4] = { + { .data = &fonts[0], .length = sizeof(fonts[0]) }, + { .data = &fonts[1], .length = sizeof(fonts[1]) }, + { .data = &fonts[2], .length = sizeof(fonts[2]) }, + { .data = &fonts[3], .length = sizeof(fonts[3]) }, + }; + + if (!ffRegReadValues(hKey, 4, (FFRegValueArg[]) { + FF_ARG(fontBuffers[0], L"CaptionFont"), + FF_ARG(fontBuffers[1], L"MenuFont"), + FF_ARG(fontBuffers[2], L"MessageFont"), + FF_ARG(fontBuffers[3], L"StatusFont"), + }, + NULL)) { + return "ffRegReadValues(HKEY_CURRENT_USER\\Control Panel\\Desktop\\WindowMetrics) failed"; + } + + for (uint32_t i = 0; i < ARRAY_SIZE(fonts); ++i) { + if (fontBuffers[i].length != sizeof(LOGFONTW)) { + continue; // Invalid data, skip + } + + LOGFONTW* logFont = &fonts[i]; + + ffStrbufSetWS(&result->fonts[i], logFont->lfFaceName); + if (logFont->lfHeight < 0) { + ffStrbufAppendF(&result->fonts[i], " (%dpt)", (int) -logFont->lfHeight); + } + } + + generateString(result); + + return NULL; +} diff --git a/src/detection/gamepad/gamepad.h b/src/detection/gamepad/gamepad.h new file mode 100644 index 0000000..f26def9 --- /dev/null +++ b/src/detection/gamepad/gamepad.h @@ -0,0 +1,11 @@ +#pragma once + +#include "fastfetch.h" + +typedef struct FFGamepadDevice { + FFstrbuf serial; + FFstrbuf name; + uint8_t battery; // 0-100% +} FFGamepadDevice; + +const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */); diff --git a/src/detection/gamepad/gamepad_apple.c b/src/detection/gamepad/gamepad_apple.c new file mode 100644 index 0000000..c262c86 --- /dev/null +++ b/src/detection/gamepad/gamepad_apple.c @@ -0,0 +1,48 @@ +#include "gamepad.h" +#include "common/apple/cf_helpers.h" +#include "common/mallocHelper.h" + +#include <IOKit/IOKitLib.h> +#include <IOKit/hid/IOHIDLib.h> + +static void enumSet(IOHIDDeviceRef value, FFlist* results) { + FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *results); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->name); + device->battery = 0; + + CFStringRef manufacturer = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDManufacturerKey)); + ffCfStrGetString(manufacturer, &device->name); + + CFStringRef product = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDProductKey)); + if (device->name.length) { + ffCfStrGetString(product, &device->serial); + ffStrbufAppendC(&device->name, ' '); + ffStrbufAppend(&device->name, &device->serial); + } else { + ffCfStrGetString(product, &device->name); + } + + CFStringRef serialNumber = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDSerialNumberKey)); + ffCfStrGetString(serialNumber, &device->serial); +} + +const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) { + IOHIDManagerRef FF_CFTYPE_AUTO_RELEASE manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) { + return "IOHIDManagerOpen() failed"; + } + + CFDictionaryRef FF_CFTYPE_AUTO_RELEASE matching1 = CFDictionaryCreate(kCFAllocatorDefault, (const void**) (CFStringRef[]) { CFSTR(kIOHIDDeviceUsagePageKey), CFSTR(kIOHIDDeviceUsageKey) }, (const void**) (CFNumberRef[]) { ffCfCreateInt(kHIDPage_GenericDesktop), ffCfCreateInt(kHIDUsage_GD_Joystick) }, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionaryRef FF_CFTYPE_AUTO_RELEASE matching2 = CFDictionaryCreate(kCFAllocatorDefault, (const void**) (CFStringRef[]) { CFSTR(kIOHIDDeviceUsagePageKey), CFSTR(kIOHIDDeviceUsageKey) }, (const void**) (CFNumberRef[]) { ffCfCreateInt(kHIDPage_GenericDesktop), ffCfCreateInt(kHIDUsage_GD_GamePad) }, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFArrayRef FF_CFTYPE_AUTO_RELEASE matchings = CFArrayCreate(kCFAllocatorDefault, (const void**) (CFTypeRef[]) { matching1, matching2 }, 2, &kCFTypeArrayCallBacks); + IOHIDManagerSetDeviceMatchingMultiple(manager, matchings); + + CFSetRef FF_CFTYPE_AUTO_RELEASE set = IOHIDManagerCopyDevices(manager); + if (set) { + CFSetApplyFunction(set, (CFSetApplierFunction) &enumSet, devices); + } + IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); + + return NULL; +} diff --git a/src/detection/gamepad/gamepad_bsd.c b/src/detection/gamepad/gamepad_bsd.c new file mode 100644 index 0000000..b67b8fd --- /dev/null +++ b/src/detection/gamepad/gamepad_bsd.c @@ -0,0 +1,65 @@ +#include "gamepad.h" +#include "common/io.h" + +#include <stdio.h> +#include <fcntl.h> +#include <usbhid.h> + +#if __has_include(<dev/usb/usb_ioctl.h>) + #include <dev/usb/usb_ioctl.h> // FreeBSD +#else + #include <bus/u4b/usb_ioctl.h> // DragonFly +#endif + +#define MAX_UHID_JOYS 64 + +const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) { + char path[16]; + for (int i = 0; i < MAX_UHID_JOYS; i++) { + snprintf(path, ARRAY_SIZE(path), "/dev/uhid%d", i); + FF_AUTO_CLOSE_FD int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + if (errno == ENOENT) { + break; // No more devices + } + continue; // Device not found + } + report_desc_t repDesc = hid_get_report_desc(fd); + if (!repDesc) { + continue; + } + + int reportId = hid_get_report_id(fd); + + struct hid_data* hData = hid_start_parse(repDesc, 0, reportId); + if (hData) { + struct hid_item hItem; + while (hid_get_item(hData, &hItem) > 0) { + if (HID_PAGE(hItem.usage) != 1) { + continue; + } + switch (HID_USAGE(hItem.usage)) { + case 1: // Pointer. FreeBSD returns 1 for my Pro Controller for some reason + case 4: // Joystick + case 5: // Gamepad + break; + default: + continue; + } + + struct usb_device_info di; + if (ioctl(fd, USB_GET_DEVICEINFO, &di) != -1) { + FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *devices); + ffStrbufInitS(&device->serial, di.udi_serial); + ffStrbufInitF(&device->name, "%s %s", di.udi_vendor, di.udi_product); + device->battery = 0; + } + } + hid_end_parse(hData); + } + + hid_dispose_report_desc(repDesc); + } + + return NULL; +} diff --git a/src/detection/gamepad/gamepad_haiku.cpp b/src/detection/gamepad/gamepad_haiku.cpp new file mode 100644 index 0000000..50b43a0 --- /dev/null +++ b/src/detection/gamepad/gamepad_haiku.cpp @@ -0,0 +1,18 @@ +extern "C" { +#include "gamepad.h" +} +#include <Joystick.h> + +const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) { + BJoystick js; + for (int32 i = 0, n = js.CountDevices(); i < n; ++i) { + char name[B_OS_NAME_LENGTH]; + if (js.GetDeviceName(i, name) == B_OK) { + FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *devices); + ffStrbufInit(&device->serial); + ffStrbufInitS(&device->name, name); + device->battery = 0; + } + } + return NULL; +} diff --git a/src/detection/gamepad/gamepad_linux.c b/src/detection/gamepad/gamepad_linux.c new file mode 100644 index 0000000..6bb4ee2 --- /dev/null +++ b/src/detection/gamepad/gamepad_linux.c @@ -0,0 +1,97 @@ +#include "gamepad.h" +#include "common/io.h" +#include "common/strutil.h" + +static void detectGamepad(FFlist* devices, FFstrbuf* name, FFstrbuf* path) { + uint32_t baseLen = path->length; + FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *devices); + ffStrbufInit(&device->serial); + ffStrbufInitMove(&device->name, name); + device->battery = 0; + + ffStrbufAppendS(path, "uniq"); + if (ffAppendFileBuffer(path->chars, &device->serial)) { + ffStrbufTrimRightSpace(&device->serial); + } + + ffStrbufSubstrBefore(path, baseLen); + ffStrbufAppendS(path, "device/power_supply/"); // /sys/class/input/jsX/device/device/power_supply + + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(path->chars); + if (dirp) { + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.' || (entry->d_type != DT_DIR && entry->d_type != DT_UNKNOWN)) { + continue; + } + ffStrbufAppendS(path, entry->d_name); + ffStrbufAppendS(path, "/capacity"); // /sys/class/input/jsX/device/device/power_supply/XXX/capacity + char capacity[32]; + ssize_t nRead = ffReadFileData(path->chars, ARRAY_SIZE(capacity) - 1, capacity); + if (nRead > 0) // Tested with a PS4 controller + { + capacity[nRead] = '\0'; + device->battery = (uint8_t) strtoul(capacity, NULL, 10); + break; + } + + ffStrbufAppendS(path, "_level"); + nRead = ffReadFileData(path->chars, ARRAY_SIZE(capacity) - 1, capacity); + if (nRead > 0) // Tested with a NS Pro controller + { + // https://github.com/torvalds/linux/blob/52b1853b080a082ec3749c3a9577f6c71b1d4a90/drivers/power/supply/power_supply_sysfs.c#L124 + switch (capacity[0]) { + case 'C': + device->battery = 1; + break; // Critical + case 'L': + device->battery = 25; + break; // Low + case 'N': + device->battery = 50; + break; // Normal + case 'H': + device->battery = 75; + break; // High + case 'F': + device->battery = 100; + break; // Full + } + } + } + } +} + +const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/input/"); + if (dirp == NULL) { + return "opendir(\"/sys/class/input/\") == NULL"; + } + + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateS("/sys/class/input/"); + uint32_t baseLen = path.length; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (!ffStrStartsWith(entry->d_name, "js")) { + continue; + } + if (!ffCharIsDigit(entry->d_name[strlen("js")])) { + continue; + } + + ffStrbufAppendS(&path, entry->d_name); + ffStrbufAppendS(&path, "/device/name"); + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + if (ffAppendFileBuffer(path.chars, &name)) { + ffStrbufTrimRightSpace(&name); + ffStrbufSubstrBefore(&path, path.length - 4); + detectGamepad(devices, &name, &path); + } + + ffStrbufSubstrBefore(&path, baseLen); + } + + return NULL; +} diff --git a/src/detection/gamepad/gamepad_nosupport.c b/src/detection/gamepad/gamepad_nosupport.c new file mode 100644 index 0000000..c0d791e --- /dev/null +++ b/src/detection/gamepad/gamepad_nosupport.c @@ -0,0 +1,5 @@ +#include "gamepad.h" + +const char* ffDetectGamepad(FF_A_UNUSED FFlist* devices /* List of FFGamepadDevice */) { + return "Not supported on this platform"; +} diff --git a/src/detection/gamepad/gamepad_windows.c b/src/detection/gamepad/gamepad_windows.c new file mode 100644 index 0000000..9cbb561 --- /dev/null +++ b/src/detection/gamepad/gamepad_windows.c @@ -0,0 +1,201 @@ +#include "gamepad.h" +#include "common/io.h" +#include "common/mallocHelper.h" +#include "common/windows/unicode.h" + +#include <windows.h> +#include <hidsdi.h> + +static const char* detectKnownDeviceName(uint32_t vendorId, uint32_t productId) { + switch (vendorId) { + // Nintendo + case 0x057E: { + switch (productId) { + case 0x2006: + return "Nintendo Switch Joycon L"; + case 0x2007: + return "Nintendo Switch Joycon R"; + case 0x2009: + return "Nintendo Switch Pro Controller"; + case 0x200E: + return "Nintendo Switch Charging Grip"; + case 0x2017: + return "Nintendo Switch SNES Controller"; + + default: + return NULL; + } + } + + // Sony + case 0x054C: { + switch (productId) { + case 0x0268: + return "Sony DualShock 3 / Six Axis"; + + case 0x05C4: + return "Sony DualShock 4 Gen1"; + case 0x09CC: + return "Sony DualShock 4 Gen2"; + case 0x0BA0: + return "Sony DualShock 4 USB receiver"; + + case 0x0CE6: + return "Sony DualSense"; + case 0x0DF2: + return "Sony DualSense Edge"; + + default: + return NULL; + } + } + + // Logitech + case 0x046D: { + switch (productId) { + case 0xC216: + return "Logitech F310, DirectInput"; + case 0xC218: + return "Logitech F510, DirectInput"; + case 0xC219: + return "Logitech F710, DirectInput"; + case 0xC21D: + return "Logitech F310"; + case 0xC21E: + return "Logitech F510"; + case 0xC21F: + return "Logitech F710"; + + default: + return NULL; + } + } + + case 0x045E: // Microsoft Xbox compatible controllers should be handled by Windows without problems + default: + return NULL; + } +} + +const char* ffDetectGamepad(FFlist* devices /* List of FFGamepadDevice */) { + UINT nDevices = 0; + if (GetRawInputDeviceList(NULL, &nDevices, sizeof(RAWINPUTDEVICELIST))) { + return "GetRawInputDeviceList(NULL) failed"; + } + if (nDevices == 0) { + return "No HID devices found"; + } + RAWINPUTDEVICELIST* FF_AUTO_FREE pRawInputDeviceList = (RAWINPUTDEVICELIST*) malloc(sizeof(RAWINPUTDEVICELIST) * nDevices); + if ((nDevices = GetRawInputDeviceList(pRawInputDeviceList, &nDevices, sizeof(RAWINPUTDEVICELIST))) == (UINT) -1) { + return "GetRawInputDeviceList(pRawInputDeviceList) failed"; + } + + for (UINT i = 0; i < nDevices; ++i) { + if (pRawInputDeviceList[i].dwType != RIM_TYPEHID) { + continue; + } + + HANDLE hDevice = pRawInputDeviceList[i].hDevice; + + RID_DEVICE_INFO rdi; + UINT rdiSize = sizeof(rdi); + if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICEINFO, &rdi, &rdiSize) == (UINT) -1) { + continue; + } + + if (rdi.hid.usUsagePage != 1 || (rdi.hid.usUsage != 4 /*Joystick*/ && rdi.hid.usUsage != 5 /*Gamepad*/)) { + continue; + } + + WCHAR devName[MAX_PATH] = L""; + UINT nameSize = MAX_PATH; + if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, devName, &nameSize) == (UINT) -1) { + continue; + } + + FFGamepadDevice* device = FF_LIST_ADD(FFGamepadDevice, *devices); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->name); + device->battery = 0; + + const char* knownGamepad = detectKnownDeviceName(rdi.hid.dwVendorId, rdi.hid.dwProductId); + if (knownGamepad) { + ffStrbufSetS(&device->name, knownGamepad); + } + HANDLE FF_AUTO_CLOSE_FD hHidFile = CreateFileW(devName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); + if (hHidFile == INVALID_HANDLE_VALUE) { + if (!knownGamepad) { + ffStrbufSetF(&device->name, "Unknown gamepad %04X-%04X", (unsigned) rdi.hid.dwVendorId, (unsigned) rdi.hid.dwProductId); + } + continue; + } + + if (!knownGamepad) { + wchar_t displayName[126]; + if (HidD_GetProductString(hHidFile, displayName, sizeof(displayName) /*in bytes*/)) { + wchar_t manufacturer[126]; + if (HidD_GetManufacturerString(hHidFile, manufacturer, sizeof(manufacturer) /*in bytes*/)) { + ffStrbufSetWS(&device->name, manufacturer); + FF_STRBUF_AUTO_DESTROY displayNameStr = ffStrbufCreateWS(displayName); + ffStrbufAppendC(&device->name, ' '); + ffStrbufAppend(&device->name, &displayNameStr); + } else { + ffStrbufSetWS(&device->name, displayName); + } + } + } + + wchar_t serialNumber[127] = L""; + if (HidD_GetSerialNumberString(hHidFile, serialNumber, sizeof(serialNumber) /*in bytes*/)) { + ffStrbufSetWS(&device->serial, serialNumber); + } + + PHIDP_PREPARSED_DATA preparsedData = NULL; + if (HidD_GetPreparsedData(hHidFile, &preparsedData)) { + HIDP_CAPS caps; + NTSTATUS capsResult = HidP_GetCaps(preparsedData, &caps); + HidD_FreePreparsedData(preparsedData); + if (!NT_SUCCESS(capsResult)) { + continue; + } + + if ( + (rdi.hid.dwVendorId == 0x054C && (rdi.hid.dwProductId == 0x05C4 || // PS4 Gen1 + rdi.hid.dwProductId == 0x09CC // PS4 Gen2 + )) || + (rdi.hid.dwVendorId == 0x057E && (rdi.hid.dwProductId == 0x2009 // NS Pro + ))) { + // Controller must be connected by other programs + FF_AUTO_FREE uint8_t* reportBuffer = malloc(caps.InputReportByteLength); + OVERLAPPED overlapped = {}; + DWORD nBytes; + if (ReadFile(hHidFile, reportBuffer, caps.InputReportByteLength, &nBytes, &overlapped) || + GetOverlappedResultEx(hHidFile, &overlapped, &nBytes, FF_IO_TERM_RESP_WAIT_MS, TRUE)) { + if (rdi.hid.dwVendorId == 0x054C) { + if (nBytes > 31) { + uint8_t batteryInfo = reportBuffer[caps.InputReportByteLength == 64 /*USB?*/ ? 30 : 32]; + device->battery = (uint8_t) ((batteryInfo & 0x0f) * 100 / (batteryInfo & 0x10 /*charging?*/ ? 11 /*BATTERY_MAX_USB*/ : 8 /*BATTERY_MAX*/)); + if (device->battery > 100) { + device->battery = 100; + } + } + } else { + if (nBytes > 3 && reportBuffer[0] == 0x30) { + uint8_t batteryInfo = reportBuffer[2]; + device->battery = (uint8_t) (((batteryInfo & 0xE0) >> 4) * 100 / 8); + if (device->battery == 0) { + device->battery = 1; + } else if (device->battery > 100) { + device->battery = 100; + } + } + } + } else { + CancelIo(hHidFile); + } + } + } + } + + return NULL; +} diff --git a/src/detection/gpu/adl.h b/src/detection/gpu/adl.h new file mode 100644 index 0000000..346eece --- /dev/null +++ b/src/detection/gpu/adl.h @@ -0,0 +1,66 @@ +#pragma once + +#include "3rdparty/display-library/adl_sdk.h" + +// https://gpuopen-librariesandsdks.github.io/adl/modules.html + +// Function to initialize the ADL2 interface and to obtain client's context handle. +extern int ADL2_Main_Control_Create(ADL_MAIN_MALLOC_CALLBACK callback, int iEnumConnectedAdapters, ADL_CONTEXT_HANDLE* context); + +// Destroy client's ADL context. +extern int ADL2_Main_Control_Destroy(ADL_CONTEXT_HANDLE context); + +// Retrieves adapter information for given adapter or all OS-known adapters. +// Return ADL_OK on success, DESPITE THE OFFICIAL DOCUMENT SAYS IT RETURNS 1 FOR SUCCESS! +extern int ADL2_Adapter_AdapterInfoX3_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, int* numAdapters, AdapterInfo** lppAdapterInfo); + +// Function to retrieve Graphic Core Info. +extern int ADL2_Adapter_Graphic_Core_Info_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, ADLGraphicCoreInfo* pGraphicCoreInfo); + +// Function to retrieve memory information from the adapter. Version 2 +extern int ADL2_Adapter_MemoryInfo2_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, ADLMemoryInfo2* lpMemoryInfo2); + +// This function retrieves the Dedicated VRAM usage of given adapter. +extern int ADL2_Adapter_DedicatedVRAMUsage_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, int* iVRAMUsageInMB); + +// Function to get the ASICFamilyType from the adapter. +extern int ADL2_Adapter_ASICFamilyType_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, int* lpAsicTypes, int* lpValids); + +// Function to retrieve current power management capabilities. +extern int ADL2_Overdrive_Caps(ADL_CONTEXT_HANDLE context, int iAdapterIndex, int* iSupported, int* iEnabled, int* iVersion); + +/////////// Overdrive 6 functions + +// Function to retrieve current Overdrive and performance-related activity. +extern int ADL2_Overdrive6_CurrentStatus_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, ADLOD6CurrentStatus* lpCurrentStatus); + +// Function to retrieve GPU temperature from the thermal controller. +extern int ADL2_Overdrive6_Temperature_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, int* lpTemperature); + +// Function to retrieve the current or default Overdrive clock ranges. +extern int ADL2_Overdrive6_StateInfo_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, int iStateType, ADLOD6StateInfo* lpStateInfo); + +/// Overdrive N functions + +// Despite the name (N means Next), this is actually Overdrive7 API +// https://github.com/GPUOpen-LibrariesAndSDKs/display-library/blob/master/Sample/OverdriveN/OverdriveN.cpp#L209 + +// Function to retrieve the OverdriveN capabilities. +extern int ADL2_OverdriveN_CapabilitiesX2_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, ADLODNCapabilitiesX2* lpODCapabilities); + +// Function to retrieve the current OD performance status. +extern int ADL2_OverdriveN_PerformanceStatus_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, ADLODNPerformanceStatus* lpODPerformanceStatus); + +// Function to retrieve the current temperature. +extern int ADL2_OverdriveN_Temperature_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, int iTemperatureType, int* iTemperature); + +// Function to retrieve the current GPU clocks settings. +extern int ADL2_OverdriveN_SystemClocksX2_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, ADLODNPerformanceLevelsX2* lpODPerformanceLevels); + +/// Overdrive 8 functions + +// Function to retrieve the Overdrive8 current settings. +extern int ADL2_Overdrive8_Current_Setting_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, ADLOD8CurrentSetting* lpCurrentSetting); + +// Function to retrieve the Overdrive8 current settings. +extern int ADL2_New_QueryPMLogData_Get(ADL_CONTEXT_HANDLE context, int iAdapterIndex, ADLPMLogDataOutput* lpDataOutput); diff --git a/src/detection/gpu/asahi_drm.h b/src/detection/gpu/asahi_drm.h new file mode 100644 index 0000000..69b537b --- /dev/null +++ b/src/detection/gpu/asahi_drm.h @@ -0,0 +1,1194 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright (C) The Asahi Linux Contributors + * Copyright (C) 2018-2023 Collabora Ltd. + * Copyright (C) 2014-2018 Broadcom + */ +#ifndef _ASAHI_DRM_H_ +#define _ASAHI_DRM_H_ + +#include <drm.h> + +#if defined(__cplusplus) +extern "C" { +#endif + +/** + * DOC: Introduction to the Asahi UAPI + * + * This documentation describes the Asahi IOCTLs. + * + * Just a few generic rules about the data passed to the Asahi IOCTLs (cribbed + * from Panthor): + * + * - Structures must be aligned on 64-bit/8-byte. If the object is not + * naturally aligned, a padding field must be added. + * - Fields must be explicitly aligned to their natural type alignment with + * pad[0..N] fields. + * - All padding fields will be checked by the driver to make sure they are + * zeroed. + * - Flags can be added, but not removed/replaced. + * - New fields can be added to the main structures (the structures + * directly passed to the ioctl). Those fields can be added at the end of + * the structure, or replace existing padding fields. Any new field being + * added must preserve the behavior that existed before those fields were + * added when a value of zero is passed. + * - New fields can be added to indirect objects (objects pointed by the + * main structure), iff those objects are passed a size to reflect the + * size known by the userspace driver (see + * drm_asahi_cmd_header::size). + * - If the kernel driver is too old to know some fields, those will be + * ignored if zero, and otherwise rejected (and so will be zero on output). + * - If userspace is too old to know some fields, those will be zeroed + * (input) before the structure is parsed by the kernel driver. + * - Each new flag/field addition must come with a driver version update so + * the userspace driver doesn't have to guess which flags are supported. + * - Structures should not contain unions, as this would defeat the + * extensibility of such structures. + * - IOCTLs can't be removed or replaced. New IOCTL IDs should be placed + * at the end of the drm_asahi_ioctl_id enum. + */ + +/** + * enum drm_asahi_ioctl_id - IOCTL IDs + * + * Place new ioctls at the end, don't re-order, don't replace or remove entries. + * + * These IDs are not meant to be used directly. Use the DRM_IOCTL_ASAHI_xxx + * definitions instead. + */ +enum drm_asahi_ioctl_id { + /** @DRM_ASAHI_GET_PARAMS: Query device properties. */ + DRM_ASAHI_GET_PARAMS = 0, + + /** @DRM_ASAHI_GET_TIME: Query device time. */ + DRM_ASAHI_GET_TIME, + + /** @DRM_ASAHI_VM_CREATE: Create a GPU VM address space. */ + DRM_ASAHI_VM_CREATE, + + /** @DRM_ASAHI_VM_DESTROY: Destroy a VM. */ + DRM_ASAHI_VM_DESTROY, + + /** @DRM_ASAHI_VM_BIND: Bind/unbind memory to a VM. */ + DRM_ASAHI_VM_BIND, + + /** @DRM_ASAHI_GEM_CREATE: Create a buffer object. */ + DRM_ASAHI_GEM_CREATE, + + /** + * @DRM_ASAHI_GEM_MMAP_OFFSET: Get offset to pass to mmap() to map a + * given GEM handle. + */ + DRM_ASAHI_GEM_MMAP_OFFSET, + + /** @DRM_ASAHI_GEM_BIND_OBJECT: Bind memory as a special object */ + DRM_ASAHI_GEM_BIND_OBJECT, + + /** @DRM_ASAHI_QUEUE_CREATE: Create a scheduling queue. */ + DRM_ASAHI_QUEUE_CREATE, + + /** @DRM_ASAHI_QUEUE_DESTROY: Destroy a scheduling queue. */ + DRM_ASAHI_QUEUE_DESTROY, + + /** @DRM_ASAHI_SUBMIT: Submit commands to a queue. */ + DRM_ASAHI_SUBMIT, +}; + +#define DRM_ASAHI_MAX_CLUSTERS 64 + +/** + * struct drm_asahi_params_global - Global parameters. + * + * This struct may be queried by drm_asahi_get_params. + */ +struct drm_asahi_params_global { + /** @features: Feature bits from drm_asahi_feature */ + __u64 features; + + /** @gpu_generation: GPU generation, e.g. 13 for G13G */ + __u32 gpu_generation; + + /** @gpu_variant: GPU variant as a character, e.g. 'C' for G13C */ + __u32 gpu_variant; + + /** + * @gpu_revision: GPU revision in BCD, e.g. 0x00 for 'A0' or + * 0x21 for 'C1' + */ + __u32 gpu_revision; + + /** @chip_id: Chip ID in BCD, e.g. 0x8103 for T8103 */ + __u32 chip_id; + + /** @num_dies: Number of dies in the SoC */ + __u32 num_dies; + + /** @num_clusters_total: Number of GPU clusters (across all dies) */ + __u32 num_clusters_total; + + /** + * @num_cores_per_cluster: Number of logical cores per cluster + * (including inactive/nonexistent) + */ + __u32 num_cores_per_cluster; + + /** @max_frequency_khz: Maximum GPU core clock frequency */ + __u32 max_frequency_khz; + + /** @core_masks: Bitmask of present/enabled cores per cluster */ + __u64 core_masks[DRM_ASAHI_MAX_CLUSTERS]; + + /** + * @vm_start: VM range start VMA. Together with @vm_end, this defines + * the window of valid GPU VAs. Userspace is expected to subdivide VAs + * out of this window. + * + * This window contains all virtual addresses that userspace needs to + * know about. There may be kernel-internal GPU VAs outside this range, + * but that detail is not relevant here. + */ + __u64 vm_start; + + /** @vm_end: VM range end VMA */ + __u64 vm_end; + + /** + * @vm_kernel_min_size: Minimum kernel VMA window size. + * + * When creating a VM, userspace is required to carve out a section of + * virtual addresses (within the range given by @vm_start and + * @vm_end). The kernel will allocate various internal structures + * within the specified VA range. + * + * Allowing userspace to choose the VA range for the kernel, rather than + * the kernel reserving VAs and requiring userspace to cope, can assist + * in implementing SVM. + */ + __u64 vm_kernel_min_size; + + /** + * @max_commands_per_submission: Maximum number of supported commands + * per submission. This mirrors firmware limits. Userspace must split up + * larger command buffers, which may require inserting additional + * synchronization. + */ + __u32 max_commands_per_submission; + + /** + * @max_attachments: Maximum number of drm_asahi_attachment's per + * command + */ + __u32 max_attachments; + + /** + * @command_timestamp_frequency_hz: Timebase frequency for timestamps + * written during command exeuction, specified via drm_asahi_timestamp + * structures. As this rate is controlled by the firmware, it is a + * queryable parameter. + * + * Userspace must divide by this frequency to convert timestamps to + * seconds, rather than hardcoding a particular firmware's rate. + */ + __u64 command_timestamp_frequency_hz; +}; + +/** + * enum drm_asahi_feature - Feature bits + * + * This covers only features that userspace cannot infer from the architecture + * version. Most features don't need to be here. + */ +enum drm_asahi_feature { + /** + * @DRM_ASAHI_FEATURE_SOFT_FAULTS: GPU has "soft fault" enabled. Shader + * loads of unmapped memory will return zero. Shader stores to unmapped + * memory will be silently discarded. Note that only shader load/store + * is affected. Other hardware units are not affected, notably including + * texture sampling. + * + * Soft fault is set when initializing the GPU and cannot be runtime + * toggled. Therefore, it is exposed as a feature bit and not a + * userspace-settable flag on the VM. When soft fault is enabled, + * userspace can speculate memory accesses more aggressively. + */ + DRM_ASAHI_FEATURE_SOFT_FAULTS = (1UL) << 0, +}; + +/** + * struct drm_asahi_get_params - Arguments passed to DRM_IOCTL_ASAHI_GET_PARAMS + */ +struct drm_asahi_get_params { + /** @param_group: Parameter group to fetch (MBZ) */ + __u32 param_group; + + /** @pad: MBZ */ + __u32 pad; + + /** @pointer: User pointer to write parameter struct */ + __u64 pointer; + + /** + * @size: Size of the user buffer. In case of older userspace, this may + * be less than sizeof(struct drm_asahi_params_global). The kernel will + * not write past the length specified here, allowing extensibility. + */ + __u64 size; +}; + +/** + * struct drm_asahi_vm_create - Arguments passed to DRM_IOCTL_ASAHI_VM_CREATE + */ +struct drm_asahi_vm_create { + /** + * @kernel_start: Start of the kernel-reserved address range. See + * drm_asahi_params_global::vm_kernel_min_size. + * + * Both @kernel_start and @kernel_end must be within the range of + * valid VAs given by drm_asahi_params_global::vm_start and + * drm_asahi_params_global::vm_end. The size of the kernel range + * (@kernel_end - @kernel_start) must be at least + * drm_asahi_params_global::vm_kernel_min_size. + * + * Userspace must not bind any memory on this VM into this reserved + * range, it is for kernel use only. + */ + __u64 kernel_start; + + /** + * @kernel_end: End of the kernel-reserved address range. See + * @kernel_start. + */ + __u64 kernel_end; + + /** @vm_id: Returned VM ID */ + __u32 vm_id; + + /** @pad: MBZ */ + __u32 pad; +}; + +/** + * struct drm_asahi_vm_destroy - Arguments passed to DRM_IOCTL_ASAHI_VM_DESTROY + */ +struct drm_asahi_vm_destroy { + /** @vm_id: VM ID to be destroyed */ + __u32 vm_id; + + /** @pad: MBZ */ + __u32 pad; +}; + +/** + * enum drm_asahi_gem_flags - Flags for GEM creation + */ +enum drm_asahi_gem_flags { + /** + * @DRM_ASAHI_GEM_WRITEBACK: BO should be CPU-mapped as writeback. + * + * Map as writeback instead of write-combine. This optimizes for CPU + * reads. + */ + DRM_ASAHI_GEM_WRITEBACK = (1L << 0), + + /** + * @DRM_ASAHI_GEM_VM_PRIVATE: BO is private to this GPU VM (no exports). + */ + DRM_ASAHI_GEM_VM_PRIVATE = (1L << 1), +}; + +/** + * struct drm_asahi_gem_create - Arguments passed to DRM_IOCTL_ASAHI_GEM_CREATE + */ +struct drm_asahi_gem_create { + /** @size: Size of the BO */ + __u64 size; + + /** @flags: Combination of drm_asahi_gem_flags flags. */ + __u32 flags; + + /** + * @vm_id: VM ID to assign to the BO, if DRM_ASAHI_GEM_VM_PRIVATE is set + */ + __u32 vm_id; + + /** @handle: Returned GEM handle for the BO */ + __u32 handle; + + /** @pad: MBZ */ + __u32 pad; +}; + +/** + * struct drm_asahi_gem_mmap_offset - Arguments passed to + * DRM_IOCTL_ASAHI_GEM_MMAP_OFFSET + */ +struct drm_asahi_gem_mmap_offset { + /** @handle: Handle for the object being mapped. */ + __u32 handle; + + /** @flags: Must be zero */ + __u32 flags; + + /** @offset: The fake offset to use for subsequent mmap call */ + __u64 offset; +}; + +/** + * enum drm_asahi_bind_flags - Flags for GEM binding + */ +enum drm_asahi_bind_flags { + /** + * @DRM_ASAHI_BIND_UNBIND: Instead of binding a GEM object to the range, + * simply unbind the GPU VMA range. + */ + DRM_ASAHI_BIND_UNBIND = (1L << 0), + + /** @DRM_ASAHI_BIND_READ: Map BO with GPU read permission */ + DRM_ASAHI_BIND_READ = (1L << 1), + + /** @DRM_ASAHI_BIND_WRITE: Map BO with GPU write permission */ + DRM_ASAHI_BIND_WRITE = (1L << 2), + + /** + * @DRM_ASAHI_BIND_SINGLE_PAGE: Map a single page of the BO repeatedly + * across the VA range. + * + * This is useful to fill a VA range with scratch pages or zero pages. + * It is intended as a mechanism to accelerate sparse. + */ + DRM_ASAHI_BIND_SINGLE_PAGE = (1L << 3), +}; + +/** + * struct drm_asahi_gem_bind_op - Description of a single GEM bind operation. + */ +struct drm_asahi_gem_bind_op { + /** @flags: Combination of drm_asahi_bind_flags flags. */ + __u32 flags; + + /** @handle: GEM object to bind (except for UNBIND) */ + __u32 handle; + + /** + * @offset: Offset into the object (except for UNBIND). + * + * For a regular bind, this is the beginning of the region of the GEM + * object to bind. + * + * For a single-page bind, this is the offset to the single page that + * will be repeatedly bound. + * + * Must be page-size aligned. + */ + __u64 offset; + + /** + * @range: Number of bytes to bind/unbind to @addr. + * + * Must be page-size aligned. + */ + __u64 range; + + /** + * @addr: Address to bind to. + * + * Must be page-size aligned. + */ + __u64 addr; +}; + +/** + * struct drm_asahi_vm_bind - Arguments passed to + * DRM_IOCTL_ASAHI_VM_BIND + */ +struct drm_asahi_vm_bind { + /** @vm_id: The ID of the VM to bind to */ + __u32 vm_id; + + /** @num_binds: number of binds in this IOCTL. */ + __u32 num_binds; + + /** + * @stride: Stride in bytes between consecutive binds. This allows + * extensibility of drm_asahi_gem_bind_op. + */ + __u32 stride; + + /** @pad: MBZ */ + __u32 pad; + + /** + * @userptr: User pointer to an array of @num_binds structures of type + * @drm_asahi_gem_bind_op and size @stride bytes. + */ + __u64 userptr; +}; + +/** + * enum drm_asahi_bind_object_op - Special object bind operation + */ +enum drm_asahi_bind_object_op { + /** @DRM_ASAHI_BIND_OBJECT_OP_BIND: Bind a BO as a special GPU object */ + DRM_ASAHI_BIND_OBJECT_OP_BIND = 0, + + /** @DRM_ASAHI_BIND_OBJECT_OP_UNBIND: Unbind a special GPU object */ + DRM_ASAHI_BIND_OBJECT_OP_UNBIND = 1, +}; + +/** + * enum drm_asahi_bind_object_flags - Special object bind flags + */ +enum drm_asahi_bind_object_flags { + /** + * @DRM_ASAHI_BIND_OBJECT_USAGE_TIMESTAMPS: Map a BO as a timestamp + * buffer. + */ + DRM_ASAHI_BIND_OBJECT_USAGE_TIMESTAMPS = (1L << 0), +}; + +/** + * struct drm_asahi_gem_bind_object - Arguments passed to + * DRM_IOCTL_ASAHI_GEM_BIND_OBJECT + */ +struct drm_asahi_gem_bind_object { + /** @op: Bind operation (enum drm_asahi_bind_object_op) */ + __u32 op; + + /** @flags: Combination of drm_asahi_bind_object_flags flags. */ + __u32 flags; + + /** @handle: GEM object to bind/unbind (BIND) */ + __u32 handle; + + /** @vm_id: The ID of the VM to operate on (MBZ currently) */ + __u32 vm_id; + + /** @offset: Offset into the object (BIND only) */ + __u64 offset; + + /** @range: Number of bytes to bind/unbind (BIND only) */ + __u64 range; + + /** @object_handle: Object handle (out for BIND, in for UNBIND) */ + __u32 object_handle; + + /** @pad: MBZ */ + __u32 pad; +}; + +/** + * enum drm_asahi_cmd_type - Command type + */ +enum drm_asahi_cmd_type { + /** + * @DRM_ASAHI_CMD_RENDER: Render command, executing on the render + * subqueue. Combined vertex and fragment operation. + * + * Followed by a @drm_asahi_cmd_render payload. + */ + DRM_ASAHI_CMD_RENDER = 0, + + /** + * @DRM_ASAHI_CMD_COMPUTE: Compute command on the compute subqueue. + * + * Followed by a @drm_asahi_cmd_compute payload. + */ + DRM_ASAHI_CMD_COMPUTE = 1, + + /** + * @DRM_ASAHI_SET_VERTEX_ATTACHMENTS: Software command to set + * attachments for subsequent vertex shaders in the same submit. + * + * Followed by (possibly multiple) @drm_asahi_attachment payloads. + */ + DRM_ASAHI_SET_VERTEX_ATTACHMENTS = 2, + + /** + * @DRM_ASAHI_SET_FRAGMENT_ATTACHMENTS: Software command to set + * attachments for subsequent fragment shaders in the same submit. + * + * Followed by (possibly multiple) @drm_asahi_attachment payloads. + */ + DRM_ASAHI_SET_FRAGMENT_ATTACHMENTS = 3, + + /** + * @DRM_ASAHI_SET_COMPUTE_ATTACHMENTS: Software command to set + * attachments for subsequent compute shaders in the same submit. + * + * Followed by (possibly multiple) @drm_asahi_attachment payloads. + */ + DRM_ASAHI_SET_COMPUTE_ATTACHMENTS = 4, +}; + +/** + * enum drm_asahi_priority - Scheduling queue priority. + * + * These priorities are forwarded to the firmware to influence firmware + * scheduling. The exact policy is ultimately decided by firmware, but + * these enums allow userspace to communicate the intentions. + */ +enum drm_asahi_priority { + /** @DRM_ASAHI_PRIORITY_LOW: Low priority queue. */ + DRM_ASAHI_PRIORITY_LOW = 0, + + /** @DRM_ASAHI_PRIORITY_MEDIUM: Medium priority queue. */ + DRM_ASAHI_PRIORITY_MEDIUM = 1, + + /** + * @DRM_ASAHI_PRIORITY_HIGH: High priority queue. + * + * Reserved for future extension. + */ + DRM_ASAHI_PRIORITY_HIGH = 2, + + /** + * @DRM_ASAHI_PRIORITY_REALTIME: Real-time priority queue. + * + * Reserved for future extension. + */ + DRM_ASAHI_PRIORITY_REALTIME = 3, +}; + +/** + * struct drm_asahi_queue_create - Arguments passed to + * DRM_IOCTL_ASAHI_QUEUE_CREATE + */ +struct drm_asahi_queue_create { + /** @flags: MBZ */ + __u32 flags; + + /** @vm_id: The ID of the VM this queue is bound to */ + __u32 vm_id; + + /** @priority: One of drm_asahi_priority */ + __u32 priority; + + /** @queue_id: The returned queue ID */ + __u32 queue_id; + + /** + * @usc_exec_base: GPU base address for all USC binaries (shaders) on + * this queue. USC addresses are 32-bit relative to this 64-bit base. + * + * This sets the following registers on all queue commands: + * + * USC_EXEC_BASE_TA (vertex) + * USC_EXEC_BASE_ISP (fragment) + * USC_EXEC_BASE_CP (compute) + * + * While the hardware lets us configure these independently per command, + * we do not have a use case for this. Instead, we expect userspace to + * fix a 4GiB VA carveout for USC memory and pass its base address here. + */ + __u64 usc_exec_base; +}; + +/** + * struct drm_asahi_queue_destroy - Arguments passed to + * DRM_IOCTL_ASAHI_QUEUE_DESTROY + */ +struct drm_asahi_queue_destroy { + /** @queue_id: The queue ID to be destroyed */ + __u32 queue_id; + + /** @pad: MBZ */ + __u32 pad; +}; + +/** + * enum drm_asahi_sync_type - Sync item type + */ +enum drm_asahi_sync_type { + /** @DRM_ASAHI_SYNC_SYNCOBJ: Binary sync object */ + DRM_ASAHI_SYNC_SYNCOBJ = 0, + + /** @DRM_ASAHI_SYNC_TIMELINE_SYNCOBJ: Timeline sync object */ + DRM_ASAHI_SYNC_TIMELINE_SYNCOBJ = 1, +}; + +/** + * struct drm_asahi_sync - Sync item + */ +struct drm_asahi_sync { + /** @sync_type: One of drm_asahi_sync_type */ + __u32 sync_type; + + /** @handle: The sync object handle */ + __u32 handle; + + /** @timeline_value: Timeline value for timeline sync objects */ + __u64 timeline_value; +}; + +/** + * define DRM_ASAHI_BARRIER_NONE - Command index for no barrier + * + * This special value may be passed in to drm_asahi_command::vdm_barrier or + * drm_asahi_command::cdm_barrier to indicate that the respective subqueue + * should not wait on any previous work. + */ +#define DRM_ASAHI_BARRIER_NONE (0xFFFFu) + +/** + * struct drm_asahi_cmd_header - Top level command structure + * + * This struct is core to the command buffer definition and therefore is not + * extensible. + */ +struct drm_asahi_cmd_header { + /** @cmd_type: One of drm_asahi_cmd_type */ + __u16 cmd_type; + + /** + * @size: Size of this command, not including this header. + * + * For hardware commands, this enables extensibility of commands without + * requiring extra command types. Passing a command that is shorter + * than expected is explicitly allowed for backwards-compatibility. + * Truncated fields will be zeroed. + * + * For the synthetic attachment setting commands, this implicitly + * encodes the number of attachments. These commands take multiple + * fixed-size @drm_asahi_attachment structures as their payload, so size + * equals number of attachments * sizeof(struct drm_asahi_attachment). + */ + __u16 size; + + /** + * @vdm_barrier: VDM (render) command index to wait on. + * + * Barriers are indices relative to the beginning of a given submit. A + * barrier of 0 waits on commands submitted to the respective subqueue + * in previous submit ioctls. A barrier of N waits on N previous + * commands on the subqueue within the current submit ioctl. As a + * special case, passing @DRM_ASAHI_BARRIER_NONE avoids waiting on any + * commands in the subqueue. + * + * Examples: + * + * 0: This waits on all previous work. + * + * NONE: This does not wait for anything on this subqueue. + * + * 1: This waits on the first render command in the submit. + * This is valid only if there are multiple render commands in the + * same submit. + * + * Barriers are valid only for hardware commands. Synthetic software + * commands to set attachments must pass NONE here. + */ + __u16 vdm_barrier; + + /** + * @cdm_barrier: CDM (compute) command index to wait on. + * + * See @vdm_barrier, and replace VDM/render with CDM/compute. + */ + __u16 cdm_barrier; +}; + +/** + * struct drm_asahi_submit - Arguments passed to DRM_IOCTL_ASAHI_SUBMIT + */ +struct drm_asahi_submit { + /** + * @syncs: An optional pointer to an array of drm_asahi_sync. The first + * @in_sync_count elements are in-syncs, then the remaining + * @out_sync_count elements are out-syncs. Using a single array with + * explicit partitioning simplifies handling. + */ + __u64 syncs; + + /** + * @cmdbuf: Pointer to the command buffer to submit. + * + * This is a flat command buffer. By design, it contains no CPU + * pointers, which makes it suitable for a virtgpu wire protocol without + * requiring any serializing/deserializing step. + * + * It consists of a series of commands. Each command begins with a + * fixed-size @drm_asahi_cmd_header header and is followed by a + * variable-length payload according to the type and size in the header. + * + * The combined count of "real" hardware commands must be nonzero and at + * most drm_asahi_params_global::max_commands_per_submission. + */ + __u64 cmdbuf; + + /** @flags: Flags for command submission (MBZ) */ + __u32 flags; + + /** @queue_id: The queue ID to be submitted to */ + __u32 queue_id; + + /** + * @in_sync_count: Number of sync objects to wait on before starting + * this job. + */ + __u32 in_sync_count; + + /** + * @out_sync_count: Number of sync objects to signal upon completion of + * this job. + */ + __u32 out_sync_count; + + /** @cmdbuf_size: Command buffer size in bytes */ + __u32 cmdbuf_size; + + /** @pad: MBZ */ + __u32 pad; +}; + +/** + * struct drm_asahi_attachment - Describe an "attachment". + * + * Attachments are any memory written by shaders, notably including render + * target attachments written by the end-of-tile program. This is purely a hint + * about the accessed memory regions. It is optional to specify, which is + * fortunate as it cannot be specified precisely with bindless access anyway. + * But where possible, it's probably a good idea for userspace to include these + * hints, forwarded to the firmware. + * + * This struct is implicitly sized and therefore is not extensible. + */ +struct drm_asahi_attachment { + /** @pointer: Base address of the attachment */ + __u64 pointer; + + /** @size: Size of the attachment in bytes */ + __u64 size; + + /** @pad: MBZ */ + __u32 pad; + + /** @flags: MBZ */ + __u32 flags; +}; + +enum drm_asahi_render_flags { + /** + * @DRM_ASAHI_RENDER_VERTEX_SCRATCH: A vertex stage shader uses scratch + * memory. + */ + DRM_ASAHI_RENDER_VERTEX_SCRATCH = (1U << 0), + + /** + * @DRM_ASAHI_RENDER_PROCESS_EMPTY_TILES: Process even empty tiles. + * This must be set when clearing render targets. + */ + DRM_ASAHI_RENDER_PROCESS_EMPTY_TILES = (1U << 1), + + /** + * @DRM_ASAHI_RENDER_NO_VERTEX_CLUSTERING: Run vertex stage on a single + * cluster (on multi-cluster GPUs) + * + * This harms performance but can workaround certain sync/coherency + * bugs, and therefore is useful for debugging. + */ + DRM_ASAHI_RENDER_NO_VERTEX_CLUSTERING = (1U << 2), + + /** + * @DRM_ASAHI_RENDER_DBIAS_IS_INT: Use integer depth bias formula. + * + * Graphics specifications contain two alternate formulas for depth + * bias, a float formula used with floating-point depth buffers and an + * integer formula using with unorm depth buffers. This flag specifies + * that the integer formula should be used. If omitted, the float + * formula is used instead. + * + * This corresponds to bit 18 of the relevant hardware control register, + * so we match that here for efficiency. + */ + DRM_ASAHI_RENDER_DBIAS_IS_INT = (1U << 18), +}; + +/** + * struct drm_asahi_zls_buffer - Describe a depth or stencil buffer. + * + * These fields correspond to hardware registers in the ZLS (Z Load/Store) unit. + * There are three hardware registers for each field respectively for loads, + * stores, and partial renders. In practice, it makes sense to set all to the + * same values, except in exceptional cases not yet implemented in userspace, so + * we do not duplicate here for simplicity/efficiency. + * + * This struct is embedded in other structs and therefore is not extensible. + */ +struct drm_asahi_zls_buffer { + /** @base: Base address of the buffer */ + __u64 base; + + /** + * @comp_base: If the load buffer is compressed, address of the + * compression metadata section. + */ + __u64 comp_base; + + /** + * @stride: If layered rendering is enabled, the number of bytes + * between each layer of the buffer. + */ + __u32 stride; + + /** + * @comp_stride: If layered rendering is enabled, the number of bytes + * between each layer of the compression metadata. + */ + __u32 comp_stride; +}; + +/** + * struct drm_asahi_timestamp - Describe a timestamp write. + * + * The firmware can optionally write the GPU timestamp at render pass + * granularities, but it needs to be mapped specially via + * DRM_IOCTL_ASAHI_GEM_BIND_OBJECT. This structure therefore describes where to + * write as a handle-offset pair, rather than a GPU address like normal. + * + * This struct is embedded in other structs and therefore is not extensible. + */ +struct drm_asahi_timestamp { + /** + * @handle: Handle of the timestamp buffer, or 0 to skip this + * timestamp. If nonzero, this must equal the value returned in + * drm_asahi_gem_bind_object::object_handle. + */ + __u32 handle; + + /** @offset: Offset to write into the timestamp buffer */ + __u32 offset; +}; + +/** + * struct drm_asahi_timestamps - Describe timestamp writes. + * + * Each operation that can be timestamped, can be timestamped at the start and + * end. Therefore, drm_asahi_timestamp structs always come in pairs, bundled + * together into drm_asahi_timestamps. + * + * This struct is embedded in other structs and therefore is not extensible. + */ +struct drm_asahi_timestamps { + /** @start: Timestamp recorded at the start of the operation */ + struct drm_asahi_timestamp start; + + /** @end: Timestamp recorded at the end of the operation */ + struct drm_asahi_timestamp end; +}; + +/** + * struct drm_asahi_helper_program - Describe helper program configuration. + * + * The helper program is a compute-like kernel required for various hardware + * functionality. Its most important role is dynamically allocating + * scratch/stack memory for individual subgroups, by partitioning a static + * allocation shared for the whole device. It is supplied by userspace via + * drm_asahi_helper_program and internally dispatched by the hardware as needed. + * + * This struct is embedded in other structs and therefore is not extensible. + */ +struct drm_asahi_helper_program { + /** + * @binary: USC address to the helper program binary. This is a tagged + * pointer with configuration in the bottom bits. + */ + __u32 binary; + + /** @cfg: Additional configuration bits for the helper program. */ + __u32 cfg; + + /** + * @data: Data passed to the helper program. This value is not + * interpreted by the kernel, firmware, or hardware in any way. It is + * simply a sideband for userspace, set with the submit ioctl and read + * via special registers inside the helper program. + * + * In practice, userspace will pass a 64-bit GPU VA here pointing to the + * actual arguments, which presumably don't fit in 64-bits. + */ + __u64 data; +}; + +/** + * struct drm_asahi_bg_eot - Describe a background or end-of-tile program. + * + * The background and end-of-tile programs are dispatched by the hardware at the + * beginning and end of rendering. As the hardware "tilebuffer" is simply local + * memory, these programs are necessary to implement API-level render targets. + * The fragment-like background program is responsible for loading either the + * clear colour or the existing render target contents, while the compute-like + * end-of-tile program stores the tilebuffer contents to memory. + * + * This struct is embedded in other structs and therefore is not extensible. + */ +struct drm_asahi_bg_eot { + /** + * @usc: USC address of the hardware USC words binding resources + * (including images and uniforms) and the program itself. Note this is + * an additional layer of indirection compared to the helper program, + * avoiding the need for a sideband for data. This is a tagged pointer + * with additional configuration in the bottom bits. + */ + __u32 usc; + + /** + * @rsrc_spec: Resource specifier for the program. This is a packed + * hardware data structure describing the required number of registers, + * uniforms, bound textures, and bound samplers. + */ + __u32 rsrc_spec; +}; + +/** + * struct drm_asahi_cmd_render - Command to submit 3D + * + * This command submits a single render pass. The hardware control stream may + * include many draws and subpasses, but within the command, the framebuffer + * dimensions and attachments are fixed. + * + * The hardware requires the firmware to set a large number of Control Registers + * setting up state at render pass granularity before each command rendering 3D. + * The firmware bundles this state into data structures. Unfortunately, we + * cannot expose either any of that directly to userspace, because the + * kernel-firmware ABI is not stable. Although we can guarantee the firmware + * updates in tandem with the kernel, we cannot break old userspace when + * upgrading the firmware and kernel. Therefore, we need to abstract well the + * data structures to avoid tying our hands with future firmwares. + * + * The bulk of drm_asahi_cmd_render therefore consists of values of hardware + * control registers, marshalled via the firmware interface. + * + * The framebuffer/tilebuffer dimensions are also specified here. In addition to + * being passed to the firmware/hardware, the kernel requires these dimensions + * to calculate various essential tiling-related data structures. It is + * unfortunate that our submits are heavier than on vendors with saner + * hardware-software interfaces. The upshot is all of this information is + * readily available to userspace with all current APIs. + * + * It looks odd - but it's not overly burdensome and it ensures we can remain + * compatible with old userspace. + */ +struct drm_asahi_cmd_render { + /** @flags: Combination of drm_asahi_render_flags flags. */ + __u32 flags; + + /** + * @isp_zls_pixels: ISP_ZLS_PIXELS register value. This contains the + * depth/stencil width/height, which may differ from the framebuffer + * width/height. + */ + __u32 isp_zls_pixels; + + /** + * @vdm_ctrl_stream_base: VDM_CTRL_STREAM_BASE register value. GPU + * address to the beginning of the VDM control stream. + */ + __u64 vdm_ctrl_stream_base; + + /** @vertex_helper: Helper program used for the vertex shader */ + struct drm_asahi_helper_program vertex_helper; + + /** @fragment_helper: Helper program used for the fragment shader */ + struct drm_asahi_helper_program fragment_helper; + + /** + * @isp_scissor_base: ISP_SCISSOR_BASE register value. GPU address of an + * array of scissor descriptors indexed in the render pass. + */ + __u64 isp_scissor_base; + + /** + * @isp_dbias_base: ISP_DBIAS_BASE register value. GPU address of an + * array of depth bias values indexed in the render pass. + */ + __u64 isp_dbias_base; + + /** + * @isp_oclqry_base: ISP_OCLQRY_BASE register value. GPU address of an + * array of occlusion query results written by the render pass. + */ + __u64 isp_oclqry_base; + + /** @depth: Depth buffer */ + struct drm_asahi_zls_buffer depth; + + /** @stencil: Stencil buffer */ + struct drm_asahi_zls_buffer stencil; + + /** @zls_ctrl: ZLS_CTRL register value */ + __u64 zls_ctrl; + + /** @ppp_multisamplectl: PPP_MULTISAMPLECTL register value */ + __u64 ppp_multisamplectl; + + /** + * @sampler_heap: Base address of the sampler heap. This heap is used + * for both vertex shaders and fragment shaders. The registers are + * per-stage, but there is no known use case for separate heaps. + */ + __u64 sampler_heap; + + /** @ppp_ctrl: PPP_CTRL register value */ + __u32 ppp_ctrl; + + /** @width_px: Framebuffer width in pixels */ + __u16 width_px; + + /** @height_px: Framebuffer height in pixels */ + __u16 height_px; + + /** @layers: Number of layers in the framebuffer */ + __u16 layers; + + /** @sampler_count: Number of samplers in the sampler heap. */ + __u16 sampler_count; + + /** @utile_width_px: Width of a logical tilebuffer tile in pixels */ + __u8 utile_width_px; + + /** @utile_height_px: Height of a logical tilebuffer tile in pixels */ + __u8 utile_height_px; + + /** @samples: # of samples in the framebuffer. Must be 1, 2, or 4. */ + __u8 samples; + + /** @sample_size_B: # of bytes in the tilebuffer required per sample. */ + __u8 sample_size_B; + + /** + * @isp_merge_upper_x: 32-bit float used in the hardware triangle + * merging. Calculate as: tan(60 deg) * width. + * + * Making these values UAPI avoids requiring floating-point calculations + * in the kernel in the hot path. + */ + __u32 isp_merge_upper_x; + + /** + * @isp_merge_upper_y: 32-bit float. Calculate as: tan(60 deg) * height. + * See @isp_merge_upper_x. + */ + __u32 isp_merge_upper_y; + + /** @bg: Background program run for each tile at the start */ + struct drm_asahi_bg_eot bg; + + /** @eot: End-of-tile program ran for each tile at the end */ + struct drm_asahi_bg_eot eot; + + /** + * @partial_bg: Background program ran at the start of each tile when + * resuming the render pass during a partial render. + */ + struct drm_asahi_bg_eot partial_bg; + + /** + * @partial_eot: End-of-tile program ran at the end of each tile when + * pausing the render pass during a partial render. + */ + struct drm_asahi_bg_eot partial_eot; + + /** + * @isp_bgobjdepth: ISP_BGOBJDEPTH register value. This is the depth + * buffer clear value, encoded in the depth buffer's format: either a + * 32-bit float or a 16-bit unorm (with upper bits zeroed). + */ + __u32 isp_bgobjdepth; + + /** + * @isp_bgobjvals: ISP_BGOBJVALS register value. The bottom 8-bits + * contain the stencil buffer clear value. + */ + __u32 isp_bgobjvals; + + /** @ts_vtx: Timestamps for the vertex portion of the render */ + struct drm_asahi_timestamps ts_vtx; + + /** @ts_frag: Timestamps for the fragment portion of the render */ + struct drm_asahi_timestamps ts_frag; +}; + +/** + * struct drm_asahi_cmd_compute - Command to submit compute + * + * This command submits a control stream consisting of compute dispatches. There + * is essentially no limit on how many compute dispatches may be included in a + * single compute command, although timestamps are at command granularity. + */ +struct drm_asahi_cmd_compute { + /** @flags: MBZ */ + __u32 flags; + + /** @sampler_count: Number of samplers in the sampler heap. */ + __u32 sampler_count; + + /** + * @cdm_ctrl_stream_base: CDM_CTRL_STREAM_BASE register value. GPU + * address to the beginning of the CDM control stream. + */ + __u64 cdm_ctrl_stream_base; + + /** + * @cdm_ctrl_stream_end: GPU base address to the end of the hardware + * control stream. Note this only considers the first contiguous segment + * of the control stream, as the stream might jump elsewhere. + */ + __u64 cdm_ctrl_stream_end; + + /** @sampler_heap: Base address of the sampler heap. */ + __u64 sampler_heap; + + /** @helper: Helper program used for this compute command */ + struct drm_asahi_helper_program helper; + + /** @ts: Timestamps for the compute command */ + struct drm_asahi_timestamps ts; +}; + +/** + * struct drm_asahi_get_time - Arguments passed to DRM_IOCTL_ASAHI_GET_TIME + */ +struct drm_asahi_get_time { + /** @flags: MBZ. */ + __u64 flags; + + /** @gpu_timestamp: On return, the GPU timestamp in nanoseconds. */ + __u64 gpu_timestamp; +}; + +/** + * DRM_IOCTL_ASAHI() - Build an Asahi IOCTL number + * @__access: Access type. Must be R, W or RW. + * @__id: One of the DRM_ASAHI_xxx id. + * @__type: Suffix of the type being passed to the IOCTL. + * + * Don't use this macro directly, use the DRM_IOCTL_ASAHI_xxx + * values instead. + * + * Return: An IOCTL number to be passed to ioctl() from userspace. + */ +#define DRM_IOCTL_ASAHI(__access, __id, __type) \ + DRM_IO##__access(DRM_COMMAND_BASE + DRM_ASAHI_##__id, \ + struct drm_asahi_##__type) + +/* Note: this is an enum so that it can be resolved by Rust bindgen. */ +enum { + DRM_IOCTL_ASAHI_GET_PARAMS = DRM_IOCTL_ASAHI(W, GET_PARAMS, get_params), + DRM_IOCTL_ASAHI_GET_TIME = DRM_IOCTL_ASAHI(WR, GET_TIME, get_time), + DRM_IOCTL_ASAHI_VM_CREATE = DRM_IOCTL_ASAHI(WR, VM_CREATE, vm_create), + DRM_IOCTL_ASAHI_VM_DESTROY = DRM_IOCTL_ASAHI(W, VM_DESTROY, vm_destroy), + DRM_IOCTL_ASAHI_VM_BIND = DRM_IOCTL_ASAHI(W, VM_BIND, vm_bind), + DRM_IOCTL_ASAHI_GEM_CREATE = DRM_IOCTL_ASAHI(WR, GEM_CREATE, gem_create), + DRM_IOCTL_ASAHI_GEM_MMAP_OFFSET = DRM_IOCTL_ASAHI(WR, GEM_MMAP_OFFSET, gem_mmap_offset), + DRM_IOCTL_ASAHI_GEM_BIND_OBJECT = DRM_IOCTL_ASAHI(WR, GEM_BIND_OBJECT, gem_bind_object), + DRM_IOCTL_ASAHI_QUEUE_CREATE = DRM_IOCTL_ASAHI(WR, QUEUE_CREATE, queue_create), + DRM_IOCTL_ASAHI_QUEUE_DESTROY = DRM_IOCTL_ASAHI(W, QUEUE_DESTROY, queue_destroy), + DRM_IOCTL_ASAHI_SUBMIT = DRM_IOCTL_ASAHI(W, SUBMIT, submit), +}; + +#if defined(__cplusplus) +} +#endif + +#endif /* _ASAHI_DRM_H_ */ diff --git a/src/detection/gpu/d3dkmthk.h b/src/detection/gpu/d3dkmthk.h new file mode 100644 index 0000000..da0087b --- /dev/null +++ b/src/detection/gpu/d3dkmthk.h @@ -0,0 +1,362 @@ +#pragma once + +#include <stdint.h> +#include <limits.h> +#include <assert.h> +#include <stdalign.h> + +#if _WIN32 + #include <ntdef.h> + #include <windef.h> +#else + #include <sys/ioctl.h> + #include <uchar.h> + #include <errno.h> + +typedef struct _LUID { + uint32_t LowPart; + uint32_t HighPart; +} LUID; + +typedef uint32_t UINT; +typedef uint64_t ULONGLONG; +typedef uint64_t UINT64; +typedef uint32_t UINT32; +typedef uint16_t UINT16; +typedef uint32_t ULONG; +typedef uint16_t USHORT; +typedef uint8_t UCHAR; +typedef void VOID; +typedef char16_t WCHAR; +typedef void* HANDLE; +typedef uint8_t BYTE; +typedef int32_t BOOL; +typedef uint8_t BOOLEAN; +typedef union { + struct { + uint32_t LowPart; + int32_t HighPart; + } u; + int64_t QuadPart; +} LARGE_INTEGER; +typedef int32_t NTSTATUS; // 0 for success, -1 for failure + #define NT_SUCCESS(Status) ((NTSTATUS) (Status) >= 0) + #define _In_range_(low, hi) +#endif + +#define D3DKMT_ALIGN64 alignas(8) + +typedef struct D3DKMT_HANDLE { + union { + struct { + uint32_t Instance : 6; + uint32_t Index : 24; + uint32_t Unique : 2; + }; + uint32_t Value; + }; +} D3DKMT_HANDLE; + +typedef struct _D3DKMT_OPENADAPTERFROMLUID { + LUID AdapterLuid; + D3DKMT_HANDLE hAdapter; +} D3DKMT_OPENADAPTERFROMLUID; + +typedef struct _D3DKMT_ADAPTERINFO { + D3DKMT_HANDLE hAdapter; + LUID AdapterLuid; + ULONG NumOfSources; + BOOL bPrecisePresentRegionsPreferred; +} D3DKMT_ADAPTERINFO; + +#define MAX_ENUM_ADAPTERS 16 +typedef struct _D3DKMT_ENUMADAPTERS { + _In_range_(0, MAX_ENUM_ADAPTERS) ULONG NumAdapters; + D3DKMT_ADAPTERINFO Adapters[MAX_ENUM_ADAPTERS]; +} D3DKMT_ENUMADAPTERS; + +typedef struct _D3DKMT_ENUMADAPTERS2 { + ULONG NumAdapters; // in/out: On input, the count of the pAdapters array buffer. On output, the number of adapters enumerated. + D3DKMT_ADAPTERINFO* pAdapters; // out: Array of enumerated adapters containing NumAdapters elements +} D3DKMT_ENUMADAPTERS2; +static_assert(sizeof(D3DKMT_ENUMADAPTERS2) == +#if SIZE_MAX == UINT64_MAX + 0x10 +#else + 0x08 +#endif + , + "D3DKMT_ENUMADAPTERS2 structure size mismatch"); + +typedef struct _D3DKMT_ADAPTERREGISTRYINFO { + WCHAR AdapterString[260]; + WCHAR BiosString[260]; + WCHAR DacType[260]; + WCHAR ChipType[260]; +} D3DKMT_ADAPTERREGISTRYINFO; + +typedef struct _D3DKMT_CLOSEADAPTER { + D3DKMT_HANDLE hAdapter; // in: adapter handle +} D3DKMT_CLOSEADAPTER; + +typedef struct _D3DKMT_SEGMENTSIZEINFO { + D3DKMT_ALIGN64 ULONGLONG DedicatedVideoMemorySize; + D3DKMT_ALIGN64 ULONGLONG DedicatedSystemMemorySize; + D3DKMT_ALIGN64 ULONGLONG SharedSystemMemorySize; +} D3DKMT_SEGMENTSIZEINFO; + +typedef struct _D3DKMT_ADAPTERTYPE { + union { + struct { + UINT RenderSupported : 1; // WDDM 1.2, Windows 8 + UINT DisplaySupported : 1; + UINT SoftwareDevice : 1; + UINT PostDevice : 1; + UINT HybridDiscrete : 1; // WDDM 1.3, Windows 8.1 + UINT HybridIntegrated : 1; + UINT IndirectDisplayDevice : 1; + UINT Paravirtualized : 1; // WDDM 2.3, Windows 10 Fall Creators Update (version 1709) + UINT ACGSupported : 1; + UINT SupportSetTimingsFromVidPn : 1; + UINT Detachable : 1; + UINT ComputeOnly : 1; // WDDM 2.6, Windows 10 May 2019 Update (Version 1903) + UINT Prototype : 1; + UINT RuntimePowerManagement : 1; // WDDM 2.9, Windows 10 Insider Preview "Iron" + UINT Reserved : 18; + }; + UINT Value; + }; +} D3DKMT_ADAPTERTYPE; + +typedef struct _D3DKMT_ADAPTERADDRESS { + UINT BusNumber; // Bus number on which the physical device is located. + UINT DeviceNumber; // Index of the physical device on the bus. + UINT FunctionNumber; // Function number of the adapter on the physical device. +} D3DKMT_ADAPTERADDRESS; + +typedef struct _D3DKMT_DEVICE_IDS { + UINT VendorID; + UINT DeviceID; + UINT SubVendorID; + UINT SubSystemID; + UINT RevisionID; + UINT BusType; +} D3DKMT_DEVICE_IDS; + +typedef struct _D3DKMT_UMD_DRIVER_VERSION { + D3DKMT_ALIGN64 LARGE_INTEGER DriverVersion; +} D3DKMT_UMD_DRIVER_VERSION; + +typedef struct _D3DKMT_QUERY_DEVICE_IDS { + UINT PhysicalAdapterIndex; // in: + D3DKMT_DEVICE_IDS DeviceIds; // out: +} D3DKMT_QUERY_DEVICE_IDS; + +typedef enum _QAI_DRIVERVERSION { + KMT_DRIVERVERSION_WDDM_1_0 = 1000, // Windows Vista + KMT_DRIVERVERSION_WDDM_1_1_PRERELEASE = 1102, // Windows Vista with prereleased Win7 features + KMT_DRIVERVERSION_WDDM_1_1 = 1105, // Windows 7 + KMT_DRIVERVERSION_WDDM_1_2 = 1200, // Windows 8 + KMT_DRIVERVERSION_WDDM_1_3 = 1300, // Windows 8.1 + KMT_DRIVERVERSION_WDDM_2_0 = 2000, // Windows 10 + KMT_DRIVERVERSION_WDDM_2_1 = 2100, // Windows 10 (1607) + KMT_DRIVERVERSION_WDDM_2_2 = 2200, // Windows 10 (1703) + KMT_DRIVERVERSION_WDDM_2_3 = 2300, // Windows 10 (1709) + KMT_DRIVERVERSION_WDDM_2_4 = 2400, // Windows 10 (1803) + KMT_DRIVERVERSION_WDDM_2_5 = 2500, // Windows 10 (1809) + KMT_DRIVERVERSION_WDDM_2_6 = 2600, // Windows 10 (1903) + KMT_DRIVERVERSION_WDDM_2_7 = 2700, // Windows 10 (2004) + KMT_DRIVERVERSION_WDDM_2_8 = 2800, // Windows 11 Insider Preview Manganese + KMT_DRIVERVERSION_WDDM_2_9 = 2900, // Windows 11 Insider Preview Iron + KMT_DRIVERVERSION_WDDM_3_0 = 3000, // Windows 11 (21H2) + KMT_DRIVERVERSION_WDDM_3_1 = 3100, // Windows 11 (22H2) + KMT_DRIVERVERSION_WDDM_3_2 = 3200, // Windows 11 (24H2) +} D3DKMT_DRIVERVERSION; + +typedef struct _D3DKMT_QUERY_ADAPTER_UNIQUE_GUID { + WCHAR AdapterUniqueGUID[40]; +} D3DKMT_QUERY_ADAPTER_UNIQUE_GUID; + +typedef enum _KMTQUERYADAPTERINFOTYPE { + KMTQAITYPE_GETSEGMENTSIZE = 3, + KMTQAITYPE_ADAPTERGUID = 4, + KMTQAITYPE_ADAPTERADDRESS = 6, + KMTQAITYPE_ADAPTERREGISTRYINFO = 8, + KMTQAITYPE_DRIVERVERSION = 13, + KMTQAITYPE_ADAPTERTYPE = 15, // WDDM 1.2, Windows 8 + KMTQAITYPE_UMD_DRIVER_VERSION = 18, + KMTQAITYPE_NODEMETADATA = 25, // WDDM 2.0, Windows 10 + KMTQAITYPE_PHYSICALADAPTERDEVICEIDS = 31, + KMTQAITYPE_QUERY_ADAPTER_UNIQUE_GUID = 60, // WDDM 2.4, Windows 10 (1803) + KMTQAITYPE_NODEPERFDATA = 61, + KMTQAITYPE_ADAPTERPERFDATA = 62, +} KMTQUERYADAPTERINFOTYPE; + +typedef struct _D3DKMT_QUERYADAPTERINFO { + D3DKMT_HANDLE hAdapter; + KMTQUERYADAPTERINFOTYPE Type; + VOID* pPrivateDriverData; + UINT PrivateDriverDataSize; +} D3DKMT_QUERYADAPTERINFO; + +#define D3DKMT_MAX_SEGMENT_COUNT 32 + +typedef enum _D3DKMT_MEMORY_SEGMENT_GROUP { + D3DKMT_MEMORY_SEGMENT_GROUP_LOCAL = 0, + D3DKMT_MEMORY_SEGMENT_GROUP_NON_LOCAL = 1 +} D3DKMT_MEMORY_SEGMENT_GROUP; + +typedef enum _D3DKMT_QUERYSTATISTICS_TYPE { + D3DKMT_QUERYSTATISTICS_SEGMENT_GROUP_USAGE = 17, // WDDM 3.1, Windows 11 2022 Update (version 22H2) +} D3DKMT_QUERYSTATISTICS_TYPE; + +typedef struct _D3DKMT_ADAPTER_PERFDATA { + UINT32 PhysicalAdapterIndex; // in: The physical adapter index, in an LDA chain + D3DKMT_ALIGN64 ULONGLONG MemoryFrequency; // out: Clock frequency of the memory in hertz + D3DKMT_ALIGN64 ULONGLONG MaxMemoryFrequency; // out: Max memory clock frequency + D3DKMT_ALIGN64 ULONGLONG MaxMemoryFrequencyOC; // out: Clock frequency of the memory while overclocked in hertz. + D3DKMT_ALIGN64 ULONGLONG MemoryBandwidth; // out: Amount of memory transferred in bytes + D3DKMT_ALIGN64 ULONGLONG PCIEBandwidth; // out: Amount of memory transferred over PCI-E in bytes + ULONG FanRPM; // out: Fan rpm + ULONG Power; // out: Power draw of the adapter in tenths of a percentage + ULONG Temperature; // out: Temperature in deci-Celsius 1 = 0.1C + UCHAR PowerStateOverride; // out: Overrides dxgkrnls power view of linked adapters. +} D3DKMT_ADAPTER_PERFDATA; + +typedef struct _D3DKMT_NODE_PERFDATA { + UINT32 NodeOrdinal; // in: Node ordinal of the requested engine. + UINT32 PhysicalAdapterIndex; // in: The physical adapter index, in an LDA chain + D3DKMT_ALIGN64 ULONGLONG Frequency; // out: Clock frequency of the engine in hertz + D3DKMT_ALIGN64 ULONGLONG MaxFrequency; // out: Max engine clock frequency + D3DKMT_ALIGN64 ULONGLONG MaxFrequencyOC; // out: Max engine over clock frequency + ULONG Voltage; // out: Voltage of the engine in milli volts mV + ULONG VoltageMax; // out: Max voltage levels in milli volts. + ULONG VoltageMaxOC; // out: Max voltage level while overclocked in milli volts. + // WDDM 2.5 + D3DKMT_ALIGN64 ULONGLONG MaxTransitionLatency; // out: Max transition latency to change the frequency in 100 nanoseconds +} D3DKMT_NODE_PERFDATA; + +typedef struct _D3DKMT_QUERYSTATISTICS_MEMORY_USAGE { + D3DKMT_ALIGN64 UINT64 AllocatedBytes; + D3DKMT_ALIGN64 UINT64 FreeBytes; + D3DKMT_ALIGN64 UINT64 ZeroBytes; + D3DKMT_ALIGN64 UINT64 ModifiedBytes; + D3DKMT_ALIGN64 UINT64 StandbyBytes; +} D3DKMT_QUERYSTATISTICS_MEMORY_USAGE; + +typedef union _D3DKMT_QUERYSTATISTICS_RESULT { + D3DKMT_QUERYSTATISTICS_MEMORY_USAGE SegmentGroupUsageInformation; + uint8_t Padding[776]; +} D3DKMT_QUERYSTATISTICS_RESULT; + +typedef struct _D3DKMT_QUERYSTATISTICS_QUERY_SEGMENT_GROUP_USAGE { + UINT16 PhysicalAdapterIndex; + UINT16 SegmentGroup; // D3DKMT_MEMORY_SEGMENT_GROUP +} D3DKMT_QUERYSTATISTICS_QUERY_SEGMENT_GROUP_USAGE; + +typedef struct _D3DKMT_QUERYSTATISTICS { + D3DKMT_QUERYSTATISTICS_TYPE Type; // in: type of data requested + LUID AdapterLuid; // in: adapter to get export / statistics from + HANDLE* hProcess; // in: process to get statistics for, if required for this query type + D3DKMT_QUERYSTATISTICS_RESULT QueryResult; // out: requested data + + union { + D3DKMT_QUERYSTATISTICS_QUERY_SEGMENT_GROUP_USAGE QuerySegmentGroupUsage; + }; +} D3DKMT_QUERYSTATISTICS; +static_assert(sizeof(D3DKMT_QUERYSTATISTICS) == +#if SIZE_MAX == UINT64_MAX + 0x328 +#else + 0x320 +#endif + , + "D3DKMT_QUERYSTATISTICS structure size mismatch"); + +#define DXGK_MAX_METADATA_NAME_LENGTH 32 +typedef enum { + DXGK_ENGINE_TYPE_OTHER, + DXGK_ENGINE_TYPE_3D, + DXGK_ENGINE_TYPE_VIDEO_DECODE, + DXGK_ENGINE_TYPE_VIDEO_ENCODE, + DXGK_ENGINE_TYPE_VIDEO_PROCESSING, + DXGK_ENGINE_TYPE_SCENE_ASSEMBLY, + DXGK_ENGINE_TYPE_COPY, + DXGK_ENGINE_TYPE_OVERLAY, + DXGK_ENGINE_TYPE_CRYPTO, + DXGK_ENGINE_TYPE_VIDEO_CODEC, + DXGK_ENGINE_TYPE_MAX +} DXGK_ENGINE_TYPE; + +typedef struct _DXGK_NODEMETADATA_FLAGS { + union { + struct { + UINT ContextSchedulingSupported : 1; // WDDM 2.2 + UINT RingBufferFenceRelease : 1; // WDDM 2.5 + UINT SupportTrackedWorkload : 1; + UINT UserModeSubmission : 1; + UINT SupportBuildTestCommandBuffer : 1; // WDDM 3.2 + UINT Reserved : 11; + UINT MaxInFlightHwQueueBuffers : 16; + }; + UINT32 Value; + }; +} DXGK_NODEMETADATA_FLAGS; + +typedef struct _DXGK_NODEMETADATA { + DXGK_ENGINE_TYPE EngineType; + WCHAR FriendlyName[DXGK_MAX_METADATA_NAME_LENGTH]; + DXGK_NODEMETADATA_FLAGS Flags; // WDDM 2.2 + BOOLEAN GpuMmuSupported; // WDDM 2.0 ??? + BOOLEAN IoMmuSupported; +} FF_A_PACKED DXGK_NODEMETADATA; + +typedef struct _D3DKMT_NODEMETADATA { + UINT NodeOrdinalAndAdapterIndex; // WDDMv2: High word is physical adapter index, low word is node ordinal + DXGK_NODEMETADATA NodeData; +} FF_A_PACKED D3DKMT_NODEMETADATA; +static_assert(sizeof(D3DKMT_NODEMETADATA) == 0x4E, "D3DKMT_NODEMETADATA structure size mismatch"); + +// Functions + +#if _WIN32 + +EXTERN_C _Check_return_ NTSTATUS APIENTRY D3DKMTOpenAdapterFromLuid(_Inout_ CONST D3DKMT_OPENADAPTERFROMLUID*); +EXTERN_C _Check_return_ NTSTATUS APIENTRY D3DKMTQueryAdapterInfo(_Inout_ CONST D3DKMT_QUERYADAPTERINFO*); +EXTERN_C _Check_return_ NTSTATUS APIENTRY D3DKMTCloseAdapter(_In_ CONST D3DKMT_CLOSEADAPTER*); +EXTERN_C _Check_return_ NTSTATUS APIENTRY D3DKMTEnumAdapters(_Inout_ CONST D3DKMT_ENUMADAPTERS*); +EXTERN_C _Check_return_ NTSTATUS APIENTRY D3DKMTEnumAdapters2(_Inout_ CONST D3DKMT_ENUMADAPTERS2*); +EXTERN_C _Check_return_ NTSTATUS APIENTRY D3DKMTQueryStatistics(_In_ CONST D3DKMT_QUERYSTATISTICS*); + +#else + + // Ref: https://github.com/microsoft/WSL2-Linux-Kernel/blob/linux-msft-wsl-6.6.y/include/uapi/misc/d3dkmthk.h + #define LX_DXOPENADAPTERFROMLUID _IOWR(0x47, 0x01, D3DKMT_OPENADAPTERFROMLUID) + #define LX_DXQUERYADAPTERINFO _IOWR(0x47, 0x09, D3DKMT_QUERYADAPTERINFO) + #define LX_DXENUMADAPTERS2 _IOWR(0x47, 0x14, D3DKMT_ENUMADAPTERS2) + #define LX_DXCLOSEADAPTER _IOWR(0x47, 0x15, D3DKMT_CLOSEADAPTER) + #define LX_DXQUERYSTATISTICS _IOWR(0x47, 0x43, D3DKMT_QUERYSTATISTICS) + +extern int dxgfd; // File descriptor for /dev/dxg, initialized in gpu_wsl.c + +static inline NTSTATUS D3DKMTOpenAdapterFromLuid(const D3DKMT_OPENADAPTERFROMLUID* params) { + return ioctl(dxgfd, LX_DXOPENADAPTERFROMLUID, params) < 0 ? -errno : 0; +} + +static inline NTSTATUS D3DKMTQueryAdapterInfo(const D3DKMT_QUERYADAPTERINFO* params) { + return ioctl(dxgfd, LX_DXQUERYADAPTERINFO, params) < 0 ? -errno : 0; +} + +static inline NTSTATUS D3DKMTCloseAdapter(const D3DKMT_CLOSEADAPTER* params) { + return ioctl(dxgfd, LX_DXCLOSEADAPTER, params) < 0 ? -errno : 0; +} + +static inline NTSTATUS D3DKMTEnumAdapters2(D3DKMT_ENUMADAPTERS2* params) { + return ioctl(dxgfd, LX_DXENUMADAPTERS2, params) < 0 ? -errno : 0; +} + +static inline NTSTATUS D3DKMTQueryStatistics(const D3DKMT_QUERYSTATISTICS* params) { + return ioctl(dxgfd, LX_DXQUERYSTATISTICS, params) < 0 ? -errno : 0; +} + +#endif diff --git a/src/detection/gpu/gpu.c b/src/detection/gpu/gpu.c new file mode 100644 index 0000000..a287298 --- /dev/null +++ b/src/detection/gpu/gpu.c @@ -0,0 +1,208 @@ +#include "gpu.h" +#include "common/debug.h" +#include "detection/vulkan/vulkan.h" +#include "detection/opencl/opencl.h" +#include "detection/opengl/opengl.h" +#include "modules/opengl/opengl.h" + +const char* FF_GPU_VENDOR_NAME_APPLE = "Apple"; +const char* FF_GPU_VENDOR_NAME_AMD = "AMD"; +const char* FF_GPU_VENDOR_NAME_INTEL = "Intel"; +const char* FF_GPU_VENDOR_NAME_NVIDIA = "NVIDIA"; +const char* FF_GPU_VENDOR_NAME_MTHREADS = "Moore Threads"; +const char* FF_GPU_VENDOR_NAME_QUALCOMM = "Qualcomm"; +const char* FF_GPU_VENDOR_NAME_MTK = "MTK"; +const char* FF_GPU_VENDOR_NAME_VMWARE = "VMware"; +const char* FF_GPU_VENDOR_NAME_PARALLELS = "Parallels"; +const char* FF_GPU_VENDOR_NAME_MICROSOFT = "Microsoft"; +const char* FF_GPU_VENDOR_NAME_REDHAT = "RedHat"; +const char* FF_GPU_VENDOR_NAME_ORACLE = "Oracle"; +const char* FF_GPU_VENDOR_NAME_BROADCOM = "Broadcom"; +const char* FF_GPU_VENDOR_NAME_LOONGSON = "Loongson"; +const char* FF_GPU_VENDOR_NAME_JINGJIA_MICRO = "Jingjia Micro"; +const char* FF_GPU_VENDOR_NAME_HUAWEI = "Huawei"; +const char* FF_GPU_VENDOR_NAME_ZHAOXIN = "Zhaoxin"; +const char* FF_GPU_VENDOR_NAME_QEMU = "QEMU"; + +const char* ffGPUGetVendorString(unsigned vendorId) { + // https://devicehunt.com/all-pci-vendors + switch (vendorId) { + case 0x106b: + return FF_GPU_VENDOR_NAME_APPLE; + case 0x1002: + case 0x1022: + case 0x1dd8: + return FF_GPU_VENDOR_NAME_AMD; + case 0x8086: + case 0x8087: + case 0x03e7: + return FF_GPU_VENDOR_NAME_INTEL; + case 0x0955: + case 0x10de: + case 0x12d2: + return FF_GPU_VENDOR_NAME_NVIDIA; + case 0x1ed5: + return FF_GPU_VENDOR_NAME_MTHREADS; + case 0x17cb: + case 0x5143: + return FF_GPU_VENDOR_NAME_QUALCOMM; + case 0x14c3: + return FF_GPU_VENDOR_NAME_MTK; + case 0x15ad: + return FF_GPU_VENDOR_NAME_VMWARE; + case 0x1af4: + return FF_GPU_VENDOR_NAME_REDHAT; + case 0x1ab8: + case 0x05404c42: // PD + return FF_GPU_VENDOR_NAME_PARALLELS; + case 0x1414: + return FF_GPU_VENDOR_NAME_MICROSOFT; + case 0x108e: + return FF_GPU_VENDOR_NAME_ORACLE; + case 0x182f: + case 0x14e4: + return FF_GPU_VENDOR_NAME_BROADCOM; + case 0x0014: + return FF_GPU_VENDOR_NAME_LOONGSON; + case 0x0731: + return FF_GPU_VENDOR_NAME_JINGJIA_MICRO; + case 0x19e5: + return FF_GPU_VENDOR_NAME_HUAWEI; + case 0x1d17: + return FF_GPU_VENDOR_NAME_ZHAOXIN; + case 0x1234: // https://admin.pci-ids.ucw.cz/read/PC/1234 + return FF_GPU_VENDOR_NAME_QEMU; + default: + return NULL; + } +} + +const char* detectByOpenGL(FFlist* gpus) { + FF_DEBUG("Starting OpenGL GPU detection fallback"); + + FFOpenGLResult result; + ffStrbufInit(&result.version); + ffStrbufInit(&result.renderer); + ffStrbufInit(&result.vendor); + ffStrbufInit(&result.slv); + ffStrbufInit(&result.library); + + FF_A_CLEANUP(ffDestroyOpenGLOptions) FFOpenGLOptions options; + ffInitOpenGLOptions(&options); + const char* error = ffDetectOpenGL(&options, &result); + FF_DEBUG("OpenGL detection returns: %s", error ?: "success"); + + if (!error) { + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus); + gpu->type = FF_GPU_TYPE_UNKNOWN; + ffStrbufInitMove(&gpu->vendor, &result.vendor); + ffStrbufInitMove(&gpu->name, &result.renderer); + ffStrbufInit(&gpu->driver); + ffStrbufInitF(&gpu->platformApi, "OpenGL %s", result.version.chars); + ffStrbufInit(&gpu->memoryType); + gpu->index = FF_GPU_INDEX_UNSET; + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + gpu->dedicated = gpu->shared = (FFGPUMemory) { 0, 0 }; + gpu->deviceId = 0; + + FF_DEBUG("OpenGL reported renderer='%s', vendor='%s', version='%s'", + gpu->name.chars, + gpu->vendor.chars, + result.version.chars); + + if (ffStrbufContainS(&gpu->name, "Apple")) { + ffStrbufSetStatic(&gpu->vendor, FF_GPU_VENDOR_NAME_APPLE); + gpu->type = FF_GPU_TYPE_INTEGRATED; + } else if (ffStrbufContainS(&gpu->name, "Intel")) { + ffStrbufSetStatic(&gpu->vendor, FF_GPU_VENDOR_NAME_INTEL); + } else if (ffStrbufContainS(&gpu->name, "AMD") || ffStrbufContainS(&gpu->name, "ATI")) { + ffStrbufSetStatic(&gpu->vendor, FF_GPU_VENDOR_NAME_AMD); + } else if (ffStrbufContainS(&gpu->name, "NVIDIA")) { + ffStrbufSetStatic(&gpu->vendor, FF_GPU_VENDOR_NAME_NVIDIA); + } else if (ffStrbufContainS(&gpu->name, "MTT")) { + ffStrbufSetStatic(&gpu->vendor, FF_GPU_VENDOR_NAME_MTHREADS); + } + + FF_DEBUG("OpenGL fallback produced GPU: name='%s', vendor='%s', type=%u", + gpu->name.chars, + gpu->vendor.chars, + gpu->type); + } + + ffStrbufDestroy(&result.version); + ffStrbufDestroy(&result.renderer); + ffStrbufDestroy(&result.vendor); + ffStrbufDestroy(&result.slv); + ffStrbufDestroy(&result.library); + return error; +} + +const char* ffDetectGPU(const FFGPUOptions* options, FFlist* result) { + FF_DEBUG("Starting GPU detection with method=%d", (int) options->detectionMethod); + + if (options->detectionMethod <= FF_GPU_DETECTION_METHOD_PCI) { + FF_DEBUG("Trying PCI/native GPU detection"); + const char* error = ffDetectGPUImpl(options, result); + if (!error && result->length > 0) { + FF_DEBUG("PCI/native GPU detection succeeded with %u GPU(s)", result->length); + return NULL; + } + + FF_DEBUG("PCI/native GPU detection did not produce results (error=%s, gpuCount=%u)", + error ?: "none", + result->length); + } + if (options->detectionMethod <= FF_GPU_DETECTION_METHOD_VULKAN) { + FF_DEBUG("Trying Vulkan GPU detection fallback"); + FFVulkanResult* vulkan = ffDetectVulkan(); + if (!vulkan->error && vulkan->gpus.length > 0) { + FF_DEBUG("Vulkan detection succeeded with %u GPU(s)", vulkan->gpus.length); + ffListDestroy(result); + ffListInitMove(result, &vulkan->gpus); + +#ifdef __ANDROID__ + double ffGPUDetectTempFromTZ(void); + if (options->temp && result->length == 1) { + FF_DEBUG("Applying Android thermal-zone temperature to single Vulkan GPU"); + FF_LIST_GET(FFGPUResult, *result, 0)->temperature = ffGPUDetectTempFromTZ(); + } +#endif + + return NULL; + } + + FF_DEBUG("Vulkan detection did not produce results (error=%s, gpuCount=%u)", + vulkan->error ?: "none", + vulkan->gpus.length); + } + if (options->detectionMethod <= FF_GPU_DETECTION_METHOD_OPENCL) { + FF_DEBUG("Trying OpenCL GPU detection fallback"); + FFOpenCLResult* opencl = ffDetectOpenCL(); + if (!opencl->error && opencl->gpus.length > 0) { + FF_DEBUG("OpenCL detection succeeded with %u GPU(s)", opencl->gpus.length); + ffListDestroy(result); + ffListInitMove(result, &opencl->gpus); + return NULL; + } + + FF_DEBUG("OpenCL detection did not produce results (error=%s, gpuCount=%u)", + opencl->error ?: "none", + opencl->gpus.length); + } + if (options->detectionMethod <= FF_GPU_DETECTION_METHOD_OPENGL) { + FF_DEBUG("Trying OpenGL GPU detection fallback"); + const char* error = detectByOpenGL(result); + if (error == NULL) { + FF_DEBUG("OpenGL fallback succeeded with %u GPU(s)", result->length); + return NULL; + } + + FF_DEBUG("OpenGL fallback failed: %s", error); + } + + FF_DEBUG("GPU detection failed in all enabled backends"); + return "GPU detection failed"; +} diff --git a/src/detection/gpu/gpu.h b/src/detection/gpu/gpu.h new file mode 100644 index 0000000..10bd02b --- /dev/null +++ b/src/detection/gpu/gpu.h @@ -0,0 +1,89 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/gpu/option.h" + +#define FF_GPU_TEMP_UNSET (-DBL_MAX) +#define FF_GPU_CORE_COUNT_UNSET -1 +#define FF_GPU_VMEM_SIZE_UNSET ((uint64_t) -1) +#define FF_GPU_FREQUENCY_UNSET 0 +#define FF_GPU_CORE_USAGE_UNSET (-DBL_MAX) +#define FF_GPU_INDEX_UNSET ((uint32_t) -1) + +extern const char* FF_GPU_VENDOR_NAME_APPLE; +extern const char* FF_GPU_VENDOR_NAME_AMD; +extern const char* FF_GPU_VENDOR_NAME_INTEL; +extern const char* FF_GPU_VENDOR_NAME_NVIDIA; +extern const char* FF_GPU_VENDOR_NAME_MTHREADS; +extern const char* FF_GPU_VENDOR_NAME_QUALCOMM; +extern const char* FF_GPU_VENDOR_NAME_MTK; +extern const char* FF_GPU_VENDOR_NAME_VMWARE; +extern const char* FF_GPU_VENDOR_NAME_PARALLELS; +extern const char* FF_GPU_VENDOR_NAME_MICROSOFT; +extern const char* FF_GPU_VENDOR_NAME_REDHAT; +extern const char* FF_GPU_VENDOR_NAME_ORACLE; +extern const char* FF_GPU_VENDOR_NAME_BROADCOM; +extern const char* FF_GPU_VENDOR_NAME_LOONGSON; +extern const char* FF_GPU_VENDOR_NAME_JINGJIA_MICRO; +extern const char* FF_GPU_VENDOR_NAME_HUAWEI; +extern const char* FF_GPU_VENDOR_NAME_ZHAOXIN; +extern const char* FF_GPU_VENDOR_NAME_QEMU; + +typedef struct FFGPUMemory { + uint64_t total; + uint64_t used; +} FFGPUMemory; + +typedef struct FFGPUResult { + uint32_t index; + FFGPUType type; + FFstrbuf vendor; + FFstrbuf name; + FFstrbuf driver; + FFstrbuf platformApi; + FFstrbuf memoryType; + double temperature; + double coreUsage; + int32_t coreCount; + uint32_t frequency; // Maximum time clock frequency in MHz + FFGPUMemory dedicated; + FFGPUMemory shared; + uint64_t deviceId; +} FFGPUResult; + +const char* ffDetectGPU(const FFGPUOptions* options, FFlist* result); +const char* ffDetectGPUImpl(const FFGPUOptions* options, FFlist* gpus); + +const char* ffGPUGetVendorString(unsigned vendorId); + +typedef struct FFGpuDriverPciBusId { + uint32_t domain; + uint32_t bus; + uint32_t device; + uint32_t func; +} FFGpuDriverPciBusId; + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__sun) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__HAIKU__) || defined(__GNU__) +void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu); +void ffGPUQueryAmdGpuName(uint16_t deviceId, uint8_t revisionId, FFGPUResult* gpu); + + #if FF_HAVE_DRM +const char* ffDrmDetectRadeon(const FFGPUOptions* options, FFGPUResult* gpu, const char* renderPath); +const char* ffDrmDetectAmdgpu(const FFGPUOptions* options, FFGPUResult* gpu, const char* renderPath); +const char* ffDrmDetectI915(FFGPUResult* gpu, int fd); +const char* ffDrmDetectXe(FFGPUResult* gpu, int fd); +const char* ffDrmDetectAsahi(FFGPUResult* gpu, int fd); +const char* ffDrmDetectNouveau(FFGPUResult* gpu, int fd); + #endif // FF_HAVE_DRM + +const char* ffGPUDetectDriverSpecific(const FFGPUOptions* options, FFGPUResult* gpu, FFGpuDriverPciBusId pciBusId); +#endif // defined(XXX) + +static inline uint64_t ffGPUPciAddr2Id(uint64_t domain, uint64_t bus, uint64_t device, uint64_t function) { + return (domain << 16) | (bus << 8) | (device << 3) | function; +} + +static inline uint64_t ffGPUGeneral2Id(uint64_t originalId) { + // Note: originalId may already have the MSB set + return (1ULL << 63) | originalId; +} diff --git a/src/detection/gpu/gpu_amd.c b/src/detection/gpu/gpu_amd.c new file mode 100644 index 0000000..f396def --- /dev/null +++ b/src/detection/gpu/gpu_amd.c @@ -0,0 +1,460 @@ +#include "gpu_driver_specific.h" + +#include "adl.h" +#include "common/library.h" +#include "common/mallocHelper.h" +#include "common/debug.h" + +// Helper function to convert ADL status code to string +FF_A_UNUSED static const char* ffAdlStatusToString(int status) { + switch (status) { +#define FF_ADL_STATUS_CASE(name) \ + case name: \ + return #name; + FF_ADL_STATUS_CASE(ADL_OK) + FF_ADL_STATUS_CASE(ADL_OK_WARNING) + FF_ADL_STATUS_CASE(ADL_OK_MODE_CHANGE) + FF_ADL_STATUS_CASE(ADL_OK_RESTART) + FF_ADL_STATUS_CASE(ADL_OK_WAIT) + FF_ADL_STATUS_CASE(ADL_ERR) + FF_ADL_STATUS_CASE(ADL_ERR_NOT_INIT) + FF_ADL_STATUS_CASE(ADL_ERR_INVALID_PARAM) + FF_ADL_STATUS_CASE(ADL_ERR_INVALID_PARAM_SIZE) + FF_ADL_STATUS_CASE(ADL_ERR_INVALID_ADL_IDX) + FF_ADL_STATUS_CASE(ADL_ERR_INVALID_CONTROLLER_IDX) + FF_ADL_STATUS_CASE(ADL_ERR_INVALID_DIPLAY_IDX) + FF_ADL_STATUS_CASE(ADL_ERR_NOT_SUPPORTED) + FF_ADL_STATUS_CASE(ADL_ERR_NULL_POINTER) + FF_ADL_STATUS_CASE(ADL_ERR_DISABLED_ADAPTER) + FF_ADL_STATUS_CASE(ADL_ERR_INVALID_CALLBACK) + FF_ADL_STATUS_CASE(ADL_ERR_RESOURCE_CONFLICT) + FF_ADL_STATUS_CASE(ADL_ERR_SET_INCOMPLETE) + FF_ADL_STATUS_CASE(ADL_ERR_NO_XDISPLAY) + FF_ADL_STATUS_CASE(ADL_ERR_CALL_TO_INCOMPATIABLE_DRIVER) + FF_ADL_STATUS_CASE(ADL_ERR_NO_ADMINISTRATOR_PRIVILEGES) + FF_ADL_STATUS_CASE(ADL_ERR_FEATURESYNC_NOT_STARTED) + FF_ADL_STATUS_CASE(ADL_ERR_INVALID_POWER_STATE) +#undef FF_ADL_STATUS_CASE + default: + return "Unknown ADL error"; + } +} + +// Memory allocation function +static void* __stdcall ffAdlMainMemoryAlloc(int iSize) { + return malloc((size_t) iSize); +} + +struct FFAdlData { + FF_LIBRARY_SYMBOL(ADL2_Main_Control_Destroy) + FF_LIBRARY_SYMBOL(ADL2_Adapter_AdapterInfoX3_Get) + FF_LIBRARY_SYMBOL(ADL2_Adapter_Graphic_Core_Info_Get) + FF_LIBRARY_SYMBOL(ADL2_Adapter_MemoryInfo2_Get) + FF_LIBRARY_SYMBOL(ADL2_Adapter_DedicatedVRAMUsage_Get) + FF_LIBRARY_SYMBOL(ADL2_Adapter_ASICFamilyType_Get) + FF_LIBRARY_SYMBOL(ADL2_Overdrive_Caps) + FF_LIBRARY_SYMBOL(ADL2_OverdriveN_CapabilitiesX2_Get) + FF_LIBRARY_SYMBOL(ADL2_OverdriveN_SystemClocksX2_Get) + FF_LIBRARY_SYMBOL(ADL2_OverdriveN_PerformanceStatus_Get) + FF_LIBRARY_SYMBOL(ADL2_OverdriveN_Temperature_Get) + FF_LIBRARY_SYMBOL(ADL2_Overdrive8_Current_Setting_Get) + FF_LIBRARY_SYMBOL(ADL2_New_QueryPMLogData_Get) + FF_LIBRARY_SYMBOL(ADL2_Overdrive6_CurrentStatus_Get) + FF_LIBRARY_SYMBOL(ADL2_Overdrive6_Temperature_Get) + FF_LIBRARY_SYMBOL(ADL2_Overdrive6_StateInfo_Get) + + bool inited; + ADL_CONTEXT_HANDLE apiHandle; +} adlData; + +static void shutdownAdl() { + if (adlData.apiHandle) { + FF_DEBUG("Destroying ADL context"); + adlData.ffADL2_Main_Control_Destroy(adlData.apiHandle); + adlData.apiHandle = NULL; + } +} + +const char* ffDetectAmdGpuInfo(const FFGpuDriverCondition* cond, FFGpuDriverResult result, const char* soName) { + FF_DEBUG("Attempting to detect AMD GPU info using '%s'", soName); + + if (!adlData.inited) { + adlData.inited = true; + FF_DEBUG("Initializing ADL library"); + FF_LIBRARY_LOAD(atiadl, "dlopen atiadlxx failed", soName, 1); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(atiadl, ADL2_Main_Control_Create) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_Main_Control_Destroy) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_Adapter_AdapterInfoX3_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_Adapter_Graphic_Core_Info_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_Adapter_MemoryInfo2_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_Adapter_DedicatedVRAMUsage_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_Adapter_ASICFamilyType_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_Overdrive_Caps) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_OverdriveN_CapabilitiesX2_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_OverdriveN_SystemClocksX2_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_OverdriveN_PerformanceStatus_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_Overdrive8_Current_Setting_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_New_QueryPMLogData_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_OverdriveN_Temperature_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_Overdrive6_CurrentStatus_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_Overdrive6_Temperature_Get) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(atiadl, adlData, ADL2_Overdrive6_StateInfo_Get) + FF_DEBUG("ADL library loaded"); + + int result = ffADL2_Main_Control_Create(ffAdlMainMemoryAlloc, 1 /*iEnumConnectedAdapters*/, &adlData.apiHandle); + FF_DEBUG("ADL2_Main_Control_Create returned %s (%d)", ffAdlStatusToString(result), result); + if (result != ADL_OK) { + return "ffADL2_Main_Control_Create() failed"; + } + + atexit(shutdownAdl); + atiadl = NULL; // don't close atiadl + FF_DEBUG("ADL initialization complete"); + } + + if (!adlData.apiHandle) { + FF_DEBUG("ADL context not initialized"); + return "ffADL2_Main_Control_Create() failed"; + } + + FF_AUTO_FREE AdapterInfo* devices = NULL; + int numDevices = 0; + int adapterResult = adlData.ffADL2_Adapter_AdapterInfoX3_Get(adlData.apiHandle, -1, &numDevices, &devices); + FF_DEBUG("ADL2_Adapter_AdapterInfoX3_Get returned %s (%d)", ffAdlStatusToString(adapterResult), adapterResult); + + if (adapterResult == ADL_OK) { + FF_DEBUG("found %d adapters", numDevices); + } else { + FF_DEBUG("ffADL2_Adapter_AdapterInfoX3_Get() failed"); + return "ffADL2_Adapter_AdapterInfoX3_Get() failed"; + } + + const AdapterInfo* device = NULL; + for (int iDev = 0; iDev < numDevices; iDev++) { + if (cond->type & FF_GPU_DRIVER_CONDITION_TYPE_BUS_ID) { + FF_DEBUG("Checking device %d: bus=%d, device=%d, func=%d against requested bus=%u, device=%u, func=%u", + iDev, + devices[iDev].iBusNumber, + devices[iDev].iDeviceNumber, + devices[iDev].iFunctionNumber, + cond->pciBusId.bus, + cond->pciBusId.device, + cond->pciBusId.func); + + if ( + cond->pciBusId.bus == (uint32_t) devices[iDev].iBusNumber && + cond->pciBusId.device == (uint32_t) devices[iDev].iDeviceNumber && + cond->pciBusId.func == (uint32_t) devices[iDev].iFunctionNumber) { + device = &devices[iDev]; + FF_DEBUG("Found matching device: %s (index: %d)", device->strAdapterName, device->iAdapterIndex); + break; + } + } + } + + if (!device) { + FF_DEBUG("Device not found"); + return "Device not found"; + } + + if (result.coreCount) { + ADLGraphicCoreInfo coreInfo; + int status = adlData.ffADL2_Adapter_Graphic_Core_Info_Get(adlData.apiHandle, device->iAdapterIndex, &coreInfo); + FF_DEBUG("ADL2_Adapter_Graphic_Core_Info_Get returned %s (%d)", ffAdlStatusToString(status), status); + + if (status == ADL_OK) { + FF_DEBUG("Core info - NumCUs: %d, NumPEsPerCU: %d", coreInfo.iNumCUs, coreInfo.iNumPEsPerCU); + *result.coreCount = (uint32_t) coreInfo.iNumCUs * (uint32_t) coreInfo.iNumPEsPerCU; + FF_DEBUG("Got core count: %u", *result.coreCount); + } else { + FF_DEBUG("Failed to get core count"); + } + } + + if (result.memory) { + int vramUsage = 0; + int status = adlData.ffADL2_Adapter_DedicatedVRAMUsage_Get(adlData.apiHandle, device->iAdapterIndex, &vramUsage); + FF_DEBUG("ADL2_Adapter_DedicatedVRAMUsage_Get returned %s (%d), usage: %d MB", + ffAdlStatusToString(status), + status, + vramUsage); + + if (status == ADL_OK && vramUsage >= 0) { + result.memory->used = (uint64_t) vramUsage * 1024 * 1024; + FF_DEBUG("Dedicated VRAM usage: %llu bytes (%d MB)", result.memory->used, vramUsage); + } else { + FF_DEBUG("Failed to get dedicated VRAM usage"); + } + } + + if (result.memoryType) { + ADLMemoryInfo2 memoryInfo; + int status = adlData.ffADL2_Adapter_MemoryInfo2_Get(adlData.apiHandle, device->iAdapterIndex, &memoryInfo); + FF_DEBUG("ADL2_Adapter_MemoryInfo2_Get returned %s (%d)", ffAdlStatusToString(status), status); + + if (status == ADL_OK) { + FF_DEBUG("Memory info - Type: %s, Size: %lld MB", memoryInfo.strMemoryType, memoryInfo.iMemorySize / 1024 / 1024); + ffStrbufSetS(result.memoryType, memoryInfo.strMemoryType); + FF_DEBUG("Got memory type: %s", memoryInfo.strMemoryType); + } else { + FF_DEBUG("Failed to get memory type"); + } + } + + if (result.type) { + int asicTypes = 0; + int valids = 0; + int status = adlData.ffADL2_Adapter_ASICFamilyType_Get(adlData.apiHandle, device->iAdapterIndex, &asicTypes, &valids); + FF_DEBUG("ADL2_Adapter_ASICFamilyType_Get returned %s (%d), asicTypes: 0x%x, valids: 0x%x", + ffAdlStatusToString(status), + status, + asicTypes, + valids); + + if (status == ADL_OK) { + asicTypes &= valids; // This design is strange + *result.type = asicTypes & ADL_ASIC_INTEGRATED ? FF_GPU_TYPE_INTEGRATED : FF_GPU_TYPE_DISCRETE; + FF_DEBUG("GPU type: %s (asicTypes: 0x%x, valids: 0x%x)", + *result.type == FF_GPU_TYPE_INTEGRATED ? "Integrated" : "Discrete", + asicTypes, + valids); + } else { + FF_DEBUG("Failed to get GPU type"); + } + } + + if (result.index) { + *result.index = (uint32_t) device->iAdapterIndex; + FF_DEBUG("Setting adapter index: %u", *result.index); + } + + if (result.name) { + ffStrbufSetS(result.name, device->strAdapterName); + FF_DEBUG("Setting adapter name: %s; UDID: %s, Present: %d, Exist: %d", device->strAdapterName, device->strUDID, device->iPresent, device->iExist); + } + + int odVersion = 0; + + { + int odSupported = 0; + int odEnabled = 0; + int status = adlData.ffADL2_Overdrive_Caps(adlData.apiHandle, device->iAdapterIndex, &odSupported, &odEnabled, &odVersion); + FF_DEBUG("ADL2_Overdrive_Caps returned %s (%d); supported %d, enabled %d; version %d", + ffAdlStatusToString(status), + status, + odSupported, + odEnabled, + odVersion); + if (status != ADL_OK) { + FF_DEBUG("Overdrive not supported, results may be inaccurate"); + // Note even if Overdrive is not supported, we can still get the OD version + } + } + + if (odVersion == 8) { + FF_DEBUG("Using Overdrive8 API (odVersion=%d)", odVersion); + + if (result.frequency) { + ADLOD8CurrentSetting currentSetting = { .count = OD8_COUNT }; + int status = adlData.ffADL2_Overdrive8_Current_Setting_Get(adlData.apiHandle, device->iAdapterIndex, ¤tSetting); + FF_DEBUG("ADL2_Overdrive8_Current_Setting_Get returned %s (%d)", ffAdlStatusToString(status), status); + if (status == ADL_OK) { + FF_DEBUG("OD8 Settings count: %d", currentSetting.count); + + *result.frequency = (uint32_t) currentSetting.Od8SettingTable[OD8_GFXCLK_FMAX]; + FF_DEBUG("Got max engine clock (OD8_GFXCLK_FMAX): %u MHz", *result.frequency); + } else { + FF_DEBUG("Failed to get max frequency information"); + } + } + + if (result.temp || result.coreUsage) { + ADLPMLogDataOutput pmLogDataOutput = {}; + int status = adlData.ffADL2_New_QueryPMLogData_Get(adlData.apiHandle, device->iAdapterIndex, &pmLogDataOutput); + FF_DEBUG("ADL2_New_QueryPMLogData_Get returned %s (%d)", ffAdlStatusToString(status), status); + if (status == ADL_OK) { + if (result.temp) { + ADLSingleSensorData* sensor = &pmLogDataOutput.sensors[ADL_PMLOG_TEMPERATURE_HOTSPOT]; + FF_DEBUG("Sensor %d: %s, supported: %d, value: %d", ADL_PMLOG_TEMPERATURE_HOTSPOT, "ADL_PMLOG_TEMPERATURE_HOTSPOT", sensor->supported, sensor->value); + if (sensor->supported) { + *result.temp = sensor->value; + FF_DEBUG("Temperature: %.1f°C (HOTSPOT)", *result.temp); + } else { + sensor = &pmLogDataOutput.sensors[ADL_PMLOG_TEMPERATURE_GFX]; + FF_DEBUG("Sensor %d: %s, supported: %d, value: %d", ADL_PMLOG_TEMPERATURE_GFX, "ADL_PMLOG_TEMPERATURE_GFX", sensor->supported, sensor->value); + if (sensor->supported) { + *result.temp = sensor->value; + FF_DEBUG("Temperature: %.1f°C (GFX)", *result.temp); + } else { + sensor = &pmLogDataOutput.sensors[ADL_PMLOG_TEMPERATURE_SOC]; + FF_DEBUG("Sensor %d: %s, supported: %d, value: %d", ADL_PMLOG_TEMPERATURE_SOC, "ADL_PMLOG_TEMPERATURE_SOC", sensor->supported, sensor->value); + if (sensor->supported) { + *result.temp = sensor->value; + FF_DEBUG("Temperature: %.1f°C (SOC)", *result.temp); + } else { + FF_DEBUG("No supported temp sensor found, temp detection failed"); + } + } + } + } + if (result.coreUsage) { + ADLSingleSensorData* activity = &pmLogDataOutput.sensors[ADL_PMLOG_INFO_ACTIVITY_GFX]; + FF_DEBUG("Sensor %d: %s, supported: %d, value: %d", ADL_PMLOG_INFO_ACTIVITY_GFX, "ADL_PMLOG_INFO_ACTIVITY_GFX", activity->supported, activity->value); + if (activity->supported) { + *result.coreUsage = activity->value; + FF_DEBUG("Core usage: %.1f%%", *result.coreUsage); + } else { + FF_DEBUG("Sensor %d not supported, GPU usage detection failed", ADL_PMLOG_INFO_ACTIVITY_GFX); + } + } + } else { + FF_DEBUG("Failed to get temperature / GPU activity"); + } + } + } else if (odVersion == 7) { + FF_DEBUG("Using OverdriveN API (odVersion=%d)", odVersion); + + if (result.frequency) { + // https://github.com/MaynardMiner/odvii/blob/master/OverdriveN.cpp#L176 + ADLODNCapabilitiesX2 odCapabilities = {}; + int status = adlData.ffADL2_OverdriveN_CapabilitiesX2_Get(adlData.apiHandle, device->iAdapterIndex, &odCapabilities); + FF_DEBUG("ADL2_OverdriveN_CapabilitiesX2_Get returned %s (%d)", ffAdlStatusToString(status), status); + + if (status == ADL_OK) { + if (odCapabilities.iMaximumNumberOfPerformanceLevels == 0) { + FF_DEBUG("ADL2_OverdriveN_CapabilitiesX2_Get: no performance levels available"); + } else { + FF_DEBUG("ODN Capabilities - MaxPerformanceLevels: %d, GPU Clock Range: [%d - %d]", + odCapabilities.iMaximumNumberOfPerformanceLevels, + odCapabilities.sEngineClockRange.iMin, + odCapabilities.sEngineClockRange.iMax); + + size_t size = sizeof(ADLODNPerformanceLevelsX2) + sizeof(ADLODNPerformanceLevelX2) * ((unsigned) odCapabilities.iMaximumNumberOfPerformanceLevels - 1); + FF_AUTO_FREE ADLODNPerformanceLevelsX2* odPerfLevels = calloc(size, 1); + odPerfLevels->iSize = (int) size; + odPerfLevels->iNumberOfPerformanceLevels = odCapabilities.iMaximumNumberOfPerformanceLevels; + odPerfLevels->iMode = ODNControlType_Current; + + int status = adlData.ffADL2_OverdriveN_SystemClocksX2_Get(adlData.apiHandle, device->iAdapterIndex, odPerfLevels); + FF_DEBUG("ADL2_OverdriveN_SystemClocksX2_Get returned %s (%d), levels: %d", + ffAdlStatusToString(status), + status, + odPerfLevels->iNumberOfPerformanceLevels); + + if (status != ADL_OK) { + FF_DEBUG("Failed to get frequency information"); + } else { + // lowest to highest + for (int i = odPerfLevels->iNumberOfPerformanceLevels - 1; i >= 0; i--) { + ADLODNPerformanceLevelX2* level = &odPerfLevels->aLevels[i]; + FF_DEBUG("Performance level %d: enabled: %d, engine clock = %d", i, level->iEnabled, level->iClock); + if (level->iEnabled) { + *result.frequency = (uint32_t) level->iClock / 100; // in 10 kHz + FF_DEBUG("Got max engine clock: %u MHz", *result.frequency); + break; + } + } + } + } + } else { + FF_DEBUG("Failed to get frequency information"); + } + } + + if (result.coreUsage) { + ADLODNPerformanceStatus performanceStatus = {}; + int status = adlData.ffADL2_OverdriveN_PerformanceStatus_Get(adlData.apiHandle, device->iAdapterIndex, &performanceStatus); + FF_DEBUG("ADL2_OverdriveN_PerformanceStatus_Get returned %s (%d)", ffAdlStatusToString(status), status); + + if (status == ADL_OK) { + FF_DEBUG("Performance Status - Activity: %d%%, CoreClock: %dMHz, MemoryClock: %dMHz", + performanceStatus.iGPUActivityPercent, + performanceStatus.iCoreClock, + performanceStatus.iMemoryClock); + + *result.coreUsage = performanceStatus.iGPUActivityPercent; + FF_DEBUG("Got GPU activity: %d%%", performanceStatus.iGPUActivityPercent); + } else { + FF_DEBUG("Failed to get GPU activity"); + } + } + + if (result.temp) { + int milliDegrees = 0; + int status = adlData.ffADL2_OverdriveN_Temperature_Get(adlData.apiHandle, device->iAdapterIndex, 1, &milliDegrees); + FF_DEBUG("ADL2_OverdriveN_Temperature_Get returned %s (%d)", ffAdlStatusToString(status), status); + + if (status == ADL_OK) { + *result.temp = milliDegrees / 1000.0; + FF_DEBUG("Temperature: %.1f°C (raw: %d milliC)", *result.temp, milliDegrees); + } else { + FF_DEBUG("Failed to get temperature"); + } + } + } else if (odVersion == 6) { + FF_DEBUG("Using Overdrive6 API (odVersion=%d)", odVersion); + + if (result.frequency) { + FF_AUTO_FREE ADLOD6StateInfo* stateInfo = calloc(sizeof(ADLOD6StateInfo) + sizeof(ADLOD6PerformanceLevel), 1); + stateInfo->iNumberOfPerformanceLevels = 2; + + int status = adlData.ffADL2_Overdrive6_StateInfo_Get(adlData.apiHandle, device->iAdapterIndex, ADL_OD6_GETSTATEINFO_CUSTOM_PERFORMANCE, stateInfo); + FF_DEBUG("ADL2_Overdrive6_StateInfo_Get returned %s (%d), performance levels: %d", + ffAdlStatusToString(status), + status, + stateInfo->iNumberOfPerformanceLevels); + + if (status == ADL_OK) { + // OD6 uses clock ranges instead of discrete performance levels. + // iNumberOfPerformanceLevels is always 2. + // The 1st level indicates the minimum clocks in the range. + // The 2nd level indicates the maximum clocks in the range. + if (stateInfo->iNumberOfPerformanceLevels != 2) { + FF_DEBUG("ADL2_Overdrive6_StateInfo_Get: unexpected number of performance levels: %d", stateInfo->iNumberOfPerformanceLevels); + } else { + FF_DEBUG("OD6 Settings - MinPerformanceLevels: %d, MaxPerformanceLevels: %d", + stateInfo->aLevels[0].iEngineClock, + stateInfo->aLevels[1].iEngineClock); + *result.frequency = (uint32_t) stateInfo->aLevels[1].iEngineClock / 100; // in 10 kHz + FF_DEBUG("Got max engine clock: %u MHz", *result.frequency); + } + } else { + FF_DEBUG("Failed to get frequency information"); + } + } + + if (result.coreUsage) { + ADLOD6CurrentStatus status = {}; + int apiStatus = adlData.ffADL2_Overdrive6_CurrentStatus_Get(adlData.apiHandle, device->iAdapterIndex, &status); + FF_DEBUG("ADL2_Overdrive6_CurrentStatus_Get returned %s (%d)", ffAdlStatusToString(apiStatus), apiStatus); + + if (apiStatus == ADL_OK) { + *result.coreUsage = status.iActivityPercent; + FF_DEBUG("Got GPU activity: %d%%", status.iActivityPercent); + } else { + FF_DEBUG("Failed to get GPU activity"); + } + } + + if (result.temp) { + int milliDegrees = 0; + int status = adlData.ffADL2_Overdrive6_Temperature_Get(adlData.apiHandle, device->iAdapterIndex, &milliDegrees); + FF_DEBUG("ADL2_Overdrive6_Temperature_Get returned %s (%d), temperature: %d milliC", + ffAdlStatusToString(status), + status, + milliDegrees); + + if (status == ADL_OK) { + *result.temp = milliDegrees / 1000.0; + FF_DEBUG("Temperature: %.1f°C", *result.temp); + } else { + FF_DEBUG("Failed to get temperature"); + } + } + } else { + FF_DEBUG("Unknown Overdrive version: %d", odVersion); + return "Unknown Overdrive version"; + } + FF_DEBUG("AMD GPU detection complete - returning success"); + return NULL; +} diff --git a/src/detection/gpu/gpu_android.c b/src/detection/gpu/gpu_android.c new file mode 100644 index 0000000..08fd48a --- /dev/null +++ b/src/detection/gpu/gpu_android.c @@ -0,0 +1,55 @@ +#include "gpu.h" +#include "common/io.h" +#include "common/strutil.h" + +#include <fcntl.h> + +static double parseTZDir(int dfd, FFstrbuf* buffer) { + if (!ffReadFileBufferRelative(dfd, "type", buffer) || !ffStrbufStartsWithS(buffer, "gpu")) { + return FF_GPU_TEMP_UNSET; + } + + if (!ffReadFileBufferRelative(dfd, "temp", buffer)) { + return FF_GPU_TEMP_UNSET; + } + + double value = ffStrbufToDouble(buffer, FF_GPU_TEMP_UNSET); // millidegree Celsius + if (value == FF_GPU_TEMP_UNSET) { + return FF_GPU_TEMP_UNSET; + } + + return value / 1000.; +} + +double ffGPUDetectTempFromTZ(void) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/thermal/"); + if (dirp) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + int dfd = dirfd(dirp); + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + if (!ffStrStartsWith(entry->d_name, "thermal_zone")) { + continue; + } + + FF_AUTO_CLOSE_FD int subfd = openat(dfd, entry->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (subfd < 0) { + continue; + } + + double result = parseTZDir(subfd, &buffer); + if (result != FF_GPU_TEMP_UNSET) { + return result; + } + } + } + return FF_GPU_TEMP_UNSET; +} + +const char* ffDetectGPUImpl(const FFGPUOptions* options, FFlist* gpus) { + FF_UNUSED(options, gpus); + return "No permission. Fallbacks to Vulkan, OpenCL or OpenGL instead"; +} diff --git a/src/detection/gpu/gpu_apple.c b/src/detection/gpu/gpu_apple.c new file mode 100644 index 0000000..3b8de20 --- /dev/null +++ b/src/detection/gpu/gpu_apple.c @@ -0,0 +1,208 @@ +#include "gpu.h" +#include "common/apple/cf_helpers.h" +#include "common/apple/smc_temps.h" + +#include <IOKit/graphics/IOGraphicsLib.h> + +const char* ffGpuDetectMetal(FFlist* gpus); +const char* ffGpuDetectDriverVersion(FFlist* gpus); + +static double detectGpuTemp(const FFstrbuf* gpuName) { + double result = 0; + const char* error = NULL; + + if (ffStrbufStartsWithS(gpuName, "Apple M")) { + switch (strtol(gpuName->chars + strlen("Apple M"), NULL, 10)) { + case 0: + error = "Invalid Apple Silicon GPU"; + break; + case 1: + error = ffDetectSmcTemps(FF_TEMP_GPU_M1X, &result); + break; + case 2: + error = ffDetectSmcTemps(FF_TEMP_GPU_M2X, &result); + break; + case 3: + error = ffDetectSmcTemps(FF_TEMP_GPU_M3X, &result); + break; + case 4: + error = ffDetectSmcTemps(FF_TEMP_GPU_M4X, &result); + break; + default: + error = "Unsupported Apple Silicon GPU"; + break; + } + } else if (ffStrbufStartsWithS(gpuName, "Intel")) { + error = ffDetectSmcTemps(FF_TEMP_GPU_INTEL, &result); + } else if (ffStrbufStartsWithS(gpuName, "Radeon") || ffStrbufStartsWithS(gpuName, "AMD")) { + error = ffDetectSmcTemps(FF_TEMP_GPU_AMD, &result); + } else { + error = ffDetectSmcTemps(FF_TEMP_GPU_UNKNOWN, &result); + } + + if (error) { + return FF_GPU_TEMP_UNSET; + } + + return result; +} + +#ifdef __aarch64__ + #include "common/apple/cf_helpers.h" + + #include <IOKit/IOKitLib.h> + +static const char* detectFrequency(FFGPUResult* gpu) { + // https://github.com/giampaolo/psutil/pull/2222/files + + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryDevice = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceNameMatching("pmgr")); + if (!entryDevice) { + return "IOServiceGetMatchingServices() failed"; + } + + if (!IOObjectConformsTo(entryDevice, "AppleARMIODevice")) { + return "\"pmgr\" should conform to \"AppleARMIODevice\""; + } + + FF_CFTYPE_AUTO_RELEASE CFDataRef freqProperty = (CFDataRef) IORegistryEntryCreateCFProperty(entryDevice, CFSTR("voltage-states9-sram"), kCFAllocatorDefault, kNilOptions); + if (!freqProperty || CFGetTypeID(freqProperty) != CFDataGetTypeID()) { + return "\"voltage-states9-sram\" in \"pmgr\" is not found"; + } + + // voltage-states9-sram stores supported <frequency / voltage> pairs of gpu from the lowest to the highest + CFIndex propLength = CFDataGetLength(freqProperty); + if (propLength == 0 || propLength % (CFIndex) sizeof(uint32_t) * 2 != 0) { + return "Invalid \"voltage-states9-sram\" length"; + } + + uint32_t* pStart = (uint32_t*) CFDataGetBytePtr(freqProperty); + uint32_t pMax = *pStart; + for (CFIndex i = 2; i < propLength / (CFIndex) sizeof(uint32_t) && pStart[i] > 0; i += 2 /* skip voltage */) { + pMax = pMax > pStart[i] ? pMax : pStart[i]; + } + + if (pMax > 0) { + // While this is not necessary for now (seems), we add this logic just in case. See cpu_apple.c + if (pMax > 100000000) { // Assume that pMax is in Hz + gpu->frequency = pMax / 1000 / 1000; + } else { // Assume that pMax is in kHz + gpu->frequency = pMax / 1000; + } + } + + return NULL; +} +#endif + +const char* ffDetectGPUImpl(const FFGPUOptions* options, FFlist* gpus) { + FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = IO_OBJECT_NULL; + { + CFMutableDictionaryRef matches = IOServiceMatching(kIOAcceleratorClassName); + CFDictionaryAddValue(matches, CFSTR("IOMatchCategory"), CFSTR(kIOAcceleratorClassName)); + if (IOServiceGetMatchingServices(MACH_PORT_NULL, matches, &iterator) != kIOReturnSuccess) { + return "IOServiceGetMatchingServices() failed"; + } + } + + io_registry_entry_t registryEntry; + while ((registryEntry = IOIteratorNext(iterator)) != IO_OBJECT_NULL) { + CFMutableDictionaryRef properties; + if (IORegistryEntryCreateCFProperties(registryEntry, &properties, kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess) { + IOObjectRelease(registryEntry); + continue; + } + + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus); + gpu->index = FF_GPU_INDEX_UNSET; + ffStrbufInit(&gpu->memoryType); + gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + gpu->type = FF_GPU_TYPE_UNKNOWN; + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + IORegistryEntryGetRegistryEntryID(registryEntry, &gpu->deviceId); + ffStrbufInitStatic(&gpu->platformApi, "IOKit"); + + ffStrbufInit(&gpu->driver); // Ok for both Apple and Intel + ffCfDictGetString(properties, CFSTR("CFBundleIdentifier"), &gpu->driver); + + if (ffCfDictGetInt(properties, CFSTR("gpu-core-count"), &gpu->coreCount) != NULL) { // For Apple + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + } + + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + CFDictionaryRef perfStatistics = NULL; + uint64_t vramUsed = 0, vramTotal = 0; + if (ffCfDictGetDict(properties, CFSTR("PerformanceStatistics"), &perfStatistics) == NULL) { + int64_t utilization; + if (ffCfDictGetInt64(perfStatistics, CFSTR("Device Utilization %"), &utilization) == NULL) { + gpu->coreUsage = (double) utilization; + } else if (ffCfDictGetInt64(perfStatistics, CFSTR("GPU Core Utilization"), &utilization) == NULL) { + gpu->coreUsage = (double) utilization / 10000000.; // Nvidia? + } + + if (ffCfDictGetInt64(perfStatistics, CFSTR("Alloc system memory"), (int64_t*) &vramTotal) == NULL) { + if (ffCfDictGetInt64(perfStatistics, CFSTR("In use system memory"), (int64_t*) &vramUsed) != NULL) { + vramTotal = 0; + } + } else if (ffCfDictGetInt64(perfStatistics, CFSTR("vramFreeBytes"), (int64_t*) &vramTotal) == NULL) { + if (ffCfDictGetInt64(perfStatistics, CFSTR("vramUsedBytes"), (int64_t*) &vramUsed) == NULL) { + vramTotal += vramUsed; + } else { + vramTotal = 0; + } + } + } + + ffStrbufInit(&gpu->name); + // IOAccelerator returns model / vendor-id properties for Apple Silicon, but not for Intel Iris GPUs. + // Still needs testing for AMD's + if (ffCfDictGetString(properties, CFSTR("model"), &gpu->name) != NULL) { + CFRelease(properties); + properties = NULL; + + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t parentEntry = 0; + if (IORegistryEntryGetParentEntry(registryEntry, kIOServicePlane, &parentEntry) != kIOReturnSuccess || + IORegistryEntryCreateCFProperties(parentEntry, &properties, kCFAllocatorDefault, kNilOptions) != kIOReturnSuccess) { + IOObjectRelease(registryEntry); + continue; + } + ffCfDictGetString(properties, CFSTR("model"), &gpu->name); + } + + ffStrbufInit(&gpu->vendor); + int vendorId; + if (ffCfDictGetInt(properties, CFSTR("vendor-id"), &vendorId) == NULL) { + const char* vendorStr = ffGPUGetVendorString((unsigned) vendorId); + ffStrbufAppendS(&gpu->vendor, vendorStr); + if (vendorStr == FF_GPU_VENDOR_NAME_APPLE || vendorStr == FF_GPU_VENDOR_NAME_INTEL) { + gpu->type = FF_GPU_TYPE_INTEGRATED; + } else if (vendorStr == FF_GPU_VENDOR_NAME_NVIDIA || vendorStr == FF_GPU_VENDOR_NAME_AMD) { + gpu->type = FF_GPU_TYPE_DISCRETE; + } + +#ifdef __aarch64__ + if (vendorStr == FF_GPU_VENDOR_NAME_APPLE) { + detectFrequency(gpu); + } +#endif + + if (gpu->type == FF_GPU_TYPE_INTEGRATED) { + gpu->shared.total = vramTotal; + gpu->shared.used = vramUsed; + } else if (gpu->type == FF_GPU_TYPE_DISCRETE) { + gpu->dedicated.total = vramTotal; + gpu->dedicated.used = vramUsed; + } + } + + gpu->temperature = options->temp ? detectGpuTemp(&gpu->name) : FF_GPU_TEMP_UNSET; + + CFRelease(properties); + IOObjectRelease(registryEntry); + } + + ffGpuDetectMetal(gpus); + if (instance.config.general.detectVersion) { + ffGpuDetectDriverVersion(gpus); + } + return NULL; +} diff --git a/src/detection/gpu/gpu_apple.m b/src/detection/gpu/gpu_apple.m new file mode 100644 index 0000000..e206bdc --- /dev/null +++ b/src/detection/gpu/gpu_apple.m @@ -0,0 +1,87 @@ +#include "gpu.h" + +#import <Metal/MTLDevice.h> +#import <IOKit/kext/KextManager.h> + +#ifndef MAC_OS_VERSION_26_0 + #define MTLGPUFamilyMetal4 ((MTLGPUFamily) 5002) +#endif +#ifndef MAC_OS_VERSION_13_0 + #define MTLGPUFamilyMetal3 ((MTLGPUFamily) 5001) +#endif +#ifndef MAC_OS_X_VERSION_10_15 + #define MTLFeatureSet_macOS_GPUFamily1_v4 ((MTLFeatureSet) 10004) + #define MTLFeatureSet_macOS_GPUFamily2_v1 ((MTLFeatureSet) 10005) +#endif + +const char* ffGpuDetectDriverVersion(FFlist* gpus) +{ + if (@available(macOS 10.7, *)) + { + NSMutableArray* arr = NSMutableArray.new; + FF_LIST_FOR_EACH(FFGPUResult, x, *gpus) + [arr addObject:@(x->driver.chars)]; + + NSDictionary* dict = CFBridgingRelease(KextManagerCopyLoadedKextInfo((__bridge CFArrayRef)arr, (__bridge CFArrayRef)@[@"CFBundleVersion"])); + FF_LIST_FOR_EACH(FFGPUResult, x, *gpus) + { + NSString* version = dict[@(x->driver.chars)][@"CFBundleVersion"]; + if (version) + { + ffStrbufAppendC(&x->driver, ' '); + ffStrbufAppendS(&x->driver, version.UTF8String); + } + } + return NULL; + } + return "Unsupported macOS version"; +} + +const char* ffGpuDetectMetal(FFlist* gpus) +{ + if (@available(macOS 10.13, *)) + { + for (id<MTLDevice> device in MTLCopyAllDevices()) + { + FFGPUResult* gpu = NULL; + FF_LIST_FOR_EACH(FFGPUResult, x, *gpus) + { + if (x->deviceId == device.registryID) + { + gpu = x; + break; + } + } + if (!gpu) continue; + + #ifndef MAC_OS_X_VERSION_10_15 + if ([device supportsFeatureSet:MTLFeatureSet_macOS_GPUFamily2_v1]) + ffStrbufSetStatic(&gpu->platformApi, "Metal Feature Set 2"); + else if ([device supportsFeatureSet:MTLFeatureSet_macOS_GPUFamily1_v1]) + ffStrbufSetStatic(&gpu->platformApi, "Metal Feature Set 1"); + #else // MAC_OS_X_VERSION_10_15 + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wunguarded-availability-new" + if ([device supportsFamily:MTLGPUFamilyMetal4]) + ffStrbufSetStatic(&gpu->platformApi, "Metal 4"); + else if ([device supportsFamily:MTLGPUFamilyMetal3]) + ffStrbufSetStatic(&gpu->platformApi, "Metal 3"); + #pragma clang diagnostic pop + else if ([device supportsFamily:MTLGPUFamilyCommon3]) + ffStrbufSetStatic(&gpu->platformApi, "Metal Common 3"); + else if ([device supportsFamily:MTLGPUFamilyCommon2]) + ffStrbufSetStatic(&gpu->platformApi, "Metal Common 2"); + else if ([device supportsFamily:MTLGPUFamilyCommon1]) + ffStrbufSetStatic(&gpu->platformApi, "Metal Common 1"); + + gpu->type = device.hasUnifiedMemory ? FF_GPU_TYPE_INTEGRATED : FF_GPU_TYPE_DISCRETE; + gpu->index = (uint32_t) device.locationNumber; + + if (device.hasUnifiedMemory && device.recommendedMaxWorkingSetSize > 0) + gpu->shared.total = device.recommendedMaxWorkingSetSize; + #endif + } + return NULL; + } + return "Metal API is not supported by this macOS version"; +} diff --git a/src/detection/gpu/gpu_bsd.c b/src/detection/gpu/gpu_bsd.c new file mode 100644 index 0000000..ae29f46 --- /dev/null +++ b/src/detection/gpu/gpu_bsd.c @@ -0,0 +1,230 @@ +#include "gpu_driver_specific.h" + +#include "common/io.h" +#include "common/mallocHelper.h" + +#include <sys/pciio.h> +#include <fcntl.h> +#if __has_include(<dev/pci/pcireg.h>) + #include <dev/pci/pcireg.h> // FreeBSD +#else + #include <bus/pci/pcireg.h> // DragonFly +#endif + +static void fillGPUTypeGeneric(FFGPUResult* gpu) { + if (gpu->type == FF_GPU_TYPE_UNKNOWN) { + if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_NVIDIA) { + if (ffStrbufStartsWithIgnCaseS(&gpu->name, "GeForce") || + ffStrbufStartsWithIgnCaseS(&gpu->name, "Quadro") || + ffStrbufStartsWithIgnCaseS(&gpu->name, "Tesla")) { + gpu->type = FF_GPU_TYPE_DISCRETE; + } + } else if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_MTHREADS) { + if (ffStrbufStartsWithIgnCaseS(&gpu->name, "MTT ")) { + gpu->type = FF_GPU_TYPE_DISCRETE; + } + } else if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_INTEL) { + // 0000:00:02.0 is reserved for Intel integrated graphics + gpu->type = gpu->deviceId == ffGPUPciAddr2Id(0, 0, 2, 0) ? FF_GPU_TYPE_INTEGRATED : FF_GPU_TYPE_DISCRETE; + } + } +} + +#if FF_HAVE_DRM + #include "common/library.h" + #include "common/strutil.h" + + #include <xf86drm.h> + +static const char* detectByDrm(const FFGPUOptions* options, FFlist* gpus) { + FF_LIBRARY_LOAD_MESSAGE(libdrm, "libdrm" FF_LIBRARY_EXTENSION, 2) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmGetDevices) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, drmFreeDevices) + + drmDevicePtr devices[64]; + int nDevices = ffdrmGetDevices(devices, ARRAY_SIZE(devices)); + if (nDevices < 0) { + return "drmGetDevices() failed"; + } + + for (int iDev = 0; iDev < nDevices; ++iDev) { + drmDevice* dev = devices[iDev]; + + if (!(dev->available_nodes & (1 << DRM_NODE_PRIMARY))) { + continue; + } + + const char* path = dev->nodes[DRM_NODE_PRIMARY]; + + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus); + ffStrbufInit(&gpu->vendor); + ffStrbufInit(&gpu->name); + ffStrbufInit(&gpu->driver); + ffStrbufInitS(&gpu->platformApi, path); + ffStrbufInit(&gpu->memoryType); + gpu->index = FF_GPU_INDEX_UNSET; + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + gpu->type = FF_GPU_TYPE_UNKNOWN; + gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + gpu->deviceId = 0; + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + + switch (dev->bustype) { + case DRM_BUS_PCI: + ffStrbufInitStatic(&gpu->vendor, ffGPUGetVendorString(dev->deviceinfo.pci->vendor_id)); + gpu->deviceId = ffGPUPciAddr2Id(dev->businfo.pci->domain, dev->businfo.pci->bus, dev->businfo.pci->dev, dev->businfo.pci->func); + break; + case DRM_BUS_HOST1X: + ffStrbufSetS(&gpu->name, dev->deviceinfo.host1x->compatible[0]); + gpu->type = FF_GPU_TYPE_INTEGRATED; + break; + case DRM_BUS_PLATFORM: + ffStrbufSetS(&gpu->name, dev->deviceinfo.platform->compatible[0]); + gpu->type = FF_GPU_TYPE_INTEGRATED; + break; + case DRM_BUS_USB: + ffStrbufSetF(&gpu->name, "USB Device (%u-%u)", dev->deviceinfo.usb->vendor, dev->deviceinfo.usb->product); + gpu->type = FF_GPU_TYPE_DISCRETE; + break; + } + + FF_AUTO_CLOSE_FD int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + continue; + } + + char driverName[64]; + driverName[0] = '\0'; + struct drm_version ver = { + .name = driverName, + .name_len = ARRAY_SIZE(driverName), + }; + if (ioctl(fd, DRM_IOCTL_VERSION, &ver) == 0) { + driverName[ver.name_len] = '\0'; + ffStrbufSetF(&gpu->driver, "%s %d.%d.%d", ver.name, ver.version_major, ver.version_minor, ver.version_patchlevel); + } + + if (ffStrStartsWith(driverName, "i915")) { + ffDrmDetectI915(gpu, fd); + } else if (ffStrStartsWith(driverName, "amdgpu")) { + ffDrmDetectAmdgpu(options, gpu, dev->nodes[DRM_NODE_RENDER]); + } else if (ffStrStartsWith(driverName, "radeon")) { + ffDrmDetectRadeon(options, gpu, dev->nodes[DRM_NODE_RENDER]); + } else if (ffStrStartsWith(driverName, "xe")) { + ffDrmDetectXe(gpu, fd); + } else if (ffStrStartsWith(driverName, "asahi")) { + ffDrmDetectAsahi(gpu, fd); + } else if (ffStrStartsWith(driverName, "nouveau")) { + ffDrmDetectNouveau(gpu, fd); + } else if (dev->bustype == DRM_BUS_PCI) { + ffGPUDetectDriverSpecific(options, gpu, (FFGpuDriverPciBusId) { + .domain = (uint32_t) dev->businfo.pci->domain, + .bus = dev->businfo.pci->bus, + .device = dev->businfo.pci->dev, + .func = dev->businfo.pci->func, + }); + } + + if (gpu->name.length == 0) { + if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_AMD) { + ffGPUQueryAmdGpuName(dev->deviceinfo.pci->device_id, dev->deviceinfo.pci->revision_id, gpu); + } + if (gpu->name.length == 0) { + ffGPUFillVendorAndName(0, dev->deviceinfo.pci->vendor_id, dev->deviceinfo.pci->device_id, gpu); + } + } + + fillGPUTypeGeneric(gpu); + } + + ffdrmFreeDevices(devices, nDevices); + + return NULL; +} +#endif + +static const char* detectByPci(const FFGPUOptions* options, FFlist* gpus) { + FF_AUTO_CLOSE_FD int fd = open("/dev/pci", O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return "open(\"/dev/pci\", O_RDONLY | O_CLOEXEC, 0) failed"; + } + + struct pci_conf confs[128]; + struct pci_match_conf match = { + .pc_class = PCIC_DISPLAY, + .flags = PCI_GETCONF_MATCH_CLASS, + }; + struct pci_conf_io pcio = { + .pat_buf_len = sizeof(match), + .num_patterns = 1, + .patterns = &match, + .match_buf_len = sizeof(confs), + .matches = confs, + }; + + if (ioctl(fd, PCIOCGETCONF, &pcio) < 0) { + return "ioctl(fd, PCIOCGETCONF, &pc) failed"; + } + + if (pcio.status == PCI_GETCONF_ERROR) { + return "ioctl(fd, PCIOCGETCONF, &pc) returned error"; + } + + for (uint32_t i = 0; i < pcio.num_matches; ++i) { + struct pci_conf* pc = &confs[i]; + + if (pc->pc_sel.pc_func > 0 && pc->pc_subclass == 0x80 /*PCI_CLASS_DISPLAY_OTHER*/) { + continue; // Likely an auxiliary display controller (#2034) + } + + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus); + ffStrbufInitStatic(&gpu->vendor, ffGPUGetVendorString(pc->pc_vendor)); + ffStrbufInit(&gpu->name); + ffStrbufInitS(&gpu->driver, pc->pd_name); + ffStrbufInitStatic(&gpu->platformApi, "/dev/pci"); + ffStrbufInit(&gpu->memoryType); + gpu->index = FF_GPU_INDEX_UNSET; + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + gpu->type = FF_GPU_TYPE_UNKNOWN; + gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + gpu->deviceId = ffGPUPciAddr2Id(pc->pc_sel.pc_domain, pc->pc_sel.pc_bus, pc->pc_sel.pc_dev, pc->pc_sel.pc_func); + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + + ffGPUDetectDriverSpecific(options, gpu, (FFGpuDriverPciBusId) { + .domain = (uint32_t) pc->pc_sel.pc_domain, + .bus = pc->pc_sel.pc_bus, + .device = pc->pc_sel.pc_dev, + .func = pc->pc_sel.pc_func, + }); + + if (gpu->name.length == 0) { + if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_AMD) { + ffGPUQueryAmdGpuName(pc->pc_device, pc->pc_revid, gpu); + } + if (gpu->name.length == 0) { + ffGPUFillVendorAndName(pc->pc_subclass, pc->pc_vendor, pc->pc_device, gpu); + } + } + + fillGPUTypeGeneric(gpu); + } + + return NULL; +} + +const char* ffDetectGPUImpl(const FFGPUOptions* options, FFlist* gpus) { +#if FF_HAVE_DRM + if (options->detectionMethod == FF_GPU_DETECTION_METHOD_AUTO) { + detectByDrm(options, gpus); + if (gpus->length > 0) { + return NULL; + } + } +#endif + + return detectByPci(options, gpus); +} diff --git a/src/detection/gpu/gpu_driver_specific.h b/src/detection/gpu/gpu_driver_specific.h new file mode 100644 index 0000000..3ddfb69 --- /dev/null +++ b/src/detection/gpu/gpu_driver_specific.h @@ -0,0 +1,90 @@ +#pragma once + +#include "gpu.h" + +typedef enum FF_A_PACKED FFGpuDriverConditionType { + FF_GPU_DRIVER_CONDITION_TYPE_BUS_ID = 1 << 0, + FF_GPU_DRIVER_CONDITION_TYPE_DEVICE_ID = 1 << 1, + FF_GPU_DRIVER_CONDITION_TYPE_LUID = 1 << 2, + FF_GPU_DRIVER_CONDITION_TYPE_FORCE_UNSIGNED = UINT8_MAX, +} FFGpuDriverConditionType; + +typedef struct FFGpuDriverPciDeviceId { + uint32_t deviceId; + uint32_t vendorId; + uint32_t subSystemId; + uint32_t revId; +} FFGpuDriverPciDeviceId; + +// Use pciBusId if not NULL; use pciDeviceId otherwise +typedef struct FFGpuDriverCondition { + FFGpuDriverConditionType type; + FFGpuDriverPciBusId pciBusId; + FFGpuDriverPciDeviceId pciDeviceId; + uint64_t luid; +} FFGpuDriverCondition; + +// detect x if not NULL +typedef struct FFGpuDriverResult { + uint32_t* index; + double* temp; + FFGPUMemory* memory; + FFstrbuf* memoryType; + FFGPUMemory* sharedMemory; + uint32_t* coreCount; + double* coreUsage; + FFGPUType* type; + uint32_t* frequency; + FFstrbuf* name; +} FFGpuDriverResult; + +const char* ffDetectNvidiaGpuInfo(const FFGpuDriverCondition* cond, FFGpuDriverResult result, const char* soName); +const char* ffDetectIntelGpuInfo(const FFGpuDriverCondition* cond, FFGpuDriverResult result, const char* soName); +const char* ffDetectAmdGpuInfo(const FFGpuDriverCondition* cond, FFGpuDriverResult result, const char* soName); +const char* ffDetectMthreadsGpuInfo(const FFGpuDriverCondition* cond, FFGpuDriverResult result, const char* soName); + +#ifndef FF_GPU_DRIVER_DLLNAME_PATH_PREFIX + #define FF_GPU_DRIVER_DLLNAME_PATH_PREFIX +#endif + +FF_A_UNUSED static inline bool getDriverSpecificDetectionFn(const char* vendor, __typeof__(&ffDetectNvidiaGpuInfo)* pDetectFn, const char** pDllName) { + if (vendor == FF_GPU_VENDOR_NAME_NVIDIA) { + *pDetectFn = ffDetectNvidiaGpuInfo; +#ifdef _WIN32 + *pDllName = FF_GPU_DRIVER_DLLNAME_PATH_PREFIX "nvml.dll"; +#else + *pDllName = FF_GPU_DRIVER_DLLNAME_PATH_PREFIX "libnvidia-ml.so"; +#endif + } else if (vendor == FF_GPU_VENDOR_NAME_MTHREADS) { + *pDetectFn = ffDetectMthreadsGpuInfo; +#ifdef _WIN32 + *pDllName = FF_GPU_DRIVER_DLLNAME_PATH_PREFIX "mtml.dll"; +#else + *pDllName = FF_GPU_DRIVER_DLLNAME_PATH_PREFIX "libmtml.so"; +#endif + } +#ifdef _WIN32 + else if (vendor == FF_GPU_VENDOR_NAME_INTEL) { + *pDetectFn = ffDetectIntelGpuInfo; + #ifdef _WIN64 + *pDllName = FF_GPU_DRIVER_DLLNAME_PATH_PREFIX "ControlLib.dll"; + #else + *pDllName = FF_GPU_DRIVER_DLLNAME_PATH_PREFIX "ControlLib32.dll"; + #endif + } else if (vendor == FF_GPU_VENDOR_NAME_AMD) { + *pDetectFn = ffDetectAmdGpuInfo; + #ifdef _WIN64 + *pDllName = FF_GPU_DRIVER_DLLNAME_PATH_PREFIX "atiadlxx.dll"; + #else + *pDllName = FF_GPU_DRIVER_DLLNAME_PATH_PREFIX "atiadlxy.dll"; + #endif + } +#endif + else { + *pDetectFn = NULL; + *pDllName = NULL; + return false; + } + + return true; +} diff --git a/src/detection/gpu/gpu_drm.c b/src/detection/gpu/gpu_drm.c new file mode 100644 index 0000000..3a7d381 --- /dev/null +++ b/src/detection/gpu/gpu_drm.c @@ -0,0 +1,380 @@ +#include "gpu.h" + +#if FF_HAVE_DRM + #include <drm.h> + #include <fcntl.h> + #include <sys/ioctl.h> + + #include "common/io.h" + #include "common/library.h" + #include "common/mallocHelper.h" + #include "common/strutil.h" + + #include "intel_drm.h" + #include "asahi_drm.h" + #include <radeon_drm.h> + #include <nouveau_drm.h> + +const char* ffDrmDetectRadeon(const FFGPUOptions* options, FFGPUResult* gpu, const char* renderPath) { + FF_AUTO_CLOSE_FD int fd = open(renderPath, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return "Failed to open DRM render device"; + } + + uint32_t value; + + // https://github.com/torvalds/linux/blob/fb4d33ab452ea254e2c319bac5703d1b56d895bf/drivers/gpu/drm/radeon/radeon_kms.c#L231 + + if (ioctl(fd, DRM_IOCTL_RADEON_INFO, &(struct drm_radeon_info) { + .request = RADEON_INFO_ACTIVE_CU_COUNT, + .value = (uintptr_t) &value, + }) >= 0) { + gpu->coreCount = (int32_t) value; + } + + if (options->temp) { + if (ioctl(fd, DRM_IOCTL_RADEON_INFO, &(struct drm_radeon_info) { + .request = RADEON_INFO_CURRENT_GPU_TEMP, // millidegrees C + .value = (uintptr_t) &value, + }) >= 0 && + value != 0) { // 0 means unavailable + gpu->temperature = (double) value / 1000.0; + } + } + + if (ioctl(fd, DRM_IOCTL_RADEON_INFO, &(struct drm_radeon_info) { + .request = RADEON_INFO_MAX_SCLK, // MHz + .value = (uintptr_t) &value, + }) >= 0) { + gpu->frequency = (uint32_t) (value / 1000u); + } + + if (options->driverSpecific) { + struct drm_radeon_gem_info gemInfo; + if (ioctl(fd, DRM_IOCTL_RADEON_GEM_INFO, &gemInfo) >= 0) { + // vram_usage can be bigger than vram_usage, so we use vram_size here + gpu->dedicated.total = gemInfo.vram_size; + gpu->shared.total = gemInfo.gart_size; + + uint64_t memSize; + if (ioctl(fd, DRM_IOCTL_RADEON_INFO, &(struct drm_radeon_info) { + .request = RADEON_INFO_VRAM_USAGE, // uint64_t + .value = (uintptr_t) &memSize, + }) >= 0) { + gpu->dedicated.used = memSize; + } + + if (ioctl(fd, DRM_IOCTL_RADEON_INFO, &(struct drm_radeon_info) { + .request = RADEON_INFO_GTT_USAGE, // uint64_t + .value = (uintptr_t) &memSize, + }) >= 0) { + gpu->shared.used = memSize; + } + } + } + + return NULL; +} + + #ifdef FF_HAVE_DRM_AMDGPU + #include <amdgpu.h> + #include <amdgpu_drm.h> + +const char* ffDrmDetectAmdgpu(const FFGPUOptions* options, FFGPUResult* gpu, const char* renderPath) { + #if FF_HAVE_DRM_AMDGPU + FF_LIBRARY_LOAD_MESSAGE(libdrm, "libdrm_amdgpu" FF_LIBRARY_EXTENSION, 1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, amdgpu_device_initialize) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, amdgpu_get_marketing_name) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, amdgpu_query_gpu_info) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, amdgpu_query_sensor_info) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, amdgpu_query_heap_info) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libdrm, amdgpu_device_deinitialize) + + FF_AUTO_CLOSE_FD int fd = open(renderPath, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return "Failed to open DRM render device"; + } + + amdgpu_device_handle handle; + uint32_t majorVersion, minorVersion; + if (ffamdgpu_device_initialize(fd, &majorVersion, &minorVersion, &handle) < 0) { + return "Failed to initialize AMDGPU device"; + } + + uint32_t value; + + if (options->temp) { + if (ffamdgpu_query_sensor_info(handle, AMDGPU_INFO_SENSOR_GPU_TEMP, sizeof(value), &value) >= 0) { + gpu->temperature = value / 1000.; + } + } + + ffStrbufSetS(&gpu->name, ffamdgpu_get_marketing_name(handle)); + + struct amdgpu_gpu_info gpuInfo; + if (ffamdgpu_query_gpu_info(handle, &gpuInfo) >= 0) { + gpu->coreCount = (int32_t) gpuInfo.cu_active_number; + gpu->frequency = (uint32_t) (gpuInfo.max_engine_clk / 1000u); + gpu->index = FF_GPU_INDEX_UNSET; + gpu->type = gpuInfo.ids_flags & AMDGPU_IDS_FLAGS_FUSION ? FF_GPU_TYPE_INTEGRATED : FF_GPU_TYPE_DISCRETE; + #define FF_VRAM_CASE(name, value) \ + case value /* AMDGPU_VRAM_TYPE_ ## name */: \ + ffStrbufSetStatic(&gpu->memoryType, #name); \ + break + switch (gpuInfo.vram_type) { + FF_VRAM_CASE(UNKNOWN, 0); + FF_VRAM_CASE(GDDR1, 1); + FF_VRAM_CASE(DDR2, 2); + FF_VRAM_CASE(GDDR3, 3); + FF_VRAM_CASE(GDDR4, 4); + FF_VRAM_CASE(GDDR5, 5); + FF_VRAM_CASE(HBM, 6); + FF_VRAM_CASE(DDR3, 7); + FF_VRAM_CASE(DDR4, 8); + FF_VRAM_CASE(GDDR6, 9); + FF_VRAM_CASE(DDR5, 10); + FF_VRAM_CASE(LPDDR4, 11); + FF_VRAM_CASE(LPDDR5, 12); + default: + ffStrbufAppendF(&gpu->memoryType, "Unknown (%u)", gpuInfo.vram_type); + break; + } + + struct amdgpu_heap_info heapInfo; + if (ffamdgpu_query_heap_info(handle, AMDGPU_GEM_DOMAIN_VRAM, 0, &heapInfo) >= 0) { + gpu->dedicated.total = heapInfo.heap_size; + gpu->dedicated.used = heapInfo.heap_usage; + } + if (ffamdgpu_query_heap_info(handle, AMDGPU_GEM_DOMAIN_GTT, 0, &heapInfo) >= 0) { + gpu->shared.total = heapInfo.heap_size; + gpu->shared.used = heapInfo.heap_usage; + } + } + + if (ffamdgpu_query_sensor_info(handle, AMDGPU_INFO_SENSOR_GPU_LOAD, sizeof(value), &value) >= 0) { + gpu->coreUsage = value; + } + + ffamdgpu_device_deinitialize(handle); + + return NULL; + #else + FF_UNUSED(options, gpu, renderPath); + return "Fastfetch is compiled without libdrm support"; + #endif +} + #endif + +const char* ffDrmDetectI915(FFGPUResult* gpu, int fd) { + { + int value; + drm_i915_getparam_t getparam = { .param = I915_PARAM_EU_TOTAL, .value = &value }; + if (ioctl(fd, DRM_IOCTL_I915_GETPARAM, &getparam) >= 0) { + gpu->coreCount = value; + } + } + { + struct drm_i915_query_item queryItem = { + .query_id = DRM_I915_QUERY_MEMORY_REGIONS, + }; + struct drm_i915_query query = { + .items_ptr = (uintptr_t) &queryItem, + .num_items = 1, + }; + if (ioctl(fd, DRM_IOCTL_I915_QUERY, &query) >= 0 && queryItem.length > 0) { // #2259 + FF_AUTO_FREE uint8_t* buffer = calloc(1, (size_t) queryItem.length); + queryItem.data_ptr = (uintptr_t) buffer; + if (ioctl(fd, DRM_IOCTL_I915_QUERY, &query) >= 0) { + gpu->dedicated.total = gpu->shared.total = gpu->dedicated.used = gpu->shared.used = 0; + struct drm_i915_query_memory_regions* regionInfo = (void*) buffer; + for (uint32_t i = 0; i < regionInfo->num_regions; i++) { + struct drm_i915_memory_region_info* region = regionInfo->regions + i; + switch (region->region.memory_class) { + case I915_MEMORY_CLASS_SYSTEM: + gpu->shared.total += region->probed_size; + gpu->shared.used += region->probed_size - region->unallocated_size; + break; + case I915_MEMORY_CLASS_DEVICE: + gpu->dedicated.total += region->probed_size; + gpu->dedicated.used += region->probed_size - region->unallocated_size; + break; + } + } + } + } + } + return NULL; +} + +static inline int popcountBytes(uint8_t* bytes, uint32_t length) { + int count = 0; + while (length >= 8) { + count += __builtin_popcountll(*(uint64_t*) bytes); + bytes += 8; + length -= 8; + } + if (length >= 4) { + count += __builtin_popcountl(*(uint32_t*) bytes); + bytes += 4; + length -= 4; + } + if (length >= 2) { + count += __builtin_popcountl(*(uint16_t*) bytes); + bytes += 2; + length -= 2; + } + if (length) { + count += __builtin_popcountl(*(uint8_t*) bytes); + } + return count; +} + +const char* ffDrmDetectXe(FFGPUResult* gpu, int fd) { + bool flag = false; + { + struct drm_xe_device_query query = { + .query = DRM_XE_DEVICE_QUERY_GT_TOPOLOGY, + }; + if (ioctl(fd, DRM_IOCTL_XE_DEVICE_QUERY, &query) >= 0) { + FF_AUTO_FREE uint8_t* buffer = malloc(query.size); + query.data = (uintptr_t) buffer; + if (ioctl(fd, DRM_IOCTL_XE_DEVICE_QUERY, &query) >= 0) { + int dssCount = 0, euPerDssCount = 0; + for (struct drm_xe_query_topology_mask* topo = (void*) buffer; + (uint8_t*) topo < buffer + query.size; + topo = (void*) (topo->mask + topo->num_bytes)) { + switch (topo->type) { + case DRM_XE_TOPO_DSS_COMPUTE: + case DRM_XE_TOPO_DSS_GEOMETRY: + dssCount += popcountBytes(topo->mask, topo->num_bytes); + break; + case DRM_XE_TOPO_EU_PER_DSS: + euPerDssCount += popcountBytes(topo->mask, topo->num_bytes); + break; + } + } + gpu->coreCount = dssCount * euPerDssCount; + flag = true; + } + } + } + + { + struct drm_xe_device_query query = { + .query = DRM_XE_DEVICE_QUERY_MEM_REGIONS, + }; + if (ioctl(fd, DRM_IOCTL_XE_DEVICE_QUERY, &query) >= 0) { + FF_AUTO_FREE uint8_t* buffer = malloc(query.size); + query.data = (uintptr_t) buffer; + if (ioctl(fd, DRM_IOCTL_XE_DEVICE_QUERY, &query) >= 0) { + gpu->dedicated.total = gpu->shared.total = gpu->dedicated.used = gpu->shared.used = 0; + struct drm_xe_query_mem_regions* regionInfo = (void*) buffer; + for (uint32_t i = 0; i < regionInfo->num_mem_regions; i++) { + struct drm_xe_mem_region* region = regionInfo->mem_regions + i; + switch (region->mem_class) { + case DRM_XE_MEM_REGION_CLASS_SYSMEM: + gpu->shared.total += region->total_size; + gpu->shared.used += region->used; + break; + case DRM_XE_MEM_REGION_CLASS_VRAM: + gpu->dedicated.total += region->total_size; + gpu->dedicated.used += region->used; + break; + } + } + flag = true; + } + } + } + return flag ? NULL : "Failed to query Xe GPU information"; +} + +const char* ffDrmDetectAsahi(FFGPUResult* gpu, int fd) { + struct drm_asahi_params_global paramsGlobal = {}; + if (ioctl(fd, DRM_IOCTL_ASAHI_GET_PARAMS, &(struct drm_asahi_get_params) { + .param_group = DRM_ASAHI_GET_PARAMS, + .pointer = (uintptr_t) ¶msGlobal, + .size = sizeof(paramsGlobal), + }) >= 0) { + // They removed `unstable_uabi_version` from the struct. Hopefully they won't introduce new ABI changes. + gpu->coreCount = (int32_t) (paramsGlobal.num_clusters_total * paramsGlobal.num_cores_per_cluster); + gpu->frequency = paramsGlobal.max_frequency_khz / 1000; + gpu->deviceId = ffGPUGeneral2Id(paramsGlobal.chip_id); + + if (!gpu->name.length) { + const char* variant = " Unknown"; + switch (paramsGlobal.gpu_variant) { + case 'G': + variant = ""; + break; + case 'S': + variant = " Pro"; + break; + case 'C': + variant = " Max"; + break; + case 'D': + variant = " Ultra"; + break; + } + ffStrbufSetF(&gpu->name, "Apple M%d%s (G%d%c %02X)", paramsGlobal.gpu_generation - 12, variant, paramsGlobal.gpu_generation, paramsGlobal.gpu_variant, paramsGlobal.gpu_revision + 0xA0); + } + + return NULL; + } + + return "Failed to query Asahi GPU information"; +} + + #ifndef DRM_IOCTL_NOUVEAU_GETPARAM + #define DRM_IOCTL_NOUVEAU_GETPARAM DRM_IOWR(DRM_COMMAND_BASE + DRM_NOUVEAU_GETPARAM, struct drm_nouveau_getparam) + #endif + +const char* ffDrmDetectNouveau(FFGPUResult* gpu, int fd) { + struct drm_nouveau_getparam getparam = {}; + + getparam.param = NOUVEAU_GETPARAM_FB_SIZE; + if (ioctl(fd, DRM_IOCTL_NOUVEAU_GETPARAM, &getparam) == 0) { + gpu->dedicated.total = getparam.value; + } + + getparam.param = NOUVEAU_GETPARAM_AGP_SIZE; + if (ioctl(fd, DRM_IOCTL_NOUVEAU_GETPARAM, &getparam) == 0) { + gpu->shared.total = getparam.value; + } + + getparam.param = NOUVEAU_GETPARAM_GRAPH_UNITS; + if (ioctl(fd, DRM_IOCTL_NOUVEAU_GETPARAM, &getparam) == 0 && getparam.value < INT32_MAX) { + gpu->coreCount = (int32_t) getparam.value; + } + + return NULL; +} + +#endif // FF_HAVE_DRM + +#include "gpu_driver_specific.h" + +const char* ffGPUDetectDriverSpecific(const FFGPUOptions* options, FFGPUResult* gpu, FFGpuDriverPciBusId pciBusId) { + __typeof__(&ffDetectNvidiaGpuInfo) detectFn; + const char* soName; + if (getDriverSpecificDetectionFn(gpu->vendor.chars, &detectFn, &soName) && (options->temp || options->driverSpecific)) { + return detectFn(&(FFGpuDriverCondition) { + .type = FF_GPU_DRIVER_CONDITION_TYPE_BUS_ID, + .pciBusId = pciBusId, + }, + (FFGpuDriverResult) { + .index = &gpu->index, + .temp = options->temp ? &gpu->temperature : NULL, + .memory = options->driverSpecific ? &gpu->dedicated : NULL, + .coreCount = options->driverSpecific ? (uint32_t*) &gpu->coreCount : NULL, + .coreUsage = options->driverSpecific ? &gpu->coreUsage : NULL, + .type = &gpu->type, + .frequency = options->driverSpecific ? &gpu->frequency : NULL, + .name = &gpu->name, + }, + soName); + } + + return "No driver-specific detection function found for the GPU vendor"; +} diff --git a/src/detection/gpu/gpu_gnu.c b/src/detection/gpu/gpu_gnu.c new file mode 100644 index 0000000..27c774b --- /dev/null +++ b/src/detection/gpu/gpu_gnu.c @@ -0,0 +1,146 @@ +#include "gpu.h" +#include "common/io.h" + +#include <hurd.h> +#include <hurd/pci.h> +#include <hurd/paths.h> + +enum { + PCI_VENDOR_ID = 0x00, + PCI_DEVICE_ID = 0x02, + PCI_REVISION_ID = 0x08, + PCI_CLASS_PROG = 0x09, + PCI_SUBCLASS = 0x0a, + PCI_CLASS_DEVICE = 0x0b, + PCI_CONF_SIZE = 0x40, +}; + +const char* ffDetectGPUImpl(FF_A_UNUSED const FFGPUOptions* options, FFlist* gpus) { + int dDomainFd = open(_SERVERS_BUS "/pci/0000", O_RDONLY | O_CLOEXEC); + if (dDomainFd < 0) { + return "open(_SERVERS_BUS \"/pci/0000\") failed"; + } + + FF_AUTO_CLOSE_DIR DIR* dirDomain = fdopendir(dDomainFd); + if (dirDomain == NULL) { + return "fdopendir(domain) failed"; + } + + struct dirent* busEntry; + while ((busEntry = readdir(dirDomain)) != NULL) { + if (busEntry->d_type != DT_DIR || busEntry->d_name[0] == '.') { + continue; + } + + char* endptr; + uint16_t pciBus = (uint16_t) strtoul(busEntry->d_name, &endptr, 16); + if (*endptr != '\0') { + continue; + } + + int dBusFd = openat(dDomainFd, busEntry->d_name, O_RDONLY | O_CLOEXEC); + if (dBusFd < 0) { + continue; + } + + FF_AUTO_CLOSE_DIR DIR* dirBus = fdopendir(dBusFd); + if (dirBus == NULL) { + continue; + } + + struct dirent* devEntry; + while ((devEntry = readdir(dirBus)) != NULL) { + if (devEntry->d_type != DT_DIR || devEntry->d_name[0] == '.') { + continue; + } + + uint8_t pciDev = (uint8_t) strtoul(devEntry->d_name, &endptr, 16); + if (*endptr != '\0') { + continue; + } + + int dDevFd = openat(dBusFd, devEntry->d_name, O_RDONLY | O_CLOEXEC); + if (dDevFd < 0) { + continue; + } + + FF_AUTO_CLOSE_DIR DIR* dirDev = fdopendir(dDevFd); + if (dirDev == NULL) { + continue; + } + + struct dirent* funcEntry; + while ((funcEntry = readdir(dirDev)) != NULL) { + if (funcEntry->d_type != DT_DIR || funcEntry->d_name[0] == '.') { + continue; + } + + uint8_t pciFunc = (uint8_t) strtoul(funcEntry->d_name, &endptr, 16); + if (*endptr != '\0') { + continue; + } + + char subpath[PATH_MAX]; + snprintf(subpath, ARRAY_SIZE(subpath), "%s/%s/%s/%s/config", _SERVERS_BUS "/pci/0000", busEntry->d_name, devEntry->d_name, funcEntry->d_name); + + mach_port_t devicePort = file_name_lookup(subpath, 0, 0); + if (devicePort == MACH_PORT_NULL) { + continue; + } + + mach_msg_type_number_t nread = 0; + + uint8_t data[PCI_CONF_SIZE]; + data_t pData = (data_t) data; + kern_return_t kr = pci_conf_read(devicePort, 0, &pData, &nread, PCI_CONF_SIZE); + mach_port_deallocate(mach_task_self(), devicePort); + if (kr != KERN_SUCCESS || nread < PCI_CONF_SIZE) { + continue; + } + + if (pData != (data_t) data) { + memcpy(data, pData, PCI_CONF_SIZE); + vm_deallocate(mach_task_self(), (vm_address_t) pData, nread); + } + + uint8_t classBase = data[PCI_CLASS_DEVICE]; + if (classBase != 0x03 /*PCI_BASE_CLASS_DISPLAY*/) { + continue; + } + + uint8_t classSub = data[PCI_SUBCLASS]; + if (pciFunc > 0 && classSub == 0x80 /*PCI_CLASS_DISPLAY_OTHER*/) { // Likely an auxiliary display controller (#2034) + continue; + } + + uint8_t revision = data[PCI_REVISION_ID]; + uint16_t vendorId = data[PCI_VENDOR_ID] | (data[PCI_VENDOR_ID + 1] << 8); + uint16_t deviceId = data[PCI_DEVICE_ID] | (data[PCI_DEVICE_ID + 1] << 8); + + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus); + ffStrbufInitStatic(&gpu->vendor, ffGPUGetVendorString(vendorId)); + ffStrbufInit(&gpu->name); + ffStrbufInit(&gpu->driver); + ffStrbufInitStatic(&gpu->platformApi, "/servers/bus/pci"); + ffStrbufInit(&gpu->memoryType); + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + gpu->type = FF_GPU_TYPE_UNKNOWN; + gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + gpu->deviceId = ffGPUPciAddr2Id(0, pciBus, pciDev, pciFunc); + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + + if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_AMD) { + ffGPUQueryAmdGpuName(deviceId, revision, gpu); + } + + if (gpu->name.length == 0) { + ffGPUFillVendorAndName(classSub, vendorId, deviceId, gpu); + } + } + } + } + + return NULL; +} diff --git a/src/detection/gpu/gpu_haiku.c b/src/detection/gpu/gpu_haiku.c new file mode 100644 index 0000000..f75f039 --- /dev/null +++ b/src/detection/gpu/gpu_haiku.c @@ -0,0 +1,51 @@ +#include "gpu.h" +#include "common/io.h" + +#include <private/drivers/poke.h> + +const char* ffDetectGPUImpl(FF_A_UNUSED const FFGPUOptions* options, FFlist* gpus) { + FF_AUTO_CLOSE_FD int pokefd = open(POKE_DEVICE_FULLNAME, O_RDWR | O_CLOEXEC); + if (pokefd < 0) { + return "open(POKE_DEVICE_FULLNAME) failed"; + } + + pci_info dev; + pci_info_args cmd = { + .signature = POKE_SIGNATURE, + .info = &dev, + }; + + for (cmd.index = 0; ioctl(pokefd, POKE_GET_NTH_PCI_INFO, &cmd, sizeof(cmd)) == B_OK && cmd.status == B_OK; ++cmd.index) { + if (dev.class_base != 0x03 /*PCI_BASE_CLASS_DISPLAY*/) { + continue; + } + + if (dev.function > 0 && dev.class_sub == 0x80 /*PCI_CLASS_DISPLAY_OTHER*/) { + continue; // Likely an auxiliary display controller (#2034) + } + + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus); + ffStrbufInitStatic(&gpu->vendor, ffGPUGetVendorString(dev.vendor_id)); + ffStrbufInit(&gpu->name); + ffStrbufInit(&gpu->driver); + ffStrbufInitStatic(&gpu->platformApi, POKE_DEVICE_FULLNAME); + ffStrbufInit(&gpu->memoryType); + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + gpu->type = FF_GPU_TYPE_UNKNOWN; + gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + gpu->deviceId = ffGPUPciAddr2Id(0, dev.bus, dev.device, dev.function); + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + + if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_AMD) { + ffGPUQueryAmdGpuName(dev.device_id, dev.revision, gpu); + } + + if (gpu->name.length == 0) { + ffGPUFillVendorAndName(dev.class_sub, dev.vendor_id, dev.device_id, gpu); + } + } + + return NULL; +} diff --git a/src/detection/gpu/gpu_intel.c b/src/detection/gpu/gpu_intel.c new file mode 100644 index 0000000..7d94c80 --- /dev/null +++ b/src/detection/gpu/gpu_intel.c @@ -0,0 +1,231 @@ +#include "gpu_driver_specific.h" + +#include "common/library.h" +#include "common/mallocHelper.h" +#include "igcl.h" + +struct FFIgclData { + FF_LIBRARY_SYMBOL(ctlClose) + + FF_LIBRARY_SYMBOL(ctlEnumerateDevices) + FF_LIBRARY_SYMBOL(ctlGetDeviceProperties) + FF_LIBRARY_SYMBOL(ctlEnumTemperatureSensors) + FF_LIBRARY_SYMBOL(ctlTemperatureGetProperties) + FF_LIBRARY_SYMBOL(ctlEnumMemoryModules) + FF_LIBRARY_SYMBOL(ctlMemoryGetProperties) + FF_LIBRARY_SYMBOL(ctlMemoryGetState) + FF_LIBRARY_SYMBOL(ctlEnumFrequencyDomains) + FF_LIBRARY_SYMBOL(ctlFrequencyGetProperties) + + bool inited; + ctl_api_handle_t apiHandle; +} igclData; + +static void shutdownIgcl() { + if (igclData.apiHandle) { + igclData.ffctlClose(igclData.apiHandle); + igclData.apiHandle = NULL; + } +} + +const char* ffDetectIntelGpuInfo(const FFGpuDriverCondition* cond, FFGpuDriverResult result, const char* soName) { + if (!igclData.inited) { + igclData.inited = true; + FF_LIBRARY_LOAD(libigcl, "dlopen igcl (ControlLib) failed", soName, 1); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libigcl, ctlInit) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libigcl, igclData, ctlClose) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libigcl, igclData, ctlEnumerateDevices) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libigcl, igclData, ctlGetDeviceProperties) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libigcl, igclData, ctlEnumTemperatureSensors) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libigcl, igclData, ctlTemperatureGetProperties) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libigcl, igclData, ctlEnumMemoryModules) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libigcl, igclData, ctlMemoryGetProperties) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libigcl, igclData, ctlMemoryGetState) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libigcl, igclData, ctlEnumFrequencyDomains) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libigcl, igclData, ctlFrequencyGetProperties) + + if (ffctlInit(&(ctl_init_args_t) { + .AppVersion = CTL_IMPL_VERSION, + .flags = CTL_INIT_FLAG_USE_LEVEL_ZERO, + .Size = sizeof(ctl_init_args_t), + .Version = 0, + }, + &igclData.apiHandle) != CTL_RESULT_SUCCESS) { + return "loading igcl library failed"; + } + atexit(shutdownIgcl); + libigcl = NULL; // don't close igcl + } + + if (!igclData.apiHandle) { + return "loading igcl library failed"; + } + + uint32_t deviceCount = 0; + if (igclData.ffctlEnumerateDevices(igclData.apiHandle, &deviceCount, NULL)) { + return "ctlEnumerateDevices(NULL) failed"; + } + if (deviceCount == 0) { + return "No Intel graphics adapter found"; + } + + FF_AUTO_FREE ctl_device_adapter_handle_t* devices = malloc(deviceCount * sizeof(*devices)); + if (igclData.ffctlEnumerateDevices(igclData.apiHandle, &deviceCount, devices)) { + return "ctlEnumerateDevices(devices) failed"; + } + + ctl_device_adapter_handle_t device = NULL; + + uint64_t /* LUID */ deviceId = 0; + ctl_device_adapter_properties_t properties = { + .Size = sizeof(properties), + .pDeviceID = &deviceId, + .device_id_size = sizeof(deviceId), + .Version = 2, + }; + for (uint32_t iDev = 0; iDev < deviceCount; iDev++) { + if (igclData.ffctlGetDeviceProperties(devices[iDev], &properties) != CTL_RESULT_SUCCESS) { + continue; + } + + if (properties.device_type != CTL_DEVICE_TYPE_GRAPHICS) { + continue; + } + + if (cond->type & FF_GPU_DRIVER_CONDITION_TYPE_BUS_ID) { + if (cond->pciBusId.bus == properties.adapter_bdf.bus && + cond->pciBusId.device == properties.adapter_bdf.device && + cond->pciBusId.func == properties.adapter_bdf.function) { + device = devices[iDev]; + break; + } + } else if (cond->type & FF_GPU_DRIVER_CONDITION_TYPE_LUID) { + if (cond->luid == deviceId) { + device = devices[iDev]; + break; + } + } else if (cond->type & FF_GPU_DRIVER_CONDITION_TYPE_DEVICE_ID) { + if ( + cond->pciDeviceId.deviceId == properties.pci_device_id && + cond->pciDeviceId.vendorId == properties.pci_vendor_id && + cond->pciDeviceId.subSystemId == (uint32_t) ((properties.pci_subsys_id << 16u) | properties.pci_subsys_vendor_id) && + cond->pciDeviceId.revId == properties.rev_id) { + device = devices[iDev]; + break; + } + } + } + + if (!device) { + return "Device not found"; + } + + if (result.coreCount) { + *result.coreCount = properties.num_slices * properties.num_sub_slices_per_slice * properties.num_eus_per_sub_slice; + } + + if (result.memory) { + ctl_mem_handle_t memoryModules[16]; + uint32_t memoryCount = ARRAY_SIZE(memoryModules); + if (igclData.ffctlEnumMemoryModules(device, &memoryCount, memoryModules) == CTL_RESULT_SUCCESS && memoryCount > 0) { + result.memory->used = 0; + result.memory->total = 0; + for (uint32_t iMem = 0; iMem < memoryCount; iMem++) { + ctl_mem_properties_t memoryProperties = { + .Size = sizeof(memoryProperties), + .Version = 0, + }; + if (igclData.ffctlMemoryGetProperties(memoryModules[iMem], &memoryProperties) == CTL_RESULT_SUCCESS) { + if (memoryProperties.location == CTL_MEM_LOC_DEVICE && result.memoryType) { + switch (memoryProperties.type) { +#define FF_ICTL_MEM_TYPE_CASE(type) \ + case CTL_MEM_TYPE_##type: \ + ffStrbufSetStatic(result.memoryType, #type); \ + break + FF_ICTL_MEM_TYPE_CASE(HBM); + FF_ICTL_MEM_TYPE_CASE(DDR); + FF_ICTL_MEM_TYPE_CASE(DDR3); + FF_ICTL_MEM_TYPE_CASE(DDR4); + FF_ICTL_MEM_TYPE_CASE(DDR5); + FF_ICTL_MEM_TYPE_CASE(LPDDR); + FF_ICTL_MEM_TYPE_CASE(LPDDR3); + FF_ICTL_MEM_TYPE_CASE(LPDDR4); + FF_ICTL_MEM_TYPE_CASE(LPDDR5); + FF_ICTL_MEM_TYPE_CASE(GDDR4); + FF_ICTL_MEM_TYPE_CASE(GDDR5); + FF_ICTL_MEM_TYPE_CASE(GDDR5X); + FF_ICTL_MEM_TYPE_CASE(GDDR6); + FF_ICTL_MEM_TYPE_CASE(GDDR6X); + FF_ICTL_MEM_TYPE_CASE(GDDR7); +#undef FF_ICTL_MEM_TYPE_CASE + default: + ffStrbufSetF(result.memoryType, "Unknown (%u)", memoryProperties.type); + break; + } + } + + ctl_mem_state_t memoryState = { + .Size = sizeof(ctl_mem_state_t), + .Version = 0, + }; + if (igclData.ffctlMemoryGetState(memoryModules[iMem], &memoryState) == CTL_RESULT_SUCCESS) { + if (memoryProperties.location == CTL_MEM_LOC_DEVICE) { + result.memory->total += memoryState.size; + result.memory->used += memoryState.size - memoryState.free; + } else if (result.sharedMemory && memoryProperties.location == CTL_MEM_LOC_SYSTEM) { + result.sharedMemory->total += memoryState.size; + result.sharedMemory->used += memoryState.size - memoryState.free; + } + } + } + } + } + } + + if (result.type) { + *result.type = properties.graphics_adapter_properties & CTL_ADAPTER_PROPERTIES_FLAG_INTEGRATED + ? FF_GPU_TYPE_INTEGRATED + : FF_GPU_TYPE_DISCRETE; + } + + if (result.temp) { + ctl_temp_handle_t sensors[16]; + uint32_t sensorCount = ARRAY_SIZE(sensors); + if (igclData.ffctlEnumTemperatureSensors(device, &sensorCount, sensors) == CTL_RESULT_SUCCESS && sensorCount > 0) { + for (uint32_t iSensor = 0; iSensor < sensorCount; iSensor++) { + ctl_temp_properties_t props = { .Size = sizeof(props) }; + // The official sample code does not set Version + // https://github.com/intel/drivers.gpu.control-library/blob/1bbacbf3814f2fd0d2b930cdf42fad83f3628db9/Samples/Telemetry_Samples/Sample_TelemetryAPP.cpp#L256 + if (igclData.ffctlTemperatureGetProperties(sensors[iSensor], &props) == CTL_RESULT_SUCCESS) { + if (props.type == CTL_TEMP_SENSORS_GPU) { + *result.temp = props.maxTemperature; + break; + } + } + } + } + } + + if (result.frequency) { + ctl_freq_handle_t domains[16]; + uint32_t domainCount = ARRAY_SIZE(domains); + if (igclData.ffctlEnumFrequencyDomains(device, &domainCount, domains) == CTL_RESULT_SUCCESS && domainCount > 0) { + double maxValue = 0; + ctl_freq_properties_t props = { .Size = sizeof(props), .Version = 0 }; + for (uint32_t iDomain = 0; iDomain < domainCount; iDomain++) { + if (igclData.ffctlFrequencyGetProperties(domains[iDomain], &props) == CTL_RESULT_SUCCESS) { + if (props.type == CTL_FREQ_DOMAIN_GPU && props.max > maxValue) { + maxValue = props.max; + } + } + } + *result.frequency = (uint32_t) (maxValue + 0.5); + } + } + + if (result.name) { + ffStrbufSetS(result.name, properties.name); + } + + return NULL; +} diff --git a/src/detection/gpu/gpu_linux.c b/src/detection/gpu/gpu_linux.c new file mode 100644 index 0000000..3aaa09c --- /dev/null +++ b/src/detection/gpu/gpu_linux.c @@ -0,0 +1,663 @@ +#include "detection/gpu/gpu.h" +#include "detection/vulkan/vulkan.h" +#include "detection/cpu/cpu.h" +#include "detection/gpu/gpu_driver_specific.h" +#include "common/io.h" +#include "common/library.h" +#include "common/FFstrbuf.h" +#include "common/strutil.h" +#include "common/mallocHelper.h" +#include "modules/gpu/option.h" + +#include <inttypes.h> +#include <stdint.h> + +#ifdef FF_HAVE_DRM_AMDGPU + #include <amdgpu.h> + #include <amdgpu_drm.h> + #include <fcntl.h> +#endif + +#ifdef FF_HAVE_DRM + #include "intel_drm.h" + #include <fcntl.h> + #include <sys/ioctl.h> +#endif + +#if defined(FF_HAVE_DRM) && defined(__aarch64__) + // https://github.com/alyssarosenzweig/linux/blob/agx-uapi-v7/include/uapi/drm/asahi_drm.h + // Found in kernel-headers-6.14.4-400.asahi.fc42.aarch64 + #if __has_include(<drm/asahi_drm.h>) + #include <drm/asahi_drm.h> + #else + #include "asahi_drm.h" + #endif + #define FF_HAVE_DRM_ASAHI 1 +#endif + +static bool pciDetectDriver(FFstrbuf* result, FFstrbuf* pciDir, FFstrbuf* buffer, FF_A_UNUSED const char* drmKey) { + uint32_t pciDirLength = pciDir->length; + ffStrbufAppendS(pciDir, "/driver"); + char pathBuf[PATH_MAX]; + ssize_t resultLength = readlink(pciDir->chars, pathBuf, ARRAY_SIZE(pathBuf)); + if (resultLength <= 0) { + return false; + } + + const char* slash = memrchr(pathBuf, '/', (size_t) resultLength); + if (slash) { + slash++; + ffStrbufSetNS(result, (uint32_t) (resultLength - (slash - pathBuf)), slash); + } + + if (ffStrbufEqualS(result, "nvidia")) { + if (ffReadFileBuffer("/proc/driver/nvidia/version", buffer)) { + if (ffStrbufContainS(buffer, " Open ")) { + ffStrbufAppendS(result, " (open source)"); + } else { + ffStrbufAppendS(result, " (proprietary)"); + } + } + } + + if (instance.config.general.detectVersion) { + ffStrbufAppendS(pciDir, "/module/version"); + if (ffReadFileBuffer(pciDir->chars, buffer)) { + ffStrbufTrimRightSpace(buffer); + ffStrbufAppendC(result, ' '); + ffStrbufAppend(result, buffer); + } else if (ffStrbufEqualS(result, "zx")) { + ffStrbufSubstrBefore(pciDir, pciDirLength); + ffStrbufAppendS(pciDir, "/zx_info/driver_version"); + if (ffReadFileBuffer(pciDir->chars, buffer)) { + ffStrbufTrimRightSpace(buffer); + ffStrbufAppendC(result, ' '); + ffStrbufAppend(result, buffer); + } + } + } + + return true; +} + +FF_A_UNUSED static const char* drmFindRenderFromCard(const char* drmCardKey, FFstrbuf* result) { + char path[PATH_MAX]; + sprintf(path, "/sys/class/drm/%s/device/drm", drmCardKey); + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(path); + if (!dirp) { + return "Failed to open `/sys/class/drm/{drmCardKey}/device/drm`"; + } + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (ffStrStartsWith(entry->d_name, "render")) { + ffStrbufSetS(result, "/dev/dri/"); + ffStrbufAppendS(result, entry->d_name); + return NULL; + } + } + return "Failed to find render device"; +} + +static const char* drmDetectAmdSpecific(const FFGPUOptions* options, FFGPUResult* gpu, const char* drmKey, FFstrbuf* buffer) { +#if FF_HAVE_DRM + const char* error = drmFindRenderFromCard(drmKey, buffer); + if (error) { + return error; + } + if (ffStrbufEqualS(&gpu->driver, "radeon")) { + return ffDrmDetectRadeon(options, gpu, buffer->chars); + } else { + #if FF_HAVE_DRM_AMDGPU + return ffDrmDetectAmdgpu(options, gpu, buffer->chars); + #else + FF_UNUSED(options, gpu, drmKey, buffer); + return "Fastfetch is not compiled with libdrm_amdgpu support"; + #endif + } +#else + FF_UNUSED(options, gpu, drmKey, buffer); + return "Fastfetch is not compiled with drm support"; +#endif +} + +static void pciDetectAmdSpecific(const FFGPUOptions* options, FFGPUResult* gpu, FFstrbuf* pciDir, FFstrbuf* buffer) { + // https://www.kernel.org/doc/html/v5.10/gpu/amdgpu.html#mem-info-vis-vram-total + const uint32_t pciDirLen = pciDir->length; + + ffStrbufAppendS(pciDir, "/hwmon/"); + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(pciDir->chars); + if (!dirp) { + return; + } + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + break; + } + if (!entry) { + return; + } + ffStrbufAppendS(pciDir, entry->d_name); + ffStrbufAppendC(pciDir, '/'); + + const uint32_t hwmonLen = pciDir->length; + uint64_t value = 0; + if (options->temp) { + ffStrbufAppendS(pciDir, "temp1_input"); // The on die GPU temperature in millidegrees Celsius + if (ffReadFileBuffer(pciDir->chars, buffer) && (value = ffStrbufToUInt(buffer, 0))) { + gpu->temperature = (double) value / 1000; + } + } + + if (ffStrbufEqualS(&gpu->driver, "amdgpu")) // Ancient radeon drivers don't have these files + { + ffStrbufSubstrBefore(pciDir, hwmonLen); + ffStrbufAppendS(pciDir, "in1_input"); // Northbridge voltage in millivolts (APUs only) + if (ffPathExists(pciDir->chars, FF_PATHTYPE_ANY)) { + gpu->type = FF_GPU_TYPE_INTEGRATED; + } else { + gpu->type = FF_GPU_TYPE_DISCRETE; + } + + if (options->driverSpecific) { + ffStrbufSubstrBefore(pciDir, pciDirLen); + ffStrbufAppendS(pciDir, "/mem_info_vis_vram_total"); + if (ffReadFileBuffer(pciDir->chars, buffer) && (value = ffStrbufToUInt(buffer, 0))) { + if (gpu->type == FF_GPU_TYPE_DISCRETE) { + gpu->dedicated.total = value; + } else { + gpu->shared.total = value; + } + + ffStrbufSubstrBefore(pciDir, pciDir->length - (uint32_t) strlen("/mem_info_vis_vram_total")); + ffStrbufAppendS(pciDir, "/mem_info_vis_vram_used"); + if (ffReadFileBuffer(pciDir->chars, buffer) && (value = ffStrbufToUInt(buffer, 0))) { + if (gpu->type == FF_GPU_TYPE_DISCRETE) { + gpu->dedicated.used = value; + } else { + gpu->shared.used = value; + } + } + } + + ffStrbufSubstrBefore(pciDir, pciDirLen); + ffStrbufAppendS(pciDir, "/gpu_busy_percent"); + if (ffReadFileBuffer(pciDir->chars, buffer) && (value = ffStrbufToUInt(buffer, 0))) { + gpu->coreUsage = (double) value; + } + } + } +} + +static void pciDetectIntelSpecific(const FFGPUOptions* options, FFGPUResult* gpu, FFstrbuf* pciDir, FFstrbuf* buffer, const char* drmKey) { + // Works for Intel GPUs + // https://patchwork.kernel.org/project/intel-gfx/patch/1422039866-11572-3-git-send-email-ville.syrjala@linux.intel.com/ + + // 0000:00:02.0 is reserved for Intel integrated graphics + gpu->type = gpu->deviceId == ffGPUPciAddr2Id(0, 0, 2, 0) ? FF_GPU_TYPE_INTEGRATED : FF_GPU_TYPE_DISCRETE; + + if (!drmKey) { + return; + } + + const uint32_t pciDirLen = pciDir->length; + + bool isXE = ffStrbufEqualS(&gpu->driver, "xe"); + if (isXE) { + ffStrbufAppendS(pciDir, "/tile0/gt0/freq0/max_freq"); + } else { + ffStrbufAppendF(pciDir, "/drm/%s/gt_max_freq_mhz", drmKey); + } + if (ffReadFileBuffer(pciDir->chars, buffer)) { + gpu->frequency = (uint32_t) ffStrbufToUInt(buffer, 0); + } + ffStrbufSubstrBefore(pciDir, pciDirLen); + + if (options->temp) { + ffStrbufAppendS(pciDir, "/hwmon/"); + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(pciDir->chars); + if (dirp) { + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + ffStrbufSubstrBefore(pciDir, pciDirLen + strlen("/hwmon/")); + ffStrbufAppendS(pciDir, entry->d_name); + // https://github.com/Syllo/nvtop/blob/73291884d926445e499d6b9b71cb7a9bdbc7c393/src/extract_gpuinfo_intel.c#L279-L281 + ffStrbufAppendS(pciDir, isXE ? "/temp2_input" : "/temp1_input"); + + if (ffReadFileBuffer(pciDir->chars, buffer)) { + uint64_t value = ffStrbufToUInt(buffer, 0); + if (value > 0) { + gpu->temperature = (double) value / 1000; + break; + } + } + } + } + ffStrbufSubstrBefore(pciDir, pciDirLen); + } +} + +static const char* drmDetectIntelSpecific(FFGPUResult* gpu, const char* drmKey, FFstrbuf* buffer) { +#if FF_HAVE_DRM + ffStrbufSetS(buffer, "/dev/dri/"); + ffStrbufAppendS(buffer, drmKey); + FF_AUTO_CLOSE_FD int fd = open(buffer->chars, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return "Failed to open drm device"; + } + + if (ffStrbufEqualS(&gpu->driver, "xe")) { + return ffDrmDetectXe(gpu, fd); + } else if (ffStrbufEqualS(&gpu->driver, "i915")) { + return ffDrmDetectI915(gpu, fd); + } + return "Unknown Intel GPU driver"; +#else + FF_UNUSED(gpu, drmKey, buffer); + return "Fastfetch is not compiled with drm support"; +#endif +} + +static const char* pciDetectTempGeneral(const FFGPUOptions* options, FFGPUResult* gpu, FFstrbuf* pciDir, FFstrbuf* buffer) { + if (options->temp) { + const uint32_t pciDirLen = pciDir->length; + ffStrbufAppendS(pciDir, "/hwmon/"); + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(pciDir->chars); + if (dirp) { + struct dirent* entry; + while ((entry = readdir(dirp))) { + if (entry->d_name[0] == '.') { + continue; + } + ffStrbufAppendS(pciDir, entry->d_name); + ffStrbufAppendS(pciDir, "/temp1_input"); + if (ffReadFileBuffer(pciDir->chars, buffer)) { + uint64_t value = ffStrbufToUInt(buffer, 0); + if (value > 0) { + gpu->temperature = (double) value / 1000.0; + } + } + break; + } + } + ffStrbufSubstrBefore(pciDir, pciDirLen); + } + return NULL; +} + +static const char* drmDetectNouveauSpecific(FFGPUResult* gpu, const char* drmKey, FFstrbuf* buffer) { +#if FF_HAVE_DRM + ffStrbufSetS(buffer, "/dev/dri/"); + ffStrbufAppendS(buffer, drmKey); + FF_AUTO_CLOSE_FD int fd = open(buffer->chars, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return "Failed to open drm device"; + } + + return ffDrmDetectNouveau(gpu, fd); +#else + FF_UNUSED(gpu, drmKey, buffer); + return "Fastfetch is not compiled with drm support"; +#endif +} + +static const char* pciDetectZxSpecific(const FFGPUOptions* options, FFGPUResult* gpu, FFstrbuf* pciDir, FFstrbuf* buffer) { + gpu->type = FF_GPU_TYPE_INTEGRATED; + + const uint32_t pciDirLen = pciDir->length; + ffStrbufAppendS(pciDir, "/zx_info/eclk"); + if (ffReadFileBuffer(pciDir->chars, buffer)) { + gpu->frequency = (uint32_t) ffStrbufToUInt(buffer, FF_GPU_FREQUENCY_UNSET); + } + ffStrbufSubstrBefore(pciDir, pciDirLen); + + if (options->driverSpecific) { + ffStrbufAppendS(pciDir, "/zx_info/engine_3d_usage"); + if (ffReadFileBuffer(pciDir->chars, buffer)) { + gpu->coreUsage = ffStrbufToDouble(buffer, FF_GPU_CORE_USAGE_UNSET); + } + ffStrbufSubstrBefore(pciDir, pciDirLen); + + ffStrbufAppendS(pciDir, "/zx_info/fb_size"); + if (ffReadFileBuffer(pciDir->chars, buffer)) { + gpu->shared.total = ffStrbufToUInt(buffer, FF_GPU_VMEM_SIZE_UNSET); + } + ffStrbufSubstrBefore(pciDir, pciDirLen); + + if (gpu->shared.total != FF_GPU_VMEM_SIZE_UNSET) { + gpu->shared.total *= 1024 * 1024; + + ffStrbufAppendS(pciDir, "/zx_info/free_fb_mem"); + if (ffReadFileBuffer(pciDir->chars, buffer)) { + gpu->shared.used = ffStrbufToUInt(buffer, FF_GPU_VMEM_SIZE_UNSET); + } + ffStrbufSubstrBefore(pciDir, pciDirLen); + + if (gpu->shared.used != FF_GPU_VMEM_SIZE_UNSET) { + gpu->shared.used *= 1024 * 1024; + gpu->shared.used = gpu->shared.total - gpu->shared.used; + } + } + } + + return NULL; +} + +static const char* detectPci(const FFGPUOptions* options, FFlist* gpus, FFstrbuf* buffer, FFstrbuf* deviceDir, const char* drmKey) { + const uint32_t drmDirPathLength = deviceDir->length; + uint32_t vendorId, deviceId, subVendorId, subDeviceId; + uint8_t classId, subclassId; + if (sscanf(buffer->chars + strlen("pci:"), "v%8" SCNx32 "d%8" SCNx32 "sv%8" SCNx32 "sd%8" SCNx32 "bc%2" SCNx8 "sc%2" SCNx8, &vendorId, &deviceId, &subVendorId, &subDeviceId, &classId, &subclassId) != 6) { + return "Failed to parse pci modalias"; + } + + if (classId != 0x03 /*PCI_BASE_CLASS_DISPLAY*/) { + return "Not a GPU device"; + } + + char pciPath[PATH_MAX]; + const char* pPciPath = NULL; + if (drmKey) { + ssize_t pathLength = readlink(deviceDir->chars, pciPath, ARRAY_SIZE(pciPath) - 1); + if (pathLength <= 0) { + return "Unable to get PCI device path"; + } + pciPath[pathLength] = '\0'; + pPciPath = strrchr(pciPath, '/'); + if (__builtin_expect(pPciPath != NULL, true)) { + pPciPath++; + } else { + pPciPath = pciPath; + } + } else { + pPciPath = memrchr(deviceDir->chars, '/', deviceDir->length); + assert(pPciPath); + pPciPath++; + } + + uint32_t pciDomain, pciBus, pciDevice, pciFunc; + if (sscanf(pPciPath, "%" SCNx32 ":%" SCNx32 ":%" SCNx32 ".%" SCNx32, &pciDomain, &pciBus, &pciDevice, &pciFunc) != 4) { + return "Invalid PCI device path"; + } + + if (pciFunc > 0 && subclassId == 0x80 /*PCI_CLASS_DISPLAY_OTHER*/) { + return "Likely an auxiliary display controller"; // #2034 + } + + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus); + ffStrbufInitStatic(&gpu->vendor, ffGPUGetVendorString((uint16_t) vendorId)); + ffStrbufInit(&gpu->name); + ffStrbufInit(&gpu->driver); + ffStrbufInit(&gpu->platformApi); + ffStrbufInit(&gpu->memoryType); + gpu->index = FF_GPU_INDEX_UNSET; + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->type = FF_GPU_TYPE_UNKNOWN; + gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + gpu->deviceId = ffGPUPciAddr2Id(pciDomain, pciBus, pciDevice, pciFunc); + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + + char drmKeyBuffer[8]; + if (!drmKey) { + ffStrbufAppendS(deviceDir, "/drm"); + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(deviceDir->chars); + if (dirp) { + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (ffStrStartsWith(entry->d_name, "card")) { + ffStrCopy(drmKeyBuffer, entry->d_name, ARRAY_SIZE(drmKeyBuffer)); + drmKey = drmKeyBuffer; + break; + } + } + } + ffStrbufSubstrBefore(deviceDir, drmDirPathLength); + } + + if (drmKey) { + ffStrbufSetF(&gpu->platformApi, "DRM (%s)", drmKey); + } + + pciDetectDriver(&gpu->driver, deviceDir, buffer, drmKey); + ffStrbufSubstrBefore(deviceDir, drmDirPathLength); + + if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_AMD) { + bool ok = false; + if (drmKey && options->driverSpecific) { + ok = drmDetectAmdSpecific(options, gpu, drmKey, buffer) == NULL; + } + + if (!ok) { + pciDetectAmdSpecific(options, gpu, deviceDir, buffer); + ffStrbufSubstrBefore(deviceDir, drmDirPathLength); + + ffStrbufAppendS(deviceDir, "/revision"); + if (ffReadFileBuffer(deviceDir->chars, buffer)) { + char* pend; + uint64_t revision = strtoul(buffer->chars, &pend, 16); + if (pend != buffer->chars) { + ffGPUQueryAmdGpuName((uint16_t) deviceId, (uint8_t) revision, gpu); + } + } + ffStrbufSubstrBefore(deviceDir, drmDirPathLength); + } + } else if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_INTEL) { + pciDetectIntelSpecific(options, gpu, deviceDir, buffer, drmKey); + ffStrbufSubstrBefore(deviceDir, drmDirPathLength); + if (options->driverSpecific && drmKey) { + drmDetectIntelSpecific(gpu, drmKey, buffer); + } + } else if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_NVIDIA && ffStrbufEqualS(&gpu->driver, "nouveau")) { + pciDetectTempGeneral(options, gpu, deviceDir, buffer); + if (options->driverSpecific && drmKey) { + drmDetectNouveauSpecific(gpu, drmKey, buffer); + } + } else if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_ZHAOXIN && ffStrbufStartsWithS(&gpu->driver, "zx")) { + pciDetectTempGeneral(options, gpu, deviceDir, buffer); + pciDetectZxSpecific(options, gpu, deviceDir, buffer); + } else { + ffGPUDetectDriverSpecific(options, gpu, (FFGpuDriverPciBusId) { + .domain = pciDomain, + .bus = pciBus, + .device = pciDevice, + .func = pciFunc, + }); + } + + if (gpu->name.length == 0) { + ffGPUFillVendorAndName(subclassId, (uint16_t) vendorId, (uint16_t) deviceId, gpu); + } + + if (gpu->type == FF_GPU_TYPE_UNKNOWN) { + if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_NVIDIA) { + if (ffStrbufStartsWithIgnCaseS(&gpu->name, "GeForce") || + ffStrbufStartsWithIgnCaseS(&gpu->name, "Quadro") || + ffStrbufStartsWithIgnCaseS(&gpu->name, "Tesla")) { + gpu->type = FF_GPU_TYPE_DISCRETE; + } + } else if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_MTHREADS) { + if (ffStrbufStartsWithIgnCaseS(&gpu->name, "MTT ")) { + gpu->type = FF_GPU_TYPE_DISCRETE; + } + } + } + + return NULL; +} + +#if __aarch64__ + +FF_A_UNUSED static const char* drmDetectAsahiSpecific(FFGPUResult* gpu, const char* name, FF_A_UNUSED FFstrbuf* buffer, FF_A_UNUSED const char* drmKey) { + if (sscanf(name, "agx-t%lu", &gpu->deviceId) == 1) { + ffStrbufSetStatic(&gpu->name, ffCPUAppleCodeToName((uint32_t) gpu->deviceId)); + } + ffStrbufSetStatic(&gpu->vendor, FF_GPU_VENDOR_NAME_APPLE); + + #if FF_HAVE_DRM_ASAHI + ffStrbufSetS(buffer, "/dev/dri/"); + ffStrbufAppendS(buffer, drmKey); + FF_AUTO_CLOSE_FD int fd = open(buffer->chars, O_RDONLY | O_CLOEXEC); + if (fd >= 0) { + return ffDrmDetectAsahi(gpu, fd); + } + #endif + + return NULL; +} +#endif + +static const char* detectOf(FFlist* gpus, FFstrbuf* buffer, FFstrbuf* drmDir, const char* drmKey) { + char compatible[256]; // vendor,model-name + if (sscanf(buffer->chars + strlen("of:"), "NgpuT%*[^C]C%255[^C]", compatible) != 1) { + return "Failed to parse of modalias or not a GPU device"; + } + + char* name = strchr(compatible, ','); + if (name) { + *name = '\0'; + ++name; + } + + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus); + gpu->index = FF_GPU_INDEX_UNSET; + gpu->deviceId = 0; + ffStrbufInit(&gpu->name); + ffStrbufInit(&gpu->vendor); + ffStrbufInit(&gpu->driver); + ffStrbufInit(&gpu->memoryType); + ffStrbufInitF(&gpu->platformApi, "DRM (%s)", drmKey); + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + gpu->type = FF_GPU_TYPE_INTEGRATED; + gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + + pciDetectDriver(&gpu->driver, drmDir, buffer, drmKey); + +#ifdef __aarch64__ + if (ffStrbufEqualS(&gpu->driver, "asahi")) { + drmDetectAsahiSpecific(gpu, name, buffer, drmKey); + } +#endif + + if (!gpu->name.length) { + ffStrbufSetS(&gpu->name, name ?: compatible); + ffStrbufTrimRightSpace(&gpu->name); + } + if (!gpu->vendor.length && name) { + if (ffStrEquals(compatible, "brcm")) { + ffStrbufSetStatic(&gpu->vendor, "Broadcom"); // Raspberry Pi + } else { + ffStrbufSetS(&gpu->vendor, compatible); + gpu->vendor.chars[0] = (char) toupper(compatible[0]); + } + } + + return NULL; +} + +static const char* drmDetectGPUs(const FFGPUOptions* options, FFlist* gpus) { + FF_STRBUF_AUTO_DESTROY drmDir = ffStrbufCreateA(64); + ffStrbufAppendS(&drmDir, "/sys/class/drm/"); + const uint32_t drmDirLength = drmDir.length; + + FF_AUTO_CLOSE_DIR DIR* dir = opendir(drmDir.chars); + if (dir == NULL) { + return "Failed to open `/sys/class/drm/`"; + } + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) { + if (!ffStrStartsWith(entry->d_name, "card") || + strchr(entry->d_name + 4, '-') != NULL) { + continue; + } + + ffStrbufAppendS(&drmDir, entry->d_name); + + ffStrbufAppendS(&drmDir, "/device/modalias"); + if (!ffReadFileBuffer(drmDir.chars, &buffer)) { + continue; + } + ffStrbufSubstrBefore(&drmDir, drmDir.length - (uint32_t) strlen("/modalias")); + + if (ffStrbufStartsWithS(&buffer, "pci:")) { + detectPci(options, gpus, &buffer, &drmDir, entry->d_name); + } else if (ffStrbufStartsWithS(&buffer, "of:")) { // Open Firmware + detectOf(gpus, &buffer, &drmDir, entry->d_name); + } + + ffStrbufSubstrBefore(&drmDir, drmDirLength); + } + + return NULL; +} + +static const char* pciDetectGPUs(const FFGPUOptions* options, FFlist* gpus) { + // https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-bus-pci + const char* pciDirPath = "/sys/bus/pci/devices/"; + + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(pciDirPath); + if (dirp == NULL) { + return "Failed to open `/sys/bus/pci/devices/`"; + } + + FF_STRBUF_AUTO_DESTROY pciDir = ffStrbufCreateA(64); + ffStrbufAppendS(&pciDir, pciDirPath); + + const uint32_t pciBaseDirLength = pciDir.length; + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + ffStrbufSubstrBefore(&pciDir, pciBaseDirLength); + ffStrbufAppendS(&pciDir, entry->d_name); + const uint32_t pciDevDirLength = pciDir.length; + + ffStrbufAppendS(&pciDir, "/modalias"); + if (!ffReadFileBuffer(pciDir.chars, &buffer)) { + continue; + } + ffStrbufSubstrBefore(&pciDir, pciDevDirLength); + assert(ffStrbufStartsWithS(&buffer, "pci:")); + + detectPci(options, gpus, &buffer, &pciDir, NULL); + ffStrbufSubstrBefore(&pciDir, pciBaseDirLength); + } + + return NULL; +} + +const char* ffDetectGPUImpl(const FFGPUOptions* options, FFlist* gpus) { +#if __x86_64__ || __aarch64__ + const char* ffGPUDetectWsl2(const FFGPUOptions* options, FFlist* gpus); + if (ffGPUDetectWsl2(options, gpus) == NULL) { + return NULL; + } +#endif + + if (options->detectionMethod == FF_GPU_DETECTION_METHOD_AUTO) { + if (drmDetectGPUs(options, gpus) == NULL && gpus->length > 0) { + return NULL; + } + } + return pciDetectGPUs(options, gpus); +} diff --git a/src/detection/gpu/gpu_mthreads.c b/src/detection/gpu/gpu_mthreads.c new file mode 100644 index 0000000..0b639f5 --- /dev/null +++ b/src/detection/gpu/gpu_mthreads.c @@ -0,0 +1,201 @@ +#include "gpu_driver_specific.h" + +#include "common/library.h" +#include "mtml.h" + +struct FFMtmlData { + FF_LIBRARY_SYMBOL(mtmlDeviceCountGpuCores) + FF_LIBRARY_SYMBOL(mtmlDeviceGetBrand) + FF_LIBRARY_SYMBOL(mtmlDeviceGetIndex) + FF_LIBRARY_SYMBOL(mtmlDeviceGetName) + FF_LIBRARY_SYMBOL(mtmlDeviceGetPciInfo) + FF_LIBRARY_SYMBOL(mtmlDeviceGetUUID) + FF_LIBRARY_SYMBOL(mtmlDeviceInitGpu) + FF_LIBRARY_SYMBOL(mtmlDeviceInitMemory) + FF_LIBRARY_SYMBOL(mtmlGpuGetMaxClock) + FF_LIBRARY_SYMBOL(mtmlGpuGetTemperature) + FF_LIBRARY_SYMBOL(mtmlGpuGetUtilization) + FF_LIBRARY_SYMBOL(mtmlLibraryCountDevice) + FF_LIBRARY_SYMBOL(mtmlLibraryInitDeviceByIndex) + FF_LIBRARY_SYMBOL(mtmlLibraryInitDeviceByPciSbdf) + FF_LIBRARY_SYMBOL(mtmlLibraryInitSystem) + FF_LIBRARY_SYMBOL(mtmlMemoryGetTotal) + FF_LIBRARY_SYMBOL(mtmlMemoryGetUsed) + FF_LIBRARY_SYMBOL(mtmlMemoryGetUtilization) + FF_LIBRARY_SYMBOL(mtmlLibraryShutDown) + + bool inited; + MtmlLibrary* lib; + MtmlSystem* sys; +} mtmlData; + +FF_A_UNUSED static void shutdownMtml(void) { + mtmlData.ffmtmlLibraryShutDown(mtmlData.lib); +} + +const char* ffDetectMthreadsGpuInfo(const FFGpuDriverCondition* cond, FFGpuDriverResult result, const char* soName) { +#ifndef FF_DISABLE_DLOPEN + + if (!mtmlData.inited) { + mtmlData.inited = true; + FF_LIBRARY_LOAD(libmtml, "dlopen mtml failed", soName, 1); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libmtml, mtmlLibraryInit) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlDeviceCountGpuCores) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlDeviceGetBrand) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlDeviceGetIndex) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlDeviceGetName) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlDeviceGetPciInfo) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlDeviceGetUUID) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlDeviceInitGpu) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlDeviceInitMemory) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlGpuGetMaxClock) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlGpuGetTemperature) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlGpuGetUtilization) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlLibraryCountDevice) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlLibraryInitDeviceByIndex) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlLibraryInitDeviceByPciSbdf) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlLibraryInitSystem) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlMemoryGetTotal) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlMemoryGetUsed) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlMemoryGetUtilization) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libmtml, mtmlData, mtmlLibraryShutDown) + + if (ffmtmlLibraryInit(&mtmlData.lib) != MTML_SUCCESS) { + mtmlData.ffmtmlLibraryInitSystem = NULL; + return "mtmlLibraryInit failed"; + } + if (mtmlData.ffmtmlLibraryInitSystem(mtmlData.lib, &mtmlData.sys) != MTML_SUCCESS) { + mtmlData.ffmtmlLibraryShutDown(mtmlData.lib); + mtmlData.ffmtmlLibraryInitSystem = NULL; + return "mtmlLibraryInitSystem failed"; + } + atexit(shutdownMtml); + libmtml = NULL; // don't close mtml + } + + if (mtmlData.ffmtmlLibraryInitSystem == NULL) { + return "loading mtml library failed"; + } + + MtmlDevice* device = NULL; + if (cond->type & FF_GPU_DRIVER_CONDITION_TYPE_BUS_ID) { + char pciBusIdStr[32]; + snprintf(pciBusIdStr, ARRAY_SIZE(pciBusIdStr) - 1, "%04x:%02x:%02x.%d", cond->pciBusId.domain, cond->pciBusId.bus, cond->pciBusId.device, cond->pciBusId.func); + + MtmlReturn ret = mtmlData.ffmtmlLibraryInitDeviceByPciSbdf(mtmlData.lib, pciBusIdStr, &device); + if (ret != MTML_SUCCESS) { + return "mtmlLibraryInitDeviceByPciSbdf() failed"; + } + } else if (cond->type & FF_GPU_DRIVER_CONDITION_TYPE_DEVICE_ID) { + uint32_t count; + if (mtmlData.ffmtmlLibraryCountDevice(mtmlData.lib, &count) != MTML_SUCCESS) { + return "mtmlLibraryCountDevice() failed"; + } + + for (uint32_t i = 0; i < count; i++, device = NULL) { + if (mtmlData.ffmtmlLibraryInitDeviceByIndex(mtmlData.lib, i, &device) != MTML_SUCCESS) { + continue; + } + + MtmlPciInfo pciInfo; + if (mtmlData.ffmtmlDeviceGetPciInfo(device, &pciInfo) != MTML_SUCCESS) { + continue; + } + + if (pciInfo.pciDeviceId != ((cond->pciDeviceId.deviceId << 16u) | cond->pciDeviceId.vendorId) || + pciInfo.pciSubsystemId != cond->pciDeviceId.subSystemId) { + continue; + } + + break; + } + if (!device) { + return "Device not found"; + } + } else { + return "Unknown condition type"; + } + + MtmlBrandType brand; + if (mtmlData.ffmtmlDeviceGetBrand(device, &brand) == MTML_SUCCESS) { + switch (brand) { + case MTML_BRAND_MTT: + *result.type = FF_GPU_TYPE_DISCRETE; + break; + default: + break; + } + } + + if (result.index) { + unsigned int value; + if (mtmlData.ffmtmlDeviceGetIndex(device, &value) == MTML_SUCCESS) { + *result.index = value; + } + } + + if (result.temp) { + MtmlGpu* gpu = NULL; + if (mtmlData.ffmtmlDeviceInitGpu(device, &gpu) == MTML_SUCCESS) { + uint32_t value; + if (mtmlData.ffmtmlGpuGetTemperature(gpu, &value) == MTML_SUCCESS) { + *result.temp = value; + } + } + } + + if (result.memory) { + MtmlMemory* mem = NULL; + if (mtmlData.ffmtmlDeviceInitMemory(device, &mem) == MTML_SUCCESS) { + unsigned long long total; + if (mtmlData.ffmtmlMemoryGetTotal(mem, &total) == MTML_SUCCESS) { + result.memory->total = total; + } + + unsigned long long used; + if (mtmlData.ffmtmlMemoryGetUsed(mem, &used) == MTML_SUCCESS) { + result.memory->used = used; + } + } + } + + if (result.coreCount) { + mtmlData.ffmtmlDeviceCountGpuCores(device, result.coreCount); + } + + if (result.frequency) { + MtmlGpu* gpu = NULL; + if (mtmlData.ffmtmlDeviceInitGpu(device, &gpu) == MTML_SUCCESS) { + uint32_t clockMHz; + if (mtmlData.ffmtmlGpuGetMaxClock(gpu, &clockMHz) == MTML_SUCCESS) { + *result.frequency = clockMHz; + } + } + } + + if (result.coreUsage) { + MtmlGpu* gpu = NULL; + if (mtmlData.ffmtmlDeviceInitGpu(device, &gpu) == MTML_SUCCESS) { + unsigned int utilization; + if (mtmlData.ffmtmlGpuGetUtilization(gpu, &utilization) == MTML_SUCCESS) { + *result.coreUsage = utilization; + } + } + } + + if (result.name) { + char name[MTML_DEVICE_NAME_BUFFER_SIZE]; + if (mtmlData.ffmtmlDeviceGetName(device, name, ARRAY_SIZE(name)) == MTML_SUCCESS) { + ffStrbufSetS(result.name, name); + } + } + + return NULL; + +#else + + FF_UNUSED(cond, result, soName); + return "dlopen is disabled"; + +#endif +} diff --git a/src/detection/gpu/gpu_nbsd.c b/src/detection/gpu/gpu_nbsd.c new file mode 100644 index 0000000..4f5dab3 --- /dev/null +++ b/src/detection/gpu/gpu_nbsd.c @@ -0,0 +1,120 @@ +#include "gpu.h" +#include "common/io.h" + +#include <sys/param.h> +#include <sys/ioctl.h> +#include <fcntl.h> +#include <dev/pci/pcireg.h> +#include <dev/pci/pcidevs.h> +#include <dev/pci/pciio.h> + +static inline int pciReadConf(int fd, uint32_t bus, uint32_t device, uint32_t func, uint32_t reg, uint32_t* result) { + struct pciio_bdf_cfgreg bdfr = { + .bus = bus, + .device = device, + .function = func, + .cfgreg = { + .reg = reg, + }, + }; + + if (ioctl(fd, PCI_IOC_BDF_CFGREAD, &bdfr) == -1) { + return -1; + } + + *result = bdfr.cfgreg.val; + return 0; +} + +const char* ffDetectGPUImpl(FF_A_UNUSED const FFGPUOptions* options, FFlist* gpus) { + char pciDevPath[] = "/dev/pciXXX"; + + for (uint32_t idev = 0; idev <= 255; idev++) { + snprintf(pciDevPath + strlen("/dev/pci"), 4, "%u", idev); + + FF_AUTO_CLOSE_FD int pcifd = open(pciDevPath, O_RDONLY | O_CLOEXEC); + if (pcifd < 0) { + if (errno == ENOENT) { + break; // No more /dev/pciN devices + } + return "open(\"/dev/pciN\", O_RDONLY | O_CLOEXEC) failed"; + } + + struct pciio_businfo businfo; + if (ioctl(pcifd, PCI_IOC_BUSINFO, &businfo) != 0) { + continue; + } + + uint32_t bus = businfo.busno; + for (uint32_t dev = 0; dev < businfo.maxdevs; dev++) { + uint32_t maxfuncs = 0; + for (uint32_t func = 0; func <= maxfuncs; func++) { + uint32_t pciid, pciclass; + if (pciReadConf(pcifd, bus, dev, func, PCI_ID_REG, &pciid) != 0) { + continue; + } + + if (PCI_VENDOR(pciid) == PCI_VENDOR_INVALID || PCI_VENDOR(pciid) == 0) { + continue; + } + + if (pciReadConf(pcifd, bus, dev, func, PCI_CLASS_REG, &pciclass) != 0) { + continue; + } + + if (func == 0) { + // For some reason, pciReadConf returns success even for non-existing devices. + // So we need to check for `PCI_VENDOR(pciid) == PCI_VENDOR_INVALID` above to filter them out. + uint32_t bhlcr; + if (pciReadConf(pcifd, bus, dev, 0, PCI_BHLC_REG, &bhlcr) != 0) { + continue; + } + + if (PCI_HDRTYPE_MULTIFN(bhlcr)) { + maxfuncs = 7; + } + } + + if (PCI_CLASS(pciclass) != PCI_CLASS_DISPLAY) { + continue; + } + + if (func > 0 && PCI_SUBCLASS(pciclass) == PCI_SUBCLASS_DISPLAY_MISC) { + continue; // Likely an auxiliary display controller (#2034) + } + + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus); + ffStrbufInitStatic(&gpu->vendor, ffGPUGetVendorString(PCI_VENDOR(pciid))); + ffStrbufInit(&gpu->name); + ffStrbufInit(&gpu->driver); + ffStrbufInitS(&gpu->platformApi, pciDevPath); + ffStrbufInit(&gpu->memoryType); + gpu->index = FF_GPU_INDEX_UNSET; + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + gpu->type = FF_GPU_TYPE_UNKNOWN; + gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + gpu->deviceId = ffGPUPciAddr2Id(0, bus, dev, func); + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + + if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_AMD) { + ffGPUQueryAmdGpuName(PCI_PRODUCT(pciid), PCI_REVISION(pciid), gpu); + } + if (gpu->name.length == 0) { + ffGPUFillVendorAndName(PCI_SUBCLASS(pciclass), PCI_VENDOR(pciid), PCI_PRODUCT(pciid), gpu); + } + + struct pciio_drvname drvname = { + .device = dev, + .function = func, + }; + if (ioctl(pcifd, PCI_IOC_DRVNAME, &drvname) == 0) { + ffStrbufInitS(&gpu->driver, drvname.name); + } + } + } + } + + return NULL; +} diff --git a/src/detection/gpu/gpu_nosupport.c b/src/detection/gpu/gpu_nosupport.c new file mode 100644 index 0000000..95fb2f3 --- /dev/null +++ b/src/detection/gpu/gpu_nosupport.c @@ -0,0 +1,6 @@ +#include "gpu.h" + +const char* ffDetectGPUImpl(const FFGPUOptions* options, FFlist* gpus) { + FF_UNUSED(options, gpus); + return "Not supported on this platform"; +} diff --git a/src/detection/gpu/gpu_nvidia.c b/src/detection/gpu/gpu_nvidia.c new file mode 100644 index 0000000..85776dd --- /dev/null +++ b/src/detection/gpu/gpu_nvidia.c @@ -0,0 +1,299 @@ +#include "gpu_driver_specific.h" + +#include "common/library.h" +#include "nvml.h" + +struct FFNvmlData { + FF_LIBRARY_SYMBOL(nvmlDeviceGetCount_v2) + FF_LIBRARY_SYMBOL(nvmlDeviceGetHandleByIndex_v2) + FF_LIBRARY_SYMBOL(nvmlDeviceGetHandleByPciBusId_v2) + FF_LIBRARY_SYMBOL(nvmlDeviceGetPciInfo_v3) + FF_LIBRARY_SYMBOL(nvmlDeviceGetTemperature) + FF_LIBRARY_SYMBOL(nvmlDeviceGetMemoryInfo_v2) + FF_LIBRARY_SYMBOL(nvmlDeviceGetMemoryInfo) + FF_LIBRARY_SYMBOL(nvmlDeviceGetNumGpuCores) + FF_LIBRARY_SYMBOL(nvmlDeviceGetMaxClockInfo) + FF_LIBRARY_SYMBOL(nvmlDeviceGetUtilizationRates) + FF_LIBRARY_SYMBOL(nvmlDeviceGetBrand) + FF_LIBRARY_SYMBOL(nvmlDeviceGetIndex) + FF_LIBRARY_SYMBOL(nvmlDeviceGetName) + + bool inited; +} nvmlData; + +#if defined(_WIN32) && !defined(FF_DISABLE_DLOPEN) + + #include "nvapi.h" + +struct FFNvapiData { + FF_LIBRARY_SYMBOL(nvapi_Unload) + FF_LIBRARY_SYMBOL(nvapi_EnumPhysicalGPUs) + FF_LIBRARY_SYMBOL(nvapi_GPU_GetRamType) + FF_LIBRARY_SYMBOL(nvapi_GPU_GetGPUType) + + bool inited; +} nvapiData; + +static const char* detectMoreByNvapi(FFGpuDriverResult* result) { + if (!nvapiData.inited) { + nvapiData.inited = true; + + FF_LIBRARY_LOAD_MESSAGE(libnvapi, + #ifdef _WIN64 + "nvapi64.dll" + #else + "nvapi.dll" + #endif + , + 1); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libnvapi, nvapi_QueryInterface) + #define FF_NVAPI_INTERFACE(iName, iOffset) \ + __typeof__(&iName) ff##iName = ffnvapi_QueryInterface(iOffset); \ + if (ff##iName == NULL) return "nvapi_QueryInterface " #iName " failed"; + + FF_NVAPI_INTERFACE(nvapi_Initialize, NVAPI_INTERFACE_OFFSET_INITIALIZE) + FF_NVAPI_INTERFACE(nvapi_Unload, NVAPI_INTERFACE_OFFSET_UNLOAD) + FF_NVAPI_INTERFACE(nvapi_EnumPhysicalGPUs, NVAPI_INTERFACE_OFFSET_ENUM_PHYSICAL_GPUS) + FF_NVAPI_INTERFACE(nvapi_GPU_GetRamType, NVAPI_INTERFACE_OFFSET_GPU_GET_RAM_TYPE) + FF_NVAPI_INTERFACE(nvapi_GPU_GetGPUType, NVAPI_INTERFACE_OFFSET_GPU_GET_GPU_TYPE) + #undef FF_NVAPI_INTERFACE + + if (ffnvapi_Initialize() < 0) { + return "NvAPI_Initialize() failed"; + } + + nvapiData.ffnvapi_EnumPhysicalGPUs = ffnvapi_EnumPhysicalGPUs; + nvapiData.ffnvapi_GPU_GetRamType = ffnvapi_GPU_GetRamType; + nvapiData.ffnvapi_GPU_GetGPUType = ffnvapi_GPU_GetGPUType; + nvapiData.ffnvapi_Unload = ffnvapi_Unload; + + atexit((void*) ffnvapi_Unload); + libnvapi = NULL; // don't close nvapi + } + + if (nvapiData.ffnvapi_EnumPhysicalGPUs == NULL) { + return "loading nvapi library failed"; + } + + NvPhysicalGpuHandle handles[32]; + int gpuCount = 0; + + if (nvapiData.ffnvapi_EnumPhysicalGPUs(handles, &gpuCount) < 0) { + return "NvAPI_EnumPhysicalGPUs() failed"; + } + + uint32_t gpuIndex = *result->index; + + if (gpuIndex >= (uint32_t) gpuCount) { + return "GPU index out of range"; + } + + // Not very sure. Need to check in multi-GPU system + NvPhysicalGpuHandle gpuHandle = handles[gpuIndex]; + + NvApiGPUMemoryType memType; + if (result->memoryType && nvapiData.ffnvapi_GPU_GetRamType(gpuHandle, &memType) == 0) { + switch (memType) { + #define FF_NVAPI_MEMORY_TYPE(type) \ + case NVAPI_GPU_MEMORY_TYPE_##type: \ + ffStrbufSetStatic(result->memoryType, #type); \ + break; + FF_NVAPI_MEMORY_TYPE(UNKNOWN) + FF_NVAPI_MEMORY_TYPE(SDRAM) + FF_NVAPI_MEMORY_TYPE(DDR1) + FF_NVAPI_MEMORY_TYPE(DDR2) + FF_NVAPI_MEMORY_TYPE(GDDR2) + FF_NVAPI_MEMORY_TYPE(GDDR3) + FF_NVAPI_MEMORY_TYPE(GDDR4) + FF_NVAPI_MEMORY_TYPE(DDR3) + FF_NVAPI_MEMORY_TYPE(GDDR5) + FF_NVAPI_MEMORY_TYPE(LPDDR2) + FF_NVAPI_MEMORY_TYPE(GDDR5X) + FF_NVAPI_MEMORY_TYPE(LPDDR3) + FF_NVAPI_MEMORY_TYPE(LPDDR4) + FF_NVAPI_MEMORY_TYPE(LPDDR5) + FF_NVAPI_MEMORY_TYPE(GDDR6) + FF_NVAPI_MEMORY_TYPE(GDDR6X) + FF_NVAPI_MEMORY_TYPE(GDDR7) + #undef FF_NVAPI_MEMORY_TYPE + default: + ffStrbufSetF(result->memoryType, "Unknown (%d)", memType); + break; + } + } + + NvApiGPUType gpuType; + if (result->type && nvapiData.ffnvapi_GPU_GetGPUType(gpuHandle, &gpuType) == 0) { + switch (gpuType) { + case NV_SYSTEM_TYPE_IGPU: + *result->type = FF_GPU_TYPE_INTEGRATED; + break; + case NV_SYSTEM_TYPE_DGPU: + *result->type = FF_GPU_TYPE_DISCRETE; + break; + default: + *result->type = FF_GPU_TYPE_UNKNOWN; + break; + } + } + + return NULL; +} + +#endif + +const char* ffDetectNvidiaGpuInfo(const FFGpuDriverCondition* cond, FFGpuDriverResult result, const char* soName) { +#ifndef FF_DISABLE_DLOPEN + + if (!nvmlData.inited) { + nvmlData.inited = true; + FF_LIBRARY_LOAD(libnvml, "dlopen nvml failed", soName, 1); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libnvml, nvmlInit_v2) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libnvml, nvmlShutdown) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetCount_v2) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetHandleByIndex_v2) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetHandleByPciBusId_v2) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetPciInfo_v3) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetTemperature) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetMemoryInfo_v2) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetMemoryInfo) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetNumGpuCores) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetMaxClockInfo) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetUtilizationRates) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetBrand) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetIndex) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libnvml, nvmlData, nvmlDeviceGetName) + + if (ffnvmlInit_v2() != NVML_SUCCESS) { + nvmlData.ffnvmlDeviceGetNumGpuCores = NULL; + return "nvmlInit_v2() failed"; + } + atexit((void*) ffnvmlShutdown); + libnvml = NULL; // don't close nvml + } + + if (nvmlData.ffnvmlDeviceGetNumGpuCores == NULL) { + return "loading nvml library failed"; + } + + nvmlDevice_t device = NULL; + if (cond->type & FF_GPU_DRIVER_CONDITION_TYPE_BUS_ID) { + char pciBusIdStr[32]; + snprintf(pciBusIdStr, ARRAY_SIZE(pciBusIdStr), "%04x:%02x:%02x.%d", cond->pciBusId.domain, cond->pciBusId.bus, cond->pciBusId.device, cond->pciBusId.func); + + nvmlReturn_t ret = nvmlData.ffnvmlDeviceGetHandleByPciBusId_v2(pciBusIdStr, &device); + if (ret != NVML_SUCCESS) { + return "nvmlDeviceGetHandleByPciBusId_v2() failed"; + } + } else if (cond->type & FF_GPU_DRIVER_CONDITION_TYPE_DEVICE_ID) { + uint32_t count; + if (nvmlData.ffnvmlDeviceGetCount_v2(&count) != NVML_SUCCESS) { + return "nvmlDeviceGetCount_v2() failed"; + } + + for (uint32_t i = 0; i < count; i++, device = NULL) { + if (nvmlData.ffnvmlDeviceGetHandleByIndex_v2(i, &device) != NVML_SUCCESS) { + continue; + } + + nvmlPciInfo_t pciInfo; + if (nvmlData.ffnvmlDeviceGetPciInfo_v3(device, &pciInfo) != NVML_SUCCESS) { + continue; + } + + if (pciInfo.pciDeviceId != ((cond->pciDeviceId.deviceId << 16u) | cond->pciDeviceId.vendorId) || + pciInfo.pciSubSystemId != cond->pciDeviceId.subSystemId) { + continue; + } + + break; + } + } + + if (!device) { + return "Device not found"; + } + + if (result.type) { + nvmlBrandType_t brand; + if (nvmlData.ffnvmlDeviceGetBrand(device, &brand) == NVML_SUCCESS) { + switch (brand) { + case NVML_BRAND_NVIDIA_RTX: + case NVML_BRAND_QUADRO_RTX: + case NVML_BRAND_GEFORCE: + case NVML_BRAND_TITAN: + case NVML_BRAND_TESLA: + case NVML_BRAND_QUADRO: + *result.type = FF_GPU_TYPE_DISCRETE; + break; + default: + break; + } + } + } + + if (result.index) { + unsigned int value; + if (nvmlData.ffnvmlDeviceGetIndex(device, &value) == NVML_SUCCESS) { + *result.index = value; + #ifdef _WIN32 + // Don't bother loading nvapi for GPU type detection only + if (result.memoryType) { + detectMoreByNvapi(&result); + } + #endif + } + } + + if (result.temp) { + uint32_t value; + if (nvmlData.ffnvmlDeviceGetTemperature(device, NVML_TEMPERATURE_GPU, &value) == NVML_SUCCESS) { + *result.temp = value; + } + } + + if (result.memory) { + nvmlMemory_v2_t memory = { .version = nvmlMemory_v2 }; + if (nvmlData.ffnvmlDeviceGetMemoryInfo_v2(device, &memory) == NVML_SUCCESS) { + result.memory->total = memory.used + memory.free; + result.memory->used = memory.used; + } else { + nvmlMemory_t memory_v1; + if (nvmlData.ffnvmlDeviceGetMemoryInfo(device, &memory_v1) == NVML_SUCCESS) { + result.memory->total = memory_v1.total; + result.memory->used = memory_v1.used; + } + } + } + + if (result.coreCount) { + nvmlData.ffnvmlDeviceGetNumGpuCores(device, result.coreCount); + } + + if (result.frequency) { + nvmlData.ffnvmlDeviceGetMaxClockInfo(device, NVML_CLOCK_GRAPHICS, result.frequency); + } + + if (result.coreUsage) { + nvmlUtilization_t utilization; + if (nvmlData.ffnvmlDeviceGetUtilizationRates(device, &utilization) == NVML_SUCCESS) { + *result.coreUsage = utilization.gpu; + } + } + + if (result.name) { + char name[NVML_DEVICE_NAME_V2_BUFFER_SIZE]; + if (nvmlData.ffnvmlDeviceGetName(device, name, ARRAY_SIZE(name)) == NVML_SUCCESS) { + ffStrbufSetS(result.name, name); + } + } + + return NULL; + +#else + + FF_UNUSED(cond, result, soName); + return "dlopen is disabled"; + +#endif +} diff --git a/src/detection/gpu/gpu_obsd.c b/src/detection/gpu/gpu_obsd.c new file mode 100644 index 0000000..dabaded --- /dev/null +++ b/src/detection/gpu/gpu_obsd.c @@ -0,0 +1,101 @@ +#include "gpu.h" +#include "common/io.h" + +#include <sys/param.h> +#include <sys/ioctl.h> +#include <fcntl.h> +#include <dev/pci/pcireg.h> +#include <dev/pci/pcidevs.h> +#include <sys/pciio.h> + +static inline int pciReadConf(int fd, uint8_t bus, uint8_t device, uint8_t func, uint8_t reg, uint32_t* result) { + struct pci_io bdfr = { + .pi_sel = { + .pc_bus = bus, + .pc_dev = device, + .pc_func = func, + }, + .pi_reg = reg, + .pi_width = 4, + }; + + if (ioctl(fd, PCIOCREAD, &bdfr) == -1) { + return -1; + } + + *result = bdfr.pi_data; + return 0; +} + +const char* ffDetectGPUImpl(FF_A_UNUSED const FFGPUOptions* options, FFlist* gpus) { + char pciDevPath[] = "/dev/pci0"; + FF_AUTO_CLOSE_FD int pcifd = open(pciDevPath, O_RDONLY | O_CLOEXEC); + if (pcifd < 0) { + return "open(\"/dev/pci0\", O_RDONLY | O_CLOEXEC) failed"; + } + + for (uint16_t bus = 0; bus <= 255; bus++) { + for (uint8_t dev = 0; dev < 32; dev++) { + uint8_t maxfuncs = 0; + for (uint8_t func = 0; func <= maxfuncs; func++) { + uint32_t pciid, pciclass; + if (pciReadConf(pcifd, (uint8_t) bus, dev, func, PCI_ID_REG, &pciid) != 0) { + continue; + } + + if (PCI_VENDOR(pciid) == PCI_VENDOR_INVALID || PCI_VENDOR(pciid) == 0) { + continue; + } + + if (pciReadConf(pcifd, (uint8_t) bus, dev, func, PCI_CLASS_REG, &pciclass) != 0) { + continue; + } + + if (func == 0) { + // For some reason, pciReadConf returns success even for non-existing devices. + // So we need to check for `PCI_VENDOR(pciid) == PCI_VENDOR_INVALID` above to filter them out. + uint32_t bhlcr; + if (pciReadConf(pcifd, (uint8_t) bus, dev, 0, PCI_BHLC_REG, &bhlcr) != 0) { + continue; + } + + if (PCI_HDRTYPE_MULTIFN(bhlcr)) { + maxfuncs = 7; + } + } + + if (PCI_CLASS(pciclass) != PCI_CLASS_DISPLAY) { + continue; + } + + if (func > 0 && PCI_SUBCLASS(pciclass) == PCI_SUBCLASS_DISPLAY_MISC) { + continue; // Likely an auxiliary display controller (#2034) + } + + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus); + ffStrbufInitStatic(&gpu->vendor, ffGPUGetVendorString(PCI_VENDOR(pciid))); + ffStrbufInit(&gpu->name); + ffStrbufInit(&gpu->driver); + ffStrbufInitS(&gpu->platformApi, "/dev/pci0"); + ffStrbufInit(&gpu->memoryType); + gpu->index = FF_GPU_INDEX_UNSET; + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + gpu->type = FF_GPU_TYPE_UNKNOWN; + gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + gpu->deviceId = ffGPUPciAddr2Id(0, bus, dev, func); + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + + if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_AMD) { + ffGPUQueryAmdGpuName(PCI_PRODUCT(pciid), PCI_REVISION(pciid), gpu); + } + if (gpu->name.length == 0) { + ffGPUFillVendorAndName(PCI_SUBCLASS(pciclass), PCI_VENDOR(pciid), PCI_PRODUCT(pciid), gpu); + } + } + } + } + + return NULL; +} diff --git a/src/detection/gpu/gpu_pci.c b/src/detection/gpu/gpu_pci.c new file mode 100644 index 0000000..63cd877 --- /dev/null +++ b/src/detection/gpu/gpu_pci.c @@ -0,0 +1,251 @@ +#include "gpu.h" +#include "common/io.h" +#include "common/properties.h" +#include "common/memrchr.h" + +#include <stdlib.h> +#ifdef __FreeBSD__ + #include <paths.h> + #ifndef _PATH_LOCALBASE + #define _PATH_LOCALBASE "/usr/local" + #endif +#elif __OpenBSD__ + #define _PATH_LOCALBASE "/usr/local" +#elif __NetBSD__ + #define _PATH_LOCALBASE "/usr/pkg" +#endif + +#if FF_HAVE_EMBEDDED_PCIIDS + #include "fastfetch_pciids.c.inc" +#endif +#if FF_HAVE_EMBEDDED_AMDGPUIDS + #include "fastfetch_amdgpuids.c.inc" +#endif + +#define FF_STR_INDIR(x) #x +#define FF_STR(x) FF_STR_INDIR(x) + +static const FFstrbuf* loadPciIds() { + static FFstrbuf pciids; + + if (pciids.chars) { + return &pciids; + } + ffStrbufInit(&pciids); + +#ifdef FF_CUSTOM_PCI_IDS_PATH + + ffReadFileBuffer(FF_STR(FF_CUSTOM_PCI_IDS_PATH), &pciids); + +#else // FF_CUSTOM_PCI_IDS_PATH + + #if __linux__ + ffReadFileBuffer(FASTFETCH_TARGET_DIR_USR "/share/hwdata/pci.ids", &pciids); + if (pciids.length == 0) { + ffReadFileBuffer(FASTFETCH_TARGET_DIR_USR "/share/misc/pci.ids", &pciids); // debian? + if (pciids.length == 0) { + ffReadFileBuffer(FASTFETCH_TARGET_DIR_USR "/local/share/hwdata/pci.ids", &pciids); + } + } + #elif __OpenBSD__ || __FreeBSD__ || __NetBSD__ + ffReadFileBuffer(_PATH_LOCALBASE "/share/hwdata/pci.ids", &pciids); + if (pciids.length == 0) { + ffReadFileBuffer(_PATH_LOCALBASE "/share/pciids/pci.ids", &pciids); + } + #elif __sun + ffReadFileBuffer(FASTFETCH_TARGET_DIR_ROOT "/usr/share/hwdata/pci.ids", &pciids); + #elif __HAIKU__ + ffReadFileBuffer(FASTFETCH_TARGET_DIR_ROOT "/system/data/hwdata/pci.ids", &pciids); + #endif + +#endif // FF_CUSTOM_PCI_IDS_PATH + + return &pciids; +} + +static void parsePciIdsFile(const FFstrbuf* content, uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) { + if (content->length) { + char buffer[32]; + + // Search for vendor + uint32_t len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n%04x ", vendor); + char* start = (char*) memmem(content->chars, content->length, buffer, len); + char* end = content->chars + content->length; + if (start) { + start += len; + end = memchr(start, '\n', (uint32_t) (end - start)); + if (!end) { + end = content->chars + content->length; + } + if (!gpu->vendor.length) { + ffStrbufSetNS(&gpu->vendor, (uint32_t) (end - start), start); + } + + start = end; // point to '\n' of vendor + end = start + 1; // point to start of devices + // find the start of next vendor + while (end[0] == '\t' || end[0] == '#') { + end = strchr(end, '\n'); + if (!end) { + end = content->chars + content->length; + break; + } else { + end++; + } + } + + // Search for device + len = (uint32_t) snprintf(buffer, ARRAY_SIZE(buffer), "\n\t%04x ", device); + start = memmem(start, (size_t) (end - start), buffer, len); + if (start) { + start += len; + end = memchr(start, '\n', (uint32_t) (end - start)); + if (!end) { + end = content->chars + content->length; + } + + char* closingBracket = end - 1; + if (*closingBracket == ']') { + char* openingBracket = memrchr(start, '[', (size_t) (closingBracket - start)); + if (openingBracket) { + openingBracket++; + ffStrbufSetNS(&gpu->name, (uint32_t) (closingBracket - openingBracket), openingBracket); + } + } + if (!gpu->name.length) { + ffStrbufSetNS(&gpu->name, (uint32_t) (end - start), start); + } + } + } + } + + if (!gpu->name.length) { + const char* subclassStr; + switch (subclass) { + case 0 /*PCI_CLASS_DISPLAY_VGA*/: + subclassStr = " (VGA compatible)"; + break; + case 1 /*PCI_CLASS_DISPLAY_XGA*/: + subclassStr = " (XGA compatible)"; + break; + case 2 /*PCI_CLASS_DISPLAY_3D*/: + subclassStr = " (3D)"; + break; + default: + subclassStr = ""; + break; + } + + ffStrbufSetF(&gpu->name, "%s Device %04X%s", gpu->vendor.length ? gpu->vendor.chars : "Unknown", device, subclassStr); + } +} + +#if FF_HAVE_EMBEDDED_PCIIDS +static inline int pciDeviceCmp(const uint16_t* key, const FFPciDevice* element) { + return (int) *key - (int) element->id; +} + +static bool loadPciidsInc(uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) { + for (const FFPciVendor* pvendor = ffPciVendors; pvendor->name; pvendor++) { + if (pvendor->id != vendor) { + continue; + } + + if (!gpu->vendor.length) { + ffStrbufSetS(&gpu->vendor, pvendor->name); + } + + const FFPciDevice* pdevice = (const FFPciDevice*) bsearch(&device, pvendor->devices, pvendor->nDevices, sizeof(*pdevice), (void*) pciDeviceCmp); + + if (pdevice) { + uint32_t nameLen = (uint32_t) strlen(pdevice->name); + const char* closingBracket = pdevice->name + nameLen - 1; + if (*closingBracket == ']') { + const char* openingBracket = memrchr(pdevice->name, '[', nameLen - 1); + if (openingBracket) { + openingBracket++; + ffStrbufSetNS(&gpu->name, (uint32_t) (closingBracket - openingBracket), openingBracket); + } + } + if (!gpu->name.length) { + ffStrbufSetNS(&gpu->name, nameLen, pdevice->name); + } + return true; + } + + if (!gpu->name.length) { + const char* subclassStr; + switch (subclass) { + case 0 /*PCI_CLASS_DISPLAY_VGA*/: + subclassStr = " (VGA compatible)"; + break; + case 1 /*PCI_CLASS_DISPLAY_XGA*/: + subclassStr = " (XGA compatible)"; + break; + case 2 /*PCI_CLASS_DISPLAY_3D*/: + subclassStr = " (3D)"; + break; + default: + subclassStr = ""; + break; + } + + ffStrbufSetF(&gpu->name, "%s Device %04X%s", gpu->vendor.length ? gpu->vendor.chars : "Unknown", device, subclassStr); + } + return true; + } + return false; +} +#endif + +void ffGPUFillVendorAndName(uint8_t subclass, uint16_t vendor, uint16_t device, FFGPUResult* gpu) { + if (vendor == 0x1234 && device == 0x1111 && subclass == 0) { // Not exist in pci.ids + ffStrbufSetStatic(&gpu->name, "Virtual Video Controller"); + return; + } + +#if FF_HAVE_EMBEDDED_PCIIDS + bool ok = loadPciidsInc(subclass, vendor, device, gpu); + if (ok) { + return; + } +#endif + return parsePciIdsFile(loadPciIds(), subclass, vendor, device, gpu); +} + +#if FF_HAVE_EMBEDDED_AMDGPUIDS +static inline int amdGpuCmp(const uint32_t* key, const FFArmGpuProduct* element) { + // Maximum value of *key is 0x00FFFFFF. `(int) *key` should never overflow + return (int) *key - (int) element->id; +} + +static bool loadAmdGpuIdsInc(uint16_t deviceId, uint8_t revision, FFGPUResult* gpu) { + uint32_t key = (deviceId << 8u) | revision; + FFArmGpuProduct* product = bsearch(&key, ffAmdGpuProducts, ARRAY_SIZE(ffAmdGpuProducts), sizeof(*ffAmdGpuProducts), (void*) amdGpuCmp); + if (product) { + ffStrbufSetS(&gpu->name, product->name); + return true; + } + return false; +} +#endif + +static void parseAmdGpuIdsFile(uint16_t deviceId, uint8_t revision, FFGPUResult* gpu) { + char query[32]; + snprintf(query, ARRAY_SIZE(query), "%X,\t%X,", (unsigned) deviceId, (unsigned) revision); +#ifdef FF_CUSTOM_AMDGPU_IDS_PATH + ffParsePropFile(FF_STR(FF_CUSTOM_AMDGPU_IDS_PATH), query, &gpu->name); +#else + ffParsePropFileData("libdrm/amdgpu.ids", query, &gpu->name); +#endif +} + +void ffGPUQueryAmdGpuName(uint16_t deviceId, uint8_t revisionId, FFGPUResult* gpu) { +#if FF_HAVE_EMBEDDED_AMDGPUIDS + bool ok = loadAmdGpuIdsInc(deviceId, revisionId, gpu); + if (ok) { + return; + } +#endif + return parseAmdGpuIdsFile(deviceId, revisionId, gpu); +} diff --git a/src/detection/gpu/gpu_sunos.c b/src/detection/gpu/gpu_sunos.c new file mode 100644 index 0000000..13d4998 --- /dev/null +++ b/src/detection/gpu/gpu_sunos.c @@ -0,0 +1,54 @@ +#include "gpu.h" +#include "common/strutil.h" + +#include <libdevinfo.h> + +static int walkDevTree(di_node_t node, FF_A_UNUSED di_minor_t minor, FFlist* gpus) { + int* vendorId; + int* deviceId; + if (di_prop_lookup_ints(DDI_DEV_T_ANY, node, "vendor-id", &vendorId) > 0 && di_prop_lookup_ints(DDI_DEV_T_ANY, node, "device-id", &deviceId) > 0) { + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus); + ffStrbufInitS(&gpu->vendor, ffGPUGetVendorString((uint16_t) *vendorId)); + ffStrbufInit(&gpu->name); + ffStrbufInitS(&gpu->driver, di_driver_name(node)); + ffStrbufInitStatic(&gpu->platformApi, "libdevinfo"); + ffStrbufInit(&gpu->memoryType); + gpu->index = FF_GPU_INDEX_UNSET; + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + gpu->type = FF_GPU_TYPE_UNKNOWN; + gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + gpu->deviceId = strtoul(di_bus_addr(node), NULL, 16); + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + + if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_AMD) { + int* revId; + if (di_prop_lookup_ints(DDI_DEV_T_ANY, node, "revision-id", &revId) > 0) { + ffGPUQueryAmdGpuName((uint16_t) *deviceId, (uint8_t) *revId, gpu); + } + } + + if (gpu->name.length == 0) { + uint8_t subclass = 0; // assume VGA + int* classCode; + if (di_prop_lookup_ints(DDI_DEV_T_ANY, node, "class-code", &classCode) > 0) { + subclass = (uint8_t) (*classCode & 0xFFFF); + } + ffGPUFillVendorAndName(subclass, (uint16_t) *vendorId, (uint16_t) *deviceId, gpu); + } + } + + return DI_WALK_CONTINUE; +} + +const char* ffDetectGPUImpl(FF_A_UNUSED const FFGPUOptions* options, FFlist* gpus) { + di_node_t rootNode = di_init("/", DINFOCPYALL); + if (rootNode == DI_NODE_NIL) { + return "di_init() failed"; + } + di_walk_minor(rootNode, DDI_NT_DISPLAY, DI_WALK_CLDFIRST, gpus, (void*) walkDevTree); + di_fini(rootNode); + + return NULL; +} diff --git a/src/detection/gpu/gpu_windows.c b/src/detection/gpu/gpu_windows.c new file mode 100644 index 0000000..796c1c5 --- /dev/null +++ b/src/detection/gpu/gpu_windows.c @@ -0,0 +1,605 @@ +#include "detection/gpu/gpu.h" +#if __linux__ + #define FF_GPU_DRIVER_DLLNAME_PATH_PREFIX "/usr/lib/wsl/lib/" +#endif +#include "detection/gpu/gpu_driver_specific.h" +#include "common/debug.h" + +#include <inttypes.h> +#include "d3dkmthk.h" + +#if _WIN32 + #include "common/windows/unicode.h" + #include "common/windows/registry.h" + + #if FF_WIN81_COMPAT + #include "common/mallocHelper.h" + #include <windows.h> + #include <cfgmgr32.h> + #include <devguid.h> + + #define GUID_DEVCLASS_DISPLAY_STRING L"{4d36e968-e325-11ce-bfc1-08002be10318}" // Found in <devguid.h> + +static bool queryDeviceIdsFallback(D3DKMT_ADAPTERADDRESS adapterAddress, D3DKMT_DEVICE_IDS* outDeviceIds) { + FF_DEBUG("KMTQAITYPE_PHYSICALADAPTERDEVICEIDS failed. Attempting queryDeviceIdsFallback: bus=%u device=%u function=%u", + adapterAddress.BusNumber, + adapterAddress.DeviceNumber, + adapterAddress.FunctionNumber); + + if (adapterAddress.BusNumber == -1u) { + FF_DEBUG("Invalid adapter address, cannot query device IDs"); + return false; + } + + static FFlist deviceIdsCache; + static bool initialized; + typedef struct { + D3DKMT_DEVICE_IDS deviceIds; + D3DKMT_ADAPTERADDRESS adapterAddress; + } CacheEntry; + + if (!initialized) { + initialized = true; + ffListInit(&deviceIdsCache); + + ULONG devIdListSize = 0; + if (CM_Get_Device_ID_List_SizeW(&devIdListSize, GUID_DEVCLASS_DISPLAY_STRING, CM_GETIDLIST_FILTER_CLASS | CM_GETIDLIST_FILTER_PRESENT) != CR_SUCCESS || devIdListSize <= 1) { + FF_DEBUG("No display devices found, list size: %lu", devIdListSize); + return false; + } + + FF_DEBUG("Found device ID list size: %lu", devIdListSize); + + FF_AUTO_FREE DEVINSTID_W devIdList = malloc(devIdListSize * sizeof(*devIdList)); + + if (CM_Get_Device_ID_ListW(GUID_DEVCLASS_DISPLAY_STRING, devIdList, devIdListSize, CM_GETIDLIST_FILTER_CLASS | CM_GETIDLIST_FILTER_PRESENT) != CR_SUCCESS) { + FF_DEBUG("CM_Get_Device_ID_ListW failed"); + return false; + } + + for (wchar_t* devId = devIdList; *devId; devId += wcslen(devId) + 1) { + FF_DEBUG("Processing device ID: %ls", devId); + + DEVINST devInst = 0; + + if (CM_Locate_DevNodeW(&devInst, devId, CM_LOCATE_DEVNODE_NORMAL) != CR_SUCCESS) { + FF_DEBUG("Failed to get device instance ID or locate device node"); + continue; + } + FF_DEBUG("Device instance ID: %lu", devInst); + + for (wchar_t* p = devId; *p; p++) { + if (*p >= L'a' && *p <= L'z') { + *p -= L'a' - L'A'; + } + } + + if (wcsncmp(devId, L"PCI\\", 4) != 0) { + FF_DEBUG("Skipping non-PCI device ID: %ls", devId); + continue; + } + + uint32_t pciBus = 0; + + ULONG pciBufLen = sizeof(pciBus); + if (CM_Get_DevNode_Registry_PropertyW(devInst, CM_DRP_BUSNUMBER, NULL, &pciBus, &pciBufLen, 0) == CR_SUCCESS) { + uint32_t pciAddr = 0; + pciBufLen = sizeof(pciAddr); + if (CM_Get_DevNode_Registry_PropertyW(devInst, CM_DRP_ADDRESS, NULL, &pciAddr, &pciBufLen, 0) == CR_SUCCESS) { + CacheEntry* entry = FF_LIST_ADD(CacheEntry, deviceIdsCache); + + entry->deviceIds = (D3DKMT_DEVICE_IDS) {}; + // L"PCI\\VEN_10DE&DEV_2782&SUBSYS_513417AA&REV_A1\\4&3674a6b9&0&0008" + if (swscanf(devId + 4, L"VEN_%x&DEV_%x&SUBSYS_%4x%4x&REV_%x", &entry->deviceIds.VendorID, &entry->deviceIds.DeviceID, &entry->deviceIds.SubSystemID, &entry->deviceIds.SubVendorID, &entry->deviceIds.RevisionID) >= 2) { + FF_DEBUG("Parsed PCI IDs - Vendor: 0x%04x, Device: 0x%04x, SubVendor: 0x%04x, SubSystem: 0x%04x, Rev: 0x%04x", entry->deviceIds.VendorID, entry->deviceIds.DeviceID, entry->deviceIds.SubVendorID, entry->deviceIds.SubSystemID, entry->deviceIds.RevisionID); + // I thought it was DXGKMDT_OPM_BUS_TYPE_PCI, but it turns out to be false + // Who TF knows what 1 actually means. It's just reported by most graphic cards + // And yeah, DXGKMDT_OPM_BUS_TYPE_PCIEXPRESS (3) exists + entry->deviceIds.BusType = 1; + } else { + FF_DEBUG("Failed to parse PCI IDs from device ID string"); + deviceIdsCache.length--; // remove the cache entry since it's not valid + continue; + } + + entry->adapterAddress = (D3DKMT_ADAPTERADDRESS) { + .BusNumber = pciBus, + .DeviceNumber = (pciAddr >> 16) & 0xFFFF, + .FunctionNumber = pciAddr & 0xFFFF, + }; + FF_DEBUG("Cached device IDs for PCI bus %u: vendor=0x%04x device=0x%04x", pciBus, entry->deviceIds.VendorID, entry->deviceIds.DeviceID); + } else { + FF_DEBUG("Failed to get PCI address"); + } + } else { + FF_DEBUG("Failed to get PCI bus number"); + } + } + } + + FF_LIST_FOR_EACH (CacheEntry, entry, deviceIdsCache) { + if (memcmp(&entry->adapterAddress, &adapterAddress, sizeof(adapterAddress)) == 0) { + FF_DEBUG("Cache hit for adapter address: bus=%u device=%u function=%u", adapterAddress.BusNumber, adapterAddress.DeviceNumber, adapterAddress.FunctionNumber); + *outDeviceIds = entry->deviceIds; + return true; + } + } + + FF_DEBUG("Cache miss for adapter address: bus=%u device=%u function=%u", adapterAddress.BusNumber, adapterAddress.DeviceNumber, adapterAddress.FunctionNumber); + return false; +} + #endif // FF_WIN81_COMPAT + +static bool queryVendorNameViaRegistry(FFstrbuf* vendor, D3DKMT_HANDLE hAdapter) { + // `KMTQAITYPE_QUERY_ADAPTER_UNIQUE_GUID` reports the GUID value used by the adapter's registry key (DirectX and Video) + + GUID guid; + NTSTATUS status = D3DKMTQueryAdapterInfo(&(D3DKMT_QUERYADAPTERINFO) { + .hAdapter = hAdapter, + .Type = KMTQAITYPE_QUERY_ADAPTER_UNIQUE_GUID, + .pPrivateDriverData = &guid, + .PrivateDriverDataSize = sizeof(guid), + }); + if (!NT_SUCCESS(status)) { + FF_DEBUG("Failed to query adapter unique GUID: %s", ffDebugNtStatus(status)); + return false; + } + + wchar_t path[PATH_MAX]; + swprintf(path, ARRAY_SIZE(path), L"SYSTEM\\CurrentControlSet\\Control\\Video\\{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}\\0000", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + + FF_DEBUG("Querying registry: HKEY_LOCAL_MACHINE\\%ls\\ProviderName", path); + FF_AUTO_CLOSE_FD HANDLE key = NULL; + if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, path, &key, NULL)) { + return false; + } + + return ffRegReadStrbuf(key, L"ProviderName", vendor, NULL); +} + +#else + #include <unistd.h> + #include <fcntl.h> + #include <sys/ioctl.h> + #include <uchar.h> + +int dxgfd = -2; + +static void ffStrbufSetWS(FFstrbuf* strbuf, const char16_t* str) { + ffStrbufClear(strbuf); + + mbstate_t state = {}; + while (*str) { + char buf[5]; + size_t len = c16rtomb(buf, *str, &state); + if (len == (size_t) -1) { + ffStrbufAppendS(strbuf, "�"); // U+FFFD REPLACEMENT CHARACTER + } else if (len > 0) { + ffStrbufAppendNS(strbuf, (uint32_t) len, buf); + } + str++; + } +} + +static void closeDxgfd(void) { + if (dxgfd >= 0) { + close(dxgfd); + dxgfd = 0; + FF_DEBUG("Closed /dev/dxg file descriptor"); + } +} + +FF_A_UNUSED static inline const char* ffDebugNtStatus(NTSTATUS status) { + return status < 0 ? strerror(-status) : "Success"; +} +#endif + +const char* +#if _WIN32 +ffDetectGPUImpl +#else +ffGPUDetectWsl2 +#endif + (const FFGPUOptions* options, FFlist* gpus) { +#if __linux__ + if (dxgfd == -2) { + dxgfd = open("/dev/dxg", O_RDWR); // Windows DXCore/D3DKMT adapter driver for WSL + if (dxgfd < 0) { + if (errno == ENOENT) { + FF_DEBUG("/dev/dxg is not available, WSL DXCore GPU driver not detected"); + return "No DXCore GPU driver detected (no /dev/dxg)"; + } else { + FF_DEBUG("Failed to open /dev/dxg: %s", strerror(errno)); + return "Failed to open /dev/dxg"; + } + } + FF_DEBUG("Opened /dev/dxg successfully"); + atexit(closeDxgfd); + } + if (dxgfd < 0) { + return "Failed to open /dev/dxg"; + } +#endif + +#if FF_WIN81_COMPAT + D3DKMT_ENUMADAPTERS enumAdapters = {}; + D3DKMT_ADAPTERINFO* const adapters = enumAdapters.Adapters; + NTSTATUS status = D3DKMTEnumAdapters(&enumAdapters); +#else + D3DKMT_ADAPTERINFO adapters[64]; + D3DKMT_ENUMADAPTERS2 enumAdapters = { + .NumAdapters = ARRAY_SIZE(adapters), + .pAdapters = adapters, + }; + NTSTATUS status = D3DKMTEnumAdapters2(&enumAdapters); +#endif + if (!NT_SUCCESS(status)) { + FF_DEBUG("D3DKMTEnumAdapters(2) failed: %s", ffDebugNtStatus(status)); + return "Failed to enumerate adapters with D3DKMTEnumAdapters2"; + } + + FF_DEBUG("D3DKMTEnumAdapters(2) succeeded, adapter count: %" PRIu32, (uint32_t) enumAdapters.NumAdapters); + + for (uint32_t i = 0; i < enumAdapters.NumAdapters; i++) { + const D3DKMT_ADAPTERINFO* adapter = &adapters[i]; + FF_DEBUG("Processing adapter #%u", i); + + D3DKMT_ADAPTERTYPE adapterType; + status = D3DKMTQueryAdapterInfo(&(D3DKMT_QUERYADAPTERINFO) { + .hAdapter = adapter->hAdapter, + .Type = KMTQAITYPE_ADAPTERTYPE, + .pPrivateDriverData = &adapterType, + .PrivateDriverDataSize = sizeof(adapterType), + }); + if (!NT_SUCCESS(status)) { + FF_DEBUG("KMTQAITYPE_ADAPTERTYPE query failed for adapter #%u: %s", i, ffDebugNtStatus(status)); + continue; + } + if (adapterType.SoftwareDevice) { + FF_DEBUG("Skipping software adapter #%u", i); + goto close_adapter; + } + + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, *gpus); + ffStrbufInit(&gpu->vendor); + ffStrbufInit(&gpu->name); + ffStrbufInit(&gpu->driver); + ffStrbufInit(&gpu->platformApi); + ffStrbufInit(&gpu->memoryType); + gpu->index = FF_GPU_INDEX_UNSET; + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + gpu->deviceId = 0; + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + gpu->type = adapterType.HybridIntegrated + ? FF_GPU_TYPE_INTEGRATED + : adapterType.HybridDiscrete + ? FF_GPU_TYPE_DISCRETE + : FF_GPU_TYPE_UNKNOWN; + + D3DKMT_DRIVERVERSION wddmVersion = KMT_DRIVERVERSION_WDDM_2_0; + status = D3DKMTQueryAdapterInfo(&(D3DKMT_QUERYADAPTERINFO) { + .hAdapter = adapter->hAdapter, + .Type = KMTQAITYPE_DRIVERVERSION, + .pPrivateDriverData = &wddmVersion, + .PrivateDriverDataSize = sizeof(wddmVersion), + }); + if (NT_SUCCESS(status)) { + ffStrbufSetF(&gpu->platformApi, "WDDM %u.%u", (uint32_t) wddmVersion / 1000, ((uint32_t) wddmVersion % 1000) / 100); + FF_DEBUG("Adapter #%u WDDM version: %u", i, (uint32_t) wddmVersion); + } else { + ffStrbufSetStatic(&gpu->platformApi, "WDDM"); + FF_DEBUG("KMTQAITYPE_DRIVERVERSION query failed for adapter #%u", i); + } + + D3DKMT_ADAPTERADDRESS adapterAddress = {}; + status = D3DKMTQueryAdapterInfo(&(D3DKMT_QUERYADAPTERINFO) { + .hAdapter = adapter->hAdapter, + .Type = KMTQAITYPE_ADAPTERADDRESS, + .pPrivateDriverData = &adapterAddress, + .PrivateDriverDataSize = sizeof(adapterAddress), + }); + if (NT_SUCCESS(status) && adapterAddress.FunctionNumber != 0xFFFF /* non-PCI device */) { + gpu->deviceId = ffGPUPciAddr2Id(0, adapterAddress.BusNumber, adapterAddress.DeviceNumber, adapterAddress.FunctionNumber); + FF_DEBUG("Adapter #%u PCI address: bus=%u device=%u function=%u", + i, + adapterAddress.BusNumber, + adapterAddress.DeviceNumber, + adapterAddress.FunctionNumber); + } else { + adapterAddress.BusNumber = -1u; + gpu->deviceId = ffGPUGeneral2Id(((uint64_t) adapter->AdapterLuid.HighPart << 32) | (uint64_t) adapter->AdapterLuid.LowPart); + FF_DEBUG("KMTQAITYPE_ADAPTERADDRESS query failed for adapter #%u, fallback to LUID-based deviceId: %s", + i, + ffDebugNtStatus(status)); + } + + D3DKMT_QUERY_DEVICE_IDS deviceIds = { .PhysicalAdapterIndex = 0 }; + status = D3DKMTQueryAdapterInfo(&(D3DKMT_QUERYADAPTERINFO) { + .hAdapter = adapter->hAdapter, + .Type = KMTQAITYPE_PHYSICALADAPTERDEVICEIDS, + .pPrivateDriverData = &deviceIds, + .PrivateDriverDataSize = sizeof(deviceIds), + }); + if (NT_SUCCESS(status) +#if FF_WIN81_COMPAT + || queryDeviceIdsFallback(adapterAddress, &deviceIds.DeviceIds) +#endif + ) { + ffStrbufSetStatic(&gpu->vendor, ffGPUGetVendorString(deviceIds.DeviceIds.VendorID)); + FF_DEBUG("Adapter #%u vendor/device IDs: vendor=0x%04x device=0x%04x", + i, + deviceIds.DeviceIds.VendorID, + deviceIds.DeviceIds.DeviceID); + } else { + deviceIds.DeviceIds.VendorID = -1u; + FF_DEBUG("KMTQAITYPE_PHYSICALADAPTERDEVICEIDS query failed for adapter #%u: %s", i, ffDebugNtStatus(status)); + } + + D3DKMT_UMD_DRIVER_VERSION umdDriverVersion; + status = D3DKMTQueryAdapterInfo(&(D3DKMT_QUERYADAPTERINFO) { + .hAdapter = adapter->hAdapter, + .Type = KMTQAITYPE_UMD_DRIVER_VERSION, + .pPrivateDriverData = &umdDriverVersion, + .PrivateDriverDataSize = sizeof(umdDriverVersion), + }); + if (NT_SUCCESS(status)) { + ffStrbufSetF(&gpu->driver, + "%u.%u.%u.%u", + (uint32_t) (umdDriverVersion.DriverVersion.QuadPart >> 48ul & 0xFFFF), + (uint32_t) (umdDriverVersion.DriverVersion.QuadPart >> 32ul & 0xFFFF), + (uint32_t) (umdDriverVersion.DriverVersion.QuadPart >> 16ul & 0xFFFF), + (uint32_t) (umdDriverVersion.DriverVersion.QuadPart >> 0ul & 0xFFFF)); + FF_DEBUG("Adapter #%u UMD driver version: %08" PRIX64, i, (uint64_t) umdDriverVersion.DriverVersion.QuadPart); + } else { + FF_DEBUG("KMTQAITYPE_UMD_DRIVER_VERSION query failed for adapter #%u: %s", i, ffDebugNtStatus(status)); + } + + __typeof__(&ffDetectNvidiaGpuInfo) detectFn; + const char* dllName; + if (options->driverSpecific && getDriverSpecificDetectionFn(gpu->vendor.chars, &detectFn, &dllName)) { + FF_DEBUG("Calling driver-specific detection function for vendor: %s, DLL: %s", gpu->vendor.chars, dllName); + FF_A_UNUSED const char* error = detectFn( + &(FFGpuDriverCondition) { + .type = FF_GPU_DRIVER_CONDITION_TYPE_LUID | + (deviceIds.DeviceIds.VendorID != -1u ? FF_GPU_DRIVER_CONDITION_TYPE_DEVICE_ID : 0) | + (adapterAddress.BusNumber != -1u ? FF_GPU_DRIVER_CONDITION_TYPE_BUS_ID : 0), + .pciDeviceId = { + .deviceId = deviceIds.DeviceIds.DeviceID, + .vendorId = deviceIds.DeviceIds.VendorID, + .subSystemId = deviceIds.DeviceIds.SubSystemID, + .revId = deviceIds.DeviceIds.RevisionID, + }, + .pciBusId = { + .domain = 0, + .bus = adapterAddress.BusNumber, + .device = adapterAddress.DeviceNumber, + .func = adapterAddress.FunctionNumber, + }, + .luid = ((uint64_t) adapter->AdapterLuid.HighPart << 32) | (uint64_t) adapter->AdapterLuid.LowPart, + }, + (FFGpuDriverResult) { + .index = &gpu->index, + .temp = options->temp ? &gpu->temperature : NULL, + .memory = options->driverSpecific ? &gpu->dedicated : NULL, + .sharedMemory = options->driverSpecific ? &gpu->shared : NULL, + .memoryType = options->driverSpecific ? &gpu->memoryType : NULL, + .coreCount = options->driverSpecific ? (uint32_t*) &gpu->coreCount : NULL, + .coreUsage = options->driverSpecific ? &gpu->coreUsage : NULL, + .type = &gpu->type, + .name = &gpu->name, + .frequency = options->driverSpecific ? &gpu->frequency : NULL, + }, + dllName); + FF_DEBUG("Driver-specific detection completed: %s", error ?: "Success"); + } else if (options->driverSpecific) { + FF_DEBUG("No driver-specific detection function found for vendor: %s", gpu->vendor.chars); + } + +#if _WIN32 + // Put this after the driver-specific detection, as `getDriverSpecificDetectionFn` never succeeds + if (gpu->vendor.length == 0 && wddmVersion >= KMT_DRIVERVERSION_WDDM_2_4) { + // For non-PCI devices + FF_DEBUG("Attempting to query vendor name via registry for adapter #%u", i); + queryVendorNameViaRegistry(&gpu->vendor, adapter->hAdapter); + } +#endif + + if (gpu->name.length == 0) { + D3DKMT_ADAPTERREGISTRYINFO registryInfo; + status = D3DKMTQueryAdapterInfo(&(D3DKMT_QUERYADAPTERINFO) { + .hAdapter = adapter->hAdapter, + .Type = KMTQAITYPE_ADAPTERREGISTRYINFO, + .pPrivateDriverData = ®istryInfo, + .PrivateDriverDataSize = sizeof(registryInfo), + }); + if (NT_SUCCESS(status)) { + ffStrbufSetWS(&gpu->name, registryInfo.AdapterString); + FF_DEBUG("Adapter #%u adapter string: %s", i, gpu->name.chars); + } else { + FF_DEBUG("KMTQAITYPE_ADAPTERREGISTRYINFO query failed for adapter #%u: %s", i, ffDebugNtStatus(status)); + } + } + + if (gpu->dedicated.total == FF_GPU_VMEM_SIZE_UNSET && gpu->shared.total == FF_GPU_VMEM_SIZE_UNSET) { + if (wddmVersion >= KMT_DRIVERVERSION_WDDM_3_1 && options->driverSpecific) { + // Supports memory usage query; requires Windows 11 (22H2) or later + D3DKMT_QUERYSTATISTICS queryStatistics = { + .Type = D3DKMT_QUERYSTATISTICS_SEGMENT_GROUP_USAGE, + .AdapterLuid = adapter->AdapterLuid, + .QuerySegmentGroupUsage = { + .PhysicalAdapterIndex = 0, + .SegmentGroup = D3DKMT_MEMORY_SEGMENT_GROUP_LOCAL, + }, + }; + status = D3DKMTQueryStatistics(&queryStatistics); + if (NT_SUCCESS(status)) { + D3DKMT_QUERYSTATISTICS_MEMORY_USAGE* info = &queryStatistics.QueryResult.SegmentGroupUsageInformation; + uint64_t used = info->AllocatedBytes + info->ModifiedBytes + info->StandbyBytes; + uint64_t total = used + info->FreeBytes + info->ZeroBytes; + gpu->dedicated.used = used; + gpu->dedicated.total = total; + FF_DEBUG("Adapter #%u local memory usage: used=%" PRIu64 " total=%" PRIu64, i, used, total); + } else { + FF_DEBUG("D3DKMT_QUERYSTATISTICS_SEGMENT_GROUP_USAGE (LOCAL) failed for adapter #%u: %s", + i, + ffDebugNtStatus(status)); + } + + queryStatistics.QuerySegmentGroupUsage.SegmentGroup = D3DKMT_MEMORY_SEGMENT_GROUP_NON_LOCAL; + status = D3DKMTQueryStatistics(&queryStatistics); + if (NT_SUCCESS(status)) { + D3DKMT_QUERYSTATISTICS_MEMORY_USAGE* info = &queryStatistics.QueryResult.SegmentGroupUsageInformation; + uint64_t used = info->AllocatedBytes + info->ModifiedBytes + info->StandbyBytes; + uint64_t total = used + info->FreeBytes + info->ZeroBytes; + gpu->shared.used = used; + gpu->shared.total = total; + FF_DEBUG("Adapter #%u non-local memory usage: used=%" PRIu64 " total=%" PRIu64, i, used, total); + } else { + FF_DEBUG("D3DKMT_QUERYSTATISTICS_SEGMENT_GROUP_USAGE (NON_LOCAL) failed for adapter #%u: %s", + i, + ffDebugNtStatus(status)); + } + } else { + // Supports basic segment (total) size query + D3DKMT_SEGMENTSIZEINFO segmentSizeInfo = {}; + status = D3DKMTQueryAdapterInfo(&(D3DKMT_QUERYADAPTERINFO) { + .hAdapter = adapter->hAdapter, + .Type = KMTQAITYPE_GETSEGMENTSIZE, + .pPrivateDriverData = &segmentSizeInfo, + .PrivateDriverDataSize = sizeof(segmentSizeInfo), + }); + if (NT_SUCCESS(status)) { + FF_DEBUG("Adapter #%u segment size - DedicatedVideoMemorySize: %" PRIu64 + ", DedicatedSystemMemorySize: %" PRIu64 ", SharedSystemMemorySize: %" PRIu64, + i, + (uint64_t) segmentSizeInfo.DedicatedVideoMemorySize, + (uint64_t) segmentSizeInfo.DedicatedSystemMemorySize, + (uint64_t) segmentSizeInfo.SharedSystemMemorySize); + gpu->dedicated.total = segmentSizeInfo.DedicatedVideoMemorySize; + gpu->shared.total = segmentSizeInfo.DedicatedSystemMemorySize + segmentSizeInfo.SharedSystemMemorySize; + } else { + FF_DEBUG("Failed to query segment size information for adapter #%u: %s", i, ffDebugNtStatus(status)); + } + } + } + + if (wddmVersion >= KMT_DRIVERVERSION_WDDM_2_4) { + if (gpu->frequency == FF_GPU_FREQUENCY_UNSET) { + for (uint32_t nodeIdx = 0;; nodeIdx++) { + D3DKMT_NODEMETADATA nodeMetadata = { + .NodeOrdinalAndAdapterIndex = (0 << 16) | nodeIdx, + }; + status = D3DKMTQueryAdapterInfo(&(D3DKMT_QUERYADAPTERINFO) { + .hAdapter = adapter->hAdapter, + .Type = KMTQAITYPE_NODEMETADATA, + .pPrivateDriverData = &nodeMetadata, + .PrivateDriverDataSize = sizeof(nodeMetadata), + }); + if (!NT_SUCCESS(status)) { break; } + + if (nodeMetadata.NodeData.EngineType != DXGK_ENGINE_TYPE_3D) { continue; } + + D3DKMT_NODE_PERFDATA nodePerfData = { + .NodeOrdinal = nodeIdx, + .PhysicalAdapterIndex = 0, + }; + status = D3DKMTQueryAdapterInfo(&(D3DKMT_QUERYADAPTERINFO) { + .hAdapter = adapter->hAdapter, + .Type = KMTQAITYPE_NODEPERFDATA, + .pPrivateDriverData = &nodePerfData, + .PrivateDriverDataSize = sizeof(nodePerfData), + }); + if (NT_SUCCESS(status)) { + if (nodePerfData.MaxFrequency != 0) { + gpu->frequency = (uint32_t) (nodePerfData.MaxFrequency / 1000 / 1000); + FF_DEBUG("Adapter #%u max graphics frequency: %u MHz", i, gpu->frequency); + } else { + FF_DEBUG("Adapter #%u does not report max graphics frequency", i); + } + break; + } else { + FF_DEBUG("Failed to query node performance data for adapter #%u node #%u: %s", + i, + nodeIdx, + ffDebugNtStatus(status)); + } + } + } + + if (options->temp && gpu->temperature == FF_GPU_TEMP_UNSET) { + D3DKMT_ADAPTER_PERFDATA adapterPerfData = { + .PhysicalAdapterIndex = 0, + }; + status = D3DKMTQueryAdapterInfo(&(D3DKMT_QUERYADAPTERINFO) { + .hAdapter = adapter->hAdapter, + .Type = KMTQAITYPE_ADAPTERPERFDATA, + .pPrivateDriverData = &adapterPerfData, + .PrivateDriverDataSize = sizeof(adapterPerfData), + }); + if (NT_SUCCESS(status)) { + if (adapterPerfData.Temperature != 0) { + gpu->temperature = adapterPerfData.Temperature / 10.0; + FF_DEBUG("Adapter #%u temperature: %.1f°C", i, gpu->temperature); + } else { + FF_DEBUG("Adapter #%u does not report temperature data", i); + } + } else { + FF_DEBUG("Failed to query temperature for adapter #%u: %s", i, ffDebugNtStatus(status)); + } + } + } + + if (gpu->type == FF_GPU_TYPE_UNKNOWN) { + FF_DEBUG("Using fallback GPU type detection"); + if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_NVIDIA) { + if (ffStrbufStartsWithIgnCaseS(&gpu->name, "GeForce") || + ffStrbufStartsWithIgnCaseS(&gpu->name, "Quadro") || + ffStrbufStartsWithIgnCaseS(&gpu->name, "Tesla")) { + gpu->type = FF_GPU_TYPE_DISCRETE; + } + } else if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_MTHREADS) { + if (ffStrbufStartsWithIgnCaseS(&gpu->name, "MTT ")) { gpu->type = FF_GPU_TYPE_DISCRETE; } + } else if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_INTEL) { + // 0000:00:02.0 is reserved for Intel integrated graphics + gpu->type = gpu->deviceId == ffGPUPciAddr2Id(0, 0, 2, 0) ? FF_GPU_TYPE_INTEGRATED : FF_GPU_TYPE_DISCRETE; + } else if (gpu->vendor.chars == FF_GPU_VENDOR_NAME_VMWARE || gpu->vendor.chars == FF_GPU_VENDOR_NAME_PARALLELS) { + // Virtualized GPUs + gpu->type = FF_GPU_TYPE_INTEGRATED; + } + + if (gpu->type != FF_GPU_TYPE_UNKNOWN) { + FF_DEBUG("Determined GPU type based on vendor (%s) and name: %u", gpu->vendor.chars, gpu->type); + } +#if _WIN32 + else if (ffIsWindows10OrGreater()) { + const char* ffGPUDetectTypeWithDXCore(LUID adapterLuid, FFGPUResult * gpu); + FF_A_UNUSED const char* error = ffGPUDetectTypeWithDXCore(adapter->AdapterLuid, gpu); + FF_DEBUG("DXCore GPU type detection result: %s", error ?: "Success"); + } +#endif + else { + FF_DEBUG("Unable to determine GPU type by any method for this adapter"); + } + } + + FF_DEBUG("Adapter #%u summary: name='%s', vendor='%s', type=%u, deviceId=%" PRIu64, + i, + gpu->name.length ? gpu->name.chars : "unknown", + gpu->vendor.length ? gpu->vendor.chars : "unknown", + (uint32_t) gpu->type, + (uint64_t) gpu->deviceId); + + close_adapter: + status = D3DKMTCloseAdapter(&(D3DKMT_CLOSEADAPTER) { .hAdapter = adapter->hAdapter }); + if (NT_SUCCESS(status)) { + FF_DEBUG("Closed adapter #%u successfully", i); + } else { + FF_DEBUG("Failed to close adapter #%u: %s", i, ffDebugNtStatus(status)); + } + } + + return NULL; +} diff --git a/src/detection/gpu/gpu_windows.cpp b/src/detection/gpu/gpu_windows.cpp new file mode 100644 index 0000000..ae6f00e --- /dev/null +++ b/src/detection/gpu/gpu_windows.cpp @@ -0,0 +1,83 @@ +extern "C" { +#include "gpu.h" +#include "common/library.h" +#include "common/debug.h" +} + +#if __has_include(<directx/dxcore.h>) && __has_include(<dxguids/dxguids.h>) + + #include <directx/dxcore.h> + #include <dxguids/dxguids.h> + #include "common/windows/util.hpp" + +static IDXCoreAdapterFactory* loadDxCoreFactory() { + static bool initialized = false; + static IDXCoreAdapterFactory* factory = nullptr; + if (initialized) { + return factory; // Already loaded + } + + initialized = true; + FF_LIBRARY_LOAD(dxcore, NULL, "dxcore" FF_LIBRARY_EXTENSION, 1) + + // DXCoreCreateAdapterFactory is a reloaded function, so we can't use FF_LIBRARY_LOAD_SYMBOL_MESSAGE here + typedef HRESULT (*DXCoreCreateAdapterFactory_t)(REFIID riid, void** ppvFactory); + + #ifndef FF_DISABLE_DLOPEN + auto ffDXCoreCreateAdapterFactory = (DXCoreCreateAdapterFactory_t) dlsym(dxcore, "DXCoreCreateAdapterFactory"); + if (ffDXCoreCreateAdapterFactory == nullptr) { + return NULL; + } + #else + auto ffDXCoreCreateAdapterFactory = (DXCoreCreateAdapterFactory_t) DXCoreCreateAdapterFactory; + #endif + + HRESULT hr = ffDXCoreCreateAdapterFactory(IID_PPV_ARGS(&factory)); + if (FAILED(hr)) { + FF_DEBUG("DXCoreCreateAdapterFactory failed with HRESULT: 0x%08lX (%s)", hr, ffDebugHResult(hr)); + return NULL; + } + + dxcore = NULL; // Don't unload + return factory; +} + +extern "C" const char* ffGPUDetectTypeWithDXCore(LUID adapterLuid, FFGPUResult* gpu) { + auto* factory = loadDxCoreFactory(); + if (!factory) { + return "Failed to load DXCore library or create adapter factory"; + } + + IDXCoreAdapter* adapter = nullptr; + HRESULT hr = factory->GetAdapterByLuid(adapterLuid, IID_PPV_ARGS(&adapter)); + if (FAILED(hr)) { + FF_DEBUG("GetAdapterByLuid failed with HRESULT: 0x%08lX (%s)", hr, ffDebugHResult(hr)); + return "Failed to get adapter by LUID"; + } + + on_scope_exit releaseAdapter{ [adapter] { adapter->Release(); } }; + + bool isIntegrated = false; + hr = adapter->GetProperty(DXCoreAdapterProperty::IsIntegrated, sizeof(isIntegrated), &isIntegrated); + if (FAILED(hr)) { + FF_DEBUG("GetProperty(IsIntegrated) failed with HRESULT: 0x%08lX (%s)", hr, ffDebugHResult(hr)); + return "Failed to get adapter properties"; + } + + gpu->type = isIntegrated ? FF_GPU_TYPE_INTEGRATED : FF_GPU_TYPE_DISCRETE; + FF_DEBUG("GPU type determined using DXCore: %s", isIntegrated ? "Integrated" : "Discrete"); + + return nullptr; +} + +#else + + #warning "DXCore headers not available, GPU type detection may be less accurate" + +extern "C" const char* ffGPUDetectTypeWithDXCore(LUID adapterLuid, FFGPUResult* gpu) { + FF_UNUSED(adapterLuid, gpu); + FF_DEBUG("DXCore not available, skipping GPU type detection with DXCore"); + return "DXCore not available"; +} + +#endif diff --git a/src/detection/gpu/igcl.h b/src/detection/gpu/igcl.h new file mode 100644 index 0000000..6bf4ffe --- /dev/null +++ b/src/detection/gpu/igcl.h @@ -0,0 +1,216 @@ +#pragma once + +// DISCLAIMER: +// THIS FILE IS CREATED FROM SCRATCH, BY READING THE OFFICIAL IGCL API +// DOCUMENTATION REFERENCED BELOW, IN ORDER TO MAKE FASTFETCH MIT COMPLIANT. + +#include <stdint.h> + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv412ctl_result_t +typedef enum ctl_result_t { + CTL_RESULT_SUCCESS = 0, +} ctl_result_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv420ctl_application_id_t +typedef struct ctl_application_id_t { + uint32_t Data1; + uint16_t Data2; + uint16_t Data3; + uint8_t Data4[8]; +} ctl_application_id_t; + +#define CTL_IMPL_VERSION ((1 /*major*/ << 16) | (1 /*minor*/ & 0x0000ffff)) + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv415ctl_init_flag_t +typedef enum ctl_init_flag_t { + CTL_INIT_FLAG_USE_LEVEL_ZERO = 1, + CTL_INIT_FLAG_MAX +} ctl_init_flag_t; + +typedef uint32_t ctl_version_info_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv415ctl_init_args_t +typedef struct ctl_init_args_t { + uint32_t Size; + uint8_t Version; + ctl_version_info_t AppVersion; + ctl_init_flag_t flags; + ctl_version_info_t SupportedVersion; + ctl_application_id_t ApplicationUID; +} ctl_init_args_t; + +typedef struct ctl_api_handle_t* ctl_api_handle_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv47ctlInitP15ctl_init_args_tP16ctl_api_handle_t +extern ctl_result_t ctlInit(ctl_init_args_t* pInitDesc, ctl_api_handle_t* phAPIHandle); +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#ctlclose +extern ctl_result_t ctlClose(ctl_api_handle_t hAPIHandle); + +typedef struct ctl_device_adapter_handle_t* ctl_device_adapter_handle_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv419ctlEnumerateDevices16ctl_api_handle_tP8uint32_tP27ctl_device_adapter_handle_t +extern ctl_result_t ctlEnumerateDevices(ctl_api_handle_t hAPIHandle, uint32_t* pCount, ctl_device_adapter_handle_t* phDevices); + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv417ctl_device_type_t +typedef enum ctl_device_type_t { + CTL_DEVICE_TYPE_GRAPHICS = 1, + CTL_DEVICE_TYPE_SYSTEM = 2, + CTL_DEVICE_TYPE_MAX +} ctl_device_type_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv422ctl_firmware_version_t +typedef struct ctl_firmware_version_t { + uint64_t major_version; + uint64_t minor_version; + uint64_t build_number; +} ctl_firmware_version_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv417ctl_adapter_bdf_t +typedef struct ctl_adapter_bdf_t { + uint8_t bus; + uint8_t device; + uint8_t function; +} ctl_adapter_bdf_t; + +#define IGCL_CTL_MAX_DEVICE_NAME_LEN 100 +#define IGCL_CTL_MAX_RESERVED_SIZE 112 + +typedef enum ctl_adapter_properties_flag_t { + CTL_ADAPTER_PROPERTIES_FLAG_INTEGRATED = 1, +} ctl_adapter_properties_flag_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv431ctl_device_adapter_properties_t +typedef struct ctl_device_adapter_properties_t { + uint32_t Size; + uint8_t Version; + void* pDeviceID; + uint32_t device_id_size; + ctl_device_type_t device_type; + uint32_t /*ctl_supported_functions_flags_t*/ supported_subfunction_flags; + uint64_t driver_version; + ctl_firmware_version_t firmware_version; + uint32_t pci_vendor_id; + uint32_t pci_device_id; + uint32_t rev_id; + uint32_t num_eus_per_sub_slice; + uint32_t num_sub_slices_per_slice; + uint32_t num_slices; + char name[IGCL_CTL_MAX_DEVICE_NAME_LEN]; + ctl_adapter_properties_flag_t graphics_adapter_properties; + uint32_t Frequency; + uint16_t pci_subsys_id; + uint16_t pci_subsys_vendor_id; + ctl_adapter_bdf_t adapter_bdf; + char reserved[IGCL_CTL_MAX_RESERVED_SIZE]; +} ctl_device_adapter_properties_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv422ctlGetDeviceProperties27ctl_device_adapter_handle_tP31ctl_device_adapter_properties_t +extern ctl_result_t ctlGetDeviceProperties(ctl_device_adapter_handle_t hDAhandle, ctl_device_adapter_properties_t* pProperties); + +typedef struct ctl_temp_handle_t* ctl_temp_handle_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#ctlenumtemperaturesensors +extern ctl_result_t ctlEnumTemperatureSensors(ctl_device_adapter_handle_t hDAhandle, uint32_t* pCount, ctl_temp_handle_t* phTemperature); +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv427ctlTemperatureGetProperties17ctl_temp_handle_tP21ctl_temp_properties_t + +typedef enum ctl_temp_sensors_t { + CTL_TEMP_SENSORS_GLOBAL = 0, + CTL_TEMP_SENSORS_GPU = 1, + CTL_TEMP_SENSORS_MEMORY = 2, + CTL_TEMP_SENSORS_GLOBAL_MIN = 3, + CTL_TEMP_SENSORS_GPU_MIN = 4, + CTL_TEMP_SENSORS_MEMORY_MIN = 5, + CTL_TEMP_SENSORS_MAX +} ctl_temp_sensors_t; + +typedef struct _ctl_temp_properties_t { + uint32_t Size; + uint8_t Version; + ctl_temp_sensors_t type; + double maxTemperature; +} ctl_temp_properties_t; + +extern ctl_result_t ctlTemperatureGetProperties(ctl_temp_handle_t hTemperature, ctl_temp_properties_t* pTemperature); +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv420ctlEnumMemoryModules27ctl_device_adapter_handle_tP8uint32_tP16ctl_mem_handle_t + +typedef struct ctl_mem_handle_t* ctl_mem_handle_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv420ctlEnumMemoryModules27ctl_device_adapter_handle_tP8uint32_tP16ctl_mem_handle_t +extern ctl_result_t ctlEnumMemoryModules(ctl_device_adapter_handle_t hDAhandle, uint32_t* pCount, ctl_mem_handle_t* phMemory); + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv415ctl_mem_state_t +typedef struct ctl_mem_state_t { + uint32_t Size; + uint8_t Version; + uint64_t free; + uint64_t size; +} ctl_mem_state_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv417ctlMemoryGetState16ctl_mem_handle_tP15ctl_mem_state_t +extern ctl_result_t ctlMemoryGetState(ctl_mem_handle_t hMemory, ctl_mem_state_t* pState); + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv414ctl_mem_type_t +typedef enum ctl_mem_type_t { + CTL_MEM_TYPE_HBM = 0, + CTL_MEM_TYPE_DDR = 1, + CTL_MEM_TYPE_DDR3 = 2, + CTL_MEM_TYPE_DDR4 = 3, + CTL_MEM_TYPE_DDR5 = 4, + CTL_MEM_TYPE_LPDDR = 5, + CTL_MEM_TYPE_LPDDR3 = 6, + CTL_MEM_TYPE_LPDDR4 = 7, + CTL_MEM_TYPE_LPDDR5 = 8, + CTL_MEM_TYPE_GDDR4 = 9, + CTL_MEM_TYPE_GDDR5 = 10, + CTL_MEM_TYPE_GDDR5X = 11, + CTL_MEM_TYPE_GDDR6 = 12, + CTL_MEM_TYPE_GDDR6X = 13, + CTL_MEM_TYPE_GDDR7 = 14, + CTL_MEM_TYPE_MAX +} ctl_mem_type_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv413ctl_mem_loc_t +typedef enum ctl_mem_loc_t { + CTL_MEM_LOC_SYSTEM = 0, + CTL_MEM_LOC_DEVICE = 1, + CTL_MEM_LOC_MAX +} ctl_mem_loc_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv420ctl_mem_properties_t +typedef struct ctl_mem_properties_t { + uint32_t Size; + uint8_t Version; + ctl_mem_type_t type; + ctl_mem_loc_t location; + uint64_t physicalSize; + int32_t busWidth; + int32_t numChannels; +} ctl_mem_properties_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv422ctlMemoryGetProperties16ctl_mem_handle_tP20ctl_mem_properties_t +extern ctl_result_t ctlMemoryGetProperties(ctl_mem_handle_t hMemory, ctl_mem_properties_t* pProperties); + +typedef struct ctl_freq_handle_t* ctl_freq_handle_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#ctlenumfrequencydomains +extern ctl_result_t ctlEnumFrequencyDomains(ctl_device_adapter_handle_t hDAhandle, uint32_t* pCount, ctl_freq_handle_t* phFrequency); + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv417ctl_freq_domain_t +typedef enum ctl_freq_domain_t { + CTL_FREQ_DOMAIN_GPU = 0, + CTL_FREQ_DOMAIN_MEMORY = 1, + CTL_FREQ_DOMAIN_MAX +} ctl_freq_domain_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv421ctl_freq_properties_t +typedef struct ctl_freq_properties_t { + uint32_t Size; + uint8_t Version; + ctl_freq_domain_t type; + bool canControl; + double min; + double max; +} ctl_freq_properties_t; + +// https://intel.github.io/drivers.gpu.control-library/Control/api.html#_CPPv425ctlFrequencyGetProperties17ctl_freq_handle_tP21ctl_freq_properties_t +extern ctl_result_t ctlFrequencyGetProperties(ctl_freq_handle_t hFrequency, ctl_freq_properties_t* pProperties); diff --git a/src/detection/gpu/intel_drm.h b/src/detection/gpu/intel_drm.h new file mode 100644 index 0000000..3e25748 --- /dev/null +++ b/src/detection/gpu/intel_drm.h @@ -0,0 +1,125 @@ +#pragma once + +/* SPDX-License-Identifier: MIT */ +#include <drm.h> + +// xe_drm.h + +/* + * Copyright © 2023 Intel Corporation + */ + +#define DRM_XE_DEVICE_QUERY 0x00 + +#define DRM_IOCTL_XE_DEVICE_QUERY DRM_IOWR(DRM_COMMAND_BASE + DRM_XE_DEVICE_QUERY, struct drm_xe_device_query) + +enum drm_xe_memory_class { + DRM_XE_MEM_REGION_CLASS_SYSMEM = 0, + DRM_XE_MEM_REGION_CLASS_VRAM +}; + +struct drm_xe_mem_region { + __u16 mem_class; + __u16 instance; + __u32 min_page_size; + __u64 total_size; + __u64 used; + __u64 cpu_visible_size; + __u64 cpu_visible_used; + __u64 reserved[6]; +}; + +struct drm_xe_query_mem_regions { + __u32 num_mem_regions; + __u32 pad; + struct drm_xe_mem_region mem_regions[]; +}; + +struct drm_xe_query_topology_mask { + __u16 gt_id; + +#define DRM_XE_TOPO_DSS_GEOMETRY 1 +#define DRM_XE_TOPO_DSS_COMPUTE 2 +#define DRM_XE_TOPO_EU_PER_DSS 4 + __u16 type; + __u32 num_bytes; + __u8 mask[]; +}; + +struct drm_xe_device_query { + __u64 extensions; + +#define DRM_XE_DEVICE_QUERY_MEM_REGIONS 1 +#define DRM_XE_DEVICE_QUERY_GT_TOPOLOGY 5 + __u32 query; + __u32 size; + __u64 data; + __u64 reserved[2]; +}; + +// i915_drm.h + +/* + * Copyright 2003 Tungsten Graphics, Inc., Cedar Park, Texas. + * All Rights Reserved. + */ + +#define DRM_IOCTL_I915_GETPARAM DRM_IOWR(DRM_COMMAND_BASE + DRM_I915_GETPARAM, drm_i915_getparam_t) + +struct drm_i915_getparam { + __s32 param; + int* value; +}; +typedef struct drm_i915_getparam drm_i915_getparam_t; + +#define DRM_I915_GETPARAM 0x06 +#define DRM_I915_QUERY 0x39 +#define DRM_I915_QUERY_MEMORY_REGIONS 4 +#define DRM_IOCTL_I915_QUERY DRM_IOWR(DRM_COMMAND_BASE + DRM_I915_QUERY, struct drm_i915_query) +#define I915_PARAM_EU_TOTAL 34 + +struct drm_i915_query_item { + __u64 query_id; +#define DRM_I915_QUERY_MEMORY_REGIONS 4 + + __s32 length; + __u32 flags; + __u64 data_ptr; +}; + +struct drm_i915_query { + __u32 num_items; + __u32 flags; + __u64 items_ptr; +}; + +enum drm_i915_gem_memory_class { + I915_MEMORY_CLASS_SYSTEM = 0, + I915_MEMORY_CLASS_DEVICE, +}; + +struct drm_i915_gem_memory_class_instance { + __u16 memory_class; + __u16 memory_instance; +}; + +struct drm_i915_memory_region_info { + struct drm_i915_gem_memory_class_instance region; + __u32 rsvd0; + __u64 probed_size; + __u64 unallocated_size; + + union { + __u64 rsvd1[8]; + struct { + __u64 probed_cpu_visible_size; + __u64 unallocated_cpu_visible_size; + }; + }; +}; + +struct drm_i915_query_memory_regions { + __u32 num_regions; + __u32 rsvd[3]; + struct drm_i915_memory_region_info regions[]; +}; diff --git a/src/detection/gpu/mtml.h b/src/detection/gpu/mtml.h new file mode 100644 index 0000000..9f62fc3 --- /dev/null +++ b/src/detection/gpu/mtml.h @@ -0,0 +1,110 @@ +#pragma once + +// DISCLAIMER: +// THIS FILE IS CREATED FROM SCRATCH, BY READING THE OFFICIAL MTML API +// DOCUMENTATION REFERENCED BELOW, IN ORDER TO MAKE FASTFETCH MIT COMPLIANT. + +#define MTML_DEVICE_PCI_SBDF_BUFFER_SIZE 32 +#define MTML_DEVICE_NAME_BUFFER_SIZE 32 + +/** + * Return values for MTML API calls. + */ +typedef enum { + MTML_SUCCESS = 0, +} MtmlReturn; + +/** + * The brand of the device. + */ +typedef enum { + MTML_BRAND_MTT = 0, //!< MTT series. +} MtmlBrandType; + +typedef struct MtmlLibrary MtmlLibrary; +typedef struct MtmlSystem MtmlSystem; +typedef struct MtmlDevice MtmlDevice; +typedef struct MtmlGpu MtmlGpu; +typedef struct MtmlMemory MtmlMemory; + +/** + * PCI information about a device. + */ +typedef struct +{ + char sbdf[MTML_DEVICE_PCI_SBDF_BUFFER_SIZE]; //!< The tuple segment:bus:device.function PCI identifier (& NULL terminator). + unsigned int segment; //!< The PCI segment group(domain) on which the device's bus resides, 0 to 0xffffffff. + unsigned int bus; //!< The bus on which the device resides, 0 to 0xff. + unsigned int device; //!< The device ID on the bus, 0 to 31. + unsigned int pciDeviceId; //!< The combined 16-bit device ID and 16-bit vendor ID. + unsigned int pciSubsystemId; //!< The 32-bit sub system device ID. + unsigned int busWidth; //!< @deprecated This value set to zero. + float pciMaxSpeed; //!< The maximum link speed (transfer rate per lane) of the device. The unit is GT/s. + float pciCurSpeed; //!< The current link speed (transfer rate per lane) of the device. The unit is GT/s. + unsigned int pciMaxWidth; //!< The maximum link width of the device. + unsigned int pciCurWidth; //!< The current link width of the device. + unsigned int pciMaxGen; //!< The maximum supported generation of the device. + unsigned int pciCurGen; //!< The current generation of the device. + int rsvd[6]; //!< Reserved for future extension. +} MtmlPciInfo; + +// Retrieves the number of cores of a device. +MtmlReturn mtmlDeviceCountGpuCores(const MtmlDevice* device, unsigned int* numCores); +// Retrieves the brand of a device. +MtmlReturn mtmlDeviceGetBrand(const MtmlDevice* dev, MtmlBrandType* type); +// Retrieves the index associated with the specified device. +MtmlReturn mtmlDeviceGetIndex(const MtmlDevice* dev, unsigned int* index); +// Retrieves the name of a device. +MtmlReturn mtmlDeviceGetName(const MtmlDevice* dev, char* name, unsigned int length); +// Retrieves the PCI attributes of a device. +MtmlReturn mtmlDeviceGetPciInfo(const MtmlDevice* dev, MtmlPciInfo* pci); +/** + * Retrieves the UUID of a specified device. The UUID is a hexadecimal string in the + * form of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, where each 'x' is an ASCII character that represents a hexadecimal + * digit. The UUID is globally unique for every single device thus can be used to identify different devices + * physically. + */ +MtmlReturn mtmlDeviceGetUUID(const MtmlDevice* dev, char* uuid, unsigned int length); +// Initializes a GPU opaque object to represent a specific graphic core on the target device that is designated by its index. +MtmlReturn mtmlDeviceInitGpu(const MtmlDevice* dev, MtmlGpu** gpu); +// Initializes a memory opaque object to represent the memory on the target device. +MtmlReturn mtmlDeviceInitMemory(const MtmlDevice* dev, MtmlMemory** mem); + +// Retrieves the maximum supported clock speed for the device's graphic core. +MtmlReturn mtmlGpuGetMaxClock(const MtmlGpu* gpu, unsigned int* clockMhz); +// Retrieves the current temperature readings for the device's graphic core, in degrees Celsius. +MtmlReturn mtmlGpuGetTemperature(const MtmlGpu* gpu, unsigned int* temp); +// Retrieves the current utilization rate for the device's graphic core. +MtmlReturn mtmlGpuGetUtilization(const MtmlGpu* gpu, unsigned int* utilization); + +// Retrieves the number of devices that can be accessed by the library opaque object. +MtmlReturn mtmlLibraryCountDevice(const MtmlLibrary* lib, unsigned int* count); +/** + * Initializes a device opaque object to represent a device that is designated by its index. + * The index ranges from (0) to (deviceCount - 1), where deviceCount is retrieved from \ref mtmlLibraryCountDevice(). + */ +MtmlReturn mtmlLibraryInit(MtmlLibrary** lib); +/** + * Initializes a device opaque object to represent a device that is designated by its index. + * The index ranges from (0) to (deviceCount - 1), where deviceCount is retrieved from \ref mtmlLibraryCountDevice(). + */ +MtmlReturn mtmlLibraryInitDeviceByIndex(const MtmlLibrary* lib, unsigned int index, MtmlDevice** dev); +/** + * Initializes a device opaque object to represent a device that is designated by its PCI Sbdf. + * The PCI Sbdf format like 00000000:3a:00.0 refer to \ref MtmlPciInfo::sbdf. + */ +MtmlReturn mtmlLibraryInitDeviceByPciSbdf(const MtmlLibrary* lib, const char* pciSbdf, MtmlDevice** dev); +// Initializes a MtmlSystem opaque pointer that is bound to a library opaque object. +MtmlReturn mtmlLibraryInitSystem(const MtmlLibrary* lib, MtmlSystem** sys); +/** + * Shuts down the library opaque object that is previously initialized by \ref mtmlLibraryInit() and releases its resources. + * The \a lib pointer cannot be used anymore after this function returns. + */ +MtmlReturn mtmlLibraryShutDown(MtmlLibrary* lib); + +// Retrieves the amount of total memory available on the device, in bytes. +MtmlReturn mtmlMemoryGetTotal(const MtmlMemory* mem, unsigned long long* total); +// Retrieves the amount of used memory on the device, in bytes. +MtmlReturn mtmlMemoryGetUsed(const MtmlMemory* mem, unsigned long long* used); +// Retrieves the current memory utilization rate for the device. +MtmlReturn mtmlMemoryGetUtilization(const MtmlMemory* mem, unsigned int* utilization); diff --git a/src/detection/gpu/nvapi.h b/src/detection/gpu/nvapi.h new file mode 100644 index 0000000..102d57a --- /dev/null +++ b/src/detection/gpu/nvapi.h @@ -0,0 +1,50 @@ +// References: +// https://github.com/NVIDIA/nvapi (MIT License) +// https://github.com/deathcamp/NVOC/blob/master/nvoc.c (Public Domain) + +typedef enum NvApiGPUMemoryType { + NVAPI_GPU_MEMORY_TYPE_UNKNOWN = 0, + NVAPI_GPU_MEMORY_TYPE_SDRAM, + NVAPI_GPU_MEMORY_TYPE_DDR1, + NVAPI_GPU_MEMORY_TYPE_DDR2, + NVAPI_GPU_MEMORY_TYPE_GDDR2, + NVAPI_GPU_MEMORY_TYPE_GDDR3, + NVAPI_GPU_MEMORY_TYPE_GDDR4, + NVAPI_GPU_MEMORY_TYPE_DDR3, + NVAPI_GPU_MEMORY_TYPE_GDDR5, + NVAPI_GPU_MEMORY_TYPE_LPDDR2, + NVAPI_GPU_MEMORY_TYPE_GDDR5X, + NVAPI_GPU_MEMORY_TYPE_LPDDR3, + NVAPI_GPU_MEMORY_TYPE_LPDDR4, + NVAPI_GPU_MEMORY_TYPE_LPDDR5, + NVAPI_GPU_MEMORY_TYPE_GDDR6, + NVAPI_GPU_MEMORY_TYPE_GDDR6X, + NVAPI_GPU_MEMORY_TYPE_GDDR7, +} NvApiGPUMemoryType; + +typedef enum { + NV_SYSTEM_TYPE_GPU_UNKNOWN = 0, + NV_SYSTEM_TYPE_IGPU = 1, // Integrated + NV_SYSTEM_TYPE_DGPU = 2, // Discrete +} NvApiGPUType; + +typedef int NvAPI_Status; // 0 = success; < 0 = error +typedef struct NvPhysicalGpuHandle* NvPhysicalGpuHandle; + +typedef enum { + NVAPI_INTERFACE_OFFSET_INITIALIZE = 0x0150E828, + NVAPI_INTERFACE_OFFSET_UNLOAD = 0xD22BDD7E, + NVAPI_INTERFACE_OFFSET_ENUM_PHYSICAL_GPUS = 0xE5AC921F, + NVAPI_INTERFACE_OFFSET_GPU_GET_RAM_TYPE = 0x57F7CAAC, + NVAPI_INTERFACE_OFFSET_GPU_GET_GPU_TYPE = 0xC33BAEB1, + + NVAPI_INTERFACE_OFFSET_FORCE_UINT32 = 0xFFFFFFFF +} NvApiInterfaceOffsets; + +extern void* nvapi_QueryInterface(NvApiInterfaceOffsets offset); + +extern NvAPI_Status nvapi_Initialize(void); +extern NvAPI_Status nvapi_Unload(void); +extern NvAPI_Status nvapi_EnumPhysicalGPUs(NvPhysicalGpuHandle* handles, int* count); +extern NvAPI_Status nvapi_GPU_GetRamType(NvPhysicalGpuHandle handle, NvApiGPUMemoryType* memtype); +extern NvAPI_Status nvapi_GPU_GetGPUType(NvPhysicalGpuHandle handle, NvApiGPUType* gpuType); diff --git a/src/detection/gpu/nvml.h b/src/detection/gpu/nvml.h new file mode 100644 index 0000000..74b1840 --- /dev/null +++ b/src/detection/gpu/nvml.h @@ -0,0 +1,152 @@ +#pragma once + +// DISCLAIMER: +// THIS FILE IS CREATED FROM SCRATCH, BY READING THE OFFICIAL NVML API +// DOCUMENTATION REFERENCED BELOW, IN ORDER TO MAKE FASTFETCH MIT COMPLIANT. + +// https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceStructs.html +#define NVML_DEVICE_PCI_BUS_ID_BUFFER_SIZE 32 +#define NVML_DEVICE_PCI_BUS_ID_BUFFER_V2_SIZE 16 +#define NVML_DEVICE_NAME_V2_BUFFER_SIZE 96 + +typedef enum { NVML_SUCCESS = 0 } nvmlReturn_t; +typedef struct nvmlDevice_t* nvmlDevice_t; + +// https://docs.nvidia.com/deploy/nvml-api/structnvmlPciInfo__t.html +// PCI information about a GPU device +typedef struct { + // The legacy tuple domain:bus:device.function PCI identifier (& NULL terminator) + char busIdLegacy[NVML_DEVICE_PCI_BUS_ID_BUFFER_V2_SIZE]; + // The PCI domain on which the device's bus resides, 0 to 0xffffffff + unsigned int domain; + // The bus on which the device resides, 0 to 0xff + unsigned int bus; + // The device's id on the bus, 0 to 31 + unsigned int device; + // The combined 16-bit device id and 16-bit vendor id + unsigned int pciDeviceId; + // The 32-bit Sub System Device ID + unsigned int pciSubSystemId; + // The tuple domain:bus:device.function PCI identifier (& NULL terminator) + char busId[NVML_DEVICE_PCI_BUS_ID_BUFFER_SIZE]; +} nvmlPciInfo_t; + +// https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceEnumvs.html#group__nvmlDeviceEnumvs_1g2650b526841fa38b8f293c2d509a1de0 +// Temperature sensors +typedef enum { + // Temperature sensor for the GPU die + NVML_TEMPERATURE_GPU = 0, + NVML_TEMPERATURE_COUNT, +} nvmlTemperatureSensors_t; + +// https://docs.nvidia.com/deploy/nvml-api/structnvmlMemory__v2__t.html#structnvmlMemory__v2__t +// Memory allocation information for a device (v2) +typedef struct { + // Structure format version (must be 2) + unsigned int version; + // Total physical device memory (in bytes) + unsigned long long total; + // Device memory (in bytes) reserved for system use (driver or firmware) + unsigned long long reserved; + // Unallocated device memory (in bytes) + unsigned long long free; + // Allocated device memory (in bytes) + unsigned long long used; +} nvmlMemory_v2_t; +// https://github.com/NVIDIA/nvidia-settings/issues/78#issuecomment-1012837988 +enum { nvmlMemory_v2 = (unsigned int) (sizeof(nvmlMemory_v2_t) | (2 << 24U)) }; + +// https://docs.nvidia.com/deploy/nvml-api/structnvmlMemory__t.html#structnvmlMemory__t +// Memory allocation information for a device (v1) +typedef struct +{ + // Total physical device memory (in bytes) + unsigned long long total; + // Unallocated device memory (in bytes) + unsigned long long free; + // Sum of Reserved and Allocated device memory (in bytes) + unsigned long long used; +} nvmlMemory_t; + +// https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceEnumvs.html#group__nvmlDeviceEnumvs_1g805c0647be9996589fc5e3f6ff680c64 +// Clock types +typedef enum { + // Graphics clock domain + NVML_CLOCK_GRAPHICS = 0, + // SM clock domain + NVML_CLOCK_SM = 1, + // Memory clock domain + NVML_CLOCK_MEM = 2, + // Video encoder/decoder clock domain + NVML_CLOCK_VIDEO = 3, + // Count of clock types + NVML_CLOCK_COUNT, +} nvmlClockType_t; + +// https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceEnumvs.html#group__nvmlDeviceEnumvs_1gfa6b01990b212f7b49089b7158eafd2b +// The Brand of the GPU +typedef enum { + NVML_BRAND_UNKNOWN = 0, + NVML_BRAND_QUADRO = 1, + NVML_BRAND_TESLA = 2, + NVML_BRAND_NVS = 3, + NVML_BRAND_GRID = 4, + NVML_BRAND_GEFORCE = 5, + NVML_BRAND_TITAN = 6, + NVML_BRAND_NVIDIA_VAPPS = 7, + NVML_BRAND_NVIDIA_VPC = 8, + NVML_BRAND_NVIDIA_VCS = 9, + NVML_BRAND_NVIDIA_VWS = 10, + NVML_BRAND_NVIDIA_CLOUD_GAMING = 11, + NVML_BRAND_NVIDIA_VGAMING = NVML_BRAND_NVIDIA_CLOUD_GAMING, + NVML_BRAND_QUADRO_RTX = 12, + NVML_BRAND_NVIDIA_RTX = 13, + NVML_BRAND_NVIDIA = 14, + NVML_BRAND_GEFORCE_RTX = 15, + NVML_BRAND_TITAN_RTX = 16, + NVML_BRAND_COUNT, +} nvmlBrandType_t; + +// https://docs.nvidia.com/deploy/nvml-api/structnvmlUtilization__t.html#structnvmlUtilization__t +// Utilization information for a device. +typedef struct +{ + // Percent of time over the past second during which one or more kernels was executing on the GPU + unsigned int gpu; + // Percent of time over the past second during which global (device) memory was being read or written + unsigned int memory; +} nvmlUtilization_t; + +// https://docs.nvidia.com/deploy/nvml-api/group__nvmlInitializationAndCleanup.html#group__nvmlInitializationAndCleanup +// Initialize NVML, but don't initialize any GPUs yet +nvmlReturn_t nvmlInit_v2(void); +// Shut down NVML by releasing all GPU resources previously allocated with nvmlInit_v2() +nvmlReturn_t nvmlShutdown(void); + +// https://docs.nvidia.com/deploy/nvml-api/group__nvmlDeviceQueries.html +// Retrieves the number of compute devices in the system. A compute device is a single GPU +extern nvmlReturn_t nvmlDeviceGetCount_v2(unsigned int* deviceCount); +// Acquire the handle for a particular device, based on its index +extern nvmlReturn_t nvmlDeviceGetHandleByIndex_v2(unsigned int index, nvmlDevice_t* device); +// Acquire the handle for a particular device, based on its PCI bus id +extern nvmlReturn_t nvmlDeviceGetHandleByPciBusId_v2(const char* pciBusId, nvmlDevice_t* device); +// Retrieves the PCI attributes of this device +extern nvmlReturn_t nvmlDeviceGetPciInfo_v3(nvmlDevice_t device, nvmlPciInfo_t* pci); +// Retrieves the current temperature readings for the device, in degrees C +extern nvmlReturn_t nvmlDeviceGetTemperature(nvmlDevice_t device, nvmlTemperatureSensors_t sensorType, unsigned int* temp); +// Retrieves the amount of used, free, reserved and total memory available on the device, in bytes. The reserved amount is supported on version 2 only +extern nvmlReturn_t nvmlDeviceGetMemoryInfo_v2(nvmlDevice_t device, nvmlMemory_v2_t* memory); +// Retrieves the amount of used, free, total memory available on the device, in bytes. +extern nvmlReturn_t nvmlDeviceGetMemoryInfo(nvmlDevice_t device, nvmlMemory_t* memory); +// Gets the device's core count +extern nvmlReturn_t nvmlDeviceGetNumGpuCores(nvmlDevice_t device, unsigned int* numCores); +// Retrieves the maximum clock speeds for the device +extern nvmlReturn_t nvmlDeviceGetMaxClockInfo(nvmlDevice_t device, nvmlClockType_t type, unsigned int* clock); +// Retrieves the brand of this device +extern nvmlReturn_t nvmlDeviceGetBrand(nvmlDevice_t device, nvmlBrandType_t* type); +// Retrieves the current utilization rates for the device +extern nvmlReturn_t nvmlDeviceGetUtilizationRates(nvmlDevice_t device, nvmlUtilization_t* utilization); +// Retrieves the globally unique immutable UUID associated with this device, as a 5 part hexadecimal string, that augments the immutable, board serial identifier. +extern nvmlReturn_t nvmlDeviceGetIndex(nvmlDevice_t device, unsigned int* index); +// Retrieves the name of this device. +extern nvmlReturn_t nvmlDeviceGetName(nvmlDevice_t device, char* name, unsigned int length); diff --git a/src/detection/gtk_qt/gtk.c b/src/detection/gtk_qt/gtk.c new file mode 100644 index 0000000..05a8db1 --- /dev/null +++ b/src/detection/gtk_qt/gtk.c @@ -0,0 +1,206 @@ +#include "fastfetch.h" +#include "common/properties.h" +#include "common/thread.h" +#include "common/settings.h" +#include "detection/gtk_qt/gtk_qt.h" +#include "detection/displayserver/displayserver.h" + +static inline bool allPropertiesSet(FFGTKResult* result) { + return result->theme.length > 0 && + result->icons.length > 0 && + result->font.length > 0; +} + +static inline void applyGTKSettings(FFGTKResult* result, const char* themeName, const char* iconsName, const char* fontName, const char* cursorTheme, int cursorSize, const char* wallpaper) { + if (result->theme.length == 0) { + ffStrbufAppendS(&result->theme, themeName); + } + + if (result->icons.length == 0) { + ffStrbufAppendS(&result->icons, iconsName); + } + + if (result->font.length == 0) { + ffStrbufAppendS(&result->font, fontName); + } + + if (result->cursor.length == 0) { + ffStrbufAppendS(&result->cursor, cursorTheme); + } + + if (result->cursorSize.length == 0 && cursorSize > 0) { + ffStrbufAppendF(&result->cursorSize, "%i", cursorSize); + } + + if (result->wallpaper.length == 0) { + ffStrbufAppendS(&result->wallpaper, wallpaper); + } +} + +static bool testXfconfWallpaperPropKey(FF_A_UNUSED void* data, const char* key) { + int count = 0; + sscanf(key, "/backdrop/screen0/monitor%*[^/]/workspace0/last-image%n", &count); + return count == 0; +} + +static void detectGTKFromSettings(FFGTKResult* result) { + static const char* themeName = NULL; + static const char* iconsName = NULL; + static const char* fontName = NULL; + static const char* cursorTheme = NULL; + static int cursorSize = 0; + static const char* wallpaper = NULL; + + static bool init = false; + + if (init) { + applyGTKSettings(result, themeName, iconsName, fontName, cursorTheme, cursorSize, wallpaper); + return; + } + + init = true; + + const FFDisplayServerResult* wmde = ffConnectDisplayServer(); + + if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_XFCE4)) { + themeName = ffSettingsGetXFConf("xsettings", "/Net/ThemeName", FF_VARIANT_TYPE_STRING).strValue; + iconsName = ffSettingsGetXFConf("xsettings", "/Net/IconThemeName", FF_VARIANT_TYPE_STRING).strValue; + fontName = ffSettingsGetXFConf("xsettings", "/Gtk/FontName", FF_VARIANT_TYPE_STRING).strValue; + cursorTheme = ffSettingsGetXFConf("xsettings", "/Gtk/CursorThemeName", FF_VARIANT_TYPE_STRING).strValue; + cursorSize = ffSettingsGetXFConf("xsettings", "/Gtk/CursorThemeSize", FF_VARIANT_TYPE_INT).intValue; + wallpaper = ffSettingsGetXFConfFirstMatch("xfce4-desktop", "/backdrop/screen0", FF_VARIANT_TYPE_STRING, NULL, testXfconfWallpaperPropKey).strValue; + } else if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_CINNAMON)) { + themeName = ffSettingsGetGnome("/org/cinnamon/desktop/interface/gtk-theme", "org.cinnamon.desktop.interface", NULL, "gtk-theme", FF_VARIANT_TYPE_STRING).strValue; + iconsName = ffSettingsGetGnome("/org/cinnamon/desktop/interface/icon-theme", "org.cinnamon.desktop.interface", NULL, "icon-theme", FF_VARIANT_TYPE_STRING).strValue; + fontName = ffSettingsGetGnome("/org/cinnamon/desktop/interface/font-name", "org.cinnamon.desktop.interface", NULL, "font-name", FF_VARIANT_TYPE_STRING).strValue; + cursorTheme = ffSettingsGetGnome("/org/cinnamon/desktop/interface/cursor-theme", "org.cinnamon.desktop.interface", NULL, "cursor-theme", FF_VARIANT_TYPE_STRING).strValue; + cursorSize = ffSettingsGetGnome("/org/cinnamon/desktop/interface/cursor-size", "org.cinnamon.desktop.interface", NULL, "cursor-size", FF_VARIANT_TYPE_INT).intValue; + wallpaper = ffSettingsGetGnome("/org/cinnamon/desktop/background/picture-uri", "org.cinnamon.desktop.background", NULL, "picture-uri", FF_VARIANT_TYPE_STRING).strValue; + } else if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_MATE)) { + themeName = ffSettingsGetGnome("/org/mate/interface/gtk-theme", "org.mate.interface", NULL, "gtk-theme", FF_VARIANT_TYPE_STRING).strValue; + iconsName = ffSettingsGetGnome("/org/mate/interface/icon-theme", "org.mate.interface", NULL, "icon-theme", FF_VARIANT_TYPE_STRING).strValue; + fontName = ffSettingsGetGnome("/org/mate/interface/font-name", "org.mate.interface", NULL, "font-name", FF_VARIANT_TYPE_STRING).strValue; + cursorTheme = ffSettingsGetGnome("/org/mate/peripherals-mouse/cursor-theme", "org.mate.peripherals-mouse", NULL, "cursor-theme", FF_VARIANT_TYPE_STRING).strValue; + cursorSize = ffSettingsGetGnome("/org/mate/peripherals-mouse/cursor-size", "org.mate.peripherals-mouse", NULL, "cursor-size", FF_VARIANT_TYPE_INT).intValue; + wallpaper = ffSettingsGetGnome("/org/mate/desktop/background", "org.mate.background", NULL, "picture-filename", FF_VARIANT_TYPE_STRING).strValue; + } else if ( + ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_GNOME) || + ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_GNOME_CLASSIC) || + ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_UNITY) || + ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_BUDGIE) || + ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_NEBIDE)) { + themeName = ffSettingsGetGnome("/org/gnome/desktop/interface/gtk-theme", "org.gnome.desktop.interface", NULL, "gtk-theme", FF_VARIANT_TYPE_STRING).strValue; + iconsName = ffSettingsGetGnome("/org/gnome/desktop/interface/icon-theme", "org.gnome.desktop.interface", NULL, "icon-theme", FF_VARIANT_TYPE_STRING).strValue; + fontName = ffSettingsGetGnome("/org/gnome/desktop/interface/font-name", "org.gnome.desktop.interface", NULL, "font-name", FF_VARIANT_TYPE_STRING).strValue; + cursorTheme = ffSettingsGetGnome("/org/gnome/desktop/interface/cursor-theme", "org.gnome.desktop.interface", NULL, "cursor-theme", FF_VARIANT_TYPE_STRING).strValue; + cursorSize = ffSettingsGetGnome("/org/gnome/desktop/interface/cursor-size", "org.gnome.desktop.interface", NULL, "cursor-size", FF_VARIANT_TYPE_INT).intValue; + wallpaper = ffSettingsGetGnome("/org/gnome/desktop/background/picture-uri", "org.gnome.desktop.background", NULL, "picture-uri", FF_VARIANT_TYPE_STRING).strValue; + } else if ( + ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_ENLIGHTENMENT)) { + ffEnlightenmentSettings settings = {}; + if (ffSettingsGetEnlightenmentProperty(&settings)) { + themeName = settings.theme; + iconsName = settings.icon_theme; + fontName = settings.font; + cursorTheme = settings.use_e_cursor ? "Enlightenment" : "Application"; + cursorSize = settings.cursor_size; + wallpaper = settings.desktop_default_background; + } + } + + applyGTKSettings(result, themeName, iconsName, fontName, cursorTheme, cursorSize, wallpaper); +} + +static void detectGTKFromConfigFile(const char* filename, FFGTKResult* result) { + ffParsePropFileValues(filename, 5, (FFpropquery[]) { { "gtk-theme-name =", &result->theme }, { "gtk-icon-theme-name =", &result->icons }, { "gtk-font-name =", &result->font }, { "gtk-cursor-theme-name =", &result->cursor }, { "gtk-cursor-theme-size =", &result->cursorSize } }); +} + +static void detectGTKFromConfigDir(FFstrbuf* configDir, const char* version, FFGTKResult* result) { + uint32_t configDirLength = configDir->length; + + // <configdir>/gtk-<version>.0/settings.ini + ffStrbufAppendS(configDir, "gtk-"); + ffStrbufAppendS(configDir, version); + ffStrbufAppendS(configDir, ".0/settings.ini"); + detectGTKFromConfigFile(configDir->chars, result); + ffStrbufSubstrBefore(configDir, configDirLength); + if (allPropertiesSet(result)) { + return; + } + + // <configdir>/gtk-<version>.0/gtkrc + ffStrbufAppendS(configDir, "gtk-"); + ffStrbufAppendS(configDir, version); + ffStrbufAppendS(configDir, ".0/gtkrc"); + detectGTKFromConfigFile(configDir->chars, result); + ffStrbufSubstrBefore(configDir, configDirLength); + if (allPropertiesSet(result)) { + return; + } + + // <configdir>/gtkrc-<version>.0 + ffStrbufAppendS(configDir, "gtkrc-"); + ffStrbufAppendS(configDir, version); + ffStrbufAppendS(configDir, ".0"); + detectGTKFromConfigFile(configDir->chars, result); + ffStrbufSubstrBefore(configDir, configDirLength); + if (allPropertiesSet(result)) { + return; + } + + // <configdir>/.gtkrc-<version>.0 + ffStrbufAppendS(configDir, ".gtkrc-"); + ffStrbufAppendS(configDir, version); + ffStrbufAppendS(configDir, ".0"); + detectGTKFromConfigFile(configDir->chars, result); + ffStrbufSubstrBefore(configDir, configDirLength); +} + +static void detectGTK(const char* version, FFGTKResult* result) { + // Mate, Cinnamon, GNOME, Unity, Budgie use dconf to save theme config + // On other DEs, this will do nothing + detectGTKFromSettings(result); + if (allPropertiesSet(result)) { + return; + } + + // We need to do this because we use multiple threads on configDirs + FF_STRBUF_AUTO_DESTROY baseDir = ffStrbufCreateA(64); + + FF_LIST_FOR_EACH (FFstrbuf, configDir, instance.state.platform.configDirs) { + ffStrbufSet(&baseDir, configDir); + detectGTKFromConfigDir(&baseDir, version, result); + if (allPropertiesSet(result)) { + break; + } + } +} + +#define FF_DETECT_GTK_IMPL(version) \ + static FFGTKResult result; \ + static bool init = false; \ + if (init) \ + return &result; \ + init = true; \ + ffStrbufInit(&result.theme); \ + ffStrbufInit(&result.icons); \ + ffStrbufInit(&result.font); \ + ffStrbufInit(&result.cursor); \ + ffStrbufInit(&result.cursorSize); \ + ffStrbufInit(&result.wallpaper); \ + detectGTK(#version, &result); \ + return &result; + +const FFGTKResult* ffDetectGTK2(void) { + FF_DETECT_GTK_IMPL(2) +} + +const FFGTKResult* ffDetectGTK3(void) { + FF_DETECT_GTK_IMPL(3) +} + +const FFGTKResult* ffDetectGTK4(void) { + FF_DETECT_GTK_IMPL(4) +} + +#undef FF_CALCULATE_GTK_IMPL diff --git a/src/detection/gtk_qt/gtk_qt.h b/src/detection/gtk_qt/gtk_qt.h new file mode 100644 index 0000000..b831bb6 --- /dev/null +++ b/src/detection/gtk_qt/gtk_qt.h @@ -0,0 +1,25 @@ +#pragma once + +#include "fastfetch.h" + +typedef struct FFGTKResult { + FFstrbuf theme; + FFstrbuf icons; + FFstrbuf font; + FFstrbuf cursor; + FFstrbuf cursorSize; + FFstrbuf wallpaper; +} FFGTKResult; + +typedef struct FFQtResult { + FFstrbuf widgetStyle; + FFstrbuf colorScheme; + FFstrbuf icons; + FFstrbuf font; + FFstrbuf wallpaper; +} FFQtResult; + +const FFGTKResult* ffDetectGTK2(void); +const FFGTKResult* ffDetectGTK4(void); +const FFGTKResult* ffDetectGTK3(void); +const FFQtResult* ffDetectQt(void); diff --git a/src/detection/gtk_qt/qt.c b/src/detection/gtk_qt/qt.c new file mode 100644 index 0000000..26c8b58 --- /dev/null +++ b/src/detection/gtk_qt/qt.c @@ -0,0 +1,209 @@ +#include "fastfetch.h" +#include "common/properties.h" +#include "common/thread.h" +#include "common/strutil.h" +#include "detection/gtk_qt/gtk_qt.h" +#include "detection/displayserver/displayserver.h" + +#include <stdlib.h> +#include <string.h> + +static inline bool allValuesSet(const FFQtResult* result) { + return result->widgetStyle.length > 0 && + result->colorScheme.length > 0 && + result->icons.length > 0 && + result->font.length > 0 && + result->wallpaper.length > 0; +} + +typedef enum FF_A_PACKED PlasmaCategory { + PLASMA_CATEGORY_GENERAL, + PLASMA_CATEGORY_KDE, + PLASMA_CATEGORY_ICONS, + PLASMA_CATEGORY_OTHER +} PlasmaCategory; + +static bool detectPlasmaFromFile(const char* filename, FFQtResult* result) { + FILE* kdeglobals = fopen(filename, "r"); + if (kdeglobals == NULL) { + return false; + } + + char* line = NULL; + size_t len = 0; + + PlasmaCategory category = PLASMA_CATEGORY_OTHER; + + while (getline(&line, &len, kdeglobals) != -1) { + if (line[0] == '[') { + char categoryName[32]; + sscanf(line, "[%31[^]]", categoryName); + + if (ffStrEqualsIgnCase(categoryName, "General")) { + category = PLASMA_CATEGORY_GENERAL; + } else if (ffStrEqualsIgnCase(categoryName, "KDE")) { + category = PLASMA_CATEGORY_KDE; + } else if (ffStrEqualsIgnCase(categoryName, "Icons")) { + category = PLASMA_CATEGORY_ICONS; + } else { + category = PLASMA_CATEGORY_OTHER; + } + + continue; + } + + if (category == PLASMA_CATEGORY_KDE && result->widgetStyle.length == 0) { + ffParsePropLine(line, "widgetStyle =", &result->widgetStyle); + } else if (category == PLASMA_CATEGORY_ICONS && result->icons.length == 0) { + ffParsePropLine(line, "Theme =", &result->icons); + } else if (category == PLASMA_CATEGORY_GENERAL) { + if (result->colorScheme.length == 0) { + ffParsePropLine(line, "ColorScheme =", &result->colorScheme); + } + + if (result->font.length == 0) { + ffParsePropLine(line, "font =", &result->font); + } + + // Before plasma 5.23, "Font" was the key instead of "font". Since a lot of distros ship older versions, we test for both. + if (result->font.length == 0) { + ffParsePropLine(line, "Font =", &result->font); + } + } + } + + free(line); + + fclose(kdeglobals); + + return true; +} + +static void detectPlasma(FFQtResult* result) { + bool foundAFile = false; + + // We need to do this because we use multiple threads on configDirs + FF_STRBUF_AUTO_DESTROY baseDir = ffStrbufCreateA(64); + + FF_LIST_FOR_EACH (FFstrbuf, configDir, instance.state.platform.configDirs) { + ffStrbufSet(&baseDir, configDir); + ffStrbufAppendS(&baseDir, "kdeglobals"); + + if (detectPlasmaFromFile(baseDir.chars, result)) { + foundAFile = true; + } + + ffStrbufSet(&baseDir, configDir); + ffStrbufAppendS(&baseDir, "plasma-org.kde.plasma.desktop-appletsrc"); + + ffParsePropFile(baseDir.chars, "Image=", &result->wallpaper); + + if (allValuesSet(result)) { + return; + } + } + + if (!foundAFile) { + return; + } + + // In Plasma the default value is never set in the config file, but the whole key-value is discarded. + /// We must set these values by our self if the file exists (it always does here) + if (result->widgetStyle.length == 0) { + ffStrbufAppendS(&result->widgetStyle, "Breeze"); + } + + if (result->colorScheme.length == 0) { + ffStrbufAppendS(&result->colorScheme, "BreezeLight"); + } + + if (result->icons.length == 0) { + ffStrbufAppendS(&result->icons, "Breeze"); + } + + if (result->font.length == 0) { + ffStrbufAppendS(&result->font, "Noto Sans, 10"); + } +} + +static void detectLXQt(FFQtResult* result) { + ffParsePropFileConfigValues("lxqt/lxqt.conf", 3, (FFpropquery[]) { { "style = ", &result->widgetStyle }, { "icon_theme = ", &result->icons }, { "font = ", &result->font } }); + + ffParsePropFileConfig("pcmanfm-qt/lxqt/settings.conf", "Wallpaper=", &result->wallpaper); +} + +static void detectQtCt(char qver, FFQtResult* result) { + // qt5ct and qt6ct are technically separate applications, but they're both + // by the same author and qt6ct understands qt5ct in qt6 applications as well. + char file[] = "qtXct/qtXct.conf"; + file[2] = file[8] = qver; + + FF_STRBUF_AUTO_DESTROY font = ffStrbufCreate(); + + ffParsePropFileConfigValues(file, 3, (FFpropquery[]) { { "style=", &result->widgetStyle }, { "icon_theme=", &result->icons }, { "general=", &font } }); + + if (ffStrbufStartsWithC(&font, '@')) { + // See QVariant notes on https://doc.qt.io/qt-5/qsettings.html and + // https://github.com/fastfetch-cli/fastfetch/issues/1053#issuecomment-2197254769 + // Thankfully, newer versions use the more common font encoding. + ffStrbufSetNS(&font, 5, file); + } else if (qver == '5') { + // #1864 + const char* p = font.chars; + + while (*p) { + if (p[0] == '\\' && p[1] == 'x' && isxdigit(p[2]) && isxdigit(p[3]) && isxdigit(p[4]) && isxdigit(p[5])) { + uint32_t codepoint = (uint32_t) strtoul((char[]) { p[2], p[3], p[4], p[5], '\0' }, NULL, 16); + ffStrbufAppendUtf32CodePoint(&result->font, codepoint); + p += 6; + } else { + ffStrbufAppendC(&result->font, *p++); + } + } + } else { + ffStrbufDestroy(&result->font); + ffStrbufInitMove(&result->font, &font); + } +} + +static void detectKvantum(FFQtResult* result) { + ffParsePropFileConfigValues("Kvantum/kvantum.kvconfig", 1, (FFpropquery[]) { + { "theme=", &result->widgetStyle }, + }); +} + +const FFQtResult* ffDetectQt(void) { + static FFQtResult result; + + static bool init = false; + if (init) { + return &result; + } + init = true; + + ffStrbufInit(&result.widgetStyle); + ffStrbufInit(&result.colorScheme); + ffStrbufInit(&result.icons); + ffStrbufInit(&result.font); + ffStrbufInit(&result.wallpaper); + + const FFDisplayServerResult* wmde = ffConnectDisplayServer(); + + if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_PLASMA)) { + detectPlasma(&result); + } else if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, FF_DE_PRETTY_LXQT)) { + detectLXQt(&result); + } else { + const char* qPlatformTheme = getenv("QT_QPA_PLATFORMTHEME"); + if (qPlatformTheme && (ffStrEquals(qPlatformTheme, "qt5ct") || ffStrEquals(qPlatformTheme, "qt6ct"))) { + detectQtCt(qPlatformTheme[2], &result); + } + } + + if (ffStrbufEqualS(&result.widgetStyle, "kvantum") || ffStrbufEqualS(&result.widgetStyle, "kvantum-dark")) { + ffStrbufClear(&result.widgetStyle); + detectKvantum(&result); + } + + return &result; +} diff --git a/src/detection/host/host.h b/src/detection/host/host.h new file mode 100644 index 0000000..a6fe3fb --- /dev/null +++ b/src/detection/host/host.h @@ -0,0 +1,20 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/host/option.h" + +typedef struct FFHostResult { + FFstrbuf family; + FFstrbuf name; + FFstrbuf version; + FFstrbuf sku; + FFstrbuf serial; + FFstrbuf uuid; + FFstrbuf vendor; +} FFHostResult; + +const char* ffHostGetMacProductNameWithHwModel(const FFstrbuf* hwModel); +#if __x86_64__ +bool ffHostDetectMac(FFHostResult* host); +#endif +const char* ffDetectHost(FFHostResult* host); diff --git a/src/detection/host/host_android.c b/src/detection/host/host_android.c new file mode 100644 index 0000000..1157a92 --- /dev/null +++ b/src/detection/host/host_android.c @@ -0,0 +1,27 @@ +#include "host.h" +#include "common/settings.h" + +#include <ctype.h> + +const char* ffDetectHost(FFHostResult* host) { + // http://newandroidbook.com/ddb/ + ffSettingsGetAndroidProperty("ro.product.device", &host->family); + + ffSettingsGetAndroidProperty("ro.product.marketname", &host->name) || ffSettingsGetAndroidProperty("ro.vendor.product.display", &host->name) || ffSettingsGetAndroidProperty("ro.vivo.market.name", &host->name) || ffSettingsGetAndroidProperty("ro.product.oppo_model", &host->name) || ffSettingsGetAndroidProperty("ro.oppo.market.name", &host->name) || ffSettingsGetAndroidProperty("ro.vendor.oplus.market.enname", &host->name) || ffSettingsGetAndroidProperty("ro.config.devicename", &host->name) || ffSettingsGetAndroidProperty("ro.config.marketing_name", &host->name) || ffSettingsGetAndroidProperty("ro.product.vendor.model", &host->name) || ffSettingsGetAndroidProperty("ro.product.brand", &host->name); + + if (ffSettingsGetAndroidProperty("ro.product.model", &host->version)) { + if (ffStrbufStartsWithIgnCase(&host->version, &host->name)) { + ffStrbufSubstrAfter(&host->version, host->name.length); + ffStrbufTrimLeft(&host->version, ' '); + } + } + + ffSettingsGetAndroidProperty("ro.product.manufacturer", &host->vendor); + + if (host->vendor.length && !ffStrbufStartsWithIgnCase(&host->name, &host->vendor)) { + ffStrbufPrependS(&host->name, " "); + ffStrbufPrepend(&host->name, &host->vendor); + } + + return NULL; +} diff --git a/src/detection/host/host_apple.c b/src/detection/host/host_apple.c new file mode 100644 index 0000000..a177bf4 --- /dev/null +++ b/src/detection/host/host_apple.c @@ -0,0 +1,68 @@ +#include "host.h" +#include "common/sysctl.h" +#include "common/apple/cf_helpers.h" + +#include <IOKit/IOKitLib.h> + +const char* getProductNameWithIokit(FFstrbuf* result) { + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t registryEntry = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/product"); + if (!registryEntry) { + return "IOServiceGetMatchingService() failed"; + } + + FF_CFTYPE_AUTO_RELEASE CFStringRef productName = IORegistryEntryCreateCFProperty(registryEntry, CFSTR("product-name"), kCFAllocatorDefault, kNilOptions); + if (!productName) { + return "IORegistryEntryCreateCFProperty() failed"; + } + + return ffCfStrGetString(productName, result); +} + +const char* getOthersByIokit(FFHostResult* host) { + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t registryEntry = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceMatching("IOPlatformExpertDevice")); + if (!registryEntry) { + return "IOServiceGetMatchingService() failed"; + } + + FF_CFTYPE_AUTO_RELEASE CFStringRef serialNumber = IORegistryEntryCreateCFProperty(registryEntry, CFSTR(kIOPlatformSerialNumberKey), kCFAllocatorDefault, kNilOptions); + if (serialNumber) { + ffCfStrGetString(serialNumber, &host->serial); + } + + FF_CFTYPE_AUTO_RELEASE CFStringRef uuid = IORegistryEntryCreateCFProperty(registryEntry, CFSTR(kIOPlatformUUIDKey), kCFAllocatorDefault, kNilOptions); + if (uuid) { + ffCfStrGetString(uuid, &host->uuid); + } + + FF_CFTYPE_AUTO_RELEASE CFStringRef manufacturer = IORegistryEntryCreateCFProperty(registryEntry, CFSTR("manufacturer"), kCFAllocatorDefault, kNilOptions); + if (manufacturer) { + ffCfStrGetString(manufacturer, &host->vendor); + } + + FF_CFTYPE_AUTO_RELEASE CFStringRef version = IORegistryEntryCreateCFProperty(registryEntry, CFSTR("version"), kCFAllocatorDefault, kNilOptions); + if (version) { + ffCfStrGetString(version, &host->version); + } + + return NULL; +} + +const char* ffDetectHost(FFHostResult* host) { + const char* error = ffSysctlGetString("hw.product", &host->family); + if (error) { + error = ffSysctlGetString("hw.model", &host->family); + } + if (error) { + return error; + } + + ffStrbufSetStatic(&host->name, ffHostGetMacProductNameWithHwModel(&host->family)); + if (host->name.length == 0) { + getProductNameWithIokit(&host->name); + } + if (host->name.length == 0) { + ffStrbufSet(&host->name, &host->family); + } + getOthersByIokit(host); + return NULL; +} diff --git a/src/detection/host/host_bsd.c b/src/detection/host/host_bsd.c new file mode 100644 index 0000000..3eb547d --- /dev/null +++ b/src/detection/host/host_bsd.c @@ -0,0 +1,26 @@ +#include "host.h" +#include "common/settings.h" +#include "common/smbios.h" + +const char* ffDetectHost(FFHostResult* host) { + ffSettingsGetFreeBSDKenv("smbios.system.product", &host->name); + ffCleanUpSmbiosValue(&host->name); + ffSettingsGetFreeBSDKenv("smbios.system.family", &host->family); + ffCleanUpSmbiosValue(&host->family); + ffSettingsGetFreeBSDKenv("smbios.system.version", &host->version); + ffCleanUpSmbiosValue(&host->version); + ffSettingsGetFreeBSDKenv("smbios.system.sku", &host->sku); + ffCleanUpSmbiosValue(&host->sku); + ffSettingsGetFreeBSDKenv("smbios.system.serial", &host->serial); + ffCleanUpSmbiosValue(&host->serial); + ffSettingsGetFreeBSDKenv("smbios.system.uuid", &host->uuid); + ffCleanUpSmbiosValue(&host->uuid); + ffSettingsGetFreeBSDKenv("smbios.system.maker", &host->vendor); + ffCleanUpSmbiosValue(&host->vendor); + +#ifdef __x86_64__ + ffHostDetectMac(host); +#endif + + return NULL; +} diff --git a/src/detection/host/host_linux.c b/src/detection/host/host_linux.c new file mode 100644 index 0000000..81418a8 --- /dev/null +++ b/src/detection/host/host_linux.c @@ -0,0 +1,117 @@ +#include "host.h" +#include "common/io.h" +#include "common/processing.h" +#include "common/smbios.h" + +#include <stdlib.h> + +static bool getHostProductName(FFstrbuf* name) { + if (ffReadFileBuffer("/sys/firmware/devicetree/base/model", name) || + ffReadFileBuffer("/sys/firmware/devicetree/base/banner-name", name)) { + ffStrbufTrimRight(name, '\0'); + return true; + } + + if (ffReadFileBuffer("/tmp/sysinfo/model", name)) { + ffStrbufTrimRightSpace(name); + ffStrbufTrimRight(name, '\0'); + if (ffIsSmbiosValueSet(name)) { + return true; + } + } + + return false; +} + +static bool getHostSerialNumber(FFstrbuf* serial) { + if (ffReadFileBuffer("/sys/firmware/devicetree/base/smbios/smbios/system/serial", serial) || + ffReadFileBuffer("/sys/firmware/devicetree/base/serial-number", serial)) { + ffStrbufTrimRight(serial, '\0'); + return true; + } + return false; +} + +static bool getHostProductFamily(FFstrbuf* family) { + if (ffReadFileBuffer("/sys/firmware/devicetree/base/smbios/smbios/system/family", family) || + ffReadFileBuffer("/sys/firmware/devicetree/base/smbios/smbios/system/product", family)) { + ffStrbufTrimRight(family, '\0'); + return true; + } + return false; +} + +static bool getHostVendor(FFstrbuf* vendor) { + if (ffReadFileBuffer("/sys/firmware/devicetree/base/smbios/smbios/system/manufacturer", vendor)) { + ffStrbufTrimRight(vendor, '\0'); + return true; + } + return false; +} + +const char* ffDetectHost(FFHostResult* host) { + // This is a hack for Asahi Linux, whose product_family is empty + bool productName = ffGetSmbiosValue("/sys/devices/virtual/dmi/id/product_name", "/sys/class/dmi/id/product_name", &host->name); + bool productFamily = ffGetSmbiosValue("/sys/devices/virtual/dmi/id/product_family", "/sys/class/dmi/id/product_family", &host->family); + if (productName || productFamily) { + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/product_version", "/sys/class/dmi/id/product_version", &host->version); + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/product_sku", "/sys/class/dmi/id/product_sku", &host->sku); + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/product_serial", "/sys/class/dmi/id/product_serial", &host->serial); + ffGetSmbiosValue("/sys/devices/virtual/dmi/id/sys_vendor", "/sys/class/dmi/id/sys_vendor", &host->vendor); + +#if __x86_64__ + ffHostDetectMac(host); +#endif + + // KVM/Qemu virtual machine + if (ffStrbufStartsWithS(&host->name, "Standard PC")) { + ffStrbufPrependS(&host->name, "KVM/QEMU "); + } +#if __aarch64__ + else if (host->family.length == 0 && ffStrbufEqualS(&host->vendor, "Apple Inc.") && ffStrbufStartsWithS(&host->name, "Mac")) { + // Hack for Asahi Linux + ffStrbufDestroy(&host->family); + ffStrbufInitMove(&host->family, &host->name); + getHostProductName(&host->name); + getHostSerialNumber(&host->serial); + } +#endif + } else { + getHostProductFamily(&host->family); + getHostProductName(&host->name); + getHostSerialNumber(&host->serial); + getHostVendor(&host->vendor); + } + + if (host->family.length == 0 && host->name.length == 0) { + const char* wslDistroName = getenv("WSL_DISTRO_NAME"); + // On WSL, the real host can't be detected. Instead use WSL as host. + if (wslDistroName != NULL || getenv("WSL_DISTRO") != NULL || getenv("WSL_INTEROP") != NULL) { + ffStrbufSetStatic(&host->name, "Windows Subsystem for Linux"); + if (wslDistroName) { + ffStrbufAppendF(&host->name, " - %s", wslDistroName); + } + ffStrbufSetStatic(&host->family, "WSL"); + ffStrbufSetStatic(&host->vendor, "Microsoft Corporation"); + + if (instance.config.general.detectVersion) { + ffProcessAppendStdOut(&host->version, (char* const[]) { + "wslinfo", + "--wsl-version", + "-n", + NULL, + }); // supported in 2.2.3 and later + } + } else if (ffStrbufStartsWithS(&instance.state.platform.sysinfo.version, "FreeBSD ")) { + ffStrbufSetStatic(&host->name, "Linux Binary Compatibility on FreeBSD"); + ffStrbufSetStatic(&host->family, "FreeBSD"); + ffStrbufSetStatic(&host->vendor, "FreeBSD Foundation"); + if (instance.config.general.detectVersion) { + ffStrbufSetS(&host->version, instance.state.platform.sysinfo.version.chars + strlen("FreeBSD ")); + ffStrbufSubstrBeforeFirstC(&host->version, ' '); + } + } + } + + return NULL; +} diff --git a/src/detection/host/host_mac.c b/src/detection/host/host_mac.c new file mode 100644 index 0000000..d680332 --- /dev/null +++ b/src/detection/host/host_mac.c @@ -0,0 +1,444 @@ +#include "host.h" +#include "common/strutil.h" + +const char* ffHostGetMacProductNameWithHwModel(const FFstrbuf* hwModel) { + // Macbook Pro: https://support.apple.com/en-us/HT201300 + // Macbook Air: https://support.apple.com/en-us/HT201862 + // Mac mini: https://support.apple.com/en-us/HT201894 + // iMac: https://support.apple.com/en-us/HT201634 + // Mac Pro: https://support.apple.com/en-us/HT202888 + // Mac Studio: https://support.apple.com/en-us/HT213073 + + if (ffStrbufStartsWithS(hwModel, "MacBookPro")) { + const char* version = hwModel->chars + strlen("MacBookPro"); + if (ffStrEquals(version, "18,3") || + ffStrEquals(version, "18,4")) { + return "MacBook Pro (14-inch, 2021)"; + } + if (ffStrEquals(version, "18,1") || + ffStrEquals(version, "18,2")) { + return "MacBook Pro (16-inch, 2021)"; + } + if (ffStrEquals(version, "17,1")) { + return "MacBook Pro (13-inch, M1, 2020)"; + } + if (ffStrEquals(version, "16,3")) { + return "MacBook Pro (13-inch, 2020, Two Thunderbolt 3 ports)"; + } + if (ffStrEquals(version, "16,2")) { + return "MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)"; + } + if (ffStrEquals(version, "16,4") || + ffStrEquals(version, "16,1")) { + return "MacBook Pro (16-inch, 2019)"; + } + if (ffStrEquals(version, "15,4")) { + return "MacBook Pro (13-inch, 2019, Two Thunderbolt 3 ports)"; + } + if (ffStrEquals(version, "15,3")) { + return "MacBook Pro (15-inch, 2019)"; + } + if (ffStrEquals(version, "15,2")) { + return "MacBook Pro (13-inch, 2018/2019, Four Thunderbolt 3 ports)"; + } + if (ffStrEquals(version, "15,1")) { + return "MacBook Pro (15-inch, 2018/2019)"; + } + if (ffStrEquals(version, "14,3")) { + return "MacBook Pro (15-inch, 2017)"; + } + if (ffStrEquals(version, "14,2")) { + return "MacBook Pro (13-inch, 2017, Four Thunderbolt 3 ports)"; + } + if (ffStrEquals(version, "14,1")) { + return "MacBook Pro (13-inch, 2017, Two Thunderbolt 3 ports)"; + } + if (ffStrEquals(version, "13,3")) { + return "MacBook Pro (15-inch, 2016)"; + } + if (ffStrEquals(version, "13,2")) { + return "MacBook Pro (13-inch, 2016, Four Thunderbolt 3 ports)"; + } + if (ffStrEquals(version, "13,1")) { + return "MacBook Pro (13-inch, 2016, Two Thunderbolt 3 ports)"; + } + if (ffStrEquals(version, "12,1")) { + return "MacBook Pro (Retina, 13-inch, Early 2015)"; + } + if (ffStrEquals(version, "11,4") || + ffStrEquals(version, "11,5")) { + return "MacBook Pro (Retina, 15-inch, Mid 2015)"; + } + if (ffStrEquals(version, "11,2") || + ffStrEquals(version, "11,3")) { + return "MacBook Pro (Retina, 15-inch, Late 2013/Mid 2014)"; + } + if (ffStrEquals(version, "11,1")) { + return "MacBook Pro (Retina, 13-inch, Late 2013/Mid 2014)"; + } + if (ffStrEquals(version, "10,2")) { + return "MacBook Pro (Retina, 13-inch, Late 2012/Early 2013)"; + } + if (ffStrEquals(version, "10,1")) { + return "MacBook Pro (Retina, 15-inch, Mid 2012/Early 2013)"; + } + if (ffStrEquals(version, "9,2")) { + return "MacBook Pro (13-inch, Mid 2012)"; + } + if (ffStrEquals(version, "9,1")) { + return "MacBook Pro (15-inch, Mid 2012)"; + } + if (ffStrEquals(version, "8,3")) { + return "MacBook Pro (17-inch, 2011)"; + } + if (ffStrEquals(version, "8,2")) { + return "MacBook Pro (15-inch, 2011)"; + } + if (ffStrEquals(version, "8,1")) { + return "MacBook Pro (13-inch, 2011)"; + } + if (ffStrEquals(version, "7,1")) { + return "MacBook Pro (13-inch, Mid 2010)"; + } + if (ffStrEquals(version, "6,2")) { + return "MacBook Pro (15-inch, Mid 2010)"; + } + if (ffStrEquals(version, "6,1")) { + return "MacBook Pro (17-inch, Mid 2010)"; + } + if (ffStrEquals(version, "5,5")) { + return "MacBook Pro (13-inch, Mid 2009)"; + } + if (ffStrEquals(version, "5,3")) { + return "MacBook Pro (15-inch, Mid 2009)"; + } + if (ffStrEquals(version, "5,2")) { + return "MacBook Pro (17-inch, Mid/Early 2009)"; + } + if (ffStrEquals(version, "5,1")) { + return "MacBook Pro (15-inch, Late 2008)"; + } + if (ffStrEquals(version, "4,1")) { + return "MacBook Pro (17/15-inch, Early 2008)"; + } + } else if (ffStrbufStartsWithS(hwModel, "MacBookAir")) { + const char* version = hwModel->chars + strlen("MacBookAir"); + if (ffStrEquals(version, "10,1")) { + return "MacBook Air (M1, 2020)"; + } + if (ffStrEquals(version, "9,1")) { + return "MacBook Air (Retina, 13-inch, 2020)"; + } + if (ffStrEquals(version, "8,2")) { + return "MacBook Air (Retina, 13-inch, 2019)"; + } + if (ffStrEquals(version, "8,1")) { + return "MacBook Air (Retina, 13-inch, 2018)"; + } + if (ffStrEquals(version, "7,2")) { + return "MacBook Air (13-inch, Early 2015/2017)"; + } + if (ffStrEquals(version, "7,1")) { + return "MacBook Air (11-inch, Early 2015)"; + } + if (ffStrEquals(version, "6,2")) { + return "MacBook Air (13-inch, Mid 2013/Early 2014)"; + } + if (ffStrEquals(version, "6,1")) { + return "MacBook Air (11-inch, Mid 2013/Early 2014)"; + } + if (ffStrEquals(version, "5,2")) { + return "MacBook Air (13-inch, Mid 2012)"; + } + if (ffStrEquals(version, "5,1")) { + return "MacBook Air (11-inch, Mid 2012)"; + } + if (ffStrEquals(version, "4,2")) { + return "MacBook Air (13-inch, Mid 2011)"; + } + if (ffStrEquals(version, "4,1")) { + return "MacBook Air (11-inch, Mid 2011)"; + } + if (ffStrEquals(version, "3,2")) { + return "MacBook Air (13-inch, Late 2010)"; + } + if (ffStrEquals(version, "3,1")) { + return "MacBook Air (11-inch, Late 2010)"; + } + if (ffStrEquals(version, "2,1")) { + return "MacBook Air (Mid 2009)"; + } + } else if (ffStrbufStartsWithS(hwModel, "Macmini")) { + const char* version = hwModel->chars + strlen("Macmini"); + if (ffStrEquals(version, "9,1")) { + return "Mac mini (M1, 2020)"; + } + if (ffStrEquals(version, "8,1")) { + return "Mac mini (2018)"; + } + if (ffStrEquals(version, "7,1")) { + return "Mac mini (Mid 2014)"; + } + if (ffStrEquals(version, "6,1") || + ffStrEquals(version, "6,2")) { + return "Mac mini (Late 2012)"; + } + if (ffStrEquals(version, "5,1") || + ffStrEquals(version, "5,2")) { + return "Mac mini (Mid 2011)"; + } + if (ffStrEquals(version, "4,1")) { + return "Mac mini (Mid 2010)"; + } + if (ffStrEquals(version, "3,1")) { + return "Mac mini (Early/Late 2009)"; + } + } else if (ffStrbufStartsWithS(hwModel, "MacBook")) { + const char* version = hwModel->chars + strlen("MacBook"); + if (ffStrEquals(version, "10,1")) { + return "MacBook (Retina, 12-inch, 2017)"; + } + if (ffStrEquals(version, "9,1")) { + return "MacBook (Retina, 12-inch, Early 2016)"; + } + if (ffStrEquals(version, "8,1")) { + return "MacBook (Retina, 12-inch, Early 2015)"; + } + if (ffStrEquals(version, "7,1")) { + return "MacBook (13-inch, Mid 2010)"; + } + if (ffStrEquals(version, "6,1")) { + return "MacBook (13-inch, Late 2009)"; + } + if (ffStrEquals(version, "5,2")) { + return "MacBook (13-inch, Early/Mid 2009)"; + } + } else if (ffStrbufStartsWithS(hwModel, "MacPro")) { + const char* version = hwModel->chars + strlen("MacPro"); + if (ffStrEquals(version, "7,1")) { + return "Mac Pro (2019)"; + } + if (ffStrEquals(version, "6,1")) { + return "Mac Pro (Late 2013)"; + } + if (ffStrEquals(version, "5,1")) { + return "Mac Pro (Mid 2010 - Mid 2012)"; + } + if (ffStrEquals(version, "4,1")) { + return "Mac Pro (Early 2009)"; + } + } else if (ffStrbufStartsWithS(hwModel, "Mac")) { + const char* version = hwModel->chars + strlen("Mac"); + if (ffStrEquals(version, "17,9")) { + return "MacBook Pro (14-inch, M5 Pro, 2026)"; + } + if (ffStrEquals(version, "17,8")) { + return "MacBook Pro (16-inch, M5 Pro, 2026)"; + } + if (ffStrEquals(version, "17,7")) { + return "MacBook Pro (14-inch, M5 Max, 2026)"; + } + if (ffStrEquals(version, "17,6")) { + return "MacBook Pro (16-inch, M5 Max, 2026)"; + } + if (ffStrEquals(version, "17,5")) { + return "MacBook Neo (13-inch, A18 Pro, 2026)"; + } + if (ffStrEquals(version, "17,4")) { + return "MacBook Air (15-inch, M5, 2026)"; + } + if (ffStrEquals(version, "17,3")) { + return "MacBook Air (13-inch, M5, 2026)"; + } + if (ffStrEquals(version, "17,2")) { + return "MacBook Pro (14-inch, M5, 2025)"; + } + if (ffStrEquals(version, "16,13")) { + return "MacBook Air (15-inch, M4, 2025)"; + } + if (ffStrEquals(version, "16,12")) { + return "MacBook Air (13-inch, M4, 2025)"; + } + if (ffStrEquals(version, "16,11") || + ffStrEquals(version, "16,10")) { + return "Mac Mini (2024)"; + } + if (ffStrEquals(version, "16,9")) { + return "Mac Studio (M4 Max, 2025)"; + } + if (ffStrEquals(version, "16,3")) { + return "iMac (24-inch, 2024, Four Thunderbolt / USB 4 ports)"; + } + if (ffStrEquals(version, "16,2")) { + return "iMac (24-inch, 2024, Two Thunderbolt / USB 4 ports)"; + } + if (ffStrEquals(version, "16,1")) { + return "MacBook Pro (14-inch, 2024, Three Thunderbolt 4 ports)"; + } + if (ffStrEquals(version, "16,6") || + ffStrEquals(version, "16,8")) { + return "MacBook Pro (14-inch, 2024, Three Thunderbolt 5 ports)"; + } + if (ffStrEquals(version, "16,7") || + ffStrEquals(version, "16,5")) { + return "MacBook Pro (16-inch, 2024, Three Thunderbolt 5 ports)"; + } + if (ffStrEquals(version, "15,14")) { + return "Mac Studio (M3 Ultra, 2025)"; + } + if (ffStrEquals(version, "15,13")) { + return "MacBook Air (15-inch, M3, 2024)"; + } + if (ffStrEquals(version, "15,12")) { + return "MacBook Air (13-inch, M3, 2024)"; + } + if (ffStrEquals(version, "15,3")) { + return "MacBook Pro (14-inch, Nov 2023, Two Thunderbolt / USB 4 ports)"; + } + if (ffStrEquals(version, "15,4")) { + return "iMac (24-inch, 2023, Two Thunderbolt / USB 4 ports)"; + } + if (ffStrEquals(version, "15,5")) { + return "iMac (24-inch, 2023, Two Thunderbolt / USB 4 ports, Two USB 3 ports)"; + } + if (ffStrEquals(version, "15,6") || + ffStrEquals(version, "15,8") || + ffStrEquals(version, "15,10")) { + return "MacBook Pro (14-inch, Nov 2023, Three Thunderbolt 4 ports)"; + } + if (ffStrEquals(version, "15,7") || + ffStrEquals(version, "15,9") || + ffStrEquals(version, "15,11")) { + return "MacBook Pro (16-inch, Nov 2023, Three Thunderbolt 4 ports)"; + } + if (ffStrEquals(version, "14,15")) { + return "MacBook Air (15-inch, M2, 2023)"; + } + if (ffStrEquals(version, "14,14")) { + return "Mac Studio (M2 Ultra, 2023, Two Thunderbolt 4 front ports)"; + } + if (ffStrEquals(version, "14,13")) { + return "Mac Studio (M2 Max, 2023, Two USB-C front ports)"; + } + if (ffStrEquals(version, "14,8")) { + return "Mac Pro (2023)"; + } + if (ffStrEquals(version, "14,6") || + ffStrEquals(version, "14,10")) { + return "MacBook Pro (16-inch, 2023)"; + } + if (ffStrEquals(version, "14,5") || + ffStrEquals(version, "14,9")) { + return "MacBook Pro (14-inch, 2023)"; + } + if (ffStrEquals(version, "14,3")) { + return "Mac mini (M2, 2023, Two Thunderbolt 4 ports)"; + } + if (ffStrEquals(version, "14,12")) { + return "Mac mini (M2 Pro, 2023, Four Thunderbolt 4 ports)"; + } + if (ffStrEquals(version, "14,7")) { + return "MacBook Pro (13-inch, M2, 2022)"; + } + if (ffStrEquals(version, "14,2")) { + return "MacBook Air (M2, 2022)"; + } + if (ffStrEquals(version, "13,1")) { + return "Mac Studio (M1 Max, 2022, Two USB-C front ports)"; + } + if (ffStrEquals(version, "13,2")) { + return "Mac Studio (M1 Ultra, 2022, Two Thunderbolt 4 front ports)"; + } + } else if (ffStrbufStartsWithS(hwModel, "iMac")) { + const char* version = hwModel->chars + strlen("iMac"); + if (ffStrEquals(version, "21,1")) { + return "iMac (24-inch, M1, 2021, Two Thunderbolt / USB 4 ports, Two USB 3 ports)"; + } + if (ffStrEquals(version, "21,2")) { + return "iMac (24-inch, M1, 2021, Two Thunderbolt / USB 4 ports)"; + } + if (ffStrEquals(version, "20,1") || + ffStrEquals(version, "20,2")) { + return "iMac (Retina 5K, 27-inch, 2020)"; + } + if (ffStrEquals(version, "19,1")) { + return "iMac (Retina 5K, 27-inch, 2019)"; + } + if (ffStrEquals(version, "19,2")) { + return "iMac (Retina 4K, 21.5-inch, 2019)"; + } + if (ffStrEquals(version, "Pro1,1")) { + return "iMac Pro (2017)"; + } + if (ffStrEquals(version, "18,3")) { + return "iMac (Retina 5K, 27-inch, 2017)"; + } + if (ffStrEquals(version, "18,2")) { + return "iMac (Retina 4K, 21.5-inch, 2017)"; + } + if (ffStrEquals(version, "18,1")) { + return "iMac (21.5-inch, 2017)"; + } + if (ffStrEquals(version, "17,1")) { + return "iMac (Retina 5K, 27-inch, Late 2015)"; + } + if (ffStrEquals(version, "16,2")) { + return "iMac (Retina 4K, 21.5-inch, Late 2015)"; + } + if (ffStrEquals(version, "16,1")) { + return "iMac (21.5-inch, Late 2015)"; + } + if (ffStrEquals(version, "15,1")) { + return "iMac (Retina 5K, 27-inch, Late 2014 - Mid 2015)"; + } + if (ffStrEquals(version, "14,4")) { + return "iMac (21.5-inch, Mid 2014)"; + } + if (ffStrEquals(version, "14,2")) { + return "iMac (27-inch, Late 2013)"; + } + if (ffStrEquals(version, "14,1")) { + return "iMac (21.5-inch, Late 2013)"; + } + if (ffStrEquals(version, "13,2")) { + return "iMac (27-inch, Late 2012)"; + } + if (ffStrEquals(version, "13,1")) { + return "iMac (21.5-inch, Late 2012)"; + } + if (ffStrEquals(version, "12,2")) { + return "iMac (27-inch, Mid 2011)"; + } + if (ffStrEquals(version, "12,1")) { + return "iMac (21.5-inch, Mid 2011)"; + } + if (ffStrEquals(version, "11,3")) { + return "iMac (27-inch, Mid 2010)"; + } + if (ffStrEquals(version, "11,2")) { + return "iMac (21.5-inch, Mid 2010)"; + } + if (ffStrEquals(version, "10,1")) { + return "iMac (27/21.5-inch, Late 2009)"; + } + if (ffStrEquals(version, "9,1")) { + return "iMac (24/20-inch, Early 2009)"; + } + } + return NULL; +} + +#ifdef __x86_64__ +bool ffHostDetectMac(FFHostResult* host) { + if (ffStrbufStartsWithS(&host->family, "Mac") && ffStrbufEqualS(&host->vendor, "Apple Inc.")) { + const char* productName = ffHostGetMacProductNameWithHwModel(&host->name); + if (productName) { + ffStrbufDestroy(&host->family); + ffStrbufInitMove(&host->family, &host->name); + ffStrbufSetStatic(&host->name, productName); + return true; + } + } + return false; +} +#endif diff --git a/src/detection/host/host_nbsd.c b/src/detection/host/host_nbsd.c new file mode 100644 index 0000000..a5a29d2 --- /dev/null +++ b/src/detection/host/host_nbsd.c @@ -0,0 +1,25 @@ +#include "host.h" +#include "common/sysctl.h" +#include "common/smbios.h" + +const char* ffDetectHost(FFHostResult* host) { + const char* error = NULL; + if ((error = ffSysctlGetString("machdep.dmi.system-product", &host->name))) { + return error; + } + ffCleanUpSmbiosValue(&host->name); + if (ffSysctlGetString("machdep.dmi.system-vendor", &host->vendor) == NULL) { + ffCleanUpSmbiosValue(&host->vendor); + } + if (ffSysctlGetString("machdep.dmi.system-version", &host->version) == NULL) { + ffCleanUpSmbiosValue(&host->version); + } + if (ffSysctlGetString("machdep.dmi.system-serial", &host->serial) == NULL) { + ffCleanUpSmbiosValue(&host->serial); + } + if (ffSysctlGetString("machdep.dmi.system-uuid", &host->uuid) == NULL) { + ffCleanUpSmbiosValue(&host->uuid); + } + + return NULL; +} diff --git a/src/detection/host/host_nosupport.c b/src/detection/host/host_nosupport.c new file mode 100644 index 0000000..61a7254 --- /dev/null +++ b/src/detection/host/host_nosupport.c @@ -0,0 +1,5 @@ +#include "host.h" + +const char* ffDetectHost(FF_A_UNUSED FFHostResult* host) { + return "Not supported on this platform"; +} diff --git a/src/detection/host/host_obsd.c b/src/detection/host/host_obsd.c new file mode 100644 index 0000000..cb16ecf --- /dev/null +++ b/src/detection/host/host_obsd.c @@ -0,0 +1,22 @@ +#include "host.h" +#include "common/sysctl.h" +#include "common/smbios.h" + +const char* ffDetectHost(FFHostResult* host) { + const char* error = NULL; + if ((error = ffSysctlGetString(CTL_HW, HW_PRODUCT, &host->name))) { + return error; + } + ffCleanUpSmbiosValue(&host->name); + if (ffSysctlGetString(CTL_HW, HW_VENDOR, &host->vendor) == NULL) { + ffCleanUpSmbiosValue(&host->vendor); + } + if (ffSysctlGetString(CTL_HW, HW_VERSION, &host->version) == NULL) { + ffCleanUpSmbiosValue(&host->version); + } + if (ffSysctlGetString(CTL_HW, HW_SERIALNO, &host->serial) == NULL) { + ffCleanUpSmbiosValue(&host->serial); + } + + return NULL; +} diff --git a/src/detection/host/host_windows.c b/src/detection/host/host_windows.c new file mode 100644 index 0000000..9f25829 --- /dev/null +++ b/src/detection/host/host_windows.c @@ -0,0 +1,74 @@ +#include "host.h" +#include "common/smbios.h" + +typedef struct FFSmbiosSystemInfo { + FFSmbiosHeader Header; + + uint8_t Manufacturer; // string + uint8_t ProductName; // string + uint8_t Version; // string + uint8_t SerialNumber; // string + + // 2.1+ + struct { + uint32_t TimeLow; + uint16_t TimeMid; + uint16_t TimeHighAndVersion; + uint8_t ClockSeqHiAndReserved; + uint8_t ClockSeqLow; + uint8_t Node[6]; + } FF_A_PACKED UUID; // varies + uint8_t WakeUpType; // enum + + // 2.4+ + uint8_t SKUNumber; // string + uint8_t Family; // string +} FF_A_PACKED FFSmbiosSystemInfo; + +static_assert(offsetof(FFSmbiosSystemInfo, Family) == 0x1A, + "FFSmbiosSystemInfo: Wrong struct alignment"); + +const char* ffDetectHost(FFHostResult* host) { + const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable(); + if (!smbiosTable) { + return "Failed to get SMBIOS data"; + } + + const FFSmbiosSystemInfo* data = (const FFSmbiosSystemInfo*) (*smbiosTable)[FF_SMBIOS_TYPE_SYSTEM_INFO]; + if (!data) { + return "System information is not found in SMBIOS data"; + } + + const char* strings = (const char*) data + data->Header.Length; + + ffStrbufSetStatic(&host->vendor, ffSmbiosLocateString(strings, data->Manufacturer)); + ffCleanUpSmbiosValue(&host->vendor); + ffStrbufSetStatic(&host->name, ffSmbiosLocateString(strings, data->ProductName)); + ffCleanUpSmbiosValue(&host->name); + ffStrbufSetStatic(&host->version, ffSmbiosLocateString(strings, data->Version)); + ffCleanUpSmbiosValue(&host->version); + ffStrbufSetStatic(&host->serial, ffSmbiosLocateString(strings, data->SerialNumber)); + ffCleanUpSmbiosValue(&host->serial); + + static_assert(offsetof(FFSmbiosSystemInfo, UUID) == 0x08, "FFSmbiosSystemInfo.UUID offset is wrong"); + if (data->Header.Length > offsetof(FFSmbiosSystemInfo, UUID)) { + ffStrbufSetF(&host->uuid, "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X", data->UUID.TimeLow, data->UUID.TimeMid, data->UUID.TimeHighAndVersion, data->UUID.ClockSeqHiAndReserved, data->UUID.ClockSeqLow, data->UUID.Node[0], data->UUID.Node[1], data->UUID.Node[2], data->UUID.Node[3], data->UUID.Node[4], data->UUID.Node[5]); + } + + static_assert(offsetof(FFSmbiosSystemInfo, SKUNumber) == 0x19, "FFSmbiosSystemInfo.SKUNumber offset is wrong"); + if (data->Header.Length > offsetof(FFSmbiosSystemInfo, SKUNumber)) { + ffStrbufSetStatic(&host->sku, ffSmbiosLocateString(strings, data->SKUNumber)); + ffCleanUpSmbiosValue(&host->sku); + } + + if (data->Header.Length > offsetof(FFSmbiosSystemInfo, Family)) { + ffStrbufSetStatic(&host->family, ffSmbiosLocateString(strings, data->Family)); + ffCleanUpSmbiosValue(&host->family); + } + +#if __x86_64__ + ffHostDetectMac(host); +#endif + + return NULL; +} diff --git a/src/detection/icons/icons.h b/src/detection/icons/icons.h new file mode 100644 index 0000000..7f3f20e --- /dev/null +++ b/src/detection/icons/icons.h @@ -0,0 +1,11 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/icons/option.h" + +typedef struct FFIconsResult { + FFstrbuf icons1; + FFstrbuf icons2; +} FFIconsResult; + +const char* ffDetectIcons(FFIconsResult* result); diff --git a/src/detection/icons/icons_linux.c b/src/detection/icons/icons_linux.c new file mode 100644 index 0000000..0ecaa07 --- /dev/null +++ b/src/detection/icons/icons_linux.c @@ -0,0 +1,30 @@ +#include "icons.h" +#include "common/parsing.h" +#include "detection/gtk_qt/gtk_qt.h" +#include "detection/displayserver/displayserver.h" + +const char* ffDetectIcons(FFIconsResult* result) { + const FFDisplayServerResult* wmde = ffConnectDisplayServer(); + + if (ffStrbufIgnCaseEqualS(&wmde->wmProtocolName, FF_WM_PROTOCOL_TTY)) { + return "Icons aren't supported in TTY"; + } + + const FFstrbuf* plasma = &ffDetectQt()->icons; + const FFstrbuf* gtk2 = &ffDetectGTK2()->icons; + const FFstrbuf* gtk3 = &ffDetectGTK3()->icons; + const FFstrbuf* gtk4 = &ffDetectGTK4()->icons; + + if (plasma->length == 0 && gtk2->length == 0 && gtk3->length == 0 && gtk4->length == 0) { + return "No icons could be found"; + } + + ffParseGTK(&result->icons2, gtk2, gtk3, gtk4); + + if (plasma->length > 0) { + ffStrbufAppend(&result->icons1, plasma); + ffStrbufAppendS(&result->icons1, " [Qt]"); + } + + return NULL; +} diff --git a/src/detection/icons/icons_nosupport.c b/src/detection/icons/icons_nosupport.c new file mode 100644 index 0000000..ab8cebb --- /dev/null +++ b/src/detection/icons/icons_nosupport.c @@ -0,0 +1,5 @@ +#include "icons.h" + +const char* ffDetectIcons(FF_A_UNUSED FFIconsResult* result) { + return "Not supported on this platform"; +} diff --git a/src/detection/icons/icons_windows.c b/src/detection/icons/icons_windows.c new file mode 100644 index 0000000..e199432 --- /dev/null +++ b/src/detection/icons/icons_windows.c @@ -0,0 +1,47 @@ +#include "icons.h" +#include "common/windows/registry.h" + +const char* ffDetectIcons(FFIconsResult* result) { + FF_AUTO_CLOSE_FD HANDLE hKey = NULL; + if (!ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\HideDesktopIcons\\NewStartPanel", &hKey, NULL) && + !ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\HideDesktopIcons\\ClassicStartMenu", &hKey, NULL)) { + // If the key doesn't exist, it means that the user never changed the default settings. + ffStrbufSetStatic(&result->icons2, "Recycle Bin"); + return NULL; + } + + // Whether these icons are hidden + uint32_t ThisPC = 1, UsersFiles = 1, RemoteNetwork = 1, RecycleBin = 0 /* Shown by default */, ControlPanel = 1; + ffRegReadUint(hKey, L"{20D04FE0-3AEA-1069-A2D8-08002B30309D}", &ThisPC, NULL); + ffRegReadUint(hKey, L"{59031a47-3f72-44a7-89c5-5595fe6b30ee}", &UsersFiles, NULL); + ffRegReadUint(hKey, L"{F02C1A0D-BE21-4350-88B0-7367FC96EF3C}", &RemoteNetwork, NULL); + ffRegReadUint(hKey, L"{645FF040-5081-101B-9F08-00AA002F954E}", &RecycleBin, NULL); + ffRegReadUint(hKey, L"{5399E694-6CE5-4D6C-8FCE-1D8870FDCBA0}", &ControlPanel, NULL); + + if (ThisPC && UsersFiles && RemoteNetwork && RecycleBin && ControlPanel) { + return "All icons are hidden"; + } + + if (!ThisPC) { + ffStrbufAppendS(&result->icons1, "This PC, "); + } + if (!UsersFiles) { + ffStrbufAppendS(&result->icons1, "User's Files"); + } + ffStrbufTrimRight(&result->icons1, ' '); + ffStrbufTrimRight(&result->icons1, ','); + + if (!RemoteNetwork) { + ffStrbufAppendS(&result->icons2, "Remote Network, "); + } + if (!RecycleBin) { + ffStrbufAppendS(&result->icons2, "Recycle Bin, "); + } + if (!ControlPanel) { + ffStrbufAppendS(&result->icons2, "Control Panel"); + } + ffStrbufTrimRight(&result->icons2, ' '); + ffStrbufTrimRight(&result->icons2, ','); + + return NULL; +} diff --git a/src/detection/initsystem/initsystem.h b/src/detection/initsystem/initsystem.h new file mode 100644 index 0000000..3c59cf0 --- /dev/null +++ b/src/detection/initsystem/initsystem.h @@ -0,0 +1,13 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/initsystem/option.h" + +typedef struct FFInitSystemResult { + FFstrbuf name; + FFstrbuf exe; + FFstrbuf version; + uint32_t pid; +} FFInitSystemResult; + +const char* ffDetectInitSystem(FFInitSystemResult* result); diff --git a/src/detection/initsystem/initsystem_haiku.c b/src/detection/initsystem/initsystem_haiku.c new file mode 100644 index 0000000..04857bc --- /dev/null +++ b/src/detection/initsystem/initsystem_haiku.c @@ -0,0 +1,35 @@ +#include "initsystem.h" +#include "common/strutil.h" +#include "common/haiku/version.h" +#include "common/io.h" + +#include <OS.h> +#include <unistd.h> + +const char* ffDetectInitSystem(FFInitSystemResult* result) { + // Since it runs first, registrar does not know about it, + // so we can't query be_roster for it. + const char* path = "/boot/system/servers/launch_daemon"; + if (!ffPathExists(path, FF_PATHTYPE_FILE)) { + return "launch_daemon is not found"; + } + + ffStrbufSetStatic(&result->exe, path); + ffStrbufSetStatic(&result->name, "launch_daemon"); + result->pid = 0; + + team_info teamInfo; + int32 cookie = 0; + while (get_next_team_info(&cookie, &teamInfo) == B_OK) { + if (ffStrEquals(teamInfo.args, path)) { + result->pid = (uint32_t) teamInfo.team; + break; + } + } + + if (instance.config.general.detectVersion) { + ffGetFileVersion(path, &result->version); + } + + return NULL; +} diff --git a/src/detection/initsystem/initsystem_linux.c b/src/detection/initsystem/initsystem_linux.c new file mode 100644 index 0000000..445657b --- /dev/null +++ b/src/detection/initsystem/initsystem_linux.c @@ -0,0 +1,116 @@ +#include "initsystem.h" +#include "common/processing.h" +#include "common/binary.h" +#include "common/strutil.h" + +#include <libgen.h> +#include <unistd.h> + +FF_A_UNUSED static bool extractSystemdVersion(const char* str, uint32_t len, void* userdata) { + if (!ffStrStartsWith(str, "systemd ")) { + return true; + } + const char* pstart = str + strlen("systemd "); + const char* pend = memmem(pstart, len - strlen("systemd "), " running in ", strlen(" running in ")); + if (!pend) { + return true; + } + ffStrbufSetNS((FFstrbuf*) userdata, (uint32_t) (pend - pstart), pstart); + return false; +} + +const char* ffDetectInitSystem(FFInitSystemResult* result) { + const char* error = ffProcessGetBasicInfoLinux((int) result->pid, &result->name, NULL, NULL); + if (error) { +#ifdef __ANDROID__ + if (access("/system/bin/init", F_OK) == 0) { + ffStrbufSetStatic(&result->exe, "/system/bin/init"); + ffStrbufSetStatic(&result->name, "init"); + return NULL; + } +#endif + return error; + } + + const char* _; + // In linux /proc/1/exe is not readable + ffProcessGetInfoLinux((int) result->pid, &result->name, &result->exe, &_, NULL); + if (result->exe.chars[0] == '/') { + // In some old system, /sbin/init is a symlink + char buf[PATH_MAX]; + if (realpath(result->exe.chars, buf)) { + ffStrbufSetS(&result->exe, buf); + ffStrbufSetS(&result->name, basename(result->exe.chars)); + } + } + + if (instance.config.general.detectVersion) { +#if (defined(__linux__) && !defined(__ANDROID__)) || defined(__GNU__) + if (ffStrbufEqualS(&result->name, "systemd")) { + ffBinaryExtractStrings(result->exe.chars, extractSystemdVersion, &result->version, (uint32_t) strlen("systemd 0.0 running in x")); + if (result->version.length == 0) { + if (ffProcessAppendStdOut(&result->version, (char* const[]) { + ffStrbufEndsWithS(&result->exe, "/systemd") ? result->exe.chars : "systemctl", // use exe path in case users have another systemd installed + "--version", + NULL, + }) == NULL && + result->version.length) { + uint32_t iStart = ffStrbufFirstIndexC(&result->version, '('); + if (iStart < result->version.length) { + uint32_t iEnd = ffStrbufNextIndexC(&result->version, iStart + 1, ')'); + ffStrbufSubstrBefore(&result->version, iEnd); + ffStrbufSubstrAfter(&result->version, iStart); + } + } + } + } else if (ffStrbufEqualS(&result->name, "dinit")) { + if (ffProcessAppendStdOut(&result->version, (char* const[]) { + ffStrbufEndsWithS(&result->exe, "/dinit") ? result->exe.chars : "dinit", + "--version", + NULL, + }) == NULL && + result->version.length) { + // Dinit version 0.18.0. + ffStrbufSubstrBeforeFirstC(&result->version, '\n'); + ffStrbufTrimRight(&result->version, '.'); + ffStrbufSubstrAfterLastC(&result->version, ' '); + } + } else if (ffStrbufEqualS(&result->name, "shepherd")) { + if (ffProcessAppendStdOut(&result->version, (char* const[]) { + ffStrbufEndsWithS(&result->exe, "/shepherd") ? result->exe.chars : "shepherd", + "--version", + NULL, + }) == NULL && + result->version.length) { + // shepherd (GNU Shepherd) 1.0.6 + // The first line in the output might not contain the version + if (!ffStrbufStartsWithS(&result->version, "shepherd")) { + ffStrbufSubstrAfterFirstC(&result->version, '\n'); + } + + ffStrbufSubstrBeforeFirstC(&result->version, '\n'); + ffStrbufSubstrAfterLastC(&result->version, ' '); + } + } +#elif __APPLE__ + if (ffStrbufEqualS(&result->name, "launchd")) { + if (ffProcessAppendStdOut(&result->version, (char* const[]) { + "/bin/launchctl", + "version", + NULL, + }) == NULL && + result->version.length) { + uint32_t iStart = ffStrbufFirstIndexS(&result->version, "Version "); + if (iStart < result->version.length) { + iStart += (uint32_t) strlen("Version"); + uint32_t iEnd = ffStrbufNextIndexC(&result->version, iStart + 1, ':'); + ffStrbufSubstrBefore(&result->version, iEnd); + ffStrbufSubstrAfter(&result->version, iStart); + } + } + } +#endif + } + + return NULL; +} diff --git a/src/detection/initsystem/initsystem_nosupport.c b/src/detection/initsystem/initsystem_nosupport.c new file mode 100644 index 0000000..e8ccb8e --- /dev/null +++ b/src/detection/initsystem/initsystem_nosupport.c @@ -0,0 +1,5 @@ +#include "initsystem.h" + +const char* ffDetectInitSystem(FF_A_UNUSED FFInitSystemResult* result) { + return "Not supported on this platform"; +} diff --git a/src/detection/keyboard/keyboard.h b/src/detection/keyboard/keyboard.h new file mode 100644 index 0000000..6838a9c --- /dev/null +++ b/src/detection/keyboard/keyboard.h @@ -0,0 +1,8 @@ +#include "fastfetch.h" + +typedef struct FFKeyboardDevice { + FFstrbuf serial; + FFstrbuf name; +} FFKeyboardDevice; + +const char* ffDetectKeyboard(FFlist* devices /* List of FFKeyboardDevice */); diff --git a/src/detection/keyboard/keyboard_apple.c b/src/detection/keyboard/keyboard_apple.c new file mode 100644 index 0000000..e2ecc37 --- /dev/null +++ b/src/detection/keyboard/keyboard_apple.c @@ -0,0 +1,36 @@ +#include "keyboard.h" +#include "common/apple/cf_helpers.h" +#include "common/mallocHelper.h" + +#include <IOKit/IOKitLib.h> +#include <IOKit/hid/IOHIDLib.h> + +static void enumSet(IOHIDDeviceRef value, FFlist* results) { + FFKeyboardDevice* device = FF_LIST_ADD(FFKeyboardDevice, *results); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->name); + + CFStringRef product = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDProductKey)); + ffCfStrGetString(product, &device->name); + + CFStringRef serialNumber = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDSerialNumberKey)); + ffCfStrGetString(serialNumber, &device->serial); +} + +const char* ffDetectKeyboard(FFlist* devices /* List of FFKeyboardDevice */) { + IOHIDManagerRef FF_CFTYPE_AUTO_RELEASE manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) { + return "IOHIDManagerOpen() failed"; + } + + CFDictionaryRef FF_CFTYPE_AUTO_RELEASE matching1 = CFDictionaryCreate(kCFAllocatorDefault, (const void**) (CFStringRef[]) { CFSTR(kIOHIDDeviceUsagePageKey), CFSTR(kIOHIDDeviceUsageKey) }, (const void**) (CFNumberRef[]) { ffCfCreateInt(kHIDPage_GenericDesktop), ffCfCreateInt(kHIDUsage_GD_Keyboard) }, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + IOHIDManagerSetDeviceMatching(manager, matching1); + + CFSetRef FF_CFTYPE_AUTO_RELEASE set = IOHIDManagerCopyDevices(manager); + if (set) { + CFSetApplyFunction(set, (CFSetApplierFunction) &enumSet, devices); + } + IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); + + return NULL; +} diff --git a/src/detection/keyboard/keyboard_bsd.c b/src/detection/keyboard/keyboard_bsd.c new file mode 100644 index 0000000..4b0ee57 --- /dev/null +++ b/src/detection/keyboard/keyboard_bsd.c @@ -0,0 +1,92 @@ +#include "keyboard.h" +#include "common/io.h" + +#include <stdio.h> +#include <fcntl.h> +#include <usbhid.h> +#include <sys/kbio.h> + +#if __has_include(<dev/usb/usb_ioctl.h>) + #include <dev/usb/usb_ioctl.h> // FreeBSD +#else + #include <bus/u4b/usb_ioctl.h> // DragonFly +#endif + +static const char* detectByIoctl(FFlist* devices) { + keyboard_info_t kbdInfo; + if (ioctl(STDIN_FILENO, KDGKBINFO, &kbdInfo) != 0) { + return "ioctl(KDGKBINFO) failed"; + } + + FFKeyboardDevice* device = FF_LIST_ADD(FFKeyboardDevice, *devices); + + switch (kbdInfo.kb_type) { + case KB_84: + ffStrbufInitS(&device->name, "AT 84-key keyboard"); + break; + case KB_101: + ffStrbufInitS(&device->name, "AT 101/102-key keyboard"); + break; + default: + ffStrbufInitS(&device->name, "Unknown keyboard"); + break; + } + + ffStrbufAppendF(&device->name, " (kbd%d)", kbdInfo.kb_index); + + ffStrbufInit(&device->serial); + return NULL; +} + +#define MAX_UHID_KBDS 64 + +static const char* detectByUsbhid(FFlist* devices) { + char path[16]; + for (int i = 0; i < MAX_UHID_KBDS; i++) { + snprintf(path, ARRAY_SIZE(path), "/dev/uhid%d", i); + FF_AUTO_CLOSE_FD int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + if (errno == ENOENT) { + break; // No more devices + } + continue; // Device not found + } + + report_desc_t repDesc = hid_get_report_desc(fd); + if (!repDesc) { + continue; + } + + int reportId = hid_get_report_id(fd); + + struct hid_data* hData = hid_start_parse(repDesc, 0, reportId); + if (hData) { + struct hid_item hItem; + while (hid_get_item(hData, &hItem) > 0) { + if (HID_PAGE(hItem.usage) != 1 || HID_USAGE(hItem.usage) != 6) { + continue; + } + + struct usb_device_info di; + if (ioctl(fd, USB_GET_DEVICEINFO, &di) != -1) { + FFKeyboardDevice* device = FF_LIST_ADD(FFKeyboardDevice, *devices); + ffStrbufInitS(&device->serial, di.udi_serial); + ffStrbufInitS(&device->name, di.udi_product); + } + } + hid_end_parse(hData); + } + + hid_dispose_report_desc(repDesc); + } + + return NULL; +} + +const char* ffDetectKeyboard(FFlist* devices /* List of FFKeyboardDevice */) { + detectByUsbhid(devices); + if (devices->length > 0) { + return NULL; + } + return detectByIoctl(devices); +} diff --git a/src/detection/keyboard/keyboard_haiku.cpp b/src/detection/keyboard/keyboard_haiku.cpp new file mode 100644 index 0000000..dc1d28a --- /dev/null +++ b/src/detection/keyboard/keyboard_haiku.cpp @@ -0,0 +1,27 @@ +extern "C" { +#include "keyboard.h" +} + +#include <interface/Input.h> +#include <support/List.h> + +const char* ffDetectKeyboard(FFlist* devices /* List of FFKeyboardDevice */) { + BList list; + + if (get_input_devices(&list) != B_OK) { + return "get_input_devices() failed"; + } + + for (int32 i = 0, n = list.CountItems(); i < n; i++) { + BInputDevice* device = (BInputDevice*) list.ItemAt(i); + if (device->Type() != B_KEYBOARD_DEVICE || !device->IsRunning()) { + continue; + } + + FFKeyboardDevice* item = FF_LIST_ADD(FFKeyboardDevice, *devices); + ffStrbufInit(&item->serial); + ffStrbufInitS(&item->name, device->Name()); + } + + return NULL; +} diff --git a/src/detection/keyboard/keyboard_linux.c b/src/detection/keyboard/keyboard_linux.c new file mode 100644 index 0000000..1a29abb --- /dev/null +++ b/src/detection/keyboard/keyboard_linux.c @@ -0,0 +1,104 @@ +#include "keyboard.h" +#include "common/io.h" +#include "common/strutil.h" + +#include <linux/input-event-codes.h> + +const char* ffDetectKeyboard(FFlist* devices /* List of FFKeyboardDevice */) { + // Parse /proc/bus/input/devices to find keyboards with a "kbd" handler. + // This detects both wired and Bluetooth keyboards uniformly. + FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); + if (!ffAppendFileBuffer("/proc/bus/input/devices", &content)) { + return "ffAppendFileBuffer(\"/proc/bus/input/devices\") == NULL"; + } + + FFstrbuf kbd = ffStrbufCreateStatic("kbd"); + + FFKeyboardDevice device = { + .name = ffStrbufCreate(), + .serial = ffStrbufCreate() + }; + + char* line = NULL; + size_t len = 0; + while (ffStrbufGetline(&line, &len, &content)) { + switch (line[0]) { + case 'N': { + const uint32_t prefixLen = strlen("N: Name="); + if (__builtin_expect(len <= prefixLen, false)) { + continue; + } + const char* name = line + prefixLen; + const uint32_t nameLen = (uint32_t) len - prefixLen; + ffStrbufSetNS(&device.name, nameLen, name); + ffStrbufTrim(&device.name, '"'); + continue; + } + case 'H': { + const uint32_t prefixLen = strlen("H: Handlers="); + if (__builtin_expect(len <= prefixLen, false)) { + continue; + } + const char* handlers = line + prefixLen; + const uint32_t handlersLen = (uint32_t) len - prefixLen; + if (!ffStrbufMatchSeparatedNS(&kbd, handlersLen, handlers, ' ')) { + goto skipDevice; + } + continue; + } + case 'B': { + const char* bits = line + strlen("B: "); + if (ffStrStartsWith(bits, "EV=")) { + // Check EV_REP (auto-repeat, bit 20) to filter pseudo-keyboards (Power Button, PC Speaker). + const char* evBits = bits + strlen("EV="); + uint64_t val = strtoull(evBits, NULL, 16); + if (!(val & (1ULL << EV_REP))) { + goto skipDevice; + } + } else if (ffStrStartsWith(bits, "KEY=")) { + // Check KEY_A (bit 30) to filter media remotes and headset controls. + // The key capability bitmap is space-separated hex longs, MSB first; + // KEY_A falls in the last (least significant) word on all architectures. + const char* keyBits = bits + strlen("KEY="); + const char* lastWord = memrchr(keyBits, ' ', len - (size_t) (keyBits - line)); + lastWord = lastWord ? lastWord + 1 : keyBits; + + uint64_t val = strtoull(lastWord, NULL, 16); + if (!(val & (1ULL << KEY_A))) { + goto skipDevice; + } + } + continue; + } + case 'U': { + const uint32_t prefixLen = strlen("U: Uniq="); + if (__builtin_expect(len <= prefixLen, false)) { + continue; + } + const char* uniq = line + prefixLen; + const uint32_t uniqLen = (uint32_t) len - prefixLen; + ffStrbufSetNS(&device.serial, uniqLen, uniq); + continue; + } + case '\0': + // End of device entry; add to list if it has a name. + if (device.name.length > 0) { + FFKeyboardDevice* added = FF_LIST_ADD(FFKeyboardDevice, *devices); + ffStrbufInitMove(&added->name, &device.name); + ffStrbufInitMove(&added->serial, &device.serial); + } + continue; + default: + continue; + } + + skipDevice: + // Skip to the end of the current device entry. + while (line[0] != '\0' && ffStrbufGetline(&line, &len, &content)); + // Despite the fn name, it resets the string buffer to initial state + ffStrbufDestroy(&device.name); + ffStrbufDestroy(&device.serial); + } + + return NULL; +} diff --git a/src/detection/keyboard/keyboard_nosupport.c b/src/detection/keyboard/keyboard_nosupport.c new file mode 100644 index 0000000..6523ed4 --- /dev/null +++ b/src/detection/keyboard/keyboard_nosupport.c @@ -0,0 +1,5 @@ +#include "keyboard.h" + +const char* ffDetectKeyboard(FF_A_UNUSED FFlist* devices /* List of FFKeyboardDevice */) { + return "No mouse support on this platform"; +} diff --git a/src/detection/keyboard/keyboard_windows.c b/src/detection/keyboard/keyboard_windows.c new file mode 100644 index 0000000..272f632 --- /dev/null +++ b/src/detection/keyboard/keyboard_windows.c @@ -0,0 +1,85 @@ +#define INITGUID + +#include "keyboard.h" +#include "common/io.h" +#include "common/mallocHelper.h" +#include "common/windows/unicode.h" + +#include <windows.h> +#include <hidsdi.h> +#include <cfgmgr32.h> +#include <devpkey.h> + +const char* ffDetectKeyboard(FFlist* devices /* List of FFKeyboardDevice */) { + UINT nDevices = 0; + if (GetRawInputDeviceList(NULL, &nDevices, sizeof(RAWINPUTDEVICELIST))) { + return "GetRawInputDeviceList(NULL) failed"; + } + if (nDevices == 0) { + return "No HID devices found"; + } + + RAWINPUTDEVICELIST* FF_AUTO_FREE pRawInputDeviceList = (RAWINPUTDEVICELIST*) malloc(sizeof(RAWINPUTDEVICELIST) * nDevices); + if ((nDevices = GetRawInputDeviceList(pRawInputDeviceList, &nDevices, sizeof(RAWINPUTDEVICELIST))) == (UINT) -1) { + return "GetRawInputDeviceList(pRawInputDeviceList) failed"; + } + + for (UINT i = 0; i < nDevices; ++i) { + if (pRawInputDeviceList[i].dwType != RIM_TYPEKEYBOARD) { + continue; + } + + HANDLE hDevice = pRawInputDeviceList[i].hDevice; + + RID_DEVICE_INFO rdi; + UINT rdiSize = sizeof(rdi); + if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICEINFO, &rdi, &rdiSize) == (UINT) -1) { + continue; + } + + WCHAR devName[MAX_PATH]; + UINT nameSize = MAX_PATH; + if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, devName, &nameSize) == (UINT) -1) { + continue; + } + + FFKeyboardDevice* device = FF_LIST_ADD(FFKeyboardDevice, *devices); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->name); + + wchar_t buffer[MAX_PATH]; + + HANDLE FF_AUTO_CLOSE_FD hHidFile = CreateFileW(devName, 0 /* must be 0 instead of GENERIC_READ */, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + if (hHidFile != INVALID_HANDLE_VALUE) { + if (HidD_GetProductString(hHidFile, buffer, (ULONG) sizeof(buffer))) { + ffStrbufSetWS(&device->name, buffer); + } + + if (HidD_GetSerialNumberString(hHidFile, buffer, sizeof(buffer))) { + ffStrbufSetWS(&device->serial, buffer); + } + } + + if (!device->name.length) { + // https://stackoverflow.com/a/64321096/9976392 + DEVPROPTYPE propertyType; + ULONG propertySize = sizeof(buffer); + + if (CM_Get_Device_Interface_PropertyW(devName, &DEVPKEY_Device_InstanceId, &propertyType, (PBYTE) buffer, &propertySize, 0) == CR_SUCCESS) { + DEVINST devInst; + if (CM_Locate_DevNodeW(&devInst, buffer, CM_LOCATE_DEVNODE_NORMAL) == CR_SUCCESS) { + propertySize = sizeof(buffer); + if (CM_Get_DevNode_PropertyW(devInst, &DEVPKEY_NAME, &propertyType, (PBYTE) buffer, &propertySize, 0) == CR_SUCCESS) { + ffStrbufSetWS(&device->name, buffer); + } + } + } + } + + if (!device->name.length) { + ffStrbufSetF(&device->name, "Unknown device %04X-%04X", (unsigned) rdi.hid.dwVendorId, (unsigned) rdi.hid.dwProductId); + } + } + + return NULL; +} diff --git a/src/detection/libc/libc.h b/src/detection/libc/libc.h new file mode 100644 index 0000000..f2718e8 --- /dev/null +++ b/src/detection/libc/libc.h @@ -0,0 +1,10 @@ +#pragma once + +#include "fastfetch.h" + +typedef struct FFLibcResult { + const char* name; + const char* version; +} FFLibcResult; + +const char* ffDetectLibc(FFLibcResult* result); diff --git a/src/detection/libc/libc_android.c b/src/detection/libc/libc_android.c new file mode 100644 index 0000000..2aec4ed --- /dev/null +++ b/src/detection/libc/libc_android.c @@ -0,0 +1,24 @@ +#include "libc.h" + +#define FF_STR_INDIR(x) #x +#define FF_STR(x) FF_STR_INDIR(x) + +#include <features.h> + +const char* ffDetectLibc(FFLibcResult* result) { +#if __ANDROID_NDK__ + result->name = "ndk-bionic"; + result->version = FF_STR(__NDK_MAJOR__) "." FF_STR(__NDK_MINOR__) "." FF_STR(__NDK_BUILD__) + + #if __NDK_BETA__ + "-beta" FF_STR(__NDK_BETA__) + #elif __NDK_CANARY__ + "-canary" + #endif + + ; + return NULL; +#else + return "Unknown Android libc"; +#endif +} diff --git a/src/detection/libc/libc_apple.c b/src/detection/libc/libc_apple.c new file mode 100644 index 0000000..07ca2b5 --- /dev/null +++ b/src/detection/libc/libc_apple.c @@ -0,0 +1,12 @@ +#include "libc.h" + +const char* ffDetectLibc(FFLibcResult* result) { + result->name = "libSystem"; + +#ifdef FF_LIBSYSTEM_VERSION + result->version = FF_LIBSYSTEM_VERSION; +#else + result->version = NULL; +#endif + return NULL; +} diff --git a/src/detection/libc/libc_bsd.c b/src/detection/libc/libc_bsd.c new file mode 100644 index 0000000..bd73165 --- /dev/null +++ b/src/detection/libc/libc_bsd.c @@ -0,0 +1,20 @@ +#include "libc.h" + +const char* ffDetectLibc(FFLibcResult* result) { + result->name = "Unknown"; + result->version = NULL; + +#ifdef __DragonFly__ // We define `__FreeBSD__` on DragonFly BSD for simplification + result->name = "DF"; + #ifdef FF_DF_VERSION + result->version = FF_DF_VERSION; + #endif +#elif __FreeBSD__ + result->name = "FBSD"; + #ifdef FF_FBSD_VERSION + result->version = FF_FBSD_VERSION; + #endif +#endif + + return NULL; +} diff --git a/src/detection/libc/libc_linux.c b/src/detection/libc/libc_linux.c new file mode 100644 index 0000000..53186a3 --- /dev/null +++ b/src/detection/libc/libc_linux.c @@ -0,0 +1,25 @@ +#include "libc.h" + +#define FF_STR_INDIR(x) #x +#define FF_STR(x) FF_STR_INDIR(x) + +#include <features.h> + +const char* ffDetectLibc(FFLibcResult* result) { +#ifdef __UCLIBC__ + result->name = "uClibc"; + result->version = FF_STR(__UCLIBC_MAJOR__) "." FF_STR(__UCLIBC_MINOR__) "." FF_STR(__UCLIBC_SUBLEVEL__); +#elif defined(__GNU_LIBRARY__) + result->name = "glibc"; + result->version = FF_STR(__GLIBC__) "." FF_STR(__GLIBC_MINOR__); +#else + result->name = "musl"; + #ifdef FF_MUSL_VERSION + result->version = FF_MUSL_VERSION; + #else + result->version = NULL; + #endif +#endif + + return NULL; +} diff --git a/src/detection/libc/libc_nosupport.c b/src/detection/libc/libc_nosupport.c new file mode 100644 index 0000000..e4687b0 --- /dev/null +++ b/src/detection/libc/libc_nosupport.c @@ -0,0 +1,7 @@ +#include "libc.h" + +const char* ffDetectLibc(FFLibcResult* result) { + result->name = "Unknown"; + result->version = NULL; + return NULL; +} diff --git a/src/detection/libc/libc_windows.cpp b/src/detection/libc/libc_windows.cpp new file mode 100644 index 0000000..c94062a --- /dev/null +++ b/src/detection/libc/libc_windows.cpp @@ -0,0 +1,68 @@ +extern "C" { +#include "libc.h" +} + +#ifdef __MINGW32__ + #include <_mingw.h> +#endif + +template <uint32_t Major, uint32_t Minor> +class version_t { + constexpr static auto buflen() noexcept { + unsigned int len = 2; // "." + if (Major == 0) { + len++; + } else { + for (auto n = Major; n; len++, n /= 10); + } + + if (Minor == 0) { + len++; + } else { + for (auto n = Minor; n; len++, n /= 10); + } + return len; + } + + char buf[buflen()] = {}; + + public: + constexpr version_t() noexcept { + auto ptr = buf + buflen(); + *--ptr = '\0'; + + if (Minor == 0) { + *--ptr = '0'; + } else { + for (auto n = Minor; n; n /= 10) { + *--ptr = "0123456789"[n % 10]; + } + } + *--ptr = '.'; + if (Major == 0) { + *--ptr = '0'; + } else { + for (auto n = Major; n; n /= 10) { + *--ptr = "0123456789"[n % 10]; + } + } + } + + constexpr operator const char*() const { + return buf; + } +}; + +template <uint32_t Major, uint32_t Minor> +constexpr version_t<Major, Minor> version; + +extern "C" const char* ffDetectLibc(FFLibcResult* result) { +#ifdef _UCRT + result->name = "ucrt"; +#else + result->name = "msvcrt"; +#endif + + result->version = version<(__MSVCRT_VERSION__ >> 8), (__MSVCRT_VERSION__ & 8)>; + return NULL; +} diff --git a/src/detection/lm/lm.h b/src/detection/lm/lm.h new file mode 100644 index 0000000..d30229a --- /dev/null +++ b/src/detection/lm/lm.h @@ -0,0 +1,12 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/lm/option.h" + +typedef struct FFLMResult { + FFstrbuf service; + FFstrbuf type; + FFstrbuf version; +} FFLMResult; + +const char* ffDetectLM(FFLMResult* result); diff --git a/src/detection/lm/lm_linux.c b/src/detection/lm/lm_linux.c new file mode 100644 index 0000000..140205d --- /dev/null +++ b/src/detection/lm/lm_linux.c @@ -0,0 +1,168 @@ +#include "lm.h" +#include "common/properties.h" +#include "common/dbus.h" +#include "common/processing.h" +#include "detection/displayserver/displayserver.h" + +#include <unistd.h> + +#define FF_SYSTEMD_SESSIONS_PATH "/run/systemd/sessions/" +#define FF_SYSTEMD_USERS_PATH "/run/systemd/users/" + +static const char* getGdmVersion(FFstrbuf* version) { + const char* error = ffProcessAppendStdOut(version, (char* const[]) { "gdm", "--version", NULL }); + if (error || version->length == 0) { + error = ffProcessAppendStdOut(version, (char* const[]) { "gdm3", "--version", NULL }); + if (error || version->length == 0) { + return "Failed to get GDM version"; + } + } + + // GDM 44.1 + ffStrbufSubstrAfterFirstC(version, ' '); + return NULL; +} + +static const char* getSshdVersion(FFstrbuf* version) { + const char* error = ffProcessAppendStdErr(version, (char* const[]) { "sshd", "-V", NULL }); + if (error) { + return error; + } + + // OpenSSH_9.0p1, OpenSSL 3.0.9 30 May 2023... + ffStrbufSubstrBeforeFirstC(version, ','); + ffStrbufSubstrAfterFirstC(version, '_'); + return NULL; +} + +#ifdef FF_HAVE_ZLIB + #include "common/library.h" + #include <stdlib.h> + #include <zlib.h> + +static const char* getSddmVersion(FFstrbuf* version) { + FF_LIBRARY_LOAD_MESSAGE(zlib, "libz" FF_LIBRARY_EXTENSION, 2) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzopen) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzread) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzerror) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gztell) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzrewind) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(zlib, gzclose) + + gzFile file = ffgzopen(FASTFETCH_TARGET_DIR_USR "/share/man/man1/sddm.1.gz", "rb"); + if (file == Z_NULL) { + return "ffgzopen(\"/usr/share/man/man1/sddm.1.gz\", \"rb\") failed"; + } + + ffStrbufEnsureFree(version, 2047); + memset(version->chars, 0, version->allocated); + int size = ffgzread(file, version->chars, version->allocated - 1); + ffgzclose(file); + + if (size <= 0) { + return "ffgzread(file, version->chars, version->length) failed"; + } + + version->length = (uint32_t) size; + uint32_t index = ffStrbufFirstIndexS(version, ".TH "); + if (index == version->length) { + ffStrbufClear(version); + return ".TH is not found"; + } + + ffStrbufSubstrBefore(version, ffStrbufNextIndexC(version, index, '\n')); + ffStrbufSubstrAfter(version, index + (uint32_t) strlen(".TH ")); + + // "SDDM" 1 "May 2014" "sddm 0.20.0" "sddm" + ffStrbufSubstrBeforeLastC(version, ' '); + ffStrbufTrimRight(version, '"'); + ffStrbufSubstrAfterLastC(version, ' '); + + return NULL; +} +#else +static const char* getSddmVersion(FF_A_UNUSED FFstrbuf* version) { + return "Fastfetch is built without libz support"; +} +#endif + +static const char* getXfwmVersion(FFstrbuf* version) { + const char* error = ffProcessAppendStdOut(version, (char* const[]) { "xfwm4", "--version", NULL }); + if (error) { + return error; + } + + // This is xfwm4 version 4.18.0 (revision 7e7473c5b) for Xfce 4.18... + ffStrbufSubstrAfterFirstS(version, "version "); + ffStrbufSubstrBeforeFirstC(version, ' '); + + return NULL; +} + +static const char* getLightdmVersion(FFstrbuf* version) { + const char* error = ffProcessAppendStdErr(version, (char* const[]) { "lightdm", "--version", NULL }); + if (error) { + return error; + } + + // lightdm 1.30.0 + ffStrbufSubstrAfterFirstC(version, ' '); + ffStrbufTrimRight(version, '\n'); + + return NULL; +} + +const char* ffDetectLM(FFLMResult* result) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + + FF_STRBUF_AUTO_DESTROY sessionId = ffStrbufCreateS(getenv("XDG_SESSION_ID")); + if (sessionId.length == 0) { + // On some incorrectly configured systems, $XDG_SESSION_ID is not set. Try finding it ourself + // WARNING: This is private data. Do not parse + ffStrbufSetF(&path, FF_SYSTEMD_USERS_PATH "%d", instance.state.platform.uid); + + // This is actually buggy, and assumes current user is using DE + // `sd_pid_get_session` can be a better option, but we need to find a pid to use + if (!ffParsePropFileValues(path.chars, 1, (FFpropquery[]) { + { "DISPLAY=", &sessionId }, + })) { + return "Failed to get $XDG_SESSION_ID"; + } + } + + ffStrbufSetS(&path, FF_SYSTEMD_SESSIONS_PATH); + ffStrbufAppend(&path, &sessionId); + + // WARNING: This is private data. Do not parse + if (!ffParsePropFileValues(path.chars, 2, (FFpropquery[]) { + { "SERVICE=", &result->service }, + { "TYPE=", &result->type }, + })) { + return "Failed to parse " FF_SYSTEMD_SESSIONS_PATH "$XDG_SESSION_ID"; + } + + if (instance.config.general.detectVersion) { + if (ffStrbufStartsWithS(&result->service, "gdm")) { + getGdmVersion(&result->version); + } else if (ffStrbufStartsWithS(&result->service, "sddm")) { + getSddmVersion(&result->version); + } else if (ffStrbufStartsWithS(&result->service, "xfwm")) { + getXfwmVersion(&result->version); + } else if (ffStrbufStartsWithS(&result->service, "lightdm")) { + getLightdmVersion(&result->version); + } else if (ffStrbufStartsWithS(&result->service, "sshd")) { + getSshdVersion(&result->version); + } + } + + // Correct char cases + if (ffStrbufIgnCaseEqualS(&result->type, FF_WM_PROTOCOL_WAYLAND)) { + ffStrbufSetS(&result->type, FF_WM_PROTOCOL_WAYLAND); + } else if (ffStrbufIgnCaseEqualS(&result->type, FF_WM_PROTOCOL_X11)) { + ffStrbufSetS(&result->type, FF_WM_PROTOCOL_X11); + } else if (ffStrbufIgnCaseEqualS(&result->type, FF_WM_PROTOCOL_TTY)) { + ffStrbufSetS(&result->type, FF_WM_PROTOCOL_TTY); + } + + return NULL; +} diff --git a/src/detection/lm/lm_nosupport.c b/src/detection/lm/lm_nosupport.c new file mode 100644 index 0000000..22c1803 --- /dev/null +++ b/src/detection/lm/lm_nosupport.c @@ -0,0 +1,5 @@ +#include "lm.h" + +const char* ffDetectLM(FF_A_UNUSED FFLMResult* result) { + return "Not supported on this platform"; +} diff --git a/src/detection/loadavg/loadavg.h b/src/detection/loadavg/loadavg.h new file mode 100644 index 0000000..4cfa696 --- /dev/null +++ b/src/detection/loadavg/loadavg.h @@ -0,0 +1,5 @@ +#pragma once + +#include "fastfetch.h" + +const char* ffDetectLoadavg(double result[3]); diff --git a/src/detection/loadavg/loadavg_bsd.c b/src/detection/loadavg/loadavg_bsd.c new file mode 100644 index 0000000..83896b2 --- /dev/null +++ b/src/detection/loadavg/loadavg_bsd.c @@ -0,0 +1,23 @@ +#include "detection/loadavg/loadavg.h" + +#include <sys/sysctl.h> + +#if __FreeBSD__ || __OpenBSD__ || __NetBSD__ + #include <sys/types.h> + #include <sys/resource.h> + #if __FreeBSD__ + #include <vm/vm_param.h> + #endif +#endif + +const char* ffDetectLoadavg(double result[3]) { + struct loadavg load; + size_t size = sizeof(load); + if (sysctl((int[]) { CTL_VM, VM_LOADAVG }, 2, &load, &size, NULL, 0) < 0) { + return "sysctl({CTL_VM, VM_LOADAVG}) failed"; + } + for (int i = 0; i < 3; i++) { + result[i] = (double) load.ldavg[i] / (double) load.fscale; + } + return NULL; +} diff --git a/src/detection/loadavg/loadavg_linux.c b/src/detection/loadavg/loadavg_linux.c new file mode 100644 index 0000000..a169312 --- /dev/null +++ b/src/detection/loadavg/loadavg_linux.c @@ -0,0 +1,33 @@ +#include "detection/loadavg/loadavg.h" +#include "common/io.h" + +#include <sys/sysinfo.h> + +const char* ffDetectLoadavg(double result[3]) { +#ifndef __ANDROID__ // cat: /proc/loadavg: Permission denied + + // Don't use syscall for container compatibility. #620 + char buf[64]; + ssize_t nRead = ffReadFileData("/proc/loadavg", sizeof(buf) - 1, buf); + if (nRead > 0) { + buf[nRead] = '\0'; + + if (sscanf(buf, "%lf%lf%lf", &result[0], &result[1], &result[2]) == 3) { + return NULL; + } + } + +#endif +#ifndef __GNU__ + // getloadavg requires higher ANDROID_API version + struct sysinfo si; + if (sysinfo(&si) < 0) { + return "sysinfo() failed"; + } + + for (int i = 0; i < 3; i++) { + result[i] = (double) si.loads[i] / (1 << SI_LOAD_SHIFT); + } +#endif + return NULL; +} diff --git a/src/detection/loadavg/loadavg_nosupport.c b/src/detection/loadavg/loadavg_nosupport.c new file mode 100644 index 0000000..f14c717 --- /dev/null +++ b/src/detection/loadavg/loadavg_nosupport.c @@ -0,0 +1,5 @@ +#include "detection/loadavg/loadavg.h" + +const char* ffDetectLoadavg(FF_A_UNUSED double result[3]) { + return "Not supported on this platform"; +} diff --git a/src/detection/loadavg/loadavg_sunos.c b/src/detection/loadavg/loadavg_sunos.c new file mode 100644 index 0000000..91e3edd --- /dev/null +++ b/src/detection/loadavg/loadavg_sunos.c @@ -0,0 +1,7 @@ +#include "detection/loadavg/loadavg.h" + +#include <sys/loadavg.h> + +const char* ffDetectLoadavg(double result[3]) { + return getloadavg(result, 3) == 3 ? NULL : "getloadavg() failed"; +} diff --git a/src/detection/locale/locale.h b/src/detection/locale/locale.h new file mode 100644 index 0000000..50f0bad --- /dev/null +++ b/src/detection/locale/locale.h @@ -0,0 +1,5 @@ +#pragma once + +#include "fastfetch.h" + +const char* ffDetectLocale(FFstrbuf* result); diff --git a/src/detection/locale/locale_linux.c b/src/detection/locale/locale_linux.c new file mode 100644 index 0000000..4c04f26 --- /dev/null +++ b/src/detection/locale/locale_linux.c @@ -0,0 +1,22 @@ +#include "detection/locale/locale.h" + +#include <locale.h> + +const char* ffDetectLocale(FFstrbuf* result) { + ffStrbufAppendS(result, getenv("LC_ALL")); + if (result->length > 0) { + return NULL; + } + + ffStrbufAppendS(result, getenv("LANG")); + if (result->length > 0) { + return NULL; + } + + ffStrbufAppendS(result, setlocale(LC_TIME, NULL)); + if (result->length > 0) { + return NULL; + } + + return "Failed to detect locale"; +} diff --git a/src/detection/locale/locale_windows.c b/src/detection/locale/locale_windows.c new file mode 100644 index 0000000..b247365 --- /dev/null +++ b/src/detection/locale/locale_windows.c @@ -0,0 +1,16 @@ +#include "detection/locale/locale.h" +#include "common/windows/unicode.h" + +#include <windows.h> + +const char* ffDetectLocale(FFstrbuf* result) { + wchar_t name[LOCALE_NAME_MAX_LENGTH]; + int size = GetUserDefaultLocaleName(name, LOCALE_NAME_MAX_LENGTH); + if (size <= 1) { // including '\0' + return "GetUserDefaultLocaleName() failed"; + } + + ffStrbufSetNWS(result, (uint32_t) size - 1, name); + + return NULL; +} diff --git a/src/detection/localip/localip.h b/src/detection/localip/localip.h new file mode 100644 index 0000000..d3b9015 --- /dev/null +++ b/src/detection/localip/localip.h @@ -0,0 +1,47 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/localip/option.h" + +#ifndef IN6_IS_ADDR_GLOBAL + /* Global Unicast: 2000::/3 (001...) */ + #define IN6_IS_ADDR_GLOBAL(a) (((a)->s6_addr[0] & 0xE0) == 0x20) +#endif +#ifndef IN6_IS_ADDR_UNIQUE_LOCAL + /* Unique Local: fc00::/7 (1111 110...) */ + #define IN6_IS_ADDR_UNIQUE_LOCAL(a) (((a)->s6_addr[0] & 0xFE) == 0xFC) +#endif +#ifndef IN6_IS_ADDR_LINKLOCAL + /* Link-Local: fe80::/10 (1111 1110 10...) */ + #define IN6_IS_ADDR_LINKLOCAL(a) (((a)->s6_addr[0] == 0xFE) && (((a)->s6_addr[1] & 0xC0) == 0x80)) +#endif + +typedef struct FFLocalIpResult { + FFstrbuf name; + FFstrbuf ipv4; + FFstrbuf ipv6; + FFstrbuf mac; + FFstrbuf flags; + int32_t mtu; + int32_t speed; // in Mbps + FFLocalIpType defaultRoute; +} FFLocalIpResult; + +typedef struct FFLocalIpNIFlag { + uint32_t flag; + const char* name; +} FFLocalIpNIFlag; + +static inline void ffLocalIpFillNIFlags(FFstrbuf* buf, uint64_t flag, const FFLocalIpNIFlag names[]) { + for (const FFLocalIpNIFlag* nf = names; flag && nf->name; ++nf) { + if (flag & nf->flag) { + if (buf->length > 0) { + ffStrbufAppendC(buf, ','); + } + ffStrbufAppendS(buf, nf->name); + flag &= ~nf->flag; + } + } +} + +const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results); diff --git a/src/detection/localip/localip_linux.c b/src/detection/localip/localip_linux.c new file mode 100644 index 0000000..d07ec76 --- /dev/null +++ b/src/detection/localip/localip_linux.c @@ -0,0 +1,1011 @@ +#include "localip.h" +#include "common/io.h" +#include "common/netif.h" +#include "common/strutil.h" +#include "common/debug.h" + +#include <string.h> +#include <ctype.h> +#include <net/if.h> +#include <ifaddrs.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <stdio.h> +#include <sys/ioctl.h> +#include <inttypes.h> +#include <fcntl.h> + +#ifdef __linux__ + #include <linux/ethtool.h> + #include <linux/sockios.h> + #include <linux/if.h> + #include <linux/if_addr.h> +#endif + +#if __has_include(<netinet6/in6_var.h>) + #include <netinet6/in6_var.h> +#endif + +#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__APPLE__) || defined(__NetBSD__) || defined(__HAIKU__) + #include <net/if_media.h> + #include <net/if_dl.h> +#elif !defined(__GNU__) + #include <netpacket/packet.h> +#endif +#if defined(__sun) || defined(__HAIKU__) + #include <sys/sockio.h> +#endif +#if defined(__sun) + #include <kstat.h> + +static inline void kstatFreeWrap(kstat_ctl_t** pkc) { + assert(pkc); + if (*pkc) { + kstat_close(*pkc); + } +} +#endif + +#define FF_LOCALIP_NIFLAG(name) { IFF_##name, #name } + +static const FFLocalIpNIFlag niFlagOptions[] = { + FF_LOCALIP_NIFLAG(UP), + FF_LOCALIP_NIFLAG(BROADCAST), +#ifdef IFF_DEBUG + FF_LOCALIP_NIFLAG(DEBUG), +#endif + FF_LOCALIP_NIFLAG(LOOPBACK), + FF_LOCALIP_NIFLAG(POINTOPOINT), +#ifdef IFF_RUNNING + FF_LOCALIP_NIFLAG(RUNNING), +#endif + FF_LOCALIP_NIFLAG(NOARP), + FF_LOCALIP_NIFLAG(PROMISC), + FF_LOCALIP_NIFLAG(ALLMULTI), +#ifdef IFF_INTELLIGENT + FF_LOCALIP_NIFLAG(INTELLIGENT), +#endif + FF_LOCALIP_NIFLAG(MULTICAST), +#ifdef IFF_NOTRAILERS + FF_LOCALIP_NIFLAG(NOTRAILERS), +#endif +#if defined(__linux__) || defined(__GNU__) + FF_LOCALIP_NIFLAG(MASTER), + FF_LOCALIP_NIFLAG(SLAVE), + FF_LOCALIP_NIFLAG(PORTSEL), + FF_LOCALIP_NIFLAG(AUTOMEDIA), + FF_LOCALIP_NIFLAG(DYNAMIC), +#endif +#ifdef __linux__ + FF_LOCALIP_NIFLAG(LOWER_UP), + FF_LOCALIP_NIFLAG(DORMANT), + FF_LOCALIP_NIFLAG(ECHO), +#endif +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) + FF_LOCALIP_NIFLAG(OACTIVE), + FF_LOCALIP_NIFLAG(SIMPLEX), + FF_LOCALIP_NIFLAG(LINK0), + FF_LOCALIP_NIFLAG(LINK1), + FF_LOCALIP_NIFLAG(LINK2), +#endif +#ifdef IFF_ALTPHYS + FF_LOCALIP_NIFLAG(ALTPHYS), +#endif +#ifdef IFF_CANTCONFIG + FF_LOCALIP_NIFLAG(CANTCONFIG), +#endif +#ifdef __HAIKU__ + FF_LOCALIP_NIFLAG(AUTOUP), + FF_LOCALIP_NIFLAG(SIMPLEX), + FF_LOCALIP_NIFLAG(LINK), + FF_LOCALIP_NIFLAG(AUTO_CONFIGURED), + FF_LOCALIP_NIFLAG(CONFIGURING), +#endif +#ifdef __sun + FF_LOCALIP_NIFLAG(MULTI_BCAST), + FF_LOCALIP_NIFLAG(UNNUMBERED), + FF_LOCALIP_NIFLAG(DHCPRUNNING), + FF_LOCALIP_NIFLAG(PRIVATE), +#endif + // sentinel + {}, +}; + +static FFLocalIpIpv6Type getIpv6Type(struct ifaddrs* ifa) { + struct sockaddr_in6* addr = (struct sockaddr_in6*) ifa->ifa_addr; + + FF_DEBUG("Checking IPv6 type for interface %s", ifa->ifa_name); + + FFLocalIpIpv6Type result = FF_LOCALIP_IPV6_TYPE_NONE; + if (IN6_IS_ADDR_GLOBAL(&addr->sin6_addr)) { + result = FF_LOCALIP_IPV6_TYPE_GUA_BIT; + FF_DEBUG("Interface %s has Global Unicast Address", ifa->ifa_name); + } else if (IN6_IS_ADDR_UNIQUE_LOCAL(&addr->sin6_addr)) { + result = FF_LOCALIP_IPV6_TYPE_ULA_BIT; + FF_DEBUG("Interface %s has Unique Local Address", ifa->ifa_name); + } else if (IN6_IS_ADDR_LINKLOCAL(&addr->sin6_addr)) { + result = FF_LOCALIP_IPV6_TYPE_LLA_BIT; + FF_DEBUG("Interface %s has Link-Local Address", ifa->ifa_name); + } else { + FF_DEBUG("Interface %s has unknown IPv6 address type", ifa->ifa_name); + return FF_LOCALIP_IPV6_TYPE_UNKNOWN_BIT; + } + +#ifdef SIOCGIFAFLAG_IN6 + static int sockfd = 0; + if (sockfd == 0) { + sockfd = socket(AF_INET6, SOCK_DGRAM + #ifdef SOCK_CLOEXEC + | SOCK_CLOEXEC + #endif + , + 0); + #ifndef SOCK_CLOEXEC + if (sockfd > 0) { + fcntl(sockfd, F_SETFD, FD_CLOEXEC); + } + #endif + } + if (sockfd < 0) { + return result; + } + + struct in6_ifreq ifr6 = {}; + ffStrCopy(ifr6.ifr_name, ifa->ifa_name, IFNAMSIZ); + ifr6.ifr_addr = *addr; + + if (ioctl(sockfd, SIOCGIFAFLAG_IN6, &ifr6) != 0) { + return result; + } + + #ifdef IN6_IFF_PREFER_SOURCE + if (ifr6.ifr_ifru.ifru_flags6 & IN6_IFF_PREFER_SOURCE) { + result |= FF_LOCALIP_IPV6_TYPE_PREFERRED_BIT; + } + #endif + if (ifr6.ifr_ifru.ifru_flags6 & (IN6_IFF_DEPRECATED | IN6_IFF_TEMPORARY | IN6_IFF_TENTATIVE | IN6_IFF_DUPLICATED + #ifdef IN6_IFF_OPTIMISTIC + | IN6_IFF_OPTIMISTIC + #endif + )) + result |= FF_LOCALIP_IPV6_TYPE_SECONDARY_BIT; + return result; +#elif __linux__ + static FFlist addresses = {}; + static bool initialized = false; + if (!initialized) { + initialized = true; + ffListInit(&addresses); + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + if (!ffReadFileBuffer("/proc/net/if_inet6", &buffer)) { + return result; + } + + char* line = NULL; + size_t len = 0; + while (ffStrbufGetline(&line, &len, &buffer)) { + struct in6_addr* entry = FF_LIST_ADD(struct in6_addr, addresses); + uint8_t flags; + if (sscanf(line, "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 "%2" SCNx8 " %*s %*s %*s %" SCNx8 " %*s", &entry->s6_addr[0], &entry->s6_addr[1], &entry->s6_addr[2], &entry->s6_addr[3], &entry->s6_addr[4], &entry->s6_addr[5], &entry->s6_addr[6], &entry->s6_addr[7], &entry->s6_addr[8], &entry->s6_addr[9], &entry->s6_addr[10], &entry->s6_addr[11], &entry->s6_addr[12], &entry->s6_addr[13], &entry->s6_addr[14], &entry->s6_addr[15], &flags) != 17 || + (!IN6_IS_ADDR_GLOBAL(entry) && !IN6_IS_ADDR_UNIQUE_LOCAL(entry)) || + (flags & (IFA_F_DEPRECATED | IFA_F_TEMPORARY | IFA_F_TENTATIVE | IFA_F_DADFAILED | IFA_F_OPTIMISTIC))) { + --addresses.length; + } + } + } + if (addresses.capacity == 0) { + return result; + } + + FF_LIST_FOR_EACH (struct in6_addr, entry, addresses) { + if (memcmp(&addr->sin6_addr, entry, sizeof(struct in6_addr)) == 0) { + return result; + } + } + result |= FF_LOCALIP_IPV6_TYPE_SECONDARY_BIT; + return result; +#elif __sun + if (ifa->ifa_flags & IFF_PREFERRED) { + result |= FF_LOCALIP_IPV6_TYPE_PREFERRED_BIT; + } + if (ifa->ifa_flags & (IFF_DEPRECATED | IFF_TEMPORARY | IFF_DUPLICATE)) { + result |= FF_LOCALIP_IPV6_TYPE_SECONDARY_BIT; + } + return result; +#else + return result; +#endif +} + +typedef struct { + struct ifaddrs* mac; + FFlist /*<struct ifaddrs*>*/ ipv4; + FFlist /*<struct ifaddrs*>*/ ipv6; +} FFAdapter; + +static void appendIpv4(const FFLocalIpOptions* options, FFstrbuf* buffer, const struct ifaddrs* ifa) { + struct sockaddr_in* ipv4 = (struct sockaddr_in*) ifa->ifa_addr; + + char addressBuffer[INET_ADDRSTRLEN + 16]; + inet_ntop(AF_INET, &ipv4->sin_addr, addressBuffer, INET_ADDRSTRLEN); + + FF_DEBUG("Adding IPv4 address %s for interface %s", addressBuffer, ifa->ifa_name); + + if (options->showType & FF_LOCALIP_TYPE_PREFIX_LEN_BIT) { + struct sockaddr_in* netmask = (struct sockaddr_in*) ifa->ifa_netmask; + int cidr = __builtin_popcount(netmask->sin_addr.s_addr); + if (cidr != 0) { + size_t len = strlen(addressBuffer); + snprintf(addressBuffer + len, 16, "/%d", cidr); + } + } + + if (buffer->length) { + ffStrbufAppendC(buffer, ','); + } + ffStrbufAppendS(buffer, addressBuffer); +} + +static void appendIpv6(const FFLocalIpOptions* options, FFstrbuf* buffer, const struct ifaddrs* ifa) { + struct sockaddr_in6* ipv6 = (struct sockaddr_in6*) ifa->ifa_addr; + + char addressBuffer[INET6_ADDRSTRLEN + 16]; + inet_ntop(AF_INET6, &ipv6->sin6_addr, addressBuffer, INET6_ADDRSTRLEN); + + FF_DEBUG("Adding IPv6 address %s for interface %s", addressBuffer, ifa->ifa_name); + + if (options->showType & FF_LOCALIP_TYPE_PREFIX_LEN_BIT) { + struct sockaddr_in6* netmask = (struct sockaddr_in6*) ifa->ifa_netmask; + int cidr = 0; + static_assert(sizeof(netmask->sin6_addr) % sizeof(uint64_t) == 0, ""); + for (uint32_t i = 0; i < sizeof(netmask->sin6_addr) / sizeof(uint64_t); ++i) { + cidr += __builtin_popcountll(((uint64_t*) &netmask->sin6_addr)[i]); + } + if (cidr != 0) { + size_t len = strlen(addressBuffer); + snprintf(addressBuffer + len, 16, "/%d", cidr); + } + } + + if (buffer->length) { + ffStrbufAppendC(buffer, ','); + } + ffStrbufAppendS(buffer, addressBuffer); +} + +const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) { + FF_DEBUG("Starting local IP detection with showType=0x%x, namePrefix='%s'", + options->showType, + options->namePrefix.chars); + + struct ifaddrs* ifAddrStruct = NULL; + if (getifaddrs(&ifAddrStruct) < 0) { + FF_DEBUG("getifaddrs() failed"); + return "getifaddrs(&ifAddrStruct) failed"; + } + + FF_DEBUG("Successfully retrieved interface addresses"); + + FF_LIST_AUTO_DESTROY adapters = ffListCreate(); + + for (struct ifaddrs* ifa = ifAddrStruct; ifa; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) { + FF_DEBUG("Skipping interface %s (no address)", ifa->ifa_name); + continue; + } + +#ifdef IFF_RUNNING + if (!(ifa->ifa_flags & IFF_RUNNING)) { + FF_DEBUG("Skipping interface %s (not running)", ifa->ifa_name); + continue; + } +#endif + + if ((ifa->ifa_flags & IFF_LOOPBACK) && !(options->showType & FF_LOCALIP_TYPE_LOOP_BIT)) { + FF_DEBUG("Skipping loopback interface %s", ifa->ifa_name); + continue; + } + + if (options->namePrefix.length && strncmp(ifa->ifa_name, options->namePrefix.chars, options->namePrefix.length) != 0) { + FF_DEBUG("Skipping interface %s (doesn't match prefix '%s')", + ifa->ifa_name, + options->namePrefix.chars); + continue; + } + + if (!(options->showType & FF_LOCALIP_TYPE_MAC_BIT) && + ifa->ifa_addr->sa_family != AF_INET && ifa->ifa_addr->sa_family != AF_INET6) { + FF_DEBUG("Skipping interface %s (unsupported address family %d)", + ifa->ifa_name, + ifa->ifa_addr->sa_family); + continue; + } + + if (options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) { + // If the interface is not the default route for either IPv4 or IPv6, skip it + if (!((options->showType & FF_LOCALIP_TYPE_IPV4_BIT) && ffStrEquals(ffNetifGetDefaultRouteV4()->ifName, ifa->ifa_name)) && + !((options->showType & FF_LOCALIP_TYPE_IPV6_BIT) && ffStrEquals(ffNetifGetDefaultRouteV6()->ifName, ifa->ifa_name))) { + FF_DEBUG("Skipping interface %s (not default route interface)", ifa->ifa_name); + continue; + } + } + + FF_DEBUG("Processing interface %s (family=%d, flags=0x%lx)", + ifa->ifa_name, + ifa->ifa_addr->sa_family, + (unsigned long) ifa->ifa_flags); + + FFAdapter* adapter = NULL; + FF_LIST_FOR_EACH (FFAdapter, x, adapters) { + if (ffStrEquals(x->mac->ifa_name, ifa->ifa_name)) { + adapter = x; + break; + } + } + if (!adapter) { + adapter = FF_LIST_ADD(FFAdapter, adapters); + *adapter = (FFAdapter) { + .mac = ifa, + .ipv4 = ffListCreate(), + .ipv6 = ffListCreate(), + }; + FF_DEBUG("Created new adapter entry for interface %s", ifa->ifa_name); + } + + switch (ifa->ifa_addr->sa_family) { + case AF_INET: + if (options->showType & FF_LOCALIP_TYPE_IPV4_BIT) { + *FF_LIST_ADD(struct ifaddrs*, adapter->ipv4) = ifa; + FF_DEBUG("Added IPv4 entry for interface %s", ifa->ifa_name); + } + break; + case AF_INET6: + if (options->showType & FF_LOCALIP_TYPE_IPV6_BIT) { + *FF_LIST_ADD(struct ifaddrs*, adapter->ipv6) = ifa; + FF_DEBUG("Added IPv6 entry for interface %s", ifa->ifa_name); + } + break; +#if __FreeBSD__ || __OpenBSD__ || __APPLE__ || __NetBSD__ || __HAIKU__ + case AF_LINK: + adapter->mac = ifa; + FF_DEBUG("Updated MAC entry for interface %s", ifa->ifa_name); + break; +#elif !__sun && !__GNU__ + case AF_PACKET: + adapter->mac = ifa; + FF_DEBUG("Updated MAC entry for interface %s", ifa->ifa_name); + break; +#endif + } + } + + FF_DEBUG("Found %u network adapters", adapters.length); + + FF_LIST_FOR_EACH (FFAdapter, adapter, adapters) { + FF_DEBUG("Processing adapter %s (IPv4 entries: %u, IPv6 entries: %u)", + adapter->mac->ifa_name, + adapter->ipv4.length, + adapter->ipv6.length); + + if (adapter->ipv4.length == 0 && adapter->ipv6.length == 0 && + !(options->showType & FF_LOCALIP_TYPE_MAC_BIT)) { + FF_DEBUG("Skipping interface %s (no IP addresses)", adapter->mac->ifa_name); + continue; + } + + FFLocalIpResult* item = FF_LIST_ADD(FFLocalIpResult, *results); + ffStrbufInitS(&item->name, adapter->mac->ifa_name); + ffStrbufInit(&item->ipv4); + ffStrbufInit(&item->ipv6); + ffStrbufInit(&item->mac); + ffStrbufInit(&item->flags); + item->defaultRoute = FF_LOCALIP_TYPE_NONE; + item->mtu = -1; + item->speed = -1; + + if (options->showType & FF_LOCALIP_TYPE_FLAGS_BIT) { + ffLocalIpFillNIFlags(&item->flags, adapter->mac->ifa_flags, niFlagOptions); + FF_DEBUG("Added flags for interface %s: %s", adapter->mac->ifa_name, item->flags.chars); + } + + if ((options->showType & FF_LOCALIP_TYPE_IPV4_BIT)) { + const FFNetifDefaultRouteResult* defaultRouteV4 = ffNetifGetDefaultRouteV4(); + bool isDefaultRouteIf = ffStrEquals(defaultRouteV4->ifName, adapter->mac->ifa_name); + + if (isDefaultRouteIf) { + item->defaultRoute |= FF_LOCALIP_TYPE_IPV4_BIT; + FF_DEBUG("Interface %s is IPv4 default route", adapter->mac->ifa_name); + } + + if (options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) { + if (!isDefaultRouteIf) { + FF_DEBUG("Skipping IPv4 for interface %s (not default route)", adapter->mac->ifa_name); + goto v6; + } + } + + if (!(options->showType & FF_LOCALIP_TYPE_ALL_IPS_BIT)) { + struct ifaddrs* ifa = NULL; + if (isDefaultRouteIf && defaultRouteV4->preferredSourceAddrV4 != 0) { + FF_LIST_FOR_EACH (struct ifaddrs*, pifa, adapter->ipv4) { + struct sockaddr_in* ipv4 = (struct sockaddr_in*) (*pifa)->ifa_addr; + if (ipv4->sin_addr.s_addr == defaultRouteV4->preferredSourceAddrV4) { + ifa = *pifa; + FF_DEBUG("Found preferred IPv4 source address for interface %s", adapter->mac->ifa_name); + break; + } + } + } + if (ifa) { + appendIpv4(options, &item->ipv4, ifa); + } else if (adapter->ipv4.length > 0) { + appendIpv4(options, &item->ipv4, *FF_LIST_FIRST(struct ifaddrs*, adapter->ipv4)); + FF_DEBUG("Using first IPv4 address for interface %s", adapter->mac->ifa_name); + } + } else { + FF_DEBUG("Adding all IPv4 addresses for interface %s", adapter->mac->ifa_name); + FF_LIST_FOR_EACH (struct ifaddrs*, pifa, adapter->ipv4) { + appendIpv4(options, &item->ipv4, *pifa); + } + } + } + v6: + if ((options->showType & FF_LOCALIP_TYPE_IPV6_BIT)) { + const FFNetifDefaultRouteResult* defaultRouteV6 = ffNetifGetDefaultRouteV6(); + bool isDefaultRouteIf = ffStrEquals(defaultRouteV6->ifName, adapter->mac->ifa_name); + + if (isDefaultRouteIf) { + item->defaultRoute |= FF_LOCALIP_TYPE_IPV6_BIT; + FF_DEBUG("Interface %s is IPv6 default route", adapter->mac->ifa_name); + } + + if (options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) { + if (!isDefaultRouteIf) { + FF_DEBUG("Skipping IPv6 for interface %s (not default route)", adapter->mac->ifa_name); + goto mac; + } + } + + if (options->ipv6Type == FF_LOCALIP_IPV6_TYPE_AUTO) { + if (!(options->showType & FF_LOCALIP_TYPE_ALL_IPS_BIT)) { + struct ifaddrs* selected = NULL; + struct ifaddrs* secondary = NULL; + + FF_LIST_FOR_EACH (struct ifaddrs*, pifa, adapter->ipv6) { + FFLocalIpIpv6Type type = getIpv6Type(*pifa); + if (type & FF_LOCALIP_IPV6_TYPE_PREFERRED_BIT) { + selected = *pifa; + FF_DEBUG("Found preferred IPv6 address for interface %s", adapter->mac->ifa_name); + break; + } else if ((type & FF_LOCALIP_IPV6_TYPE_GUA_BIT) && !(type & FF_LOCALIP_IPV6_TYPE_SECONDARY_BIT) && !selected) { + selected = *pifa; + FF_DEBUG("Found GUA IPv6 address for interface %s", adapter->mac->ifa_name); + } else if ((type & FF_LOCALIP_IPV6_TYPE_ULA_BIT) && !(type & FF_LOCALIP_IPV6_TYPE_SECONDARY_BIT) && !secondary) { + secondary = *pifa; + FF_DEBUG("Found ULA IPv6 address for interface %s", adapter->mac->ifa_name); + } + } + if (!selected) { + selected = secondary; + } + + if (selected) { + appendIpv6(options, &item->ipv6, selected); + } else if (adapter->ipv6.length > 0) { + appendIpv6(options, &item->ipv6, *FF_LIST_FIRST(struct ifaddrs*, adapter->ipv6)); + FF_DEBUG("Using first IPv6 address for interface %s", adapter->mac->ifa_name); + } + } else { + FF_DEBUG("Adding all IPv6 addresses for interface %s", adapter->mac->ifa_name); + FF_LIST_FOR_EACH (struct ifaddrs*, pifa, adapter->ipv6) { + appendIpv6(options, &item->ipv6, *pifa); + } + } + } else { + FF_LIST_FOR_EACH (struct ifaddrs*, pifa, adapter->ipv6) { + FFLocalIpIpv6Type type = getIpv6Type(*pifa); + if (type & options->ipv6Type) { + if ((options->showType & FF_LOCALIP_TYPE_ALL_IPS_BIT) || !(type & FF_LOCALIP_IPV6_TYPE_SECONDARY_BIT)) { + appendIpv6(options, &item->ipv6, *pifa); + if (!(options->showType & FF_LOCALIP_TYPE_ALL_IPS_BIT)) { + break; + } + } + } + } + } + } + mac: +#if !defined(__sun) && !defined(__GNU__) + if (options->showType & FF_LOCALIP_TYPE_MAC_BIT) { + if (adapter->mac->ifa_addr) { + #if __FreeBSD__ || __OpenBSD__ || __APPLE__ || __NetBSD__ || __HAIKU__ + uint8_t* ptr = (uint8_t*) LLADDR((struct sockaddr_dl*) adapter->mac->ifa_addr); + #else + uint8_t* ptr = ((struct sockaddr_ll*) adapter->mac->ifa_addr)->sll_addr; + #endif + ffStrbufSetF(&item->mac, "%02x:%02x:%02x:%02x:%02x:%02x", ptr[0], ptr[1], ptr[2], ptr[3], ptr[4], ptr[5]); + FF_DEBUG("Added MAC address %s for interface %s", item->mac.chars, adapter->mac->ifa_name); + } else { + FF_DEBUG("No MAC address available for interface %s", adapter->mac->ifa_name); + } + } +#else + (void) adapter; +#endif + } + + FF_LIST_FOR_EACH (FFAdapter, adapter, adapters) { + ffListDestroy(&adapter->ipv4); + ffListDestroy(&adapter->ipv6); + } + + if (ifAddrStruct) { + freeifaddrs(ifAddrStruct); + ifAddrStruct = NULL; + FF_DEBUG("Cleaned up interface address structures"); + } + + if ((options->showType & FF_LOCALIP_TYPE_MTU_BIT) || (options->showType & FF_LOCALIP_TYPE_SPEED_BIT) +#ifdef __sun + || (options->showType & FF_LOCALIP_TYPE_MAC_BIT) +#endif + ) { + FF_DEBUG("Retrieving additional interface properties (MTU/Speed/MAC)"); + FF_AUTO_CLOSE_FD int sockfd = socket(AF_INET, SOCK_DGRAM, 0); + if (sockfd > 0) { + FF_LIST_FOR_EACH (FFLocalIpResult, iface, *results) { + struct ifreq ifr = {}; + ffStrCopy(ifr.ifr_name, iface->name.chars, IFNAMSIZ); + + if (options->showType & FF_LOCALIP_TYPE_MTU_BIT) { + if (ioctl(sockfd, SIOCGIFMTU, &ifr) == 0) { + iface->mtu = (int32_t) ifr.ifr_mtu; + FF_DEBUG("Interface %s MTU: %d", iface->name.chars, iface->mtu); + } else { + FF_DEBUG("Failed to get MTU for interface %s", iface->name.chars); + } + } + + if (options->showType & FF_LOCALIP_TYPE_SPEED_BIT) { +#ifdef __linux__ + struct ethtool_cmd edata = { .cmd = ETHTOOL_GSET }; + ifr.ifr_data = (void*) &edata; + if (ioctl(sockfd, SIOCETHTOOL, &ifr) == 0) { + iface->speed = (edata.speed_hi << 16) | edata.speed; + FF_DEBUG("Interface %s speed: %d Mbps", iface->name.chars, iface->speed); + } else { + // ethtool_cmd_speed is not available on Android + FF_DEBUG("Failed to get speed for interface %s via ethtool", iface->name.chars); + } +#elif __FreeBSD__ || __APPLE__ || __OpenBSD__ || __NetBSD__ + struct ifmediareq ifmr = {}; + ffStrCopy(ifmr.ifm_name, iface->name.chars, IFNAMSIZ); + if (ioctl(sockfd, SIOCGIFMEDIA, &ifmr) == 0 && (IFM_TYPE(ifmr.ifm_active) & IFM_ETHER)) { + FF_DEBUG("Interface %s media type: 0x%x", iface->name.chars, (unsigned) IFM_SUBTYPE(ifmr.ifm_active)); + switch (IFM_SUBTYPE(ifmr.ifm_active)) { + #ifdef IFM_HPNA_1 + case IFM_HPNA_1: + #endif + iface->speed = 1; + break; + #ifdef IFM_1000_CX + case IFM_1000_CX: + #endif + #ifdef IFM_1000_CX_SGMII + case IFM_1000_CX_SGMII: + #endif + #ifdef IFM_1000_KX + case IFM_1000_KX: + #endif + #ifdef IFM_1000_LX + case IFM_1000_LX: + #endif + #ifdef IFM_1000_SGMII + case IFM_1000_SGMII: + #endif + #ifdef IFM_1000_SX + case IFM_1000_SX: + #endif + #ifdef IFM_1000_T + case IFM_1000_T: + #endif + iface->speed = 1000; + break; + #ifdef IFM_100G_AUI2 + case IFM_100G_AUI2: + #endif + #ifdef IFM_100G_AUI2_AC + case IFM_100G_AUI2_AC: + #endif + #ifdef IFM_100G_AUI4 + case IFM_100G_AUI4: + #endif + #ifdef IFM_100G_AUI4_AC + case IFM_100G_AUI4_AC: + #endif + #ifdef IFM_100G_CAUI2 + case IFM_100G_CAUI2: + #endif + #ifdef IFM_100G_CAUI2_AC + case IFM_100G_CAUI2_AC: + #endif + #ifdef IFM_100G_CAUI4 + case IFM_100G_CAUI4: + #endif + #ifdef IFM_100G_CAUI4_AC + case IFM_100G_CAUI4_AC: + #endif + #ifdef IFM_100G_CP2 + case IFM_100G_CP2: + #endif + #ifdef IFM_100G_CR4 + case IFM_100G_CR4: + #endif + #ifdef IFM_100G_CR_PAM4 + case IFM_100G_CR_PAM4: + #endif + #ifdef IFM_100G_DR + case IFM_100G_DR: + #endif + #ifdef IFM_100G_KR2_PAM4 + case IFM_100G_KR2_PAM4: + #endif + #ifdef IFM_100G_KR4 + case IFM_100G_KR4: + #endif + #ifdef IFM_100G_KR_PAM4 + case IFM_100G_KR_PAM4: + #endif + #ifdef IFM_100G_LR4 + case IFM_100G_LR4: + #endif + #ifdef IFM_100G_SR2 + case IFM_100G_SR2: + #endif + #ifdef IFM_100G_SR4 + case IFM_100G_SR4: + #endif + iface->speed = 100000; + break; + #ifdef IFM_100_FX + case IFM_100_FX: + #endif + #ifdef IFM_100_SGMII + case IFM_100_SGMII: + #endif + #ifdef IFM_100_T + case IFM_100_T: + #endif + #ifdef IFM_100_T2 + case IFM_100_T2: + #endif + #ifdef IFM_100_T4 + case IFM_100_T4: + #endif + #ifdef IFM_100_TX + case IFM_100_TX: + #endif + #ifdef IFM_100_VG + case IFM_100_VG: + #endif + iface->speed = 100; + break; + #ifdef IFM_10G_AOC + case IFM_10G_AOC: + #endif + #ifdef IFM_10G_CR1 + case IFM_10G_CR1: + #endif + #ifdef IFM_10G_CX4 + case IFM_10G_CX4: + #endif + #ifdef IFM_10G_ER + case IFM_10G_ER: + #endif + #ifdef IFM_10G_KR + case IFM_10G_KR: + #endif + #ifdef IFM_10G_KX4 + case IFM_10G_KX4: + #endif + #ifdef IFM_10G_LR + case IFM_10G_LR: + #endif + #ifdef IFM_10G_LRM + case IFM_10G_LRM: + #endif + #ifdef IFM_10G_SFI + case IFM_10G_SFI: + #endif + #ifdef IFM_10G_SR + case IFM_10G_SR: + #endif + #ifdef IFM_10G_T + case IFM_10G_T: + #endif + #ifdef IFM_10G_TWINAX + case IFM_10G_TWINAX: + #endif + #ifdef IFM_10G_TWINAX_LONG + case IFM_10G_TWINAX_LONG: + #endif + iface->speed = 10000; + break; + #ifdef IFM_10_2 + case IFM_10_2: + #endif + #ifdef IFM_10_5 + case IFM_10_5: + #endif + #ifdef IFM_10_FL + case IFM_10_FL: + #endif + #ifdef IFM_10_STP + case IFM_10_STP: + #endif + #ifdef IFM_10_T + case IFM_10_T: + #endif + iface->speed = 10; + break; + #ifdef IFM_200G_AUI4 + case IFM_200G_AUI4: + #endif + #ifdef IFM_200G_AUI4_AC + case IFM_200G_AUI4_AC: + #endif + #ifdef IFM_200G_AUI8 + case IFM_200G_AUI8: + #endif + #ifdef IFM_200G_AUI8_AC + case IFM_200G_AUI8_AC: + #endif + #ifdef IFM_200G_CR4_PAM4 + case IFM_200G_CR4_PAM4: + #endif + #ifdef IFM_200G_DR4 + case IFM_200G_DR4: + #endif + #ifdef IFM_200G_FR4 + case IFM_200G_FR4: + #endif + #ifdef IFM_200G_KR4_PAM4 + case IFM_200G_KR4_PAM4: + #endif + #ifdef IFM_200G_LR4 + case IFM_200G_LR4: + #endif + #ifdef IFM_200G_SR4 + case IFM_200G_SR4: + #endif + iface->speed = 200000; + break; + #ifdef IFM_20G_KR2 + case IFM_20G_KR2: + #endif + iface->speed = 20000; + break; + #ifdef IFM_2500_KX + case IFM_2500_KX: + #endif + #ifdef IFM_2500_SX + case IFM_2500_SX: + #endif + #ifdef IFM_2500_T + case IFM_2500_T: + #endif + #ifdef IFM_2500_X + case IFM_2500_X: + #endif + iface->speed = 2500; + break; + #ifdef IFM_25G_ACC + case IFM_25G_ACC: + #endif + #ifdef IFM_25G_AOC + case IFM_25G_AOC: + #endif + #ifdef IFM_25G_AUI + case IFM_25G_AUI: + #endif + #ifdef IFM_25G_CR + case IFM_25G_CR: + #endif + #ifdef IFM_25G_CR1 + case IFM_25G_CR1: + #endif + #ifdef IFM_25G_CR_S + case IFM_25G_CR_S: + #endif + #ifdef IFM_25G_KR + case IFM_25G_KR: + #endif + #ifdef IFM_25G_KR1 + case IFM_25G_KR1: + #endif + #ifdef IFM_25G_KR_S + case IFM_25G_KR_S: + #endif + #ifdef IFM_25G_LR + case IFM_25G_LR: + #endif + #ifdef IFM_25G_PCIE + case IFM_25G_PCIE: + #endif + #ifdef IFM_25G_SR + case IFM_25G_SR: + #endif + #ifdef IFM_25G_T + case IFM_25G_T: + #endif + iface->speed = 25000; + break; + #ifdef IFM_400G_AUI8 + case IFM_400G_AUI8: + #endif + #ifdef IFM_400G_AUI8_AC + case IFM_400G_AUI8_AC: + #endif + #ifdef IFM_400G_DR4 + case IFM_400G_DR4: + #endif + #ifdef IFM_400G_FR8 + case IFM_400G_FR8: + #endif + #ifdef IFM_400G_LR8 + case IFM_400G_LR8: + #endif + iface->speed = 400000; + break; + #ifdef IFM_40G_CR4 + case IFM_40G_CR4: + #endif + #ifdef IFM_40G_ER4 + case IFM_40G_ER4: + #endif + #ifdef IFM_40G_KR4 + case IFM_40G_KR4: + #endif + #ifdef IFM_40G_LR4 + case IFM_40G_LR4: + #endif + #ifdef IFM_40G_SR4 + case IFM_40G_SR4: + #endif + #ifdef IFM_40G_XLAUI + case IFM_40G_XLAUI: + #endif + #ifdef IFM_40G_XLAUI_AC + case IFM_40G_XLAUI_AC: + #endif + #ifdef IFM_40G_XLPPI + case IFM_40G_XLPPI: + #endif + #ifdef IFM_40G_LM4 + case IFM_40G_LM4: + #endif + iface->speed = 40000; + break; + #ifdef IFM_5000_KR + case IFM_5000_KR: + #endif + #ifdef IFM_5000_KR1 + case IFM_5000_KR1: + #endif + #ifdef IFM_5000_KR_S + case IFM_5000_KR_S: + #endif + #ifdef IFM_5000_T + case IFM_5000_T: + #endif + iface->speed = 5000; + break; + #ifdef IFM_50G_AUI1 + case IFM_50G_AUI1: + #endif + #ifdef IFM_50G_AUI1_AC + case IFM_50G_AUI1_AC: + #endif + #ifdef IFM_50G_AUI2 + case IFM_50G_AUI2: + #endif + #ifdef IFM_50G_AUI2_AC + case IFM_50G_AUI2_AC: + #endif + #ifdef IFM_50G_CP + case IFM_50G_CP: + #endif + #ifdef IFM_50G_CR2 + case IFM_50G_CR2: + #endif + #ifdef IFM_50G_FR + case IFM_50G_FR: + #endif + #ifdef IFM_50G_KR2 + case IFM_50G_KR2: + #endif + #ifdef IFM_50G_KR_PAM4 + case IFM_50G_KR_PAM4: + #endif + #ifdef IFM_50G_LAUI2 + case IFM_50G_LAUI2: + #endif + #ifdef IFM_50G_LAUI2_AC + case IFM_50G_LAUI2_AC: + #endif + #ifdef IFM_50G_LR + case IFM_50G_LR: + #endif + #ifdef IFM_50G_LR2 + case IFM_50G_LR2: + #endif + #ifdef IFM_50G_PCIE + case IFM_50G_PCIE: + #endif + #ifdef IFM_50G_SR + case IFM_50G_SR: + #endif + #ifdef IFM_50G_SR2 + case IFM_50G_SR2: + #endif + #ifdef IFM_50G_KR4 + case IFM_50G_KR4: + #endif + iface->speed = 50000; + break; + #ifdef IFM_56G_R4 + case IFM_56G_R4: + #endif + iface->speed = 56000; + break; + default: + iface->speed = -1; + FF_DEBUG("Unknown media subtype for interface %s", iface->name.chars); + break; + } + if (iface->speed > 0) { + FF_DEBUG("Interface %s speed: %d Mbps", iface->name.chars, iface->speed); + } + } else { + FF_DEBUG("Failed to get media info for interface %s", iface->name.chars); + } +#endif + } + +#if __sun || __GNU__ + if ((options->showType & FF_LOCALIP_TYPE_MAC_BIT) && ioctl(sockfd, SIOCGIFHWADDR, &ifr) == 0) { + const uint8_t* ptr = (uint8_t*) ifr.ifr_addr.sa_data; // NOT ifr_enaddr + ffStrbufSetF(&iface->mac, "%02x:%02x:%02x:%02x:%02x:%02x", ptr[0], ptr[1], ptr[2], ptr[3], ptr[4], ptr[5]); + FF_DEBUG("Added MAC address %s for interface %s (Solaris/GNU)", iface->mac.chars, iface->name.chars); + } +#endif +#if __sun + if (options->showType & FF_LOCALIP_TYPE_SPEED_BIT) { + FF_A_CLEANUP(kstatFreeWrap) kstat_ctl_t* kc = kstat_open(); + for (kstat_t* ks = kc->kc_chain; ks; ks = ks->ks_next) { + if (!ffStrEquals(ks->ks_class, "net") || !ffStrEquals(ks->ks_module, "link")) { + continue; + } + if (ffStrbufEqualS(&iface->name, ks->ks_name)) { + if (kstat_read(kc, ks, NULL) >= 0) { + kstat_named_t* ifspeed = (kstat_named_t*) kstat_data_lookup(ks, "ifspeed"); + if (ifspeed) { + iface->speed = (int32_t) (ifspeed->value.ui64 / 1000 / 1000); + FF_DEBUG("Interface %s speed: %d Mbps (kstat)", iface->name.chars, iface->speed); + } + } + break; + } + } + } +#endif + } + } else { + FF_DEBUG("Failed to create socket for interface property retrieval"); + } + } + + FF_DEBUG("Local IP detection completed, found %u interfaces", results->length); + return NULL; +} diff --git a/src/detection/localip/localip_windows.c b/src/detection/localip/localip_windows.c new file mode 100644 index 0000000..6a7e0e7 --- /dev/null +++ b/src/detection/localip/localip_windows.c @@ -0,0 +1,270 @@ +#include <ws2tcpip.h> +#include <iphlpapi.h> + +#include "common/netif.h" +#include "common/mallocHelper.h" +#include "common/windows/unicode.h" +#include "common/debug.h" +#include "localip.h" + +#define FF_LOCALIP_NIFLAG(name) { IP_ADAPTER_##name, #name } + +static const FFLocalIpNIFlag niFlagOptions[] = { + FF_LOCALIP_NIFLAG(DDNS_ENABLED), + FF_LOCALIP_NIFLAG(REGISTER_ADAPTER_SUFFIX), + FF_LOCALIP_NIFLAG(DHCP_ENABLED), + FF_LOCALIP_NIFLAG(RECEIVE_ONLY), + FF_LOCALIP_NIFLAG(NO_MULTICAST), + FF_LOCALIP_NIFLAG(IPV6_OTHER_STATEFUL_CONFIG), + FF_LOCALIP_NIFLAG(NETBIOS_OVER_TCPIP_ENABLED), + FF_LOCALIP_NIFLAG(IPV4_ENABLED), + FF_LOCALIP_NIFLAG(IPV6_ENABLED), + FF_LOCALIP_NIFLAG(IPV6_MANAGE_ADDRESS_CONFIG), + // sentinel + {}, +}; + +const char* ffDetectLocalIps(const FFLocalIpOptions* options, FFlist* results) { + FF_DEBUG("Starting local IP detection with showType=0x%X, namePrefix='%.*s'", + options->showType, + (int) options->namePrefix.length, + options->namePrefix.chars); + + IP_ADAPTER_ADDRESSES* FF_AUTO_FREE adapter_addresses = NULL; + + // Multiple attempts in case interfaces change while + // we are in the middle of querying them. + DWORD adapter_addresses_buffer_size = 0; + for (int attempts = 0;; ++attempts) { + FF_DEBUG("Attempt %d to get adapter addresses, buffer size: %lu", attempts + 1, adapter_addresses_buffer_size); + + if (adapter_addresses_buffer_size) { + adapter_addresses = (IP_ADAPTER_ADDRESSES*) realloc(adapter_addresses, adapter_addresses_buffer_size); + assert(adapter_addresses); + } + + DWORD family = options->showType & FF_LOCALIP_TYPE_IPV4_BIT + ? options->showType & FF_LOCALIP_TYPE_IPV6_BIT ? AF_UNSPEC : AF_INET + : AF_INET6; + FF_DEBUG("Calling GetAdaptersAddresses with family=%u, flags=0x%X", (unsigned) family, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER); + + DWORD error = GetAdaptersAddresses( + family, + GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, + NULL, + adapter_addresses, + &adapter_addresses_buffer_size); + + if (error == ERROR_SUCCESS) { + FF_DEBUG("GetAdaptersAddresses succeeded on attempt %d", attempts + 1); + break; + } else if (ERROR_BUFFER_OVERFLOW == error && attempts < 4) { + FF_DEBUG("Buffer overflow, need %lu bytes, retrying", adapter_addresses_buffer_size); + continue; + } else { + FF_DEBUG("GetAdaptersAddresses failed with error %lu after %d attempts", error, attempts + 1); + return "GetAdaptersAddresses() failed"; + } + } + + FF_A_UNUSED int adapterCount = 0, processedCount = 0; + + // Iterate through all of the adapters + for (IP_ADAPTER_ADDRESSES* adapter = adapter_addresses; adapter; adapter = adapter->Next) { + adapterCount++; + + FF_DEBUG("Processing adapter %d: IfIndex=%u, IfType=%u, OperStatus=%u", + adapterCount, + (unsigned) adapter->IfIndex, + (unsigned) adapter->IfType, + (unsigned) adapter->OperStatus); + + if (adapter->OperStatus != IfOperStatusUp) { + FF_DEBUG("Skipping adapter %u (not operational, status=%d)", (unsigned) adapter->IfIndex, adapter->OperStatus); + continue; + } + + bool isLoop = adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK; + FF_DEBUG("Adapter %u: isLoopback=%s", (unsigned) adapter->IfIndex, isLoop ? "true" : "false"); + + if (isLoop && !(options->showType & FF_LOCALIP_TYPE_LOOP_BIT)) { + FF_DEBUG("Skipping loopback adapter %u (loopback not requested)", (unsigned) adapter->IfIndex); + continue; + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(adapter->FriendlyName); + FF_DEBUG("Adapter %u name: '%s'", (unsigned) adapter->IfIndex, name.chars); + + if (options->namePrefix.length && !ffStrbufStartsWith(&name, &options->namePrefix)) { + FF_DEBUG("Skipping adapter %u (name doesn't match prefix '%.*s')", + (unsigned) adapter->IfIndex, + (int) options->namePrefix.length, + options->namePrefix.chars); + continue; + } + + if (options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) { + if (!((options->showType & FF_LOCALIP_TYPE_IPV4_BIT) && ffNetifGetDefaultRouteV4()->ifIndex == adapter->IfIndex) && + !((options->showType & FF_LOCALIP_TYPE_IPV6_BIT) && ffNetifGetDefaultRouteV6()->ifIndex == adapter->IfIndex)) { + FF_DEBUG("Skipping interface %u (not default route interface)", (unsigned) adapter->IfIndex); + continue; + } + } + + processedCount++; + FF_DEBUG("Creating result item for adapter %u ('%s')", (unsigned) adapter->IfIndex, name.chars); + + FFLocalIpResult* item = FF_LIST_ADD(FFLocalIpResult, *results); + ffStrbufInitMove(&item->name, &name); + ffStrbufInit(&item->ipv4); + ffStrbufInit(&item->ipv6); + ffStrbufInit(&item->mac); + ffStrbufInit(&item->flags); + item->defaultRoute = FF_LOCALIP_TYPE_NONE; + item->speed = -1; + item->mtu = -1; + + uint32_t typesToAdd = options->showType & (FF_LOCALIP_TYPE_IPV4_BIT | FF_LOCALIP_TYPE_IPV6_BIT | FF_LOCALIP_TYPE_ALL_IPS_BIT); + FF_DEBUG("Types to add for adapter %u: 0x%X", (unsigned) adapter->IfIndex, typesToAdd); + + FF_A_UNUSED int ipv4Count = 0, ipv6Count = 0; + + for (IP_ADAPTER_UNICAST_ADDRESS* ifa = adapter->FirstUnicastAddress; ifa; ifa = ifa->Next) { + FF_DEBUG("Processing unicast address: prefix origin=%d, suffix origin=%d, family=%d, DadState=%d", + ifa->PrefixOrigin, + ifa->SuffixOrigin, + ifa->Address.lpSockaddr->sa_family, + ifa->DadState); + + if (!(options->showType & FF_LOCALIP_TYPE_ALL_IPS_BIT)) { + if (ifa->DadState != IpDadStatePreferred) { + FF_DEBUG("Skipping address (not preferred)"); + continue; + } + + if (ifa->SuffixOrigin == IpSuffixOriginRandom) { + FF_DEBUG("Skipping temporary address (random suffix)"); + continue; + } + + // MIB_UNICASTIPADDRESS_ROW::SkipAsSource + } + + if (ifa->Address.lpSockaddr->sa_family == AF_INET) { + if (!(typesToAdd & (FF_LOCALIP_TYPE_IPV4_BIT | FF_LOCALIP_TYPE_ALL_IPS_BIT))) { + FF_DEBUG("Skipping IPv4 address (not requested in typesToAdd=0x%X)", typesToAdd); + continue; + } + + bool isDefaultRoute = ((options->showType & FF_LOCALIP_TYPE_IPV4_BIT) && ffNetifGetDefaultRouteV4()->ifIndex == adapter->IfIndex); + if ((options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) && !isDefaultRoute) { + FF_DEBUG("Skipping IPv4 address (not on default route interface)"); + continue; + } + + SOCKADDR_IN* ipv4 = (SOCKADDR_IN*) ifa->Address.lpSockaddr; + char addressBuffer[INET_ADDRSTRLEN + 10]; + char* end = RtlIpv4AddressToStringA(&ipv4->sin_addr, addressBuffer); + + if ((options->showType & FF_LOCALIP_TYPE_PREFIX_LEN_BIT) && ifa->OnLinkPrefixLength) { + end += snprintf(end, 10, "/%u", (unsigned) ifa->OnLinkPrefixLength); + } + + FF_DEBUG("Adding IPv4 address: %s (isDefaultRoute=%s)", addressBuffer, isDefaultRoute ? "true" : "false"); + + if (item->ipv4.length) { + ffStrbufAppendC(&item->ipv4, ','); + } + ffStrbufAppendNS(&item->ipv4, (uint32_t) (end - addressBuffer), addressBuffer); + if (isDefaultRoute) { + item->defaultRoute |= FF_LOCALIP_TYPE_IPV4_BIT; + } + + ipv4Count++; + typesToAdd &= ~(unsigned) FF_LOCALIP_TYPE_IPV4_BIT; + if (typesToAdd == 0) { + break; + } + } else if (ifa->Address.lpSockaddr->sa_family == AF_INET6) { + if (!(typesToAdd & (FF_LOCALIP_TYPE_IPV6_BIT | FF_LOCALIP_TYPE_ALL_IPS_BIT))) { + FF_DEBUG("Skipping IPv6 address (not requested in typesToAdd=0x%X)", typesToAdd); + continue; + } + + SOCKADDR_IN6* ipv6 = (SOCKADDR_IN6*) ifa->Address.lpSockaddr; + + FFLocalIpIpv6Type ipv6Type = FF_LOCALIP_IPV6_TYPE_NONE; + if (IN6_IS_ADDR_GLOBAL(&ipv6->sin6_addr)) { + ipv6Type |= FF_LOCALIP_IPV6_TYPE_GUA_BIT; + } else if (IN6_IS_ADDR_UNIQUE_LOCAL(&ipv6->sin6_addr)) { + ipv6Type |= FF_LOCALIP_IPV6_TYPE_ULA_BIT; + } else if (IN6_IS_ADDR_LINKLOCAL(&ipv6->sin6_addr)) { + ipv6Type |= FF_LOCALIP_IPV6_TYPE_LLA_BIT; + } else { + ipv6Type |= FF_LOCALIP_IPV6_TYPE_UNKNOWN_BIT; + } + + if (!(options->ipv6Type & ipv6Type)) { + FF_DEBUG("Skipping IPv6 address (doesn't match requested type 0x%X)", options->ipv6Type); + continue; + } + + bool isDefaultRoute = ((options->showType & FF_LOCALIP_TYPE_IPV6_BIT) && ffNetifGetDefaultRouteV6()->ifIndex == adapter->IfIndex); + if ((options->showType & FF_LOCALIP_TYPE_DEFAULT_ROUTE_ONLY_BIT) && !isDefaultRoute) { + FF_DEBUG("Skipping IPv6 address (not on default route interface)"); + continue; + } + + char addressBuffer[INET6_ADDRSTRLEN + 10]; + char* end = RtlIpv6AddressToStringA(&ipv6->sin6_addr, addressBuffer); + + if ((options->showType & FF_LOCALIP_TYPE_PREFIX_LEN_BIT) && ifa->OnLinkPrefixLength) { + end += snprintf(end, 10, "/%u", (unsigned) ifa->OnLinkPrefixLength); + } + + FF_DEBUG("Adding IPv6 address: %s (isDefaultRoute=%s)", addressBuffer, isDefaultRoute ? "true" : "false"); + + if (item->ipv6.length) { + ffStrbufAppendC(&item->ipv6, ','); + } + ffStrbufAppendNS(&item->ipv6, (uint32_t) (end - addressBuffer), addressBuffer); + if (isDefaultRoute) { + item->defaultRoute |= FF_LOCALIP_TYPE_IPV6_BIT; + } + + ipv6Count++; + typesToAdd &= ~(unsigned) FF_LOCALIP_TYPE_IPV6_BIT; + if (typesToAdd == 0) { + break; + } + } + } + + FF_DEBUG("Adapter %u: collected %d IPv4 and %d IPv6 addresses", (unsigned) adapter->IfIndex, ipv4Count, ipv6Count); + + if (options->showType & FF_LOCALIP_TYPE_SPEED_BIT) { + item->speed = (int32_t) (adapter->ReceiveLinkSpeed / 1000000); + FF_DEBUG("Adapter %u speed: %d Mbps (raw: %llu)", (unsigned) adapter->IfIndex, item->speed, adapter->ReceiveLinkSpeed); + } + if (options->showType & FF_LOCALIP_TYPE_MTU_BIT) { + item->mtu = (int32_t) adapter->Mtu; + FF_DEBUG("Adapter %u MTU: %d", (unsigned) adapter->IfIndex, item->mtu); + } + if (options->showType & FF_LOCALIP_TYPE_FLAGS_BIT) { + ffLocalIpFillNIFlags(&item->flags, adapter->Flags, niFlagOptions); + FF_DEBUG("Adapter %u flags: 0x%lX -> '%s'", (unsigned) adapter->IfIndex, adapter->Flags, item->flags.chars); + } + if (options->showType & FF_LOCALIP_TYPE_MAC_BIT && adapter->PhysicalAddressLength == 6) { + uint8_t* ptr = adapter->PhysicalAddress; + ffStrbufSetF(&item->mac, "%02x:%02x:%02x:%02x:%02x:%02x", ptr[0], ptr[1], ptr[2], ptr[3], ptr[4], ptr[5]); + FF_DEBUG("Adapter %u MAC: %s", (unsigned) adapter->IfIndex, item->mac.chars); + } + } + + FF_DEBUG("Local IP detection completed: scanned %d adapters, processed %d, results count: %u", + adapterCount, + processedCount, + results->length); + + return NULL; +} diff --git a/src/detection/media/media.c b/src/detection/media/media.c new file mode 100644 index 0000000..7adfb99 --- /dev/null +++ b/src/detection/media/media.c @@ -0,0 +1,45 @@ +#include "media.h" +#include "common/io.h" + +void ffDetectMediaImpl(FFMediaResult* media, bool saveCover); + +static FFMediaResult result; + +static void removeMediaCoverFile(void) { + if (result.cover.length > 0) { + ffRemoveFile(result.cover.chars); + ffStrbufDestroy(&result.cover); + } +} + +const FFMediaResult* ffDetectMedia(bool saveCover) { + if (result.error.chars == NULL) { + ffStrbufInit(&result.error); + ffStrbufInit(&result.playerId); + ffStrbufInit(&result.player); + ffStrbufInit(&result.song); + ffStrbufInit(&result.artist); + ffStrbufInit(&result.album); + ffStrbufInit(&result.url); + ffStrbufInit(&result.status); + ffStrbufInit(&result.cover); + result.length = 0; + result.position = 0; + result.removeCoverAfterUse = false; + ffDetectMediaImpl(&result, saveCover); + + if (result.song.length == 0 && result.error.length == 0) { + ffStrbufAppendS(&result.error, "No media found"); + } + ffStrbufTrimRightSpace(&result.song); + ffStrbufTrimRightSpace(&result.artist); + ffStrbufTrimRightSpace(&result.album); + ffStrbufTrimRightSpace(&result.player); + + if (saveCover && result.removeCoverAfterUse) { + atexit(removeMediaCoverFile); + } + } + + return &result; +} diff --git a/src/detection/media/media.h b/src/detection/media/media.h new file mode 100644 index 0000000..ea241e6 --- /dev/null +++ b/src/detection/media/media.h @@ -0,0 +1,21 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/media/option.h" + +typedef struct FFMediaResult { + FFstrbuf error; + FFstrbuf playerId; // Bus name on Linux, app bundle name on macOS. e.g. plasma-browser-integration + FFstrbuf player; // e.g. Google Chrome + FFstrbuf song; + FFstrbuf artist; + FFstrbuf album; + FFstrbuf url; + FFstrbuf status; + FFstrbuf cover; + uint32_t length; // In milliseconds + uint32_t position; // In milliseconds + bool removeCoverAfterUse; +} FFMediaResult; + +const FFMediaResult* ffDetectMedia(bool saveCover); diff --git a/src/detection/media/media_apple.m b/src/detection/media/media_apple.m new file mode 100644 index 0000000..d9501d1 --- /dev/null +++ b/src/detection/media/media_apple.m @@ -0,0 +1,209 @@ +#include "fastfetch.h" +#include "common/processing.h" +#include "common/apple/cf_helpers.h" +#include "common/time.h" +#include "detection/media/media.h" + +#import <Foundation/Foundation.h> +#import <CoreFoundation/CoreFoundation.h> +#import <CoreServices/CoreServices.h> + +// https://github.com/andrewwiik/iOS-Blocks/blob/master/Widgets/Music/MediaRemote.h +extern void MRMediaRemoteGetNowPlayingInfo(dispatch_queue_t dispatcher, void (^callback)(_Nullable CFDictionaryRef info)) FF_A_WEAK_IMPORT; +extern void MRMediaRemoteGetNowPlayingApplicationIsPlaying(dispatch_queue_t queue, void (^callback)(BOOL playing)) FF_A_WEAK_IMPORT; +extern void MRMediaRemoteGetNowPlayingApplicationDisplayID(dispatch_queue_t queue, void (^callback)(_Nullable CFStringRef displayID)) FF_A_WEAK_IMPORT; +extern void MRMediaRemoteGetNowPlayingApplicationDisplayName(int unknown, dispatch_queue_t queue, void (^callback)(_Nullable CFStringRef name)) FF_A_WEAK_IMPORT; + +static uint32_t getTrueElapsedTime(CFDictionaryRef info) { + double elapsedTime; + if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoElapsedTime"), &elapsedTime) != NULL) { + return 0; + } + + elapsedTime *= 1000; + + double playbackRate; + uint64_t timestampEpoch; + if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoPlaybackRate"), &playbackRate) == NULL && + ffCfDictGetDateAsEpoch(info, CFSTR("kMRMediaRemoteNowPlayingInfoTimestamp"), ×tampEpoch) == NULL) { + uint64_t timeDiff = ffTimeGetNow() - timestampEpoch; + elapsedTime += (double) timeDiff * playbackRate; + } + + return (uint32_t) elapsedTime; +} + +static const char* getMediaByMediaRemote(FFMediaResult* result, bool saveCover) { +#define FF_TEST_FN_EXISTENCE(fn) \ + if (!fn) return "MediaRemote function " #fn " is not available" + FF_TEST_FN_EXISTENCE(MRMediaRemoteGetNowPlayingInfo); + FF_TEST_FN_EXISTENCE(MRMediaRemoteGetNowPlayingApplicationIsPlaying); +#undef FF_TEST_FN_EXISTENCE + + dispatch_group_t group = dispatch_group_create(); + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); + + dispatch_group_enter(group); + __block const char* error = NULL; + MRMediaRemoteGetNowPlayingInfo(queue, ^(_Nullable CFDictionaryRef info) { + if (info != nil) { + ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoTitle"), &result->song); + ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtist"), &result->artist); + ffCfDictGetString(info, CFSTR("kMRMediaRemoteNowPlayingInfoAlbum"), &result->album); + double value; + if (ffCfDictGetDouble(info, CFSTR("kMRMediaRemoteNowPlayingInfoDuration"), &value) == NULL) { + result->length = (uint32_t) (value * 1000); + result->position = getTrueElapsedTime(info); + } + + if (saveCover) { + NSData* artworkData = (__bridge NSData*) CFDictionaryGetValue(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtworkData")); + if (artworkData) { + CFStringRef mime = (CFStringRef) CFDictionaryGetValue(info, CFSTR("kMRMediaRemoteNowPlayingInfoArtworkMIMEType")); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + FF_CFTYPE_AUTO_RELEASE CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mime, NULL); + FF_CFTYPE_AUTO_RELEASE CFStringRef ext = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassFilenameExtension); +#pragma clang diagnostic pop + NSString* tmpDir = NSTemporaryDirectory(); + NSString* uuid = NSUUID.UUID.UUIDString; + NSString* path = [tmpDir stringByAppendingPathComponent:[NSString stringWithFormat:@"ff_%@.%@", uuid, ext ? (__bridge NSString*) ext : @"img"]]; + if ([artworkData writeToFile:path atomically:NO]) + ffStrbufSetS(&result->cover, path.UTF8String); + } + } + } else + error = "MRMediaRemoteGetNowPlayingInfo() failed"; + + dispatch_group_leave(group); + }); + + dispatch_group_enter(group); + MRMediaRemoteGetNowPlayingApplicationIsPlaying(queue, ^(BOOL playing) { + ffStrbufSetStatic(&result->status, playing ? "Playing" : "Paused"); + dispatch_group_leave(group); + }); + + if (MRMediaRemoteGetNowPlayingApplicationDisplayID) { + dispatch_group_enter(group); + MRMediaRemoteGetNowPlayingApplicationDisplayID(queue, ^(_Nullable CFStringRef displayID) { + ffCfStrGetString(displayID, &result->playerId); + dispatch_group_leave(group); + }); + } + + if (MRMediaRemoteGetNowPlayingApplicationDisplayName) { + dispatch_group_enter(group); + MRMediaRemoteGetNowPlayingApplicationDisplayName(0, queue, ^(_Nullable CFStringRef name) { + ffCfStrGetString(name, &result->player); + dispatch_group_leave(group); + }); + } + + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + // Don't dispatch_release because we are using ARC + + if (result->song.length > 0) { + return NULL; + } + + return error; +} + +#if !FF_MODULE_DISABLE_MEDIA +__attribute__((visibility("default"), used)) +int ffPrintMediaByMediaRemote(bool saveCover) { + FFMediaResult media = { + .status = ffStrbufCreate(), + .song = ffStrbufCreate(), + .artist = ffStrbufCreate(), + .album = ffStrbufCreate(), + .playerId = ffStrbufCreate(), + .player = ffStrbufCreate(), + .cover = ffStrbufCreate(), + }; + if (getMediaByMediaRemote(&media, saveCover) != NULL) { + return 1; + } + ffStrbufAppendC(&media.status, '\n'); + ffStrbufAppend(&media.status, &media.song); + ffStrbufAppendC(&media.status, '\n'); + ffStrbufAppend(&media.status, &media.artist); + ffStrbufAppendC(&media.status, '\n'); + ffStrbufAppend(&media.status, &media.album); + ffStrbufAppendC(&media.status, '\n'); + ffStrbufAppend(&media.status, &media.playerId); + ffStrbufAppendC(&media.status, '\n'); + ffStrbufAppend(&media.status, &media.player); + ffStrbufAppendC(&media.status, '\n'); + if (saveCover) { + ffStrbufAppend(&media.status, &media.cover); + } + ffStrbufAppendC(&media.status, '\n'); + ffStrbufAppendUInt(&media.status, media.position); + ffStrbufAppendC(&media.status, '\n'); + ffStrbufAppendUInt(&media.status, media.length); + write(STDOUT_FILENO, media.status.chars, media.status.length); + ffStrbufDestroy(&media.status); + ffStrbufDestroy(&media.song); + ffStrbufDestroy(&media.artist); + ffStrbufDestroy(&media.album); + ffStrbufDestroy(&media.playerId); + ffStrbufDestroy(&media.player); + ffStrbufDestroy(&media.cover); + return 0; +} +#endif + +static const char* getMediaByAuthorizedProcess(FFMediaResult* result, bool saveCover) { + // #1737 + FF_STRBUF_AUTO_DESTROY script = ffStrbufCreateF("import ctypes;ctypes.CDLL('%s').ffPrintMediaByMediaRemote(%s)", instance.state.platform.exePath.chars, saveCover ? "True" : "False"); + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + const char* error = ffProcessAppendStdOut( + &buffer, + (char* const[]) { "/usr/bin/python3", // Must be signed by Apple. Homebrew python doesn't work + "-c", + script.chars, + nil }); + if (error) { + return error; + } + if (buffer.length == 0) { + return "No media found"; + } + + // status\ntitle\nartist\nalbum\nbundleName\nappName\ncoverPath\nelapsedTimeMS\ndurationMS + FFstrbuf* const strList[] = { &result->status, &result->song, &result->artist, &result->album, &result->playerId, &result->player, &result->cover }; + char* line = NULL; + size_t len = 0; + for (uint32_t i = 0; i < ARRAY_SIZE(strList) && ffStrbufGetline(&line, &len, &buffer); ++i) { + ffStrbufSetS(strList[i], line); + } + uint32_t* const numList[] = { &result->position, &result->length }; + for (uint32_t i = 0; i < ARRAY_SIZE(numList) && ffStrbufGetline(&line, &len, &buffer); ++i) { + *numList[i] = (uint32_t) strtoul(line, NULL, 10); + } + return NULL; +} + +void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) { + const char* error; + if (@available(macOS 15.4, *)) { + error = getMediaByAuthorizedProcess(media, saveCover); + } else { + error = getMediaByMediaRemote(media, saveCover); + } + if (error) { + ffStrbufAppendS(&media->error, error); + } else if (media->player.length == 0 && media->playerId.length > 0) { + ffStrbufSet(&media->player, &media->playerId); + if (ffStrbufStartsWithIgnCaseS(&media->player, "com.")) { + ffStrbufSubstrAfter(&media->player, strlen("com.") - 1); + } + ffStrbufReplaceAllC(&media->player, '.', ' '); + if (media->cover.length > 0) { + media->removeCoverAfterUse = true; + } + } +} diff --git a/src/detection/media/media_linux.c b/src/detection/media/media_linux.c new file mode 100644 index 0000000..724e834 --- /dev/null +++ b/src/detection/media/media_linux.c @@ -0,0 +1,288 @@ +#include "fastfetch.h" +#include "common/io.h" +#include "common/strutil.h" +#include "common/memrchr.h" +#include "detection/media/media.h" + +#include <string.h> + +#define FF_DBUS_MPRIS_PREFIX "org.mpris.MediaPlayer2." + +#ifdef FF_HAVE_DBUS + #include "common/dbus.h" + + #define FF_DBUS_ITER_CONTINUE(dbus, iterator) \ + { \ + if (!(dbus)->lib->ffdbus_message_iter_next(iterator)) \ + break; \ + continue; \ + } + +static bool parseMprisMetadata(FFDBusData* data, DBusMessageIter* rootIterator, FFMediaResult* result) { + DBusMessageIter arrayIterator; + + if (data->lib->ffdbus_message_iter_get_arg_type(rootIterator) == DBUS_TYPE_VARIANT) { + DBusMessageIter variantIterator; + data->lib->ffdbus_message_iter_recurse(rootIterator, &variantIterator); + if (data->lib->ffdbus_message_iter_get_arg_type(&variantIterator) != DBUS_TYPE_ARRAY) { + return false; + } + data->lib->ffdbus_message_iter_recurse(&variantIterator, &arrayIterator); + } else { + data->lib->ffdbus_message_iter_recurse(rootIterator, &arrayIterator); + } + + while (true) { + if (data->lib->ffdbus_message_iter_get_arg_type(&arrayIterator) != DBUS_TYPE_DICT_ENTRY) { + FF_DBUS_ITER_CONTINUE(data, &arrayIterator) + } + + DBusMessageIter dictIterator; + data->lib->ffdbus_message_iter_recurse(&arrayIterator, &dictIterator); + + if (data->lib->ffdbus_message_iter_get_arg_type(&dictIterator) != DBUS_TYPE_STRING) { + FF_DBUS_ITER_CONTINUE(data, &arrayIterator) + } + + if (!data->lib->ffdbus_message_iter_has_next(&dictIterator)) { + FF_DBUS_ITER_CONTINUE(data, &arrayIterator) + } + + const char* key; + data->lib->ffdbus_message_iter_get_basic(&dictIterator, &key); + + data->lib->ffdbus_message_iter_next(&dictIterator); + + if (ffStrStartsWith(key, "xesam:")) { + const char* xesam = key + strlen("xesam:"); + if (ffStrEquals(xesam, "title")) { + ffDBusGetString(data, &dictIterator, &result->song); + } else if (ffStrEquals(xesam, "album")) { + ffDBusGetString(data, &dictIterator, &result->album); + } else if (ffStrEquals(xesam, "artist")) { + ffDBusGetString(data, &dictIterator, &result->artist); + } else if (ffStrEquals(xesam, "url")) { + ffDBusGetString(data, &dictIterator, &result->url); + } + + if (result->song.length > 0 && result->artist.length > 0 && result->album.length > 0 && result->url.length > 0) { + break; + } + } else if (ffStrStartsWith(key, "mpris:")) { + const char* mpris = key + strlen("mpris:"); + if (ffStrEquals(mpris, "artUrl")) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + ffDBusGetString(data, &dictIterator, &path); + if (ffStrbufStartsWithS(&path, "file:///")) { + ffStrbufEnsureFree(&result->cover, path.length - (uint32_t) strlen("file://")); + for (uint32_t i = (uint32_t) strlen("file://"); i < path.length; ++i) { + if (path.chars[i] == '%') { + if (i + 2 >= path.length) { + break; + } + char str[] = { path.chars[i + 1], path.chars[i + 2], 0 }; + char* end = NULL; + const char decodedChar = (char) strtoul(str, &end, 16); + if (end == &str[2]) { + i += 2; + ffStrbufAppendC(&result->cover, decodedChar); + } else { + ffStrbufAppendC(&result->cover, '%'); + } + } else { + ffStrbufAppendC(&result->cover, path.chars[i]); + } + } + } + } else if (ffStrEquals(mpris, "length")) { + int64_t length = 0; // microseconds + if (ffDBusGetInt(data, &dictIterator, &length) && length > 0) { + result->length = (uint32_t) (length / 1000); + } + } + } + + FF_DBUS_ITER_CONTINUE(data, &arrayIterator) + } + + return true; +} + +static bool getBusProperties(FFDBusData* data, const char* busName, FFMediaResult* result) { + // Get all properties at once to reduce the number of IPCs + DBusMessage* reply = ffDBusGetAllProperties(data, busName, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2.Player"); + if (reply == NULL) { + return false; + } + + DBusMessageIter rootIterator; + if (!data->lib->ffdbus_message_iter_init(reply, &rootIterator) && + data->lib->ffdbus_message_iter_get_arg_type(&rootIterator) != DBUS_TYPE_ARRAY) { + data->lib->ffdbus_message_unref(reply); + return false; + } + + DBusMessageIter arrayIterator; + data->lib->ffdbus_message_iter_recurse(&rootIterator, &arrayIterator); + + while (true) { + if (data->lib->ffdbus_message_iter_get_arg_type(&arrayIterator) != DBUS_TYPE_DICT_ENTRY) { + FF_DBUS_ITER_CONTINUE(data, &arrayIterator) + } + + DBusMessageIter dictIterator; + data->lib->ffdbus_message_iter_recurse(&arrayIterator, &dictIterator); + + const char* key; + data->lib->ffdbus_message_iter_get_basic(&dictIterator, &key); + + data->lib->ffdbus_message_iter_next(&dictIterator); + + if (ffStrEquals(key, "Metadata")) { + parseMprisMetadata(data, &dictIterator, result); + } else if (ffStrEquals(key, "PlaybackStatus")) { + ffDBusGetString(data, &dictIterator, &result->status); + } else if (ffStrEquals(key, "Position")) { + int64_t position = 0; // microseconds + if (ffDBusGetInt(data, &dictIterator, &position) && position > 0) { + result->position = (uint32_t) (position / 1000); + } + } + + FF_DBUS_ITER_CONTINUE(data, &arrayIterator) + } + + if (result->song.length == 0) { + if (result->url.length) { + const char* fileName = memrchr(result->url.chars, '/', result->url.length); + assert(fileName); + ++fileName; + ffStrbufEnsureFixedLengthFree(&result->song, result->url.length - (uint32_t) (fileName - result->url.chars)); + for (; *fileName && *fileName != '?'; ++fileName) { + if (*fileName != '%') { + ffStrbufAppendC(&result->song, *fileName); + } else { + if (fileName[1] == 0 || fileName[2] == 0) { + break; + } + char str[] = { fileName[1], fileName[2], 0 }; + ffStrbufAppendC(&result->song, (char) strtoul(str, NULL, 16)); + fileName += 2; + } + } + } else { + ffStrbufClear(&result->artist); + ffStrbufClear(&result->album); + ffStrbufClear(&result->url); + ffStrbufClear(&result->status); + data->lib->ffdbus_message_unref(reply); + return false; + } + } + + // Set short bus name + ffStrbufAppendS(&result->playerId, busName + sizeof(FF_DBUS_MPRIS_PREFIX) - 1); + + // We found a song, get the player name + if (ffStrbufStartsWithS(&result->playerId, "musikcube.instance")) { + // dbus calls are EXTREMELY slow on musikcube, so we set the player name manually + ffStrbufSetStatic(&result->player, "musikcube"); + } else { + ffDBusGetPropertyString(data, busName, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2", "Identity", &result->player); + if (result->player.length == 0) { + ffDBusGetPropertyString(data, busName, "/org/mpris/MediaPlayer2", "org.mpris.MediaPlayer2", "DesktopEntry", &result->player); + } + if (result->player.length == 0) { + ffStrbufAppend(&result->player, &result->playerId); + } + } + + data->lib->ffdbus_message_unref(reply); + + return true; +} + +static void getCustomBus(FFDBusData* data, const FFstrbuf* playerName, FFMediaResult* result) { + if (ffStrbufStartsWithS(playerName, FF_DBUS_MPRIS_PREFIX)) { + getBusProperties(data, playerName->chars, result); + return; + } + + FF_STRBUF_AUTO_DESTROY busName = ffStrbufCreateS(FF_DBUS_MPRIS_PREFIX); + ffStrbufAppend(&busName, playerName); + getBusProperties(data, busName.chars, result); +} + +static void getBestBus(FFDBusData* data, FFMediaResult* result) { + if ( + getBusProperties(data, FF_DBUS_MPRIS_PREFIX "spotify", result) || + getBusProperties(data, FF_DBUS_MPRIS_PREFIX "vlc", result) || + getBusProperties(data, FF_DBUS_MPRIS_PREFIX "plasma-browser-integration", result)) { + return; + } + + DBusMessage* reply = ffDBusGetMethodReply(data, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ListNames", NULL, NULL); + if (reply == NULL) { + return; + } + + DBusMessageIter rootIterator; + if (!data->lib->ffdbus_message_iter_init(reply, &rootIterator) || data->lib->ffdbus_message_iter_get_arg_type(&rootIterator) != DBUS_TYPE_ARRAY) { + return; + } + + DBusMessageIter arrayIterator; + data->lib->ffdbus_message_iter_recurse(&rootIterator, &arrayIterator); + + while (true) { + if (data->lib->ffdbus_message_iter_get_arg_type(&arrayIterator) != DBUS_TYPE_STRING) { + FF_DBUS_ITER_CONTINUE(data, &arrayIterator) + } + + const char* busName; + data->lib->ffdbus_message_iter_get_basic(&arrayIterator, &busName); + + if (!ffStrStartsWith(busName, FF_DBUS_MPRIS_PREFIX) || + ffStrEquals(busName + strlen(FF_DBUS_MPRIS_PREFIX), "playerctld")) { + FF_DBUS_ITER_CONTINUE(data, &arrayIterator) + } + + if (getBusProperties(data, busName, result)) { + break; + } + + FF_DBUS_ITER_CONTINUE(data, &arrayIterator) + } + + data->lib->ffdbus_message_unref(reply); +} + +static const char* getMedia(FFMediaResult* result) { + FF_DBUS_AUTO_DESTROY_DATA FFDBusData data = {}; + const char* error = ffDBusLoadData(DBUS_BUS_SESSION, &data); + if (error != NULL) { + return error; + } + + // FIXME: This is shared for both player and media module. + // However it uses an option in one specific module + if (instance.config.general.playerName.length > 0) { + getCustomBus(&data, &instance.config.general.playerName, result); + } else { + getBestBus(&data, result); + } + + return NULL; +} + +#endif + +void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) { + FF_UNUSED(saveCover); // We don't save the cover to a file for Mpris implementation +#ifdef FF_HAVE_DBUS + const char* error = getMedia(media); + ffStrbufAppendS(&media->error, error); +#else + ffStrbufAppendS(&media->error, "Fastfetch was compiled without DBus support"); +#endif +} diff --git a/src/detection/media/media_nosupport.c b/src/detection/media/media_nosupport.c new file mode 100644 index 0000000..e7d1c0f --- /dev/null +++ b/src/detection/media/media_nosupport.c @@ -0,0 +1,5 @@ +#include "media.h" + +void ffDetectMediaImpl(FFMediaResult* media) { + ffStrbufAppendS(&media->error, "Not supported on this platform"); +} diff --git a/src/detection/media/media_windows.cpp b/src/detection/media/media_windows.cpp new file mode 100644 index 0000000..f0ebd35 --- /dev/null +++ b/src/detection/media/media_windows.cpp @@ -0,0 +1,421 @@ +extern "C" { +#include "media.h" +#include "common/time.h" +#include "common/windows/unicode.h" +#include "common/windows/com.h" +} + +#if FF_HAVE_WINRT + #include <roapi.h> + #include <robuffer.h> + #include <winstring.h> + #include <asyncinfo.h> + + #include <winrt/Windows.ApplicationModel.h> + #include <winrt/Windows.Foundation.h> + #include <winrt/Windows.Media.Control.h> + #include <winrt/Windows.Storage.Streams.h> + +using winrt::impl::abi_t; +using winrt::Windows::Foundation::IAsyncOperation; +using winrt::Windows::Foundation::IAsyncOperationWithProgress; + +static inline void deleteHstring(HSTRING* pstr) { + if (*pstr) { + WindowsDeleteString(*pstr); + } +} + +static inline void ffStrbufSetHstring(FFstrbuf* destination, HSTRING value) { + uint32_t length; + const wchar_t* raw = WindowsGetStringRawBuffer(value, &length); + ffStrbufSetNWS(destination, length, raw); +} + +template <typename Interface> +static inline HRESULT ffGetActivationFactory(const wchar_t* className, REFIID iid, Interface** factory) { + HSTRING_HEADER header; + HSTRING runtimeClass; + HRESULT hr = WindowsCreateStringReference(className, (UINT32)::wcslen(className), &header, &runtimeClass); + if (FAILED(hr)) { + return hr; + } + + return RoGetActivationFactory(runtimeClass, iid, reinterpret_cast<void**>(factory)); +} + +template <typename TargetProjection, typename SourceAbi> +static inline HRESULT ffQueryInterface(SourceAbi* source, abi_t<TargetProjection>** target) { + return source->QueryInterface(winrt::guid_of<TargetProjection>(), reinterpret_cast<void**>(target)); +} + +template <typename TOperationAbi, typename TResultAbi> +static HRESULT ffWaitForAsyncOperation(TOperationAbi* operation, TResultAbi** result) { + IAsyncInfo* FF_AUTO_RELEASE_COM_OBJECT asyncInfo = NULL; + HRESULT hr = ffQueryInterface<IAsyncInfo>(operation, &asyncInfo); + if (FAILED(hr)) { + return hr; + } + + AsyncStatus status = AsyncStatus::Started; + + for (;;) { + hr = asyncInfo->get_Status(&status); + if (FAILED(hr)) { + return hr; + } + if (status == AsyncStatus::Started) { + ffTimeSleep(0); + } else { + break; + } + } + + if (status != AsyncStatus::Completed) { + HRESULT errorCode = E_FAIL; + asyncInfo->get_ErrorCode(&errorCode); + return FAILED(errorCode) ? errorCode : E_FAIL; + } + + return operation->GetResults((void**) result); +} + +template <typename TResultProjection, typename TOperation> +static HRESULT ffRunAndWait(TOperation&& operation, abi_t<TResultProjection>** result) { + abi_t<IAsyncOperation<TResultProjection>>* FF_AUTO_RELEASE_COM_OBJECT opResult = NULL; + HRESULT hr = operation(reinterpret_cast<void**>(&opResult)); + if (FAILED(hr) || !opResult) { + return hr; + } + + return ffWaitForAsyncOperation(opResult, result); +} + +template <typename TResultProjection, typename TOperation> +static HRESULT ffRunAndWait2(TOperation&& operation, abi_t<TResultProjection>** result) { + *result = NULL; + + abi_t<IAsyncOperationWithProgress<TResultProjection, int32_t>>* FF_AUTO_RELEASE_COM_OBJECT opResult = NULL; + HRESULT hr = operation(reinterpret_cast<void**>(&opResult)); + if (FAILED(hr) || !opResult) { + return hr; + } + + return ffWaitForAsyncOperation(opResult, result); +} + +static HRESULT ffSaveThumbnailToTempPath( + abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamReference>* thumbnail, + FFstrbuf* destination) { + abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamWithContentType>* FF_AUTO_RELEASE_COM_OBJECT contentStream = NULL; + HRESULT hr = ffRunAndWait<winrt::Windows::Storage::Streams::IRandomAccessStreamWithContentType>([=](void** result) { + return thumbnail->OpenReadAsync(result); + }, + &contentStream); + if (FAILED(hr) || !contentStream) { + return FAILED(hr) ? hr : E_FAIL; + } + + abi_t<winrt::Windows::Storage::Streams::IRandomAccessStream>* FF_AUTO_RELEASE_COM_OBJECT randomAccessStream = NULL; + hr = ffQueryInterface<winrt::Windows::Storage::Streams::IRandomAccessStream>(contentStream, &randomAccessStream); + if (FAILED(hr)) { + return hr; + } + + UINT64 size = 0; + hr = randomAccessStream->get_Size(&size); + if (FAILED(hr) || size == 0) { + return FAILED(hr) ? hr : S_FALSE; + } + + if (size > 0xFFFFFFFFu) { + return HRESULT_FROM_WIN32(ERROR_FILE_TOO_LARGE); + } + + abi_t<winrt::Windows::Storage::Streams::IBufferFactory>* FF_AUTO_RELEASE_COM_OBJECT bufferFactory = NULL; + hr = ffGetActivationFactory(L"Windows.Storage.Streams.Buffer", winrt::guid_of<winrt::Windows::Storage::Streams::IBufferFactory>(), &bufferFactory); + if (FAILED(hr)) { + return hr; + } + + abi_t<winrt::Windows::Storage::Streams::IBuffer>* FF_AUTO_RELEASE_COM_OBJECT buffer = NULL; + hr = bufferFactory->Create((UINT32) size, reinterpret_cast<void**>(&buffer)); + if (FAILED(hr) || !buffer) { + return FAILED(hr) ? hr : E_FAIL; + } + + abi_t<winrt::Windows::Storage::Streams::IInputStream>* FF_AUTO_RELEASE_COM_OBJECT inputStream = NULL; + hr = ffQueryInterface<winrt::Windows::Storage::Streams::IInputStream>(contentStream, &inputStream); + if (FAILED(hr)) { + return hr; + } + + abi_t<winrt::Windows::Storage::Streams::IBuffer>* FF_AUTO_RELEASE_COM_OBJECT readBuffer = NULL; + hr = ffRunAndWait2<winrt::Windows::Storage::Streams::IBuffer>([=](void** result) { + return inputStream->ReadAsync(buffer, (uint32_t) size, (uint32_t) winrt::Windows::Storage::Streams::InputStreamOptions::None, result); + }, + &readBuffer); + if (FAILED(hr) || !readBuffer) { + return FAILED(hr) ? hr : E_FAIL; + } + + UINT32 length = 0; + hr = readBuffer->get_Length(&length); + if (FAILED(hr) || length == 0) { + return FAILED(hr) ? hr : S_FALSE; + } + + Windows::Storage::Streams::IBufferByteAccess* FF_AUTO_RELEASE_COM_OBJECT byteAccess = NULL; + hr = readBuffer->QueryInterface(IID_PPV_ARGS(&byteAccess)); + if (FAILED(hr)) { + return hr; + } + + byte* bytes = NULL; + hr = byteAccess->Buffer(&bytes); + if (FAILED(hr) || !bytes) { + return FAILED(hr) ? hr : E_FAIL; + } + + wchar_t tempDirectory[MAX_PATH]; + DWORD tempLength = GetTempPathW(MAX_PATH, tempDirectory); + if (tempLength == 0 || tempLength >= MAX_PATH) { + return HRESULT_FROM_WIN32(GetLastError()); + } + + wchar_t tempFilePath[MAX_PATH]; + if (!GetTempFileNameW(tempDirectory, L"fft", 0, tempFilePath)) { + return HRESULT_FROM_WIN32(GetLastError()); + } + + HANDLE file = CreateFileW(tempFilePath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (file == INVALID_HANDLE_VALUE) { + DWORD writeError = GetLastError(); + DeleteFileW(tempFilePath); + return HRESULT_FROM_WIN32(writeError); + } + + DWORD written = 0; + BOOL writtenOk = WriteFile(file, bytes, length, &written, NULL); + NtClose(file); + file = NULL; + + if (!writtenOk || written != length) { + DWORD writeError = GetLastError(); + DeleteFileW(tempFilePath); + return HRESULT_FROM_WIN32(writtenOk ? ERROR_WRITE_FAULT : writeError); + } + + ffStrbufSetWS(destination, tempFilePath); + return S_OK; +} + +static const char* getMedia(FFMediaResult* result, bool saveCover) { + const char* error = ffInitCom(); + if (error) { + return error; + } + + do { + abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManagerStatics>* FF_AUTO_RELEASE_COM_OBJECT managerStatics = NULL; + HRESULT hr = ffGetActivationFactory(L"Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager", winrt::guid_of<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManagerStatics>(), &managerStatics); + if (FAILED(hr) || !managerStatics) { + error = "winrt: RoGetActivationFactory(GlobalSystemMediaTransportControlsSessionManager) failed"; + break; + } + + abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManager>* FF_AUTO_RELEASE_COM_OBJECT manager = NULL; + hr = ffRunAndWait<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionManager>([=](void** result) { + return managerStatics->RequestAsync(result); + }, + &manager); + if (FAILED(hr) || !manager) { + error = "winrt: RequestAsync().GetResults() failed"; + break; + } + + FF_A_CLEANUP(deleteHstring) HSTRING playerId = NULL; + + abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSession>* FF_AUTO_RELEASE_COM_OBJECT session = NULL; + if (instance.config.general.playerName.length) { + abi_t<winrt::Windows::Foundation::Collections::IVectorView<winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSession>>* FF_AUTO_RELEASE_COM_OBJECT sessions = NULL; + hr = manager->GetSessions(reinterpret_cast<void**>(&sessions)); + if (FAILED(hr) || !sessions) { + error = "winrt: GetSessions() failed"; + break; + } + uint32_t sessionCount = 0; + hr = sessions->get_Size(&sessionCount); + if (FAILED(hr)) { + error = "winrt: GetSessions().get_Size() failed"; + break; + } + for (uint32_t i = 0; i < sessionCount; i++) { + abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSession>* FF_AUTO_RELEASE_COM_OBJECT currentSession = NULL; + hr = sessions->GetAt(i, reinterpret_cast<void**>(¤tSession)); + if (FAILED(hr) || !currentSession) { + continue; + } + + hr = currentSession->get_SourceAppUserModelId(reinterpret_cast<void**>(&playerId)); + if (FAILED(hr) || !playerId) { + continue; + } + + ffStrbufSetHstring(&result->playerId, playerId); + + if (ffStrbufContainIgnCase(&result->playerId, &instance.config.general.playerName)) { + session = currentSession; + currentSession = NULL; // Don't release the session object + break; + } + deleteHstring(&playerId); + ffStrbufClear(&result->playerId); + } + + if (!session) { + error = "winrt: No media session found with the specified player name"; + break; + } + } else { + hr = manager->GetCurrentSession(reinterpret_cast<void**>(&session)); + + if (FAILED(hr) || !session) { + error = "winrt: GetCurrentSession() failed"; + break; + } + + hr = session->get_SourceAppUserModelId(reinterpret_cast<void**>(&playerId)); + if (FAILED(hr)) { + error = "winrt: get_SourceAppUserModelId() failed"; + break; + } + + ffStrbufSetHstring(&result->playerId, playerId); + } + + abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionMediaProperties>* FF_AUTO_RELEASE_COM_OBJECT mediaProps = NULL; + hr = ffRunAndWait<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionMediaProperties>([=](void** result) { + return session->TryGetMediaPropertiesAsync(result); + }, + &mediaProps); + if (FAILED(hr) || !mediaProps) { + error = "winrt: TryGetMediaPropertiesAsync().GetResults() failed"; + break; + } + + abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionPlaybackInfo>* FF_AUTO_RELEASE_COM_OBJECT playbackInfo = NULL; + hr = session->GetPlaybackInfo(reinterpret_cast<void**>(&playbackInfo)); + bool isPlaying = false; + double playbackRate = 1.0; + if (SUCCEEDED(hr) && playbackInfo) { + int32_t playbackStatusValue = 0; + if (SUCCEEDED(playbackInfo->get_PlaybackStatus(&playbackStatusValue))) { + isPlaying = playbackStatusValue == static_cast<int32_t>(winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing); + switch (static_cast<winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus>(playbackStatusValue)) { + #define FF_MEDIA_SET_STATUS(status_code) \ + case winrt::Windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus::status_code: \ + ffStrbufSetStatic(&result->status, #status_code); \ + break; + FF_MEDIA_SET_STATUS(Closed) + FF_MEDIA_SET_STATUS(Opened) + FF_MEDIA_SET_STATUS(Changing) + FF_MEDIA_SET_STATUS(Stopped) + FF_MEDIA_SET_STATUS(Playing) + FF_MEDIA_SET_STATUS(Paused) + #undef FF_MEDIA_SET_STATUS + } + } + + abi_t<winrt::Windows::Foundation::IReference<double>>* FF_AUTO_RELEASE_COM_OBJECT playbackRateRef = NULL; + if (SUCCEEDED(playbackInfo->get_PlaybackRate(reinterpret_cast<void**>(&playbackRateRef))) && playbackRateRef) { + if (SUCCEEDED(playbackRateRef->get_Value(&playbackRate)) && playbackRate < 0.0) { + playbackRate = 0.0; + } + } + } + + FF_A_CLEANUP(deleteHstring) HSTRING title = NULL; + if (SUCCEEDED(mediaProps->get_Title(reinterpret_cast<void**>(&title)))) { + ffStrbufSetHstring(&result->song, title); + } + + FF_A_CLEANUP(deleteHstring) HSTRING artist = NULL; + if (SUCCEEDED(mediaProps->get_Artist(reinterpret_cast<void**>(&artist)))) { + ffStrbufSetHstring(&result->artist, artist); + } + + FF_A_CLEANUP(deleteHstring) HSTRING album = NULL; + if (SUCCEEDED(mediaProps->get_AlbumTitle(reinterpret_cast<void**>(&album)))) { + ffStrbufSetHstring(&result->album, album); + } + + abi_t<winrt::Windows::Media::Control::IGlobalSystemMediaTransportControlsSessionTimelineProperties>* FF_AUTO_RELEASE_COM_OBJECT timelineProps = NULL; + hr = session->GetTimelineProperties(reinterpret_cast<void**>(&timelineProps)); + if (SUCCEEDED(hr) && timelineProps) { + int64_t duration = 0; + if (SUCCEEDED(timelineProps->get_EndTime(&duration)) && duration > 0) { + result->length = (uint32_t) (duration / 10000); // Convert from 100-nanosecond units to milliseconds + + int64_t position = 0; + if (SUCCEEDED(timelineProps->get_Position(&position))) { + result->position = (uint32_t) (position / 10000); // Convert from 100-nanosecond units to milliseconds + + int64_t lastUpdatedTime = 0; + if (isPlaying && SUCCEEDED(timelineProps->get_LastUpdatedTime(&lastUpdatedTime)) && lastUpdatedTime > 0) { + uint64_t lastUpdatedTimeMs = ffFileTimeToUnixMs((uint64_t) lastUpdatedTime); + uint64_t nowMs = ffTimeGetNow(); + if (nowMs > lastUpdatedTimeMs) { + result->position += (uint32_t) (((double) (nowMs - lastUpdatedTimeMs)) * playbackRate); + } + } + } + } + } + + abi_t<winrt::Windows::ApplicationModel::IAppInfoStatics>* FF_AUTO_RELEASE_COM_OBJECT appInfoStatics = NULL; + hr = ffGetActivationFactory(L"Windows.ApplicationModel.AppInfo", winrt::guid_of<winrt::Windows::ApplicationModel::IAppInfoStatics>(), &appInfoStatics); + if (SUCCEEDED(hr) && appInfoStatics) { + abi_t<winrt::Windows::ApplicationModel::IAppInfo>* FF_AUTO_RELEASE_COM_OBJECT appInfo = NULL; + if (SUCCEEDED(appInfoStatics->GetFromAppUserModelId(reinterpret_cast<void*>(playerId), reinterpret_cast<void**>(&appInfo))) && appInfo) { + abi_t<winrt::Windows::ApplicationModel::IAppDisplayInfo>* FF_AUTO_RELEASE_COM_OBJECT displayInfo = NULL; + if (SUCCEEDED(appInfo->get_DisplayInfo(reinterpret_cast<void**>(&displayInfo))) && displayInfo) { + FF_A_CLEANUP(deleteHstring) HSTRING displayName = NULL; + if (SUCCEEDED(displayInfo->get_DisplayName(reinterpret_cast<void**>(&displayName)))) { + ffStrbufSetHstring(&result->player, displayName); + } + } + } + } + + if (result->player.length == 0) { + ffStrbufSet(&result->player, &result->playerId); + if (ffStrbufEndsWithIgnCaseS(&result->player, ".exe")) { + ffStrbufSubstrBefore(&result->player, result->player.length - 4); + } + } + + if (saveCover) { + abi_t<winrt::Windows::Storage::Streams::IRandomAccessStreamReference>* FF_AUTO_RELEASE_COM_OBJECT thumbnail = NULL; + hr = mediaProps->get_Thumbnail(reinterpret_cast<void**>(&thumbnail)); + if (SUCCEEDED(hr) && thumbnail) { + if (SUCCEEDED(ffSaveThumbnailToTempPath(thumbnail, &result->cover)) && result->cover.length > 0) { + result->removeCoverAfterUse = true; + } + } + } + } while (false); + + return error; +} +#else +static const char* getMedia(FFMediaResult* media, bool saveCover) { + FF_UNUSED(media, saveCover); + return "Fastfetch is not compiled with WinRT support"; +} +#endif // FF_HAVE_WINRT + +extern "C" void ffDetectMediaImpl(FFMediaResult* media, bool saveCover) { + const char* error = getMedia(media, saveCover); + ffStrbufAppendS(&media->error, error); +} diff --git a/src/detection/memory/memory.h b/src/detection/memory/memory.h new file mode 100644 index 0000000..1c23828 --- /dev/null +++ b/src/detection/memory/memory.h @@ -0,0 +1,11 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/memory/option.h" + +typedef struct FFMemoryResult { + uint64_t bytesUsed; + uint64_t bytesTotal; +} FFMemoryResult; + +const char* ffDetectMemory(FFMemoryResult* ram); diff --git a/src/detection/memory/memory_apple.c b/src/detection/memory/memory_apple.c new file mode 100644 index 0000000..6203949 --- /dev/null +++ b/src/detection/memory/memory_apple.c @@ -0,0 +1,35 @@ +#include "memory.h" +#include "common/debug.h" + +#include <string.h> +#include <mach/mach.h> +#include <sys/sysctl.h> +#include <unistd.h> + +const char* ffDetectMemory(FFMemoryResult* ram) { + size_t length = sizeof(ram->bytesTotal); + +#if FF_APPLE_MEMSIZE_USABLE + if (sysctlbyname("hw.memsize_usable", &ram->bytesTotal, &length, NULL, 0) != 0) { + return "Failed to read hw.memsize_usable"; + } +#else + if (sysctl((int[]) { CTL_HW, HW_MEMSIZE }, 2, &ram->bytesTotal, &length, NULL, 0) != 0) { + return "Failed to read hw.memsize"; + } +#endif + + mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; + vm_statistics64_data_t vmstat; + if (host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t) (&vmstat), &count) != KERN_SUCCESS) { + return "Failed to read host_statistics64"; + } + + // https://github.com/st3fan/osx-10.9/blob/34e34a6a539b5a822cda4074e56a7ced9b57da71/system_cmds-597.1.1/vm_stat.tproj/vm_stat.c#L139 + + uint64_t pagesFree = vmstat.free_count - vmstat.speculative_count; + uint64_t pagesFileBacked = vmstat.external_page_count; // Cached files + ram->bytesUsed = ram->bytesTotal - (pagesFree + pagesFileBacked) * instance.state.platform.sysinfo.pageSize; + + return NULL; +} diff --git a/src/detection/memory/memory_bsd.c b/src/detection/memory/memory_bsd.c new file mode 100644 index 0000000..f76f6b8 --- /dev/null +++ b/src/detection/memory/memory_bsd.c @@ -0,0 +1,16 @@ +#include "memory.h" +#include "common/sysctl.h" + +const char* ffDetectMemory(FFMemoryResult* ram) { + size_t length = sizeof(ram->bytesTotal); + if (sysctl((int[]) { CTL_HW, HW_PHYSMEM }, 2, &ram->bytesTotal, &length, NULL, 0)) { + return "Failed to read hw.physmem"; + } + + // vm.stats.vm.* are int values + int32_t pagesFree = ffSysctlGetInt("vm.stats.vm.v_free_count", 0) + ffSysctlGetInt("vm.stats.vm.v_inactive_count", 0) + ffSysctlGetInt("vm.stats.vm.v_cache_count", 0); + + ram->bytesUsed = ram->bytesTotal - (uint64_t) pagesFree * instance.state.platform.sysinfo.pageSize; + + return NULL; +} diff --git a/src/detection/memory/memory_haiku.c b/src/detection/memory/memory_haiku.c new file mode 100644 index 0000000..7888801 --- /dev/null +++ b/src/detection/memory/memory_haiku.c @@ -0,0 +1,16 @@ +#include "memory.h" + +#include <OS.h> + +const char* ffDetectMemory(FFMemoryResult* ram) { + system_info info; + if (get_system_info(&info) != B_OK) { + return "Error getting system info"; + } + + uint32_t pageSize = instance.state.platform.sysinfo.pageSize; + ram->bytesTotal = pageSize * info.max_pages; + ram->bytesUsed = pageSize * info.used_pages; + + return NULL; +} diff --git a/src/detection/memory/memory_linux.c b/src/detection/memory/memory_linux.c new file mode 100644 index 0000000..74d20a0 --- /dev/null +++ b/src/detection/memory/memory_linux.c @@ -0,0 +1,62 @@ +#include "memory.h" +#include "common/io.h" +#include "common/mallocHelper.h" + +#include <inttypes.h> + +const char* ffDetectMemory(FFMemoryResult* ram) { + char buf[PROC_FILE_BUFFSIZ]; + ssize_t nRead = ffReadFileData("/proc/meminfo", ARRAY_SIZE(buf) - 1, buf); + if (nRead < 0) { + return "ffReadFileData(\"/proc/meminfo\", ARRAY_SIZE(buf)-1, buf)"; + } + buf[nRead] = '\0'; + + uint64_t memTotal = 0, + memAvailable = 0, + shmem = 0, + memFree = 0, + buffers = 0, + cached = 0, + sReclaimable = 0; + + char* token = NULL; + if ((token = strstr(buf, "MemTotal:")) != NULL) { + memTotal = strtoul(token + strlen("MemTotal:"), NULL, 10); + } else { + return "MemTotal not found in /proc/meminfo"; + } + + if ((token = strstr(buf, "MemAvailable:")) != NULL) { + memAvailable = strtoul(token + strlen("MemAvailable:"), NULL, 10); + } + if (memAvailable == 0 || memAvailable >= memTotal) // MemAvailable can be unreasonable. #1988 + { + if ((token = strstr(buf, "MemFree:")) != NULL) { + memFree = strtoul(token + strlen("MemFree:"), NULL, 10); + } + + if ((token = strstr(buf, "Buffers:")) != NULL) { + buffers = strtoul(token + strlen("Buffers:"), NULL, 10); + } + + if ((token = strstr(buf, "Cached:")) != NULL) { + cached = strtoul(token + strlen("Cached:"), NULL, 10); + } + + if ((token = strstr(buf, "Shmem:")) != NULL) { + shmem = strtoul(token + strlen("Shmem:"), NULL, 10); + } + + if ((token = strstr(buf, "SReclaimable:")) != NULL) { + sReclaimable = strtoul(token + strlen("SReclaimable:"), NULL, 10); + } + + memAvailable = memFree + buffers + cached + sReclaimable - shmem; + } + + ram->bytesTotal = memTotal * 1024lu; + ram->bytesUsed = (memTotal - memAvailable) * 1024lu; + + return NULL; +} diff --git a/src/detection/memory/memory_nbsd.c b/src/detection/memory/memory_nbsd.c new file mode 100644 index 0000000..ca09475 --- /dev/null +++ b/src/detection/memory/memory_nbsd.c @@ -0,0 +1,18 @@ +#include "memory.h" +#include "common/sysctl.h" + +#include <sys/param.h> +#include <uvm/uvm_extern.h> + +const char* ffDetectMemory(FFMemoryResult* ram) { + struct uvmexp_sysctl buf; + size_t length = sizeof(buf); + if (sysctl((int[]) { CTL_VM, VM_UVMEXP2 }, 2, &buf, &length, NULL, 0) < 0) { + return "sysctl(CTL_VM, VM_UVMEXP2) failed"; + } + + ram->bytesTotal = (uint64_t) buf.npages * instance.state.platform.sysinfo.pageSize; + ram->bytesUsed = ((uint64_t) buf.active + (uint64_t) buf.inactive + (uint64_t) buf.wired) * instance.state.platform.sysinfo.pageSize; + + return NULL; +} diff --git a/src/detection/memory/memory_nosupport.c b/src/detection/memory/memory_nosupport.c new file mode 100644 index 0000000..b44aaad --- /dev/null +++ b/src/detection/memory/memory_nosupport.c @@ -0,0 +1,5 @@ +#include "memory.h" + +const char* ffDetectMemory(FF_A_UNUSED FFMemoryResult* ram) { + return "Not supported on this platform"; +} diff --git a/src/detection/memory/memory_obsd.c b/src/detection/memory/memory_obsd.c new file mode 100644 index 0000000..c6da087 --- /dev/null +++ b/src/detection/memory/memory_obsd.c @@ -0,0 +1,18 @@ +#include "memory.h" +#include "common/sysctl.h" + +#include <sys/param.h> +#include <uvm/uvm_extern.h> + +const char* ffDetectMemory(FFMemoryResult* ram) { + struct uvmexp buf; + size_t length = sizeof(buf); + if (sysctl((int[]) { CTL_VM, VM_UVMEXP }, 2, &buf, &length, NULL, 0) < 0) { + return "sysctl(CTL_VM, VM_UVMEXP) failed"; + } + + ram->bytesTotal = (uint64_t) buf.npages * instance.state.platform.sysinfo.pageSize; + ram->bytesUsed = ((uint64_t) buf.active + (uint64_t) buf.inactive + (uint64_t) buf.wired) * instance.state.platform.sysinfo.pageSize; + + return NULL; +} diff --git a/src/detection/memory/memory_sunos.c b/src/detection/memory/memory_sunos.c new file mode 100644 index 0000000..7dfdeab --- /dev/null +++ b/src/detection/memory/memory_sunos.c @@ -0,0 +1,9 @@ +#include "memory.h" +#include <unistd.h> + +const char* ffDetectMemory(FFMemoryResult* ram) { + ram->bytesTotal = (uint64_t) sysconf(_SC_PHYS_PAGES) * instance.state.platform.sysinfo.pageSize; + ram->bytesUsed = ram->bytesTotal - (uint64_t) sysconf(_SC_AVPHYS_PAGES) * instance.state.platform.sysinfo.pageSize; + + return NULL; +} diff --git a/src/detection/memory/memory_windows.c b/src/detection/memory/memory_windows.c new file mode 100644 index 0000000..779425d --- /dev/null +++ b/src/detection/memory/memory_windows.c @@ -0,0 +1,19 @@ +#include "memory.h" + +#include <windows.h> + +const char* ffDetectMemory(FFMemoryResult* ram) { + MEMORYSTATUSEX statex = { + .dwLength = sizeof(statex), + }; + // GlobalMemoryStatusEx() internally uses + // NtQuerySystemInformation(SystemBasicPerformanceInformation) in Win 7, and + // NtQuerySystemInformation(SystemMemoryUsageInformation) in Win 10 + if (!GlobalMemoryStatusEx(&statex)) { + return "GlobalMemoryStatusEx() failed"; + } + + ram->bytesTotal = statex.ullTotalPhys; + ram->bytesUsed = statex.ullTotalPhys - statex.ullAvailPhys; + return NULL; +} diff --git a/src/detection/mouse/mouse.h b/src/detection/mouse/mouse.h new file mode 100644 index 0000000..3247eeb --- /dev/null +++ b/src/detection/mouse/mouse.h @@ -0,0 +1,8 @@ +#include "fastfetch.h" + +typedef struct FFMouseDevice { + FFstrbuf serial; + FFstrbuf name; +} FFMouseDevice; + +const char* ffDetectMouse(FFlist* devices /* List of FFMouseDevice */); diff --git a/src/detection/mouse/mouse_apple.c b/src/detection/mouse/mouse_apple.c new file mode 100644 index 0000000..2c5b2d7 --- /dev/null +++ b/src/detection/mouse/mouse_apple.c @@ -0,0 +1,36 @@ +#include "mouse.h" +#include "common/apple/cf_helpers.h" +#include "common/mallocHelper.h" + +#include <IOKit/IOKitLib.h> +#include <IOKit/hid/IOHIDLib.h> + +static void enumSet(IOHIDDeviceRef value, FFlist* results) { + FFMouseDevice* device = FF_LIST_ADD(FFMouseDevice, *results); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->name); + + CFStringRef product = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDProductKey)); + ffCfStrGetString(product, &device->name); + + CFStringRef serialNumber = IOHIDDeviceGetProperty(value, CFSTR(kIOHIDSerialNumberKey)); + ffCfStrGetString(serialNumber, &device->serial); +} + +const char* ffDetectMouse(FFlist* devices /* List of FFMouseDevice */) { + IOHIDManagerRef FF_CFTYPE_AUTO_RELEASE manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) { + return "IOHIDManagerOpen() failed"; + } + + CFDictionaryRef FF_CFTYPE_AUTO_RELEASE matching1 = CFDictionaryCreate(kCFAllocatorDefault, (const void**) (CFStringRef[]) { CFSTR(kIOHIDDeviceUsagePageKey), CFSTR(kIOHIDDeviceUsageKey) }, (const void**) (CFNumberRef[]) { ffCfCreateInt(kHIDPage_GenericDesktop), ffCfCreateInt(kHIDUsage_GD_Mouse) }, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + IOHIDManagerSetDeviceMatching(manager, matching1); + + CFSetRef FF_CFTYPE_AUTO_RELEASE set = IOHIDManagerCopyDevices(manager); + if (set) { + CFSetApplyFunction(set, (CFSetApplierFunction) &enumSet, devices); + } + IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); + + return NULL; +} diff --git a/src/detection/mouse/mouse_bsd.c b/src/detection/mouse/mouse_bsd.c new file mode 100644 index 0000000..da7783b --- /dev/null +++ b/src/detection/mouse/mouse_bsd.c @@ -0,0 +1,56 @@ +#include "mouse.h" +#include "common/io.h" + +#include <stdio.h> +#include <fcntl.h> +#include <usbhid.h> + +#if __has_include(<dev/usb/usb_ioctl.h>) + #include <dev/usb/usb_ioctl.h> // FreeBSD +#else + #include <bus/u4b/usb_ioctl.h> // DragonFly +#endif + +#define MAX_UHID_MICE 64 + +const char* ffDetectMouse(FFlist* devices /* List of FFMouseDevice */) { + char path[16]; + for (int i = 0; i < MAX_UHID_MICE; i++) { + snprintf(path, ARRAY_SIZE(path), "/dev/uhid%d", i); + FF_AUTO_CLOSE_FD int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + if (errno == ENOENT) { + break; // No more devices + } + continue; // Device not found + } + report_desc_t repDesc = hid_get_report_desc(fd); + if (!repDesc) { + continue; + } + + int reportId = hid_get_report_id(fd); + + struct hid_data* hData = hid_start_parse(repDesc, 0, reportId); + if (hData) { + struct hid_item hItem; + while (hid_get_item(hData, &hItem) > 0) { + if (HID_PAGE(hItem.usage) != 1 || HID_USAGE(hItem.usage) != 2) { + continue; + } + + struct usb_device_info di; + if (ioctl(fd, USB_GET_DEVICEINFO, &di) != -1) { + FFMouseDevice* device = FF_LIST_ADD(FFMouseDevice, *devices); + ffStrbufInitS(&device->serial, di.udi_serial); + ffStrbufInitS(&device->name, di.udi_product); + } + } + hid_end_parse(hData); + } + + hid_dispose_report_desc(repDesc); + } + + return NULL; +} diff --git a/src/detection/mouse/mouse_haiku.cpp b/src/detection/mouse/mouse_haiku.cpp new file mode 100644 index 0000000..5835c8e --- /dev/null +++ b/src/detection/mouse/mouse_haiku.cpp @@ -0,0 +1,27 @@ +extern "C" { +#include "mouse.h" +} + +#include <interface/Input.h> +#include <support/List.h> + +const char* ffDetectMouse(FFlist* devices /* List of FFMouseDevice */) { + BList list; + + if (get_input_devices(&list) != B_OK) { + return "get_input_devices() failed"; + } + + for (int32 i = 0, n = list.CountItems(); i < n; i++) { + BInputDevice* device = (BInputDevice*) list.ItemAt(i); + if (device->Type() != B_POINTING_DEVICE || !device->IsRunning()) { + continue; + } + + FFMouseDevice* item = FF_LIST_ADD(FFMouseDevice, *devices); + ffStrbufInit(&item->serial); + ffStrbufInitS(&item->name, device->Name()); + } + + return NULL; +} diff --git a/src/detection/mouse/mouse_linux.c b/src/detection/mouse/mouse_linux.c new file mode 100644 index 0000000..939c332 --- /dev/null +++ b/src/detection/mouse/mouse_linux.c @@ -0,0 +1,45 @@ +#include "mouse.h" +#include "common/io.h" +#include "common/strutil.h" + +const char* ffDetectMouse(FFlist* devices /* List of FFMouseDevice */) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/input/"); + if (dirp == NULL) { + return "opendir(\"/sys/class/input/\") == NULL"; + } + + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateS("/sys/class/input/"); + uint32_t baseLen = path.length; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (!ffStrStartsWith(entry->d_name, "mouse")) { + continue; + } + if (!ffCharIsDigit(entry->d_name[strlen("mouse")])) { + continue; + } + + ffStrbufAppendS(&path, entry->d_name); + ffStrbufAppendS(&path, "/device/name"); + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + if (ffAppendFileBuffer(path.chars, &name)) { + ffStrbufTrimRightSpace(&name); + ffStrbufSubstrBefore(&path, path.length - 4); + + FFMouseDevice* device = FF_LIST_ADD(FFMouseDevice, *devices); + ffStrbufInitMove(&device->name, &name); + ffStrbufInit(&device->serial); + + ffStrbufAppendS(&path, "uniq"); + if (ffAppendFileBuffer(path.chars, &device->serial)) { + ffStrbufTrimRightSpace(&device->serial); + } + } + + ffStrbufSubstrBefore(&path, baseLen); + } + + return NULL; +} diff --git a/src/detection/mouse/mouse_nosupport.c b/src/detection/mouse/mouse_nosupport.c new file mode 100644 index 0000000..aa1332d --- /dev/null +++ b/src/detection/mouse/mouse_nosupport.c @@ -0,0 +1,5 @@ +#include "mouse.h" + +const char* ffDetectMouse(FF_A_UNUSED FFlist* devices /* List of FFMouseDevice */) { + return "No mouse support on this platform"; +} diff --git a/src/detection/mouse/mouse_windows.c b/src/detection/mouse/mouse_windows.c new file mode 100644 index 0000000..3f2370a --- /dev/null +++ b/src/detection/mouse/mouse_windows.c @@ -0,0 +1,84 @@ +#define INITGUID + +#include "mouse.h" +#include "common/io.h" +#include "common/mallocHelper.h" +#include "common/windows/unicode.h" + +#include <windows.h> +#include <hidsdi.h> +#include <cfgmgr32.h> +#include <devpkey.h> + +const char* ffDetectMouse(FFlist* devices /* List of FFMouseDevice */) { + UINT nDevices = 0; + if (GetRawInputDeviceList(NULL, &nDevices, sizeof(RAWINPUTDEVICELIST))) { + return "GetRawInputDeviceList(NULL) failed"; + } + if (nDevices == 0) { + return "No HID devices found"; + } + RAWINPUTDEVICELIST* FF_AUTO_FREE pRawInputDeviceList = (RAWINPUTDEVICELIST*) malloc(sizeof(RAWINPUTDEVICELIST) * nDevices); + if ((nDevices = GetRawInputDeviceList(pRawInputDeviceList, &nDevices, sizeof(RAWINPUTDEVICELIST))) == (UINT) -1) { + return "GetRawInputDeviceList(pRawInputDeviceList) failed"; + } + + for (UINT i = 0; i < nDevices; ++i) { + if (pRawInputDeviceList[i].dwType != RIM_TYPEMOUSE) { + continue; + } + + HANDLE hDevice = pRawInputDeviceList[i].hDevice; + + RID_DEVICE_INFO rdi; + UINT rdiSize = sizeof(rdi); + if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICEINFO, &rdi, &rdiSize) == (UINT) -1) { + continue; + } + + WCHAR devName[MAX_PATH]; + UINT nameSize = MAX_PATH; + if (GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, devName, &nameSize) == (UINT) -1) { + continue; + } + + FFMouseDevice* device = FF_LIST_ADD(FFMouseDevice, *devices); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->name); + + wchar_t buffer[MAX_PATH]; + + HANDLE FF_AUTO_CLOSE_FD hHidFile = CreateFileW(devName, 0 /* must be 0 instead of GENERIC_READ */, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + if (hHidFile != INVALID_HANDLE_VALUE) { + if (HidD_GetProductString(hHidFile, buffer, (ULONG) sizeof(buffer))) { + ffStrbufSetWS(&device->name, buffer); + } + + if (HidD_GetSerialNumberString(hHidFile, buffer, sizeof(buffer))) { + ffStrbufSetWS(&device->serial, buffer); + } + } + + if (!device->name.length) { + // https://stackoverflow.com/a/64321096/9976392 + DEVPROPTYPE propertyType; + ULONG propertySize = sizeof(buffer); + + if (CM_Get_Device_Interface_PropertyW(devName, &DEVPKEY_Device_InstanceId, &propertyType, (PBYTE) buffer, &propertySize, 0) == CR_SUCCESS) { + DEVINST devInst; + if (CM_Locate_DevNodeW(&devInst, buffer, CM_LOCATE_DEVNODE_NORMAL) == CR_SUCCESS) { + propertySize = sizeof(buffer); + if (CM_Get_DevNode_PropertyW(devInst, &DEVPKEY_NAME, &propertyType, (PBYTE) buffer, &propertySize, 0) == CR_SUCCESS) { + ffStrbufSetWS(&device->name, buffer); + } + } + } + } + + if (!device->name.length) { + ffStrbufSetF(&device->name, "Unknown device %04X-%04X", (unsigned) rdi.hid.dwVendorId, (unsigned) rdi.hid.dwProductId); + } + } + + return NULL; +} diff --git a/src/detection/netio/netio.c b/src/detection/netio/netio.c new file mode 100644 index 0000000..c6b9da6 --- /dev/null +++ b/src/detection/netio/netio.c @@ -0,0 +1,81 @@ +#include "netio.h" + +#include "common/time.h" + +static FFlist ioCounters1; +static uint64_t time1; + +void ffPrepareNetIO(FFNetIOOptions* options) { + if (options->detectTotal) { + return; + } + + if (time1 != 0) { + return; // Already prepared + } + + ffListInit(&ioCounters1); + ffNetIOGetIoCounters(&ioCounters1, options); + time1 = ffTimeGetNow(); +} + +const char* ffDetectNetIO(FFlist* result, FFNetIOOptions* options) { + const char* error = NULL; + + if (options->detectTotal) { + error = ffNetIOGetIoCounters(result, options); + if (error) { + return error; + } + return NULL; + } + + if (time1 == 0) { + ffListInit(&ioCounters1); + error = ffNetIOGetIoCounters(&ioCounters1, options); + if (error) { + return error; + } + time1 = ffTimeGetNow(); + } + + if (ioCounters1.length == 0) { + return "No network interfaces found"; + } + + uint64_t time2 = ffTimeGetNow(); + while (time2 - time1 < options->waitTime) { + ffTimeSleep((uint32_t) (options->waitTime - (time2 - time1))); + time2 = ffTimeGetNow(); + } + + error = ffNetIOGetIoCounters(result, options); + if (error) { + return error; + } + + if (result->length != ioCounters1.length) { + return "Different number of network interfaces. Network change?"; + } + + for (uint32_t i = 0; i < result->length; ++i) { + FFNetIOResult* icPrev = FF_LIST_GET(FFNetIOResult, ioCounters1, i); + FFNetIOResult* icCurr = FF_LIST_GET(FFNetIOResult, *result, i); + if (!ffStrbufEqual(&icPrev->name, &icCurr->name)) { + return "Network interface name changed"; + } + + static_assert(sizeof(FFNetIOResult) - offsetof(FFNetIOResult, txBytes) == sizeof(uint64_t) * 8, "Unexpected struct FFNetIOResult layout"); + for (size_t off = offsetof(FFNetIOResult, txBytes); off < sizeof(FFNetIOResult); off += sizeof(uint64_t)) { + uint64_t* prevValue = (uint64_t*) ((uint8_t*) icPrev + off); + uint64_t* currValue = (uint64_t*) ((uint8_t*) icCurr + off); + uint64_t temp = *currValue; + *currValue -= *prevValue; + *currValue /= (time2 - time1) / 1000 /* seconds */; + *prevValue = temp; + } + } + time1 = time2; + + return NULL; +} diff --git a/src/detection/netio/netio.h b/src/detection/netio/netio.h new file mode 100644 index 0000000..1e48b53 --- /dev/null +++ b/src/detection/netio/netio.h @@ -0,0 +1,20 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/netio/option.h" + +typedef struct FFNetIOResult { + FFstrbuf name; + bool defaultRoute; + uint64_t txBytes; + uint64_t rxBytes; + uint64_t txPackets; + uint64_t rxPackets; + uint64_t rxErrors; + uint64_t txErrors; + uint64_t rxDrops; + uint64_t txDrops; +} FFNetIOResult; + +const char* ffDetectNetIO(FFlist* result, FFNetIOOptions* options); +const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options); diff --git a/src/detection/netio/netio_apple.c b/src/detection/netio/netio_apple.c new file mode 100644 index 0000000..125cfd9 --- /dev/null +++ b/src/detection/netio/netio_apple.c @@ -0,0 +1,55 @@ +#include "netio.h" + +#include "common/netif.h" +#include "common/mallocHelper.h" + +#include <net/if.h> +#include <net/if_mib.h> +#include <sys/sysctl.h> + +const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { + int mib[] = { CTL_NET, PF_LINK, NETLINK_GENERIC, options->defaultRouteOnly ? IFMIB_IFDATA : IFMIB_IFALLDATA, options->defaultRouteOnly ? (int) ffNetifGetDefaultRouteV4()->ifIndex : 0, IFDATA_GENERAL }; + + size_t bufSize = 0; + if (sysctl(mib, ARRAY_SIZE(mib), NULL, &bufSize, 0, 0) < 0) { + return "sysctl(mib, ARRAY_SIZE(mib), NULL, &bufSize, 0, 0) failed"; + } + + assert(bufSize % sizeof(struct ifmibdata) == 0); + + FF_AUTO_FREE struct ifmibdata* buf = (struct ifmibdata*) malloc(bufSize); + if (sysctl(mib, ARRAY_SIZE(mib), buf, &bufSize, 0, 0) < 0) { + return "sysctl(mib, ARRAY_SIZE(mib), buf, &bufSize, 0, 0) failed"; + } + + size_t ifCount = bufSize / sizeof(struct ifmibdata); + + const char* defaultRouteIfName = ffNetifGetDefaultRouteV4()->ifName; + + for (size_t i = 0; i < ifCount; i++) { + struct ifmibdata* mibdata = &buf[i]; + if (!(mibdata->ifmd_flags & IFF_RUNNING) || (mibdata->ifmd_flags & IFF_NOARP)) { + continue; + } + + if (options->namePrefix.length && strncmp(mibdata->ifmd_name, options->namePrefix.chars, options->namePrefix.length) != 0) { + continue; + } + + FFNetIOResult* counters = FF_LIST_ADD(FFNetIOResult, *result); + *counters = (FFNetIOResult) { + .name = ffStrbufCreateS(mibdata->ifmd_name), + .txBytes = mibdata->ifmd_data.ifi_obytes, + .rxBytes = mibdata->ifmd_data.ifi_ibytes, + .txPackets = mibdata->ifmd_data.ifi_opackets, + .rxPackets = mibdata->ifmd_data.ifi_ipackets, + .txErrors = mibdata->ifmd_data.ifi_oerrors, + .rxErrors = mibdata->ifmd_data.ifi_ierrors, + .txDrops = mibdata->ifmd_snd_drops, + .rxDrops = mibdata->ifmd_data.ifi_iqdrops, + .defaultRoute = strncmp(mibdata->ifmd_name, defaultRouteIfName, IFNAMSIZ) == 0, + }; + } + + return NULL; +} diff --git a/src/detection/netio/netio_bsd.c b/src/detection/netio/netio_bsd.c new file mode 100644 index 0000000..83c1b6b --- /dev/null +++ b/src/detection/netio/netio_bsd.c @@ -0,0 +1,62 @@ +#include "netio.h" + +#include "common/netif.h" +#include "common/mallocHelper.h" + +#include <net/if.h> +#include <net/if_dl.h> +#include <net/if_types.h> +#include <net/route.h> +#include <sys/sysctl.h> +#include <sys/socket.h> + +const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { + uint32_t defaultRouteIfIndex = ffNetifGetDefaultRouteV4()->ifIndex; + + size_t bufSize = 0; + if (sysctl((int[]) { CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, (options->defaultRouteOnly ? (int) defaultRouteIfIndex : 0) }, 6, NULL, &bufSize, 0, 0) < 0) { + return "sysctl({ CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, ifIndex }, 6, NULL, &bufSize, 0, 0) failed"; + } + + FF_AUTO_FREE struct if_msghdr* buf = (struct if_msghdr*) malloc(bufSize); + if (sysctl((int[]) { CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, (options->defaultRouteOnly ? (int) defaultRouteIfIndex : 0) }, 6, buf, &bufSize, 0, 0) < 0) { + return "sysctl({ CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, ifIndex }, 6, buf, &bufSize, 0, 0) failed"; + } + + for (struct if_msghdr* ifm = buf; + ifm < (struct if_msghdr*) ((uint8_t*) buf + bufSize); + ifm = (struct if_msghdr*) ((uint8_t*) ifm + ifm->ifm_msglen)) { + if (ifm->ifm_type != RTM_IFINFO || !(ifm->ifm_flags & IFF_RUNNING) || (ifm->ifm_flags & IFF_NOARP)) { + continue; + } + + struct sockaddr_dl* sdl = (struct sockaddr_dl*) (ifm + 1); + assert(sdl->sdl_family == AF_LINK); + + sdl->sdl_data[sdl->sdl_nlen] = 0; + + if (options->namePrefix.length && strncmp(sdl->sdl_data, options->namePrefix.chars, options->namePrefix.length) != 0) { + continue; + } + + FFNetIOResult* counters = FF_LIST_ADD(FFNetIOResult, *result); + *counters = (FFNetIOResult) { + .name = ffStrbufCreateNS(sdl->sdl_nlen, sdl->sdl_data), + .txBytes = ifm->ifm_data.ifi_obytes, + .rxBytes = ifm->ifm_data.ifi_ibytes, + .txPackets = ifm->ifm_data.ifi_opackets, + .rxPackets = ifm->ifm_data.ifi_ipackets, + .txErrors = ifm->ifm_data.ifi_oerrors, + .rxErrors = ifm->ifm_data.ifi_ierrors, +#ifdef FF_HAVE_IFI_OQDROPS + .txDrops = ifm->ifm_data.ifi_oqdrops, +#else + .txDrops = 0, // unsupported +#endif + .rxDrops = ifm->ifm_data.ifi_iqdrops, + .defaultRoute = sdl->sdl_index == defaultRouteIfIndex, + }; + } + + return NULL; +} diff --git a/src/detection/netio/netio_haiku.cpp b/src/detection/netio/netio_haiku.cpp new file mode 100644 index 0000000..88a123d --- /dev/null +++ b/src/detection/netio/netio_haiku.cpp @@ -0,0 +1,52 @@ +extern "C" { +#include "netio.h" +#include "common/netif.h" +} + +#include <NetworkInterface.h> +#include <NetworkRoster.h> + +const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { + BNetworkRoster& roster = BNetworkRoster::Default(); + + BNetworkInterface interface; + uint32 cookie = 0; + + uint32_t defaultRouteIfIndex = ffNetifGetDefaultRouteV4()->ifIndex; + + while (roster.GetNextInterface(&cookie, interface) == B_OK) { + if (!interface.Exists()) { + continue; + } + + bool defaultRoute = interface.Index() == defaultRouteIfIndex; + if (options->defaultRouteOnly && !defaultRoute) { + continue; + } + + if (options->namePrefix.length && strncmp(interface.Name(), options->namePrefix.chars, options->namePrefix.length) != 0) { + continue; + } + + ifreq_stats stats = {}; + if (interface.GetStats(stats) != B_OK) { + continue; + } + + FFNetIOResult* counters = FF_LIST_ADD(FFNetIOResult, *result); + *counters = (FFNetIOResult) { + .name = ffStrbufCreateS(interface.Name()), + .defaultRoute = defaultRoute, + .txBytes = stats.send.bytes, + .rxBytes = stats.receive.bytes, + .txPackets = stats.send.packets, + .rxPackets = stats.receive.packets, + .rxErrors = stats.receive.errors, + .txErrors = stats.send.errors, + .rxDrops = stats.receive.dropped, + .txDrops = stats.send.dropped + }; + } + + return NULL; +} diff --git a/src/detection/netio/netio_linux.c b/src/detection/netio/netio_linux.c new file mode 100644 index 0000000..5870d3f --- /dev/null +++ b/src/detection/netio/netio_linux.c @@ -0,0 +1,91 @@ +#include "netio.h" + +#include "common/io.h" +#include "common/netif.h" +#include "common/strutil.h" + +#include <fcntl.h> +#include <net/if.h> + +static void getData(FFstrbuf* buffer, const char* ifName, bool isDefaultRoute, int basefd, FFlist* result) { + FF_AUTO_CLOSE_FD int dfd = openat(basefd, ifName, O_RDONLY | O_DIRECTORY); + if (dfd < 0) { + return; + } + + char operstate; + if (!ffReadFileDataRelative(dfd, "operstate", 1, &operstate) || operstate != 'u' /* up or unknown */) { + return; + } + + FFNetIOResult* counters = FF_LIST_ADD(FFNetIOResult, *result); + ffStrbufInitS(&counters->name, ifName); + counters->defaultRoute = isDefaultRoute; + + if (ffReadFileBufferRelative(dfd, "statistics/rx_bytes", buffer)) { + counters->rxBytes = ffStrbufToUInt(buffer, 0); + } + + if (ffReadFileBufferRelative(dfd, "statistics/tx_bytes", buffer)) { + counters->txBytes = ffStrbufToUInt(buffer, 0); + } + + if (ffReadFileBufferRelative(dfd, "statistics/rx_packets", buffer)) { + counters->rxPackets = ffStrbufToUInt(buffer, 0); + } + + if (ffReadFileBufferRelative(dfd, "statistics/tx_packets", buffer)) { + counters->txPackets = ffStrbufToUInt(buffer, 0); + } + + if (ffReadFileBufferRelative(dfd, "statistics/rx_errors", buffer)) { + counters->rxErrors = ffStrbufToUInt(buffer, 0); + } + + if (ffReadFileBufferRelative(dfd, "statistics/tx_errors", buffer)) { + counters->txErrors = ffStrbufToUInt(buffer, 0); + } + + if (ffReadFileBufferRelative(dfd, "statistics/rx_dropped", buffer)) { + counters->rxDrops = ffStrbufToUInt(buffer, 0); + } + + if (ffReadFileBufferRelative(dfd, "statistics/tx_dropped", buffer)) { + counters->txDrops = ffStrbufToUInt(buffer, 0); + } +} + +const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/net"); + if (!dirp) { + return "opendir(\"/sys/class/net\") == NULL"; + } + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + const char* defaultRouteIfName = ffNetifGetDefaultRouteV4()->ifName; + + if (options->defaultRouteOnly) { + if (options->namePrefix.length && strncmp(defaultRouteIfName, options->namePrefix.chars, options->namePrefix.length) != 0) { + return NULL; + } + + getData(&buffer, defaultRouteIfName, true, dirfd(dirp), result); + } else { + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + const char* ifName = entry->d_name; + if (ifName[0] == '.') { + continue; + } + + if (options->namePrefix.length && strncmp(ifName, options->namePrefix.chars, options->namePrefix.length) != 0) { + continue; + } + + getData(&buffer, ifName, ffStrEquals(ifName, defaultRouteIfName), dirfd(dirp), result); + } + } + + return NULL; +} diff --git a/src/detection/netio/netio_nosupport.c b/src/detection/netio/netio_nosupport.c new file mode 100644 index 0000000..30f73a7 --- /dev/null +++ b/src/detection/netio/netio_nosupport.c @@ -0,0 +1,5 @@ +#include "netio.h" + +const char* ffNetIOGetIoCounters(FF_A_UNUSED FFlist* result, FF_A_UNUSED FFNetIOOptions* options) { + return "Not supported on this platform"; +} diff --git a/src/detection/netio/netio_sunos.c b/src/detection/netio/netio_sunos.c new file mode 100644 index 0000000..3789e45 --- /dev/null +++ b/src/detection/netio/netio_sunos.c @@ -0,0 +1,68 @@ +#include "netio.h" +#include "common/netif.h" +#include "common/strutil.h" + +#include <kstat.h> + +static inline void kstatFreeWrap(kstat_ctl_t** pkc) { + assert(pkc); + if (*pkc) { + kstat_close(*pkc); + } +} + +const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { + FF_A_CLEANUP(kstatFreeWrap) kstat_ctl_t* kc = kstat_open(); + if (!kc) { + return "kstat_open() failed"; + } + + const char* defaultRouteIfName = ffNetifGetDefaultRouteV4()->ifName; + + for (kstat_t* ks = kc->kc_chain; ks; ks = ks->ks_next) { + if (!ffStrEquals(ks->ks_class, "net") || !ffStrEquals(ks->ks_module, "link")) { + continue; + } + + if (options->namePrefix.length && strncmp(ks->ks_name, options->namePrefix.chars, options->namePrefix.length) != 0) { + continue; + } + + bool isDefaultRoute = ffStrEquals(ks->ks_name, defaultRouteIfName); + if (options->defaultRouteOnly && !isDefaultRoute) { + continue; + } + + if (kstat_read(kc, ks, NULL) < 0) { + continue; + } + + FFNetIOResult* counters = FF_LIST_ADD(FFNetIOResult, *result); + + kstat_named_t* wbytes = (kstat_named_t*) kstat_data_lookup(ks, "obytes64"); + kstat_named_t* rbytes = (kstat_named_t*) kstat_data_lookup(ks, "rbytes64"); + kstat_named_t* wpkts = (kstat_named_t*) kstat_data_lookup(ks, "opackets64"); + kstat_named_t* rpkts = (kstat_named_t*) kstat_data_lookup(ks, "ipackets64"); + kstat_named_t* werrs = (kstat_named_t*) kstat_data_lookup(ks, "oerrors"); + kstat_named_t* rerrs = (kstat_named_t*) kstat_data_lookup(ks, "ierrors"); + + *counters = (FFNetIOResult) { + .name = ffStrbufCreateS(ks->ks_name), + .txBytes = wbytes->value.ui64, + .rxBytes = rbytes->value.ui64, + .txPackets = wpkts->value.ui64, + .rxPackets = rpkts->value.ui64, + .txErrors = werrs->value.ui64, + .rxErrors = rerrs->value.ui64, + .txDrops = 0, // unsupported + .rxDrops = 0, + .defaultRoute = isDefaultRoute, + }; + + if (options->defaultRouteOnly) { + break; + } + } + + return NULL; +} diff --git a/src/detection/netio/netio_windows.c b/src/detection/netio/netio_windows.c new file mode 100644 index 0000000..3c64334 --- /dev/null +++ b/src/detection/netio/netio_windows.c @@ -0,0 +1,71 @@ +#include "netio.h" + +#include "common/netif.h" +#include "common/mallocHelper.h" +#include "common/windows/unicode.h" + +#include <ws2tcpip.h> +#include <iphlpapi.h> + +const char* ffNetIOGetIoCounters(FFlist* result, FFNetIOOptions* options) { + IP_ADAPTER_ADDRESSES* FF_AUTO_FREE adapter_addresses = NULL; + + // Multiple attempts in case interfaces change while + // we are in the middle of querying them. + DWORD adapter_addresses_buffer_size = 0; + for (int attempts = 0;; ++attempts) { + if (adapter_addresses_buffer_size) { + adapter_addresses = (IP_ADAPTER_ADDRESSES*) realloc(adapter_addresses, adapter_addresses_buffer_size); + assert(adapter_addresses); + } + + DWORD error = GetAdaptersAddresses( + AF_UNSPEC, + GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, + NULL, + adapter_addresses, + &adapter_addresses_buffer_size); + + if (error == ERROR_SUCCESS) { + break; + } else if (ERROR_BUFFER_OVERFLOW == error && attempts < 4) { + continue; + } else { + return "GetAdaptersAddresses() failed"; + } + } + + uint32_t defaultRouteIfIndex = ffNetifGetDefaultRouteV4()->ifIndex; + + // Iterate through all of the adapters + for (IP_ADAPTER_ADDRESSES* adapter = adapter_addresses; adapter; adapter = adapter->Next) { + bool isDefaultRoute = adapter->IfIndex == defaultRouteIfIndex; + if (options->defaultRouteOnly && !isDefaultRoute) { + continue; + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateWS(adapter->FriendlyName); + if (options->namePrefix.length && !ffStrbufStartsWith(&name, &options->namePrefix)) { + continue; + } + + MIB_IF_ROW2 ifRow = { .InterfaceIndex = adapter->IfIndex }; + if (GetIfEntry2(&ifRow) == NO_ERROR) { + FFNetIOResult* counters = FF_LIST_ADD(FFNetIOResult, *result); + *counters = (FFNetIOResult) { + .name = ffStrbufCreateMove(&name), + .txBytes = ifRow.OutOctets, + .rxBytes = ifRow.InOctets, + .txPackets = (ifRow.OutUcastPkts + ifRow.OutNUcastPkts), + .rxPackets = (ifRow.InUcastPkts + ifRow.InNUcastPkts), + .rxErrors = ifRow.InErrors, + .txErrors = ifRow.OutErrors, + .rxDrops = ifRow.InDiscards, + .txDrops = ifRow.OutDiscards, + .defaultRoute = isDefaultRoute, + }; + } + } + + return NULL; +} diff --git a/src/detection/opencl/opencl.c b/src/detection/opencl/opencl.c new file mode 100644 index 0000000..14216ac --- /dev/null +++ b/src/detection/opencl/opencl.c @@ -0,0 +1,218 @@ +#include "detection/opencl/opencl.h" +#include "detection/gpu/gpu.h" + +#if !defined(FF_HAVE_OPENCL) && defined(__APPLE__) && defined(MAC_OS_X_VERSION_10_15) + #define FF_HAVE_OPENCL 1 +#endif + +#ifdef FF_HAVE_OPENCL + + #include "common/library.h" + #include "common/parsing.h" + #include "common/strutil.h" + #include <string.h> + + #define CL_TARGET_OPENCL_VERSION 110 + #ifndef __APPLE__ + #include <CL/cl.h> + #include <CL/cl_ext.h> + #else + #include <OpenCL/cl.h> + #include <OpenCL/cl_ext.h> + #endif + +typedef struct OpenCLData { + FF_LIBRARY_SYMBOL(clGetPlatformIDs) + FF_LIBRARY_SYMBOL(clGetPlatformInfo) + FF_LIBRARY_SYMBOL(clGetDeviceInfo) + FF_LIBRARY_SYMBOL(clGetDeviceIDs) +} OpenCLData; + +static const char* openCLHandleData(OpenCLData* data, FFOpenCLResult* result) { + cl_platform_id platforms[32]; + cl_uint numPlatforms = 0; + cl_int ret = data->ffclGetPlatformIDs(ARRAY_SIZE(platforms), platforms, &numPlatforms); + if (ret != CL_SUCCESS) { + switch (ret) { + #ifdef CL_PLATFORM_NOT_FOUND_KHR // not available on macOS + case CL_PLATFORM_NOT_FOUND_KHR: + return "clGetPlatformIDs() failed: CL_PLATFORM_NOT_FOUND_KHR"; + #endif + case CL_INVALID_VALUE: + return "clGetPlatformIDs() failed: CL_INVALID_VALUE"; + case CL_OUT_OF_HOST_MEMORY: + return "clGetPlatformIDs() failed: CL_OUT_OF_HOST_MEMORY"; + default: + return "clGetPlatformIDs() failed: unknown error"; + } + } + + if (numPlatforms == 0) { + return "clGetPlatformIDs returned 0 platforms"; + } + + char buffer[1024]; + for (cl_uint iplat = 0; iplat < numPlatforms; ++iplat) { + if (data->ffclGetPlatformInfo(platforms[iplat], CL_PLATFORM_VERSION, sizeof(buffer), buffer, NULL) != CL_SUCCESS) { + return "clGetPlatformInfo() failed"; + } + + // Use the newest supported OpenCL version + if (ffStrbufCompS(&result->version, buffer) < 0) { + const char* versionPretty = buffer; + if (ffStrStartsWithIgnCase(buffer, "OpenCL ")) { + versionPretty = buffer + strlen("OpenCL "); + } + ffStrbufSetS(&result->version, versionPretty); + ffStrbufTrim(&result->version, ' '); + + if (data->ffclGetPlatformInfo(platforms[iplat], CL_PLATFORM_NAME, sizeof(buffer), buffer, NULL) == CL_SUCCESS) { + ffStrbufSetS(&result->name, buffer); + } + + if (data->ffclGetPlatformInfo(platforms[iplat], CL_PLATFORM_VENDOR, sizeof(buffer), buffer, NULL) == CL_SUCCESS) { + ffStrbufSetS(&result->vendor, buffer); + } + } + + cl_device_id deviceIDs[32]; + cl_uint numDevices = (cl_uint) ARRAY_SIZE(deviceIDs); + if (data->ffclGetDeviceIDs(platforms[iplat], CL_DEVICE_TYPE_GPU, numDevices, deviceIDs, &numDevices) != CL_SUCCESS) { + continue; + } + + for (cl_uint idev = 0; idev < numDevices; ++idev) { + cl_device_id deviceID = deviceIDs[idev]; + if (data->ffclGetDeviceInfo(deviceID, CL_DEVICE_NAME, sizeof(buffer), buffer, NULL) != CL_SUCCESS) { + continue; + } + + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, result->gpus); + ffStrbufInitS(&gpu->name, buffer); + ffStrbufInit(&gpu->vendor); + ffStrbufInit(&gpu->driver); + ffStrbufInit(&gpu->platformApi); + ffStrbufInit(&gpu->memoryType); + gpu->index = FF_GPU_INDEX_UNSET; + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->type = FF_GPU_TYPE_UNKNOWN; + gpu->dedicated.total = gpu->dedicated.used = gpu->shared.total = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + gpu->deviceId = (size_t) deviceID; + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + + if (data->ffclGetDeviceInfo(deviceID, CL_DEVICE_VERSION, sizeof(buffer), buffer, NULL) == CL_SUCCESS) { + ffStrbufSetS(&gpu->platformApi, buffer); + ffStrbufTrimRight(&gpu->platformApi, ' '); + } else { + ffStrbufSetStatic(&gpu->platformApi, "OpenCL"); + } + + { + cl_uint vendorId; + if (data->ffclGetDeviceInfo(deviceID, CL_DEVICE_VENDOR_ID, sizeof(vendorId), &vendorId, NULL) == CL_SUCCESS) { + ffStrbufSetStatic(&gpu->vendor, ffGPUGetVendorString(vendorId)); + } + if (gpu->vendor.length == 0 && data->ffclGetDeviceInfo(deviceID, CL_DEVICE_VENDOR, sizeof(buffer), buffer, NULL) == CL_SUCCESS) { + ffStrbufSetS(&gpu->vendor, buffer); + } + } + + if (data->ffclGetDeviceInfo(deviceID, CL_DRIVER_VERSION, sizeof(buffer), buffer, NULL) == CL_SUCCESS) { + const char* versionPretty = strchr(buffer, ' '); + if (versionPretty && *versionPretty) { + ffStrbufSetS(&gpu->driver, versionPretty + 1); + } else { + ffStrbufSetS(&gpu->driver, buffer); + } + } + + { + cl_uint value; + if (data->ffclGetDeviceInfo(deviceID, CL_DEVICE_MAX_COMPUTE_UNITS, sizeof(value), &value, NULL) == CL_SUCCESS) { + gpu->coreCount = (int32_t) value; + } + } + + { + cl_uint value; + if (data->ffclGetDeviceInfo(deviceID, CL_DEVICE_MAX_CLOCK_FREQUENCY, sizeof(value), &value, NULL) == CL_SUCCESS) { + gpu->frequency = value; + } + } + + { + cl_bool value; + if (data->ffclGetDeviceInfo(deviceID, CL_DEVICE_HOST_UNIFIED_MEMORY, sizeof(value), &value, NULL) == CL_SUCCESS) { + gpu->type = value ? FF_GPU_TYPE_INTEGRATED : FF_GPU_TYPE_DISCRETE; + + cl_ulong memSize; + if (data->ffclGetDeviceInfo(deviceID, CL_DEVICE_GLOBAL_MEM_SIZE, sizeof(memSize), &memSize, NULL) == CL_SUCCESS) { + if (gpu->type == FF_GPU_TYPE_INTEGRATED) { + gpu->shared.total = memSize; + } else { + gpu->dedicated.total = memSize; + } + } + } + } + } + } + + return NULL; +} + +static const char* detectOpenCL(FFOpenCLResult* result) { + OpenCLData data; + + #ifndef __APPLE__ + + FF_LIBRARY_LOAD_MESSAGE(opencl, + #ifdef _WIN32 + "OpenCL" FF_LIBRARY_EXTENSION, + -1, + #endif + "libOpenCL" FF_LIBRARY_EXTENSION, + 1); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(opencl, data, clGetPlatformIDs); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(opencl, data, clGetPlatformInfo); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(opencl, data, clGetDeviceIDs); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(opencl, data, clGetDeviceInfo); + + return openCLHandleData(&data, result); + + #else + + data.ffclGetPlatformIDs = clGetPlatformIDs; + data.ffclGetPlatformInfo = clGetPlatformInfo; + data.ffclGetDeviceIDs = clGetDeviceIDs; + data.ffclGetDeviceInfo = clGetDeviceInfo; + + return openCLHandleData(&data, result); + + #endif +} + +#endif // defined(FF_HAVE_OPENCL) + +FFOpenCLResult* ffDetectOpenCL(void) { + static FFOpenCLResult result; + static bool initialized; + + if (!initialized) { + initialized = true; + ffStrbufInit(&result.version); + ffStrbufInit(&result.name); + ffStrbufInit(&result.vendor); + ffListInit(&result.gpus); + +#ifdef FF_HAVE_OPENCL + result.error = detectOpenCL(&result); +#else + result.error = "fastfetch was compiled without OpenCL support"; +#endif + } + + return &result; +} diff --git a/src/detection/opencl/opencl.h b/src/detection/opencl/opencl.h new file mode 100644 index 0000000..d96eb50 --- /dev/null +++ b/src/detection/opencl/opencl.h @@ -0,0 +1,14 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/opencl/option.h" + +typedef struct FFOpenCLResult { + FFstrbuf version; + FFstrbuf name; + FFstrbuf vendor; + FFlist gpus; // List of FFGPUResult, see detection/gpu/gpu.h + const char* error; +} FFOpenCLResult; + +FFOpenCLResult* ffDetectOpenCL(); diff --git a/src/detection/opengl/opengl.h b/src/detection/opengl/opengl.h new file mode 100644 index 0000000..f7ff9d5 --- /dev/null +++ b/src/detection/opengl/opengl.h @@ -0,0 +1,17 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/opengl/option.h" + +typedef struct FFOpenGLResult { + FFstrbuf version; + FFstrbuf renderer; + FFstrbuf vendor; + FFstrbuf slv; + FFstrbuf library; +} FFOpenGLResult; + +#define FF_OPENGL_BUFFER_WIDTH 1 +#define FF_OPENGL_BUFFER_HEIGHT 1 + +const char* ffDetectOpenGL(FFOpenGLOptions* options, FFOpenGLResult* result); diff --git a/src/detection/opengl/opengl_apple.c b/src/detection/opengl/opengl_apple.c new file mode 100644 index 0000000..9128593 --- /dev/null +++ b/src/detection/opengl/opengl_apple.c @@ -0,0 +1,66 @@ + +#include "fastfetch.h" +#include "opengl.h" + +#define GL_SILENCE_DEPRECATION +#include <OpenGL/gl.h> +#include <OpenGL/OpenGL.h> // This brings in CGL, not GL + +void ffOpenGLHandleResult(FFOpenGLResult* result, __typeof__(&glGetString) ffglGetString); + +static const char* cglHandleContext(FFOpenGLResult* result, CGLContextObj context) { + if (CGLSetCurrentContext(context) != kCGLNoError) { + return "CGLSetCurrentContext() failed"; + } + + ffOpenGLHandleResult(result, &glGetString); + + GLint major, minor; + CGLGetVersion(&major, &minor); + ffStrbufSetF(&result->library, "CGL %d.%d", major, minor); + + return NULL; +} + +static const char* cglHandlePixelFormat(FFOpenGLResult* result, CGLPixelFormatObj pixelFormat) { + CGLContextObj context; + + if (CGLCreateContext(pixelFormat, NULL, &context) != kCGLNoError) { + return "CGLCreateContext() failed"; + } + + const char* error = cglHandleContext(result, context); + CGLDestroyContext(context); + return error; +} + +const char* cglDetectOpenGL(FFOpenGLResult* result) { + CGLPixelFormatObj pixelFormat; + CGLPixelFormatAttribute attrs[] = { + kCGLPFAOpenGLProfile, (CGLPixelFormatAttribute) kCGLOGLPVersion_3_2_Core, kCGLPFAAccelerated, 0 + }; + + GLint num; + if (CGLChoosePixelFormat(attrs, &pixelFormat, &num) != kCGLNoError) { + return "CGLChoosePixelFormat() failed"; + } + + const char* error = cglHandlePixelFormat(result, pixelFormat); + CGLDestroyPixelFormat(pixelFormat); + return error; +} + +const char* ffDetectOpenGL(FFOpenGLOptions* options, FFOpenGLResult* result) { + if (options->library == FF_OPENGL_LIBRARY_AUTO) { + return cglDetectOpenGL(result); + } else if (options->library == FF_OPENGL_LIBRARY_EGL) { +#if __has_include(<EGL/egl.h>) + const char* ffOpenGLDetectByEGL(FFOpenGLResult * result); + return ffOpenGLDetectByEGL(result); +#else + return "fastfetch was compiled without egl support"; +#endif + } else { + return "Unsupported OpenGL library"; + } +} diff --git a/src/detection/opengl/opengl_haiku.cpp b/src/detection/opengl/opengl_haiku.cpp new file mode 100644 index 0000000..64a21a0 --- /dev/null +++ b/src/detection/opengl/opengl_haiku.cpp @@ -0,0 +1,38 @@ +#include <OpenGLKit.h> + +extern "C" { +#include "opengl.h" +#include "common/io.h" +#if FF_HAVE_EGL +const char* ffOpenGLDetectByEGL(FFOpenGLResult* result); +#endif +void ffOpenGLHandleResult(FFOpenGLResult* result, __typeof__(&glGetString) ffglGetString); +} + +static const char* oglDetectOpenGL(FFOpenGLResult* result) { + BApplication app("application/x-vnd.fastfetch-cli-fastfetch"); + FF_SUPPRESS_IO(); + + BGLView glView(BRect(), "ff_ogl_view", B_FOLLOW_NONE, B_WILL_DRAW, BGL_RGB); + auto ffglGetString = (decltype(&glGetString)) glView.GetGLProcAddress("glGetString"); + if (!ffglGetString) { + return "glView.GetGLProcAddress() failed"; + } + ffOpenGLHandleResult(result, ffglGetString); + ffStrbufSetStatic(&result->library, "OpenGLKit"); + return NULL; +} + +const char* ffDetectOpenGL(FFOpenGLOptions* options, FFOpenGLResult* result) { + if (options->library == FF_OPENGL_LIBRARY_AUTO) { + return oglDetectOpenGL(result); + } else if (options->library == FF_OPENGL_LIBRARY_EGL) { +#if FF_HAVE_EGL + return ffOpenGLDetectByEGL(result); +#else + return "fastfetch was compiled without egl support"; +#endif + } else { + return "Unsupported OpenGL library"; + } +} diff --git a/src/detection/opengl/opengl_linux.c b/src/detection/opengl/opengl_linux.c new file mode 100644 index 0000000..5f88b7e --- /dev/null +++ b/src/detection/opengl/opengl_linux.c @@ -0,0 +1,200 @@ +#include "fastfetch.h" +#include "opengl.h" +#include "common/io.h" + +#include <string.h> + +#if __ANDROID__ && !defined(FF_HAVE_EGL) + // On Android, installing OpenGL headers is enough (mesa-dev) + #if __has_include(<EGL/egl.h>) + #define FF_HAVE_EGL 1 + #endif +#endif + +#if defined(FF_HAVE_EGL) || defined(FF_HAVE_GLX) + #define FF_HAVE_GL 1 + + #include "common/library.h" + + #include <GL/gl.h> + +void ffOpenGLHandleResult(FFOpenGLResult* result, __typeof__(&glGetString) ffglGetString); + +#endif // FF_HAVE_GL + +#ifdef FF_HAVE_GLX + #include <GL/glx.h> + +typedef struct GLXData { + FF_LIBRARY_SYMBOL(glGetString) + FF_LIBRARY_SYMBOL(glXGetProcAddress) + FF_LIBRARY_SYMBOL(glXQueryVersion) + FF_LIBRARY_SYMBOL(XOpenDisplay) + FF_LIBRARY_SYMBOL(glXChooseVisual) + FF_LIBRARY_SYMBOL(XCreatePixmap) + FF_LIBRARY_SYMBOL(glXCreateGLXPixmap) + FF_LIBRARY_SYMBOL(glXCreateContext) + FF_LIBRARY_SYMBOL(glXMakeCurrent) + FF_LIBRARY_SYMBOL(glXDestroyContext) + FF_LIBRARY_SYMBOL(glXDestroyGLXPixmap) + FF_LIBRARY_SYMBOL(XFreePixmap) + FF_LIBRARY_SYMBOL(XCloseDisplay) + FF_LIBRARY_SYMBOL(XFree) + + Display* display; + XVisualInfo* visualInfo; + Pixmap pixmap; + GLXPixmap glxPixmap; + GLXContext context; +} GLXData; + +static const char* glxHandleContext(FFOpenGLResult* result, GLXData* data) { + if (data->ffglXMakeCurrent(data->display, data->glxPixmap, data->context) != True) { + return "glXMakeCurrent returned False"; + } + ffOpenGLHandleResult(result, data->ffglGetString); + + int major, minor; + if (data->ffglXQueryVersion(data->display, &major, &minor)) { + ffStrbufSetF(&result->library, "GLX %d.%d", major, minor); + } else { + ffStrbufSetStatic(&result->library, "GLX"); + } + + return NULL; +} + +static const char* glxHandleGLXPixmap(FFOpenGLResult* result, GLXData* data) { + data->context = data->ffglXCreateContext(data->display, data->visualInfo, NULL, True); + if (data->context == NULL) { + return "glXCreateContext returned NULL"; + } + + const char* error = glxHandleContext(result, data); + data->ffglXDestroyContext(data->display, data->context); + return error; +} + +static const char* glxHandlePixmap(FFOpenGLResult* result, GLXData* data) { + data->glxPixmap = data->ffglXCreateGLXPixmap(data->display, data->visualInfo, data->pixmap); + if (data->glxPixmap == None) { + return "glXCreateGLXPixmap returned None"; + } + + const char* error = glxHandleGLXPixmap(result, data); + data->ffglXDestroyGLXPixmap(data->display, data->glxPixmap); + return error; +} + +static const char* glxHandleVisualInfo(FFOpenGLResult* result, GLXData* data) { + data->pixmap = data->ffXCreatePixmap(data->display, DefaultRootWindow(data->display), FF_OPENGL_BUFFER_WIDTH, FF_OPENGL_BUFFER_HEIGHT, (unsigned int) data->visualInfo->depth); + if (data->pixmap == None) { + return "XCreatePixmap returned None"; + } + + const char* error = glxHandlePixmap(result, data); + data->ffXFreePixmap(data->display, data->pixmap); + return error; +} + +static const char* glxHandleDisplay(FFOpenGLResult* result, GLXData* data) { + data->visualInfo = data->ffglXChooseVisual(data->display, DefaultScreen(data->display), (int[]) { None }); + if (data->visualInfo == NULL) { + return "glXChooseVisual returned NULL"; + } + + const char* error = glxHandleVisualInfo(result, data); + data->ffXFree(data->visualInfo); + return error; +} + +static const char* glxHandleData(FFOpenGLResult* result, GLXData* data) { + data->ffglGetString = (__typeof__(data->ffglGetString)) data->ffglXGetProcAddress((const GLubyte*) "glGetString"); + if (data->ffglGetString == NULL) { + return "glXGetProcAddress(glGetString) returned NULL"; + } + + data->display = data->ffXOpenDisplay(NULL); + if (data->display == NULL) { + return "XOpenDisplay returned NULL"; + } + + const char* error = glxHandleDisplay(result, data); + data->ffXCloseDisplay(data->display); + return error; +} + +static const char* detectByGlx(FFOpenGLResult* result) { + GLXData data; + + FF_LIBRARY_LOAD_MESSAGE(glx, + #if !__OpenBSD__ && !__NetBSD__ + "libGLX" + #else + "libGL" + #endif + FF_LIBRARY_EXTENSION, + 1); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, glXGetProcAddress); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, glXQueryVersion); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, XOpenDisplay); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, glXChooseVisual); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, XCreatePixmap); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, glXCreateGLXPixmap); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, glXCreateContext); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, glXMakeCurrent); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, glXDestroyContext); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, glXDestroyGLXPixmap); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, XFreePixmap); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, XCloseDisplay); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(glx, data, XFree); + + FF_SUPPRESS_IO(); + + return glxHandleData(result, &data); +} + +#endif // FF_HAVE_GLX + +const char* ffDetectOpenGL(FFOpenGLOptions* options, FFOpenGLResult* result) { +#if FF_HAVE_GL + + if (options->library == FF_OPENGL_LIBRARY_GLX) { + #ifdef FF_HAVE_GLX + return detectByGlx(result); + #else + return "fastfetch was compiled without glx support"; + #endif + } + + if (options->library == FF_OPENGL_LIBRARY_EGL) { + #ifdef FF_HAVE_EGL + const char* ffOpenGLDetectByEGL(FFOpenGLResult * result); + return ffOpenGLDetectByEGL(result); + #else + return "fastfetch was compiled without egl support"; + #endif + } + + const char* error = ""; // not NULL dummy value + + #ifdef FF_HAVE_EGL + const char* ffOpenGLDetectByEGL(FFOpenGLResult * result); + error = ffOpenGLDetectByEGL(result); + #endif + + #ifdef FF_HAVE_GLX + if (error != NULL) { + error = detectByGlx(result); + } + #endif + + return error; + +#else + + FF_UNUSED(options, result); + return "Fastfetch was built without gl support."; + +#endif // FF_HAVE_GL +} diff --git a/src/detection/opengl/opengl_shared.c b/src/detection/opengl/opengl_shared.c new file mode 100644 index 0000000..307e71d --- /dev/null +++ b/src/detection/opengl/opengl_shared.c @@ -0,0 +1,214 @@ +#include "opengl.h" +#include "common/debug.h" +#include "common/library.h" + +#if __has_include(<GL/gl.h>) + #include <GL/gl.h> +#elif __APPLE__ + #define GL_SILENCE_DEPRECATION 1 + #include <OpenGL/gl.h> +#else + #define FF_HAVE_NO_GL 1 +#endif + +#ifndef FF_HAVE_NO_GL + + #ifndef GL_SHADING_LANGUAGE_VERSION // For WGL + #define GL_SHADING_LANGUAGE_VERSION 0x8B8C + #endif + +void ffOpenGLHandleResult(FFOpenGLResult* result, __typeof__(&glGetString) ffglGetString) { + ffStrbufAppendS(&result->version, (const char*) ffglGetString(GL_VERSION)); + ffStrbufAppendS(&result->renderer, (const char*) ffglGetString(GL_RENDERER)); + ffStrbufAppendS(&result->vendor, (const char*) ffglGetString(GL_VENDOR)); + ffStrbufAppendS(&result->slv, (const char*) ffglGetString(GL_SHADING_LANGUAGE_VERSION)); +} + + #if defined(FF_HAVE_EGL) || __has_include(<EGL/egl.h>) + #include "common/io.h" + + #define EGL_EGL_PROTOTYPES 1 + #define EGL_EGLEXT_PROTOTYPES 1 + #include <EGL/egl.h> + #include <EGL/eglext.h> + +typedef struct EGLData { + FF_LIBRARY_SYMBOL(glGetString) + FF_LIBRARY_SYMBOL(eglGetProcAddress) + FF_LIBRARY_SYMBOL(eglGetDisplay) + FF_LIBRARY_SYMBOL(eglQueryString) + FF_LIBRARY_SYMBOL(eglInitialize) + FF_LIBRARY_SYMBOL(eglBindAPI) + FF_LIBRARY_SYMBOL(eglGetConfigs) + FF_LIBRARY_SYMBOL(eglCreatePbufferSurface) + FF_LIBRARY_SYMBOL(eglCreateContext) + FF_LIBRARY_SYMBOL(eglMakeCurrent) + FF_LIBRARY_SYMBOL(eglDestroyContext) + FF_LIBRARY_SYMBOL(eglDestroySurface) + FF_LIBRARY_SYMBOL(eglTerminate) + + EGLDisplay display; + EGLConfig config; + EGLSurface surface; + EGLContext context; +} EGLData; + +static const char* eglHandleContext(FFOpenGLResult* result, EGLData* data) { + FF_DEBUG("Making EGL context current"); + if (data->ffeglMakeCurrent(data->display, data->surface, data->surface, data->context) != EGL_TRUE) { + FF_DEBUG("eglMakeCurrent() returned EGL_FALSE"); + return "eglMakeCurrent returned EGL_FALSE"; + } + + ffOpenGLHandleResult(result, data->ffglGetString); + ffStrbufSetF(&result->library, "EGL %s", data->ffeglQueryString(data->display, EGL_VERSION)); + FF_DEBUG("OpenGL via EGL detected: version='%s', renderer='%s', vendor='%s', slv='%s', library='%s'", + result->version.chars, + result->renderer.chars, + result->vendor.chars, + result->slv.chars, + result->library.chars); + return NULL; +} + +static const char* eglHandleSurface(FFOpenGLResult* result, EGLData* data, bool gles) { + FF_DEBUG("Creating EGL context (preferred API=%s, client version=%d)", gles ? "OpenGL ES" : "OpenGL", gles ? 2 : 1); + data->context = data->ffeglCreateContext(data->display, data->config, EGL_NO_CONTEXT, (EGLint[]) { EGL_CONTEXT_CLIENT_VERSION, gles ? 2 : 1, // Try GLES 2.0+ first + EGL_NONE }); + if (data->context == EGL_NO_CONTEXT && gles) // Some ANGLE builds support GLES 1.1 only + { + FF_DEBUG("EGL context creation with GLES 2.x failed, retrying with default attributes (GLES 1.1 fallback)"); + data->context = data->ffeglCreateContext(data->display, data->config, EGL_NO_CONTEXT, (EGLint[]) { EGL_NONE }); + } + if (data->context == EGL_NO_CONTEXT) { + FF_DEBUG("eglCreateContext() returned EGL_NO_CONTEXT"); + return "eglCreateContext returned EGL_NO_CONTEXT"; + } + + FF_DEBUG("EGL context created successfully"); + + const char* error = eglHandleContext(result, data); + FF_DEBUG("eglHandleContext() returns: %s", error ?: "success"); + + FF_DEBUG("Releasing current EGL context"); + data->ffeglMakeCurrent(data->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + + FF_DEBUG("Destroying EGL context"); + data->ffeglDestroyContext(data->display, data->context); + return error; +} + +static const char* eglHandleDisplay(FFOpenGLResult* result, EGLData* data) { + // try use OpenGL API. If failed, use the default API (usually OpenGL ES) + bool gles = !data->ffeglBindAPI(EGL_OPENGL_API); + FF_DEBUG("eglBindAPI(EGL_OPENGL_API) %s, effective API=%s", + gles ? "failed" : "succeeded", + gles ? "default (usually OpenGL ES)" : "OpenGL"); + + EGLint eglConfigCount; + data->ffeglGetConfigs(data->display, &data->config, 1, &eglConfigCount); + FF_DEBUG("eglGetConfigs() returned %d config(s)", eglConfigCount); + + if (eglConfigCount == 0) { + FF_DEBUG("No EGL config is available"); + return "eglGetConfigs returned 0 configs"; + } + + FF_DEBUG("Creating EGL pbuffer surface (%dx%d)", FF_OPENGL_BUFFER_WIDTH, FF_OPENGL_BUFFER_HEIGHT); + data->surface = data->ffeglCreatePbufferSurface(data->display, data->config, (EGLint[]) { EGL_WIDTH, FF_OPENGL_BUFFER_WIDTH, EGL_HEIGHT, FF_OPENGL_BUFFER_HEIGHT, EGL_NONE }); + + if (data->surface == EGL_NO_SURFACE) { + FF_DEBUG("eglCreatePbufferSurface() returned EGL_NO_SURFACE"); + return "eglCreatePbufferSurface returned EGL_NO_SURFACE"; + } + + FF_DEBUG("EGL pbuffer surface created successfully"); + + const char* error = eglHandleSurface(result, data, gles); + FF_DEBUG("eglHandleSurface() returns: %s", error ?: "success"); + + FF_DEBUG("Destroying EGL surface"); + data->ffeglDestroySurface(data->display, data->surface); + return error; +} + +static const char* eglHandleData(FFOpenGLResult* result, EGLData* data) { + FF_DEBUG("Resolving glGetString via eglGetProcAddress()"); + data->ffglGetString = (__typeof__(&glGetString)) data->ffeglGetProcAddress("glGetString"); + if (!data->ffglGetString) { + FF_DEBUG("eglGetProcAddress('glGetString') returned NULL"); + return "eglGetProcAddress(glGetString) returned NULL"; + } + + #if EGL_VERSION_1_5 + PFNEGLGETPLATFORMDISPLAYEXTPROC ffeglGetPlatformDisplay = (PFNEGLGETPLATFORMDISPLAYEXTPROC) data->ffeglGetProcAddress("eglGetPlatformDisplay"); + if (ffeglGetPlatformDisplay) { + FF_DEBUG("Trying eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA)"); + data->display = ffeglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, NULL, NULL); + FF_DEBUG("eglGetPlatformDisplay() %s", data->display == EGL_NO_DISPLAY ? "failed" : "succeeded"); + } else { + FF_DEBUG("eglGetPlatformDisplay is unavailable, falling back to eglGetDisplay"); + } + + if (!ffeglGetPlatformDisplay || data->display == EGL_NO_DISPLAY) + #endif + + { + FF_DEBUG("Trying eglGetDisplay(EGL_DEFAULT_DISPLAY)"); + data->display = data->ffeglGetDisplay(EGL_DEFAULT_DISPLAY); + if (data->display == EGL_NO_DISPLAY) { + FF_DEBUG("eglGetDisplay() returned EGL_NO_DISPLAY"); + return "eglGetDisplay returned EGL_NO_DISPLAY"; + } + + FF_DEBUG("eglGetDisplay() succeeded"); + } + + EGLint major, minor; + if (data->ffeglInitialize(data->display, &major, &minor) == EGL_FALSE) { + FF_DEBUG("eglInitialize() returned EGL_FALSE"); + return "eglInitialize returned EGL_FALSE"; + } + + FF_DEBUG("EGL initialized successfully: %d.%d", major, minor); + + const char* error = eglHandleDisplay(result, data); + FF_DEBUG("eglHandleDisplay() returns: %s", error ?: "success"); + + FF_DEBUG("Terminating EGL display connection"); + data->ffeglTerminate(data->display); + return error; +} + +const char* ffOpenGLDetectByEGL(FFOpenGLResult* result) { + FF_DEBUG("Starting OpenGL detection via EGL"); + EGLData eglData; + + FF_LIBRARY_LOAD_MESSAGE(egl, "libEGL" FF_LIBRARY_EXTENSION, 1); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(egl, eglData, eglGetProcAddress); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(egl, eglData, eglGetDisplay); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(egl, eglData, eglQueryString); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(egl, eglData, eglInitialize); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(egl, eglData, eglBindAPI); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(egl, eglData, eglGetConfigs); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(egl, eglData, eglCreatePbufferSurface); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(egl, eglData, eglCreateContext); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(egl, eglData, eglMakeCurrent); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(egl, eglData, eglDestroyContext); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(egl, eglData, eglDestroySurface); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(egl, eglData, eglTerminate); + + FF_DEBUG("Loaded EGL library and required symbols"); + + FF_SUPPRESS_IO(); + FF_DEBUG("Suppressed stdout/stderr during EGL probing"); + + const char* error = eglHandleData(result, &eglData); + FF_DEBUG("OpenGL detection via EGL returns: %s", error ?: "success"); + + return error; +} + + #endif // FF_HAVE_EGL + +#endif // FF_HAVE_NO_GL diff --git a/src/detection/opengl/opengl_windows.c b/src/detection/opengl/opengl_windows.c new file mode 100644 index 0000000..01bd986 --- /dev/null +++ b/src/detection/opengl/opengl_windows.c @@ -0,0 +1,120 @@ +#include "opengl.h" +#include "common/library.h" +#include "common/printing.h" +#include "common/windows/nt.h" + +#include <windows.h> +#include <GL/gl.h> + +typedef struct WGLData { + FF_LIBRARY_SYMBOL(glGetString) + FF_LIBRARY_SYMBOL(wglMakeCurrent) + FF_LIBRARY_SYMBOL(wglCreateContext) + FF_LIBRARY_SYMBOL(wglDeleteContext) +} WGLData; + +void ffOpenGLHandleResult(FFOpenGLResult* result, __typeof__(&glGetString) ffglGetString); + +static const char* wglHandleContext(WGLData* wglData, FFOpenGLResult* result, HDC hdc, HGLRC context) { + if (wglData->ffwglMakeCurrent(hdc, context) == FALSE) { + return "wglMakeCurrent() failed"; + } + ffOpenGLHandleResult(result, wglData->ffglGetString); + ffStrbufSetStatic(&result->library, "WGL 1.0"); + if (wglData->ffwglMakeCurrent(NULL, NULL) == FALSE) { + return "wglMakeCurrent(NULL, NULL) failed"; + } + return NULL; +} + +static const char* wglHandlePixelFormat(WGLData* wglData, FFOpenGLResult* result, HWND hWnd) { + HDC hdc = GetDC(hWnd); + + if (hdc == NULL) { + return "GetDC() failed"; + } + + PIXELFORMATDESCRIPTOR pfd = { + .nSize = sizeof(PIXELFORMATDESCRIPTOR), + .nVersion = 1, + .dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, + .iPixelType = PFD_TYPE_RGBA, + .cColorBits = 32, + .cDepthBits = 24, + .iLayerType = PFD_MAIN_PLANE + }; + int pixelFormat = ChoosePixelFormat(hdc, &pfd); + if (pixelFormat == 0) { + ReleaseDC(hWnd, hdc); + return "ChoosePixelFormat() failed"; + } + + if (SetPixelFormat(hdc, pixelFormat, &pfd) == FALSE) { + ReleaseDC(hWnd, hdc); + return "SetPixelFormat() failed"; + } + + HGLRC context = wglData->ffwglCreateContext(hdc); + if (context == NULL) { + ReleaseDC(hWnd, hdc); + return "wglCreateContext() failed"; + } + + const char* error = wglHandleContext(wglData, result, hdc, context); + wglData->ffwglDeleteContext(context); + + ReleaseDC(hWnd, hdc); + + return error; +} + +static const char* wglDetectOpenGL(FFOpenGLResult* result) { + FF_LIBRARY_LOAD_MESSAGE(opengl32, "opengl32" FF_LIBRARY_EXTENSION, 1); + + WGLData data = {}; + + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(opengl32, data, wglMakeCurrent); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(opengl32, data, wglCreateContext); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(opengl32, data, wglDeleteContext); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(opengl32, data, glGetString); + + HINSTANCE hInstance = ffGetPeb()->ImageBaseAddress; + + WNDCLASSW wc = { + .lpfnWndProc = DefWindowProcW, + .hInstance = hInstance, + .hbrBackground = (HBRUSH) COLOR_BACKGROUND, + .lpszClassName = L"ogl_version_check", + .style = CS_OWNDC, + }; + if (!RegisterClassW(&wc)) { + return "RegisterClassW() failed"; + } + + HWND hWnd = CreateWindowW(wc.lpszClassName, L"ogl_version_check", 0, 0, 0, FF_OPENGL_BUFFER_WIDTH, FF_OPENGL_BUFFER_HEIGHT, NULL, NULL, hInstance, NULL); + if (!hWnd) { + return "CreateWindowW() failed"; + } + + const char* error = wglHandlePixelFormat(&data, result, hWnd); + + DestroyWindow(hWnd); + UnregisterClassW(wc.lpszClassName, hInstance); + + return error; +} + +const char* ffDetectOpenGL(FFOpenGLOptions* options, FFOpenGLResult* result) { + if (options->library == FF_OPENGL_LIBRARY_AUTO) { + return wglDetectOpenGL(result); + } else if (options->library == FF_OPENGL_LIBRARY_EGL) { +#if __has_include(<EGL/egl.h>) + const char* ffOpenGLDetectByEGL(FFOpenGLResult * result); + return ffOpenGLDetectByEGL(result); +#else + return "fastfetch was compiled without egl support"; +#endif + } else { + return "Unsupported OpenGL library"; + } +} diff --git a/src/detection/os/os.c b/src/detection/os/os.c new file mode 100644 index 0000000..7f66733 --- /dev/null +++ b/src/detection/os/os.c @@ -0,0 +1,21 @@ +#include "os.h" + +void ffDetectOSImpl(FFOSResult* os); + +const FFOSResult* ffDetectOS(void) { + static FFOSResult result; + if (result.name.chars == NULL) { + ffStrbufInit(&result.name); + ffStrbufInit(&result.prettyName); + ffStrbufInit(&result.id); + ffStrbufInit(&result.version); + ffStrbufInit(&result.versionID); + ffStrbufInit(&result.codename); + ffStrbufInit(&result.buildID); + ffStrbufInit(&result.idLike); + ffStrbufInit(&result.variant); + ffStrbufInit(&result.variantID); + ffDetectOSImpl(&result); + } + return &result; +} diff --git a/src/detection/os/os.h b/src/detection/os/os.h new file mode 100644 index 0000000..5b62aab --- /dev/null +++ b/src/detection/os/os.h @@ -0,0 +1,19 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/os/option.h" + +typedef struct FFOSResult { + FFstrbuf name; + FFstrbuf prettyName; + FFstrbuf id; + FFstrbuf idLike; + FFstrbuf variant; + FFstrbuf variantID; + FFstrbuf version; + FFstrbuf versionID; + FFstrbuf codename; + FFstrbuf buildID; +} FFOSResult; + +const FFOSResult* ffDetectOS(); diff --git a/src/detection/os/os_android.c b/src/detection/os/os_android.c new file mode 100644 index 0000000..47ea40b --- /dev/null +++ b/src/detection/os/os_android.c @@ -0,0 +1,16 @@ +#include "os.h" +#include "common/settings.h" + +void ffDetectOSImpl(FFOSResult* os) { + ffStrbufSetStatic(&os->name, "Android"); + + ffStrbufSetStatic(&os->id, "android"); + + ffSettingsGetAndroidProperty("ro.build.version.release", &os->version); + + ffSettingsGetAndroidProperty("ro.build.version.release", &os->versionID); + + ffSettingsGetAndroidProperty("ro.build.version.codename", &os->codename); + + ffSettingsGetAndroidProperty("ro.build.id", &os->buildID); +} diff --git a/src/detection/os/os_apple.m b/src/detection/os/os_apple.m new file mode 100644 index 0000000..2890fc3 --- /dev/null +++ b/src/detection/os/os_apple.m @@ -0,0 +1,105 @@ +#include "os.h" +#include "common/io.h" +#include "common/sysctl.h" +#include "common/strutil.h" +#include "common/mallocHelper.h" + +#include <stdlib.h> +#include <string.h> +#import <Foundation/Foundation.h> + +static bool parseSystemVersion(FFOSResult* os) +{ + NSError* error; + NSString* fileName = @"file:///System/Library/CoreServices/SystemVersion.plist"; + NSDictionary* dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL URLWithString:fileName] + error:&error]; + if(error) + return false; + + NSString* value; + + if((value = dict[@"ProductName"])) + ffStrbufInitS(&os->name, value.UTF8String); + if((value = dict[@"ProductUserVisibleVersion"])) + ffStrbufInitS(&os->version, value.UTF8String); + if((value = dict[@"ProductBuildVersion"])) + ffStrbufInitS(&os->buildID, value.UTF8String); + if (ffStrbufStartsWithS(&os->version, "16.")) + { + // macOS 26 Tahoe. #1809 + os->version.chars[0] = '2'; + } + + return true; +} + +static bool detectOSCodeName(FFOSResult* os) +{ + // https://en.wikipedia.org/wiki/MacOS_version_history + char* str_end; + const char* version = os->version.chars; + unsigned long num = strtoul(version, &str_end, 10); + if (str_end == version) return false; + + switch (num) + { + case 26: + case 16: ffStrbufSetStatic(&os->codename, "Tahoe"); return true; + case 15: ffStrbufSetStatic(&os->codename, "Sequoia"); return true; + case 14: ffStrbufSetStatic(&os->codename, "Sonoma"); return true; + case 13: ffStrbufSetStatic(&os->codename, "Ventura"); return true; + case 12: ffStrbufSetStatic(&os->codename, "Monterey"); return true; + case 11: ffStrbufSetStatic(&os->codename, "Big Sur"); return true; + case 10: { + version = str_end + 1; + num = strtoul(version, &str_end, 10); + if (str_end == version) return false; + + switch (num) + { + case 16: ffStrbufSetStatic(&os->codename, "Big Sur"); return true; + case 15: ffStrbufSetStatic(&os->codename, "Catalina"); return true; + case 14: ffStrbufSetStatic(&os->codename, "Mojave"); return true; + case 13: ffStrbufSetStatic(&os->codename, "High Sierra"); return true; + case 12: ffStrbufSetStatic(&os->codename, "Sierra"); return true; + case 11: ffStrbufSetStatic(&os->codename, "El Capitan"); return true; + case 10: ffStrbufSetStatic(&os->codename, "Yosemite"); return true; + case 9: ffStrbufSetStatic(&os->codename, "Mavericks"); return true; + case 8: ffStrbufSetStatic(&os->codename, "Mountain Lion"); return true; + case 7: ffStrbufSetStatic(&os->codename, "Lion"); return true; + case 6: ffStrbufSetStatic(&os->codename, "Snow Leopard"); return true; + case 5: ffStrbufSetStatic(&os->codename, "Leopard"); return true; + case 4: ffStrbufSetStatic(&os->codename, "Tiger"); return true; + case 3: ffStrbufSetStatic(&os->codename, "Panther"); return true; + case 2: ffStrbufSetStatic(&os->codename, "Jaguar"); return true; + case 1: ffStrbufSetStatic(&os->codename, "Puma"); return true; + case 0: ffStrbufSetStatic(&os->codename, "Cheetah"); return true; + } + } + } + + return false; +} + +void ffDetectOSImpl(FFOSResult* os) +{ + parseSystemVersion(os); + + ffStrbufSetStatic(&os->id, "macos"); + + if(__builtin_expect(os->name.length == 0, 0)) + ffStrbufSetStatic(&os->name, "macOS"); + + if(__builtin_expect(os->version.length == 0, 0)) + ffSysctlGetString("kern.osproductversion", &os->version); + + if(__builtin_expect(os->buildID.length == 0, 0)) + ffSysctlGetString("kern.osversion", &os->buildID); + + ffStrbufAppend(&os->versionID, &os->version); + + detectOSCodeName(os); + + ffStrbufSetF(&os->prettyName, "%s %s %s (%s)", os->name.chars, os->codename.chars, os->version.chars, os->buildID.chars); +} diff --git a/src/detection/os/os_haiku.c b/src/detection/os/os_haiku.c new file mode 100644 index 0000000..e8e9f4a --- /dev/null +++ b/src/detection/os/os_haiku.c @@ -0,0 +1,66 @@ +#include "os.h" +#include <OS.h> +#include <image.h> +#include <inttypes.h> + +void ffDetectOSImpl(FFOSResult* os) { + ffStrbufSetStatic(&os->name, "Haiku"); + + ffStrbufSetStatic(&os->id, "haiku"); + + image_info image; + int32 cookie = 0; + while (get_next_image_info(B_SYSTEM_TEAM, &cookie, &image) == B_OK) { + int32 ver = image.api_version; + if (ver == 0) { + continue; + } + + // https://github.com/haiku/haiku/blob/e63683b2fb337d2034059a7e053c170eaf978142/headers/os/BeBuild.h#L36 + if (ver < B_HAIKU_VERSION_1_ALPHA_1) { + switch (ver) { + case B_HAIKU_VERSION_BEOS: + ffStrbufSetStatic(&os->version, "BEOS"); + break; + case B_HAIKU_VERSION_BONE: + ffStrbufSetStatic(&os->version, "BONE"); + break; + case B_HAIKU_VERSION_DANO: + ffStrbufSetStatic(&os->version, "DANO"); + break; + } + } else { + int32 relVer = ver / 0x10000; + ver %= 0x10000; + if (ver == 0) { + ffStrbufSetF(&os->version, "R%" PRIi32, relVer); + } else { + relVer++; + + bool isPre = !!(ver & 1); + if (ver < B_HAIKU_VERSION_1_PRE_BETA_1) { + int32 alphaVer = ver / 0x100; + if (isPre) { + ffStrbufSetF(&os->version, "R%" PRId32 "A%" PRIi32 "-", relVer, alphaVer + 1); + } else { + ffStrbufSetF(&os->version, "R%" PRIi32 "A%" PRIi32, relVer, alphaVer); + } + } else if (ver < 0x00010000 /* B_HAIKU_VERSION_1 */) { + int32 betaVer = (ver - B_HAIKU_VERSION_1_ALPHA_4) / 0x100; + if (isPre) { + ffStrbufSetF(&os->version, "R%" PRIi32 "B%" PRIi32 "-", relVer, betaVer + 1); + } else { + ffStrbufSetF(&os->version, "R%" PRIi32 "B%" PRIi32, relVer, betaVer); + } + } + } + } + } + + if (!os->version.length) { + system_info sys; + if (get_system_info(&sys) == B_OK) { + ffStrbufAppendF(&os->version, "R%" PRIx64, sys.kernel_version); + } + } +} diff --git a/src/detection/os/os_linux.c b/src/detection/os/os_linux.c new file mode 100644 index 0000000..fc653a3 --- /dev/null +++ b/src/detection/os/os_linux.c @@ -0,0 +1,397 @@ +#include "os.h" +#include "common/properties.h" +#include "common/parsing.h" +#include "common/io.h" +#include "common/processing.h" +#include "common/strutil.h" + +#include <string.h> +#include <stdlib.h> + +#define FF_STR_INDIR(x) #x +#define FF_STR(x) FF_STR_INDIR(x) + +static bool parseLsbRelease(const char* fileName, FFOSResult* result) { + return ffParsePropFileValues(fileName, 4, (FFpropquery[]) { + { "DISTRIB_ID =", &result->id }, + { "DISTRIB_DESCRIPTION =", &result->prettyName }, + { "DISTRIB_RELEASE =", &result->version }, + { "DISTRIB_CODENAME =", &result->codename }, + }); +} + +static bool parseOsRelease(const char* fileName, FFOSResult* result) { + return ffParsePropFileValues(fileName, 11, (FFpropquery[]) { + { "PRETTY_NAME =", &result->prettyName }, + { "NAME =", &result->name }, + { "ID =", &result->id }, + { "ID_LIKE =", &result->idLike }, + { "VARIANT =", &result->variant }, + { "VARIANT_ID =", &result->variantID }, + { "VERSION =", &result->version }, + { "VERSION_ID =", &result->versionID }, + { "VERSION_CODENAME =", &result->codename }, + { "CODENAME =", &result->codename }, + { "BUILD_ID =", &result->buildID }, + }); +} + +// Common logic for detecting Armbian image version +FF_A_UNUSED static bool detectArmbianVersion(FFOSResult* result) { + // Possible values `PRETTY_NAME` starts with on Armbian: + // - `Armbian` for official releases + // - `Armbian_community` for community releases + // - `Armbian_Security` for images with kali repo added + // - `Armbian-unofficial` for an unofficial image built from source, e.g. during development and testing + if (ffStrbufStartsWithS(&result->prettyName, "Armbian")) { + ffStrbufSetStatic(&result->name, "Armbian"); + } else { + return false; + } + ffStrbufSet(&result->idLike, &result->id); + ffStrbufSetS(&result->id, "armbian"); + ffStrbufClear(&result->versionID); + uint32_t versionStart = ffStrbufFirstIndexC(&result->prettyName, ' ') + 1; + uint32_t versionEnd = ffStrbufNextIndexC(&result->prettyName, versionStart, ' '); + ffStrbufSetNS(&result->versionID, versionEnd - versionStart, result->prettyName.chars + versionStart); + return true; +} + +// Returns false if PrettyName should be updated by caller +FF_A_UNUSED static bool getUbuntuFlavour(FFOSResult* result) { + if (detectArmbianVersion(result)) { + return true; + } else if (ffStrbufStartsWithS(&result->prettyName, "Linux Lite ")) { + ffStrbufSetStatic(&result->name, "Linux Lite"); + ffStrbufSetStatic(&result->id, "linuxlite"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + ffStrbufSetS(&result->versionID, result->prettyName.chars + strlen("Linux Lite ")); + return true; + } else if (ffStrbufStartsWithS(&result->prettyName, "Rhino Linux ")) { + ffStrbufSetStatic(&result->name, "Rhino Linux"); + ffStrbufSetStatic(&result->id, "rhinolinux"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + ffStrbufSetS(&result->versionID, result->prettyName.chars + strlen("Rhino Linux ")); + return true; + } else if (ffStrbufStartsWithS(&result->prettyName, "VanillaOS ")) { + ffStrbufSetStatic(&result->id, "vanilla"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return true; + } + + if (ffPathExists("/usr/bin/lliurex-version", FF_PATHTYPE_FILE)) { + ffStrbufSetStatic(&result->name, "LliureX"); + ffStrbufSetStatic(&result->id, "lliurex"); + ffStrbufClear(&result->version); + if (ffProcessAppendStdOut(&result->version, (char* const[]) { + "/usr/bin/lliurex-version", + NULL, + }) == NULL) { // 8.2.2 + ffStrbufTrimRightSpace(&result->version); + } + ffStrbufSetF(&result->prettyName, "LliureX %s", result->version.chars); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return true; + } + + // xdgConfigDirs contains plasma only + if (ffPathExists("/var/lib/dpkg/info/ubuntustudio-desktop.list", FF_PATHTYPE_FILE)) { + ffStrbufSetStatic(&result->name, "Ubuntu Studio"); + ffStrbufSetStatic(&result->id, "ubuntu-studio"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return false; + } + + const char* xdgConfigDirs = getenv("XDG_CONFIG_DIRS"); + if (!ffStrSet(xdgConfigDirs)) { + return false; + } + + if (ffStrContains(xdgConfigDirs, "kde") || ffStrContains(xdgConfigDirs, "plasma") || ffStrContains(xdgConfigDirs, "kubuntu")) { + ffStrbufSetStatic(&result->name, "Kubuntu"); + ffStrbufSetStatic(&result->id, "kubuntu"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return false; + } + + if (ffStrContains(xdgConfigDirs, "xfce") || ffStrContains(xdgConfigDirs, "xubuntu")) { + ffStrbufSetStatic(&result->name, "Xubuntu"); + ffStrbufSetStatic(&result->id, "xubuntu"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return false; + } + + if (ffStrContains(xdgConfigDirs, "lxqt") || ffStrContains(xdgConfigDirs, "lubuntu")) { + ffStrbufSetStatic(&result->name, "Lubuntu"); + ffStrbufSetStatic(&result->id, "lubuntu"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return false; + } + + if (ffStrContains(xdgConfigDirs, "budgie")) { + ffStrbufSetStatic(&result->name, "Ubuntu Budgie"); + ffStrbufSetStatic(&result->id, "ubuntu-budgie"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return false; + } + + if (ffStrContains(xdgConfigDirs, "cinnamon")) { + ffStrbufSetStatic(&result->name, "Ubuntu Cinnamon"); + ffStrbufSetStatic(&result->id, "ubuntu-cinnamon"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return false; + } + + if (ffStrContains(xdgConfigDirs, "mate")) { + ffStrbufSetStatic(&result->name, "Ubuntu MATE"); + ffStrbufSetStatic(&result->id, "ubuntu-mate"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return false; + } + + if (ffStrContains(xdgConfigDirs, "ukui")) { + ffStrbufSetStatic(&result->name, "Ubuntu Kylin"); + ffStrbufSetStatic(&result->id, "ubuntu-kylin"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return false; + } + + if (ffStrContains(xdgConfigDirs, "sway")) { + ffStrbufSetStatic(&result->name, "Ubuntu Sway"); + ffStrbufSetStatic(&result->id, "ubuntu-sway"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return false; + } + + if (ffStrContains(xdgConfigDirs, "touch")) { + ffStrbufSetStatic(&result->name, "Ubuntu Touch"); + ffStrbufSetStatic(&result->id, "ubuntu-touch"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return false; + } + + if (ffStrContains(xdgConfigDirs, "unity")) { + ffStrbufSetStatic(&result->name, "Ubuntu Unity"); + ffStrbufSetStatic(&result->id, "ubuntu-unity"); + ffStrbufSetStatic(&result->idLike, "ubuntu"); + return false; + } + + return false; +} + +FF_A_UNUSED static void getDebianVersion(FFOSResult* result) { + FF_STRBUF_AUTO_DESTROY debianVersion = ffStrbufCreate(); + ffAppendFileBuffer("/etc/debian_version", &debianVersion); + ffStrbufTrimRightSpace(&debianVersion); + if (!debianVersion.length) { + return; + } + ffStrbufDestroy(&result->versionID); + ffStrbufInitMove(&result->versionID, &debianVersion); + + ffStrbufSetF(&result->prettyName, "%s %s (%s)", result->name.chars, result->versionID.chars, result->codename.chars); +} + +FF_A_UNUSED static bool detectDebianDerived(FFOSResult* result) { + if (detectArmbianVersion(result)) { + return true; + } else if (ffStrbufStartsWithS(&result->name, "Loc-OS")) { + ffStrbufSetStatic(&result->id, "locos"); + ffStrbufSetStatic(&result->idLike, "debian"); + return true; + } else if (ffStrbufEqualS(&result->name, "Parrot Security")) { + // https://github.com/ParrotSec/base-files/blob/c06f6d42ddf8d79564882306576576eddab7d907/etc/os-release + ffStrbufSetS(&result->id, "parrot"); + ffStrbufSetS(&result->idLike, "debian"); + return true; + } else if (ffStrbufStartsWithS(&result->name, "Lilidog GNU/Linux")) { + // https://github.com/fastfetch-cli/fastfetch/issues/1373 + ffStrbufSetStatic(&result->id, "lilidog"); + ffStrbufSetStatic(&result->idLike, "debian"); + return true; + } else if (access("/usr/bin/pveversion", X_OK) == 0) { + ffStrbufSetStatic(&result->id, "pve"); + ffStrbufSetStatic(&result->idLike, "debian"); + ffStrbufSetStatic(&result->name, "Proxmox VE"); + ffStrbufClear(&result->versionID); + if (ffProcessAppendStdOut(&result->versionID, (char* const[]) { + "/usr/bin/dpkg-query", + "--showformat=${version}", + "--show", + "pve-manager", + NULL, + }) == NULL) { // 8.2.2 + ffStrbufTrimRightSpace(&result->versionID); + } + ffStrbufSetF(&result->prettyName, "Proxmox VE %s", result->versionID.chars); + return true; + } else if (ffPathExists("/etc/rpi-issue", FF_PATHTYPE_FILE)) { + // Raspberry Pi OS + ffStrbufSetStatic(&result->id, "raspbian"); + ffStrbufSetStatic(&result->idLike, "debian"); + ffStrbufSetStatic(&result->name, "Raspberry Pi OS"); + getDebianVersion(result); + return true; + } else if (ffPathExists("/boot/dietpi/.version", FF_PATHTYPE_FILE)) { + // DietPi + ffStrbufSetStatic(&result->id, "dietpi"); + ffStrbufSetStatic(&result->name, "DietPi"); + ffStrbufSetStatic(&result->prettyName, "DietPi"); + ffStrbufSetStatic(&result->idLike, "debian"); + FF_STRBUF_AUTO_DESTROY core = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY sub = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY rc = ffStrbufCreate(); + if (ffParsePropFileValues("/boot/dietpi/.version", 3, (FFpropquery[]) { + { "G_DIETPI_VERSION_CORE=", &core }, + { "G_DIETPI_VERSION_SUB=", &sub }, + { "G_DIETPI_VERSION_RC=", &rc }, + })) { + ffStrbufAppendF(&result->prettyName, " %s.%s.%s", core.chars, sub.chars, rc.chars); + } + return true; + } else if (ffStrbufEndsWithS(&instance.state.platform.sysinfo.release, "+truenas")) { + // TrueNAS Scale + ffStrbufSetStatic(&result->id, "truenas-scale"); + ffStrbufSetStatic(&result->idLike, "debian"); + ffStrbufSetStatic(&result->name, "TrueNAS Scale"); + ffStrbufSetStatic(&result->prettyName, "TrueNAS Scale"); + return true; + } else if (ffPathExists("/usr/bin/emmabuntus_config.sh", FF_PATHTYPE_FILE)) { + // Emmabuntüs + ffStrbufSetStatic(&result->id, "emmabuntus"); + ffStrbufSetStatic(&result->idLike, "debian"); + ffStrbufSetStatic(&result->name, "Emmabuntüs"); + getDebianVersion(result); + return true; + } else { + // Hack for MX Linux. See #847 + FF_STRBUF_AUTO_DESTROY lsbRelease = ffStrbufCreate(); + if (ffAppendFileBuffer("/etc/lsb-release", &lsbRelease) && ffStrbufContainS(&lsbRelease, "DISTRIB_ID=MX")) { + ffStrbufSetStatic(&result->id, "mx"); + ffStrbufSetStatic(&result->idLike, "debian"); + ffStrbufSetStatic(&result->name, "MX"); + + ffStrbufClear(&result->version); + ffParsePropLines(lsbRelease.chars, "DISTRIB_RELEASE=", &result->version); + ffStrbufSet(&result->versionID, &result->version); + + ffStrbufClear(&result->codename); + ffParsePropLines(lsbRelease.chars, "DISTRIB_CODENAME=", &result->codename); + + ffStrbufClear(&result->prettyName); + ffParsePropLines(lsbRelease.chars, "DISTRIB_DESCRIPTION=", &result->prettyName); + return true; + } + } + return false; +} + +FF_A_UNUSED static bool detectFedoraVariant(FFOSResult* result) { + if (ffStrbufEqualS(&result->variantID, "coreos") || ffStrbufEqualS(&result->variantID, "kinoite") || ffStrbufEqualS(&result->variantID, "sericea") || ffStrbufEqualS(&result->variantID, "silverblue")) { + ffStrbufAppendC(&result->id, '-'); + ffStrbufAppend(&result->id, &result->variantID); + ffStrbufSetStatic(&result->idLike, "fedora"); + return true; + } + return false; +} + +FF_A_UNUSED static bool detectBedrock(FFOSResult* os) { + const char* bedrockRestrict = getenv("BEDROCK_RESTRICT"); + if (bedrockRestrict && bedrockRestrict[0] == '1') { + return false; + } + return parseOsRelease(FASTFETCH_TARGET_DIR_ROOT "/bedrock/strata/bedrock/etc/os-release", os); +} + +FF_A_UNUSED static void detectDeepinEnhancement(FFOSResult* result) { + if (ffStrbufContainC(&result->prettyName, '(')) { + return; + } + + FF_STRBUF_AUTO_DESTROY minor = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY edition = ffStrbufCreate(); + + if (!ffParsePropFileValues( + FASTFETCH_TARGET_DIR_ETC "/os-version", + 2, + (FFpropquery[]) { + { "MinorVersion=", &minor }, + { "EditionName=", &edition }, + }) || + minor.length == 0) { + return; + } + + ffStrbufSet(&result->versionID, &minor); + + if (edition.length > 0) { + ffStrbufSetF(&result->prettyName, "%s %s (%s)", result->name.chars, minor.chars, edition.chars); + } else { + ffStrbufSetF(&result->prettyName, "%s %s", result->name.chars, minor.chars); + } +} + +static void detectOS(FFOSResult* os) { +#ifdef FF_CUSTOM_OS_RELEASE_PATH + parseOsRelease(FF_STR(FF_CUSTOM_OS_RELEASE_PATH), os); + #ifdef FF_CUSTOM_LSB_RELEASE_PATH + parseLsbRelease(FF_STR(FF_CUSTOM_LSB_RELEASE_PATH), os); + #endif + return; +#endif + +#ifdef __linux__ + if (detectBedrock(os)) { + return; + } +#endif + + // Refer: https://gist.github.com/natefoo/814c5bf936922dad97ff + + parseOsRelease(FASTFETCH_TARGET_DIR_ETC "/os-release", os); + + if (os->id.length == 0 || os->version.length == 0 || os->prettyName.length == 0 || os->codename.length == 0) { + parseLsbRelease(FASTFETCH_TARGET_DIR_ETC "/lsb-release", os); + } + if (os->id.length == 0 || os->name.length == 0 || os->prettyName.length == 0) { + parseOsRelease(FASTFETCH_TARGET_DIR_USR "/lib/os-release", os); + } + if (os->id.length == 0 && os->name.length == 0 && os->prettyName.length == 0) { + // HarmonyOS has no os-release file + if (ffStrbufEqualS(&instance.state.platform.sysinfo.name, "HarmonyOS")) { + ffStrbufSetS(&os->id, "harmonyos"); + ffStrbufSetS(&os->idLike, "harmonyos"); + ffStrbufSetS(&os->name, "HarmonyOS"); + ffStrbufSetS(&os->prettyName, "HarmonyOS"); + } + } +} + +void ffDetectOSImpl(FFOSResult* os) { + detectOS(os); + +#if __linux__ || __GNU__ + if (ffStrbufEqualS(&os->id, "ubuntu")) { + if (!getUbuntuFlavour(os)) { + if (!ffStrbufEndsWithS(&os->prettyName, " (development branch)")) { + ffStrbufSetF(&os->prettyName, "%s %s", os->name.chars, os->version.chars); // os->version contains code name + } + } + } else if (ffStrbufEqualS(&os->id, "debian")) { + if (!detectDebianDerived(os)) { + getDebianVersion(os); + } + } else if (ffStrbufEqualS(&os->id, "fedora")) { + detectFedoraVariant(os); + } else if (ffStrbufEqualS(&os->id, "linuxmint")) { + if (ffStrbufEqualS(&os->name, "LMDE")) { + ffStrbufSetS(&os->id, "lmde"); + ffStrbufSetS(&os->idLike, "linuxmint"); + } + } else if (ffStrbufEqualS(&os->id, "deepin")) { + detectDeepinEnhancement(os); + } +#endif +} diff --git a/src/detection/os/os_nbsd.c b/src/detection/os/os_nbsd.c new file mode 100644 index 0000000..200d0f2 --- /dev/null +++ b/src/detection/os/os_nbsd.c @@ -0,0 +1,6 @@ +#include "os.h" + +void ffDetectOSImpl(FFOSResult* os) { + ffStrbufSetStatic(&os->name, "NetBSD"); + ffStrbufSet(&os->version, &instance.state.platform.sysinfo.release); +} diff --git a/src/detection/os/os_obsd.c b/src/detection/os/os_obsd.c new file mode 100644 index 0000000..4baf185 --- /dev/null +++ b/src/detection/os/os_obsd.c @@ -0,0 +1,6 @@ +#include "os.h" + +void ffDetectOSImpl(FFOSResult* os) { + ffStrbufSetStatic(&os->name, "OpenBSD"); + ffStrbufSet(&os->version, &instance.state.platform.sysinfo.release); +} diff --git a/src/detection/os/os_sunos.c b/src/detection/os/os_sunos.c new file mode 100644 index 0000000..72f9946 --- /dev/null +++ b/src/detection/os/os_sunos.c @@ -0,0 +1,17 @@ +#include "os.h" +#include "common/io.h" + +void ffDetectOSImpl(FFOSResult* os) { + if (!ffReadFileBuffer("/etc/release", &os->prettyName)) { + return; + } + + ffStrbufSubstrBeforeFirstC(&os->prettyName, '\n'); + ffStrbufSubstrBeforeLastC(&os->prettyName, '('); + ffStrbufTrim(&os->prettyName, ' '); + + // OpenIndiana Hipster 2024.04 + uint32_t idx = ffStrbufFirstIndexC(&os->prettyName, ' '); + ffStrbufSetNS(&os->id, idx, os->prettyName.chars); + ffStrbufSetStatic(&os->idLike, "sunos"); +} diff --git a/src/detection/os/os_windows.c b/src/detection/os/os_windows.c new file mode 100644 index 0000000..2f280c8 --- /dev/null +++ b/src/detection/os/os_windows.c @@ -0,0 +1,92 @@ +#include "os.h" +#include "common/strutil.h" +#include "common/windows/registry.h" +#include "common/windows/unicode.h" + +#include <windows.h> + +PWSTR WINAPI BrandingFormatString(PCWSTR format); + +static bool getCodeName(FFOSResult* os) { + FF_AUTO_CLOSE_FD HANDLE hKey = NULL; + if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", &hKey, NULL)) { + return false; + } + + if (!ffRegReadStrbuf(hKey, L"DisplayVersion", &os->codename, NULL)) { + if (!ffRegReadStrbuf(hKey, L"CSDVersion", &os->codename, NULL)) { // For Windows 7 and Windows 8 + if (!ffRegReadStrbuf(hKey, L"ReleaseId", &os->codename, NULL)) { // For old Windows 10 + return false; + } + } + } + + return true; +} + +void ffDetectOSImpl(FFOSResult* os) { + // https://dennisbabkin.com/blog/?t=how-to-tell-the-real-version-of-windows-your-app-is-running-on#ver_string + const wchar_t* rawName = BrandingFormatString(L"%WINDOWS_LONG%"); + ffStrbufSetWS(&os->variant, rawName); + GlobalFree((HGLOBAL) rawName); + ffStrbufSet(&os->prettyName, &os->variant); + ffStrbufTrimRight(&os->variant, ' '); + + // WMI returns the "Microsoft" prefix while BrandingFormatString doesn't. Make them consistent. + if (ffStrbufStartsWithS(&os->variant, "Microsoft ")) { + ffStrbufSubstrAfter(&os->variant, strlen("Microsoft ") - 1); + } + + if (os->variant.length == 0) // Windows PE? + { + FF_AUTO_CLOSE_FD HANDLE hKey = NULL; + if (ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", &hKey, NULL)) { + ffRegReadStrbuf(hKey, L"ProductName", &os->variant, NULL); + } + } + + ffStrbufSet(&os->prettyName, &os->variant); + + if (ffStrbufStartsWithS(&os->variant, "Windows ")) { + ffStrbufAppendS(&os->name, "Windows"); + + ffStrbufSubstrAfter(&os->variant, strlen("Windows ") - 1); + + if (ffStrbufStartsWithS(&os->variant, "Server ")) { + ffStrbufAppendS(&os->name, " Server"); + ffStrbufSubstrAfter(&os->variant, strlen(" Server") - 1); + } else if (ffStrbufStartsWithS(&os->variant, "Embedded ")) { + ffStrbufAppendS(&os->name, " Embedded"); + ffStrbufSubstrAfter(&os->variant, strlen(" Embedded") - 1); + } + + if (ffStrbufStartsWithIgnCaseS(&os->variant, "(TM) ")) { + ffStrbufSubstrAfter(&os->variant, strlen(" (TM)") - 1); + } + + uint32_t index = ffStrbufFirstIndexC(&os->variant, ' '); + ffStrbufAppendNS(&os->version, index, os->variant.chars); + ffStrbufSubstrAfter(&os->variant, index); + + // Windows Server 20xx Rx + if (ffStrbufEndsWithC(&os->name, 'r')) { + if (os->variant.chars[0] == 'R' && + ffCharIsDigit(os->variant.chars[1]) && + (os->variant.chars[2] == '\0' || os->variant.chars[2] == ' ')) { + ffStrbufAppendF(&os->version, " R%c", os->variant.chars[1]); + ffStrbufSubstrAfter(&os->variant, strlen("Rx ") - 1); + } + } + } else { + // Unknown Windows name, please report this + ffStrbufAppend(&os->name, &os->variant); + ffStrbufClear(&os->variant); + } + + ffStrbufAppendF(&os->id, "%s %s", os->name.chars, os->version.chars); + ffStrbufSetStatic(&os->idLike, "Windows"); + + if (getCodeName(os) && os->codename.length > 0) { + ffStrbufAppendF(&os->prettyName, " (%s)", os->codename.chars); + } +} diff --git a/src/detection/packages/packages.c b/src/detection/packages/packages.c new file mode 100644 index 0000000..25f59b3 --- /dev/null +++ b/src/detection/packages/packages.c @@ -0,0 +1,119 @@ +#include "packages.h" +#include "common/io.h" +#include "common/time.h" + +#include <inttypes.h> +#include <stddef.h> + +#ifdef __APPLE__ + #define st_mtim st_mtimespec +#endif + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options); + +const char* ffDetectPackages(FFPackagesResult* result, FFPackagesOptions* options) { + ffDetectPackagesImpl(result, options); + + for (uint32_t i = 0; i < offsetof(FFPackagesResult, all) / sizeof(uint32_t); ++i) { + result->all += ((uint32_t*) result)[i]; + } + + return NULL; +} + +bool ffPackagesReadCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, const char* filePath, const char* packageId, uint32_t* result) { +#ifndef _WIN32 + struct stat st; + if (stat(filePath, &st) < 0) // file doesn't exist or isn't accessible + { + *result = 0; + return true; + } + + if (__builtin_expect(st.st_mtim.tv_sec <= 0, false)) { + return false; + } + + uint64_t mtime_current = (uint64_t) st.st_mtim.tv_sec * 1000ull + (uint64_t) st.st_mtim.tv_nsec / 1000000ull; +#else + FF_AUTO_CLOSE_FD HANDLE handle = CreateFileA(filePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + + if (handle == INVALID_HANDLE_VALUE) // file doesn't exist or isn't accessible + { + *result = 0; + return true; + } + + uint64_t mtime_current; + FILE_BASIC_INFORMATION fileInfo; + IO_STATUS_BLOCK iosb; + if (!NT_SUCCESS(NtQueryInformationFile(handle, &iosb, &fileInfo, sizeof(fileInfo), FileBasicInformation))) { + return false; + } + + mtime_current = ffFileTimeToUnixMs((uint64_t) fileInfo.LastWriteTime.QuadPart); +#endif + + ffStrbufSet(cacheDir, &instance.state.platform.cacheDir); + ffStrbufEnsureEndsWithC(cacheDir, '/'); + ffStrbufAppendF(cacheDir, "fastfetch/packages/%s.txt", packageId); + + if (ffReadFileBuffer(cacheDir->chars, cacheContent)) { + uint64_t mtime_cached; + uint32_t num_cached; + if (sscanf(cacheContent->chars, "%" SCNu64 " %" SCNu32, &mtime_cached, &num_cached) == 2 && + mtime_cached == mtime_current && num_cached > 0) { + *result = num_cached; + return true; + } + } + + ffStrbufSetF(cacheContent, "%" PRIu64 " ", mtime_current); + + return false; +} + +bool ffPackagesWriteCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, uint32_t num_elements) { + if (__builtin_expect(cacheContent->length == 0, false)) { + return false; + } + + ffStrbufAppendF(cacheContent, "%" PRIu32, num_elements); + return ffWriteFileBuffer(cacheDir->chars, cacheContent); +} + +#ifndef _WIN32 +uint32_t ffPackagesGetNumElements(const char* dirname, bool isdir) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(dirname); + if (dirp == NULL) { + return 0; + } + + uint32_t num_elements = 0; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + bool ok = false; + + if (entry->d_name[0] != '.') { + #if !defined(__sun) && !defined(__HAIKU__) + if (entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK) { + ok = entry->d_type == (isdir ? DT_DIR : DT_REG); + } else + #endif + { + struct stat stbuf; + if (fstatat(dirfd(dirp), entry->d_name, &stbuf, 0) == 0) { + ok = isdir ? S_ISDIR(stbuf.st_mode) : S_ISREG(stbuf.st_mode); + } + } + } + + if (ok) { + ++num_elements; + } + } + + return num_elements; +} +#endif diff --git a/src/detection/packages/packages.h b/src/detection/packages/packages.h new file mode 100644 index 0000000..f0d4200 --- /dev/null +++ b/src/detection/packages/packages.h @@ -0,0 +1,72 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/packages/option.h" + +typedef struct FFPackagesResult { + uint32_t amSystem; + uint32_t amUser; + uint32_t apk; + uint32_t appimage; + uint32_t brew; + uint32_t brewCask; + uint32_t cards; + uint32_t choco; + uint32_t dpkg; + uint32_t emerge; + uint32_t eopkg; + uint32_t flatpakSystem; + uint32_t flatpakUser; + uint32_t guixHome; + uint32_t guixSystem; + uint32_t guixUser; + uint32_t hpkgSystem; + uint32_t hpkgUser; + uint32_t kiss; + uint32_t linglong; + uint32_t lpkg; + uint32_t lpkgbuild; + uint32_t macports; + uint32_t moss; + uint32_t mport; + uint32_t nixDefault; + uint32_t nixSystem; + uint32_t nixUser; + uint32_t opkg; + uint32_t pacman; + uint32_t pacstall; + uint32_t paludis; + uint32_t pisi; + uint32_t pkg; + uint32_t pkgsrc; + uint32_t pkgtool; + uint32_t rpm; + uint32_t scoopGlobal; + uint32_t scoopUser; + uint32_t snap; + uint32_t soar; + uint32_t sorcery; + uint32_t winget; + uint32_t xbps; + + uint32_t all; // Make sure this goes last + + FFstrbuf pacmanBranch; +} FFPackagesResult; + +#if FF_PACKAGES_REMOVE_DISABLED + #define FF_PACKAGES_IS_ENABLED(options, pkgName) ({ (void) options; !((FF_PACKAGES_DISABLE_LIST) & (FF_PACKAGES_FLAG_ ## pkgName ## _BIT)); }) +#else + #define FF_PACKAGES_IS_ENABLED(options, pkgName) (!((options)->disabled & (FF_PACKAGES_FLAG_ ## pkgName ## _BIT))) +#endif + +const char* ffDetectPackages(FFPackagesResult* result, FFPackagesOptions* options); +bool ffPackagesReadCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, const char* filePath, const char* packageId, uint32_t* result); +bool ffPackagesWriteCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, uint32_t num_elements); + +#if defined(__linux__) || defined(__APPLE__) || defined(__GNU__) +uint32_t ffPackagesGetNix(FFstrbuf* baseDir, const char* dirname); +#endif +#ifndef _WIN32 +uint32_t ffPackagesGetNumElements(const char* dirname, bool isdir); +#endif diff --git a/src/detection/packages/packages_apple.c b/src/detection/packages/packages_apple.c new file mode 100644 index 0000000..43de97d --- /dev/null +++ b/src/detection/packages/packages_apple.c @@ -0,0 +1,56 @@ +#include "packages.h" +#include "common/io.h" +#include "common/parsing.h" +#include "common/processing.h" +#include "common/strutil.h" + +static void countBrewPackages(FFstrbuf* baseDir, FFPackagesResult* result) { + uint32_t baseDirLength = baseDir->length; + + ffStrbufAppendS(baseDir, "/Caskroom"); + result->brewCask += ffPackagesGetNumElements(baseDir->chars, true); + ffStrbufSubstrBefore(baseDir, baseDirLength); + + ffStrbufAppendS(baseDir, "/Cellar"); + result->brew += ffPackagesGetNumElements(baseDir->chars, true); + ffStrbufSubstrBefore(baseDir, baseDirLength); +} + +static uint32_t getMacPortsPackages(FFstrbuf* baseDir) { + ffStrbufAppendS(baseDir, "/var/macports/software"); + return ffPackagesGetNumElements(baseDir->chars, true); +} + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + FF_STRBUF_AUTO_DESTROY baseDir = ffStrbufCreate(); + if (FF_PACKAGES_IS_ENABLED(options, BREW)) { + const char* prefix = getenv("HOMEBREW_PREFIX"); + if (ffStrSet(prefix)) { + ffStrbufSetS(&baseDir, prefix); + } else { +#ifdef __aarch64__ + ffStrbufSetS(&baseDir, FASTFETCH_TARGET_DIR_ROOT "/opt/homebrew"); +#else + ffStrbufSetS(&baseDir, FASTFETCH_TARGET_DIR_USR "/local"); +#endif + } + countBrewPackages(&baseDir, result); + } + if (FF_PACKAGES_IS_ENABLED(options, MACPORTS)) { + const char* prefix = getenv("MACPORTS_PREFIX"); + if (ffStrSet(prefix)) { + ffStrbufSetS(&baseDir, prefix); + } else { + ffStrbufSetS(&baseDir, FASTFETCH_TARGET_DIR_ROOT "/opt/local"); + } + + result->macports = getMacPortsPackages(&baseDir); + } + if (FF_PACKAGES_IS_ENABLED(options, NIX)) { + ffStrbufSetS(&baseDir, FASTFETCH_TARGET_DIR_ROOT); + result->nixDefault += ffPackagesGetNix(&baseDir, "/nix/var/nix/profiles/default"); + result->nixSystem += ffPackagesGetNix(&baseDir, "/run/current-system"); + ffStrbufSet(&baseDir, &instance.state.platform.homeDir); + result->nixUser = ffPackagesGetNix(&baseDir, "/.nix-profile"); + } +} diff --git a/src/detection/packages/packages_bsd.c b/src/detection/packages/packages_bsd.c new file mode 100644 index 0000000..73772ac --- /dev/null +++ b/src/detection/packages/packages_bsd.c @@ -0,0 +1,28 @@ +#include "packages.h" + +#include "common/settings.h" + +static uint32_t getSQLite3Int(const char* dbPath, const char* query, const char* packageId) { + FF_STRBUF_AUTO_DESTROY cacheDir = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate(); + + uint32_t num_elements; + if (ffPackagesReadCache(&cacheDir, &cacheContent, dbPath, packageId, &num_elements)) { + return num_elements; + } + + num_elements = (uint32_t) ffSettingsGetSQLite3Int(dbPath, query); + + ffPackagesWriteCache(&cacheDir, &cacheContent, num_elements); + + return num_elements; +} + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + if (FF_PACKAGES_IS_ENABLED(options, PKG)) { + result->pkg = getSQLite3Int(FASTFETCH_TARGET_DIR_ROOT "/var/db/pkg/local.sqlite", "SELECT count(*) FROM packages", "pkg"); + } + if (FF_PACKAGES_IS_ENABLED(options, MPORT)) { + result->mport = getSQLite3Int(FASTFETCH_TARGET_DIR_ROOT "/var/db/mport/master.db", "SELECT count(*) FROM packages", "mport"); + } +} diff --git a/src/detection/packages/packages_haiku.c b/src/detection/packages/packages_haiku.c new file mode 100644 index 0000000..eee14cf --- /dev/null +++ b/src/detection/packages/packages_haiku.c @@ -0,0 +1,12 @@ +#include "packages.h" + +#include "common/io.h" + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + // TODO: Use the Package Kit C++ API instead (would account for disabled packages) + + if (FF_PACKAGES_IS_ENABLED(options, HPKG)) { + result->hpkgSystem = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/system/packages", false); + result->hpkgUser = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/boot/home/config/packages", false); + } +} diff --git a/src/detection/packages/packages_linux.c b/src/detection/packages/packages_linux.c new file mode 100644 index 0000000..79de8cb --- /dev/null +++ b/src/detection/packages/packages_linux.c @@ -0,0 +1,651 @@ +#include "packages.h" +#include "common/io.h" +#include "common/parsing.h" +#include "common/properties.h" +#include "common/settings.h" +#include "common/strutil.h" +#include "detection/os/os.h" + +static uint32_t getNumElements(FFstrbuf* baseDir, const char* dirname, bool isdir) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + uint32_t num_elements = ffPackagesGetNumElements(baseDir->chars, isdir); + ffStrbufSubstrBefore(baseDir, baseDirLength); + return num_elements; +} + +static uint32_t getNumStringsImpl(const char* filename, const char* needle) { + FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); + if (!ffReadFileBuffer(filename, &content)) { + return 0; + } + + uint32_t count = 0; + char* iter = content.chars; + size_t needleLength = strlen(needle); + while ((iter = memmem(iter, content.length - (size_t) (iter - content.chars), needle, needleLength)) != NULL) { + ++count; + iter += needleLength; + } + + return count; +} + +static uint32_t getNumStrings(FFstrbuf* baseDir, const char* filename, const char* needle, const char* packageId) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, filename); + + FF_STRBUF_AUTO_DESTROY cacheDir = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate(); + + uint32_t num_elements; + if (ffPackagesReadCache(&cacheDir, &cacheContent, baseDir->chars, packageId, &num_elements)) { + ffStrbufSubstrBefore(baseDir, baseDirLength); + return num_elements; + } + + num_elements = getNumStringsImpl(baseDir->chars, needle); + ffStrbufSubstrBefore(baseDir, baseDirLength); + + ffPackagesWriteCache(&cacheDir, &cacheContent, num_elements); + + return num_elements; +} + +static uint32_t getSQLite3Int(FFstrbuf* baseDir, const char* dbPath, const char* query, const char* packageId) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dbPath); + + FF_STRBUF_AUTO_DESTROY cacheDir = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate(); + + uint32_t num_elements; + if (ffPackagesReadCache(&cacheDir, &cacheContent, baseDir->chars, packageId, &num_elements)) { + ffStrbufSubstrBefore(baseDir, baseDirLength); + return num_elements; + } + + num_elements = (uint32_t) ffSettingsGetSQLite3Int(baseDir->chars, query); + ffStrbufSubstrBefore(baseDir, baseDirLength); + + ffPackagesWriteCache(&cacheDir, &cacheContent, num_elements); + + return num_elements; +} + +static uint32_t countFilesRecursiveImpl(FFstrbuf* baseDirPath, const char* filename) { + uint32_t baseDirPathLength = baseDirPath->length; + + ffStrbufAppendC(baseDirPath, '/'); + ffStrbufAppendS(baseDirPath, filename); + bool exists = ffPathExists(baseDirPath->chars, FF_PATHTYPE_FILE); + ffStrbufSubstrBefore(baseDirPath, baseDirPathLength); + if (exists) { + return 1; + } + + DIR* dirp = opendir(baseDirPath->chars); + if (dirp == NULL) { + return 0; + } + + ffStrbufAppendC(baseDirPath, '/'); + baseDirPathLength = baseDirPath->length; + + uint32_t sum = 0; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + // According to the PMS, neither category nor package name can begin with '.', so no need to check for . or .. specifically + if (entry->d_type != DT_DIR || entry->d_name[0] == '.') { + continue; + } + + ffStrbufAppendS(baseDirPath, entry->d_name); + sum += countFilesRecursiveImpl(baseDirPath, filename); + ffStrbufSubstrBefore(baseDirPath, baseDirPathLength); + } + + closedir(dirp); + return sum; +} + +static uint32_t countFilesRecursive(FFstrbuf* baseDir, const char* dirname, const char* filename) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + uint32_t sum = countFilesRecursiveImpl(baseDir, filename); + ffStrbufSubstrBefore(baseDir, baseDirLength); + return sum; +} + +static uint32_t getNumElementsBySuffix(FFstrbuf* baseDir, const char* dirname, const char* suffix) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars); + ffStrbufSubstrBefore(baseDir, baseDirLength); + if (dirp == NULL) { + return 0; + } + + uint32_t count = 0; + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] != '.' && ffStrEndsWithIgnCase(entry->d_name, suffix)) { + ++count; + } + } + return count; +} + +static uint32_t getXBPSImpl(FFstrbuf* baseDir) { + DIR* dir = opendir(baseDir->chars); + if (dir == NULL) { + return 0; + } + + uint32_t result = 0; + + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_type != DT_REG || !ffStrStartsWithIgnCase(entry->d_name, "pkgdb-")) { + continue; + } + + ffStrbufAppendC(baseDir, '/'); + ffStrbufAppendS(baseDir, entry->d_name); + result = getNumStringsImpl(baseDir->chars, "<string>installed</string>"); + break; + } + + closedir(dir); + return result; +} + +static uint32_t getXBPS(FFstrbuf* baseDir, const char* dirname) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + uint32_t result = getXBPSImpl(baseDir); + ffStrbufSubstrBefore(baseDir, baseDirLength); + return result; +} + +static uint32_t getSnap(FFstrbuf* baseDir) { + uint32_t result = getNumElements(baseDir, "/snap", true); + + if (result == 0) { + result = getNumElements(baseDir, "/var/lib/snapd/snap", true); + } + + // Accounting for the /snap/bin folder + return result > 0 ? result - 1 : 0; +} + +#ifdef FF_HAVE_RPM + #include "common/library.h" + #include <rpm/rpmlib.h> + #include <rpm/rpmts.h> + #include <rpm/rpmdb.h> + #include <rpm/rpmlog.h> + +static uint32_t getRpmFromLibrpm(void) { + FF_LIBRARY_LOAD(rpm, 0, "librpm" FF_LIBRARY_EXTENSION, 12) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmReadConfigFiles, 0) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmtsCreate, 0) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmtsInitIterator, 0) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmdbGetIteratorCount, 0) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmdbFreeIterator, 0) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmtsFree, 0) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmlogSetMask, 0) + + // Don't print any error messages + ffrpmlogSetMask(RPMLOG_MASK(RPMLOG_EMERG)); + + if (ffrpmReadConfigFiles(NULL, NULL) != 0) { + return 0; + } + + rpmts ts = ffrpmtsCreate(); + if (ts == NULL) { + return 0; + } + + rpmdbMatchIterator mi = ffrpmtsInitIterator(ts, RPMDBI_LABEL, NULL, 0); + if (mi == NULL) { + ffrpmtsFree(ts); + return 0; + } + + int count = ffrpmdbGetIteratorCount(mi); + + ffrpmdbFreeIterator(mi); + ffrpmtsFree(ts); + + return count > 0 ? (uint32_t) count : 0; +} + +#endif // FF_HAVE_RPM + +static uint32_t getAMPackages(FFstrbuf* baseDir) { + uint32_t baseLength = baseDir->length; + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars); + if (!dirp) { + return 0; + } + + uint32_t result = 0; + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + if (entry->d_type == DT_DIR) { + ffStrbufAppendF(baseDir, "/%s/remove", entry->d_name); + if (ffPathExists(baseDir->chars, FF_PATHTYPE_FILE)) { + ++result; + } + ffStrbufSubstrBefore(baseDir, baseLength); + } + } + return result; +} + +static uint32_t getAMSystem(FFstrbuf* baseDir) { + // #771 + uint32_t baseDirLength = baseDir->length; + + ffStrbufAppendS(baseDir, "/opt"); + uint32_t optDirLength = baseDir->length; + + uint32_t result = 0; + + ffStrbufAppendS(baseDir, "/am/APP-MANAGER"); + if (ffPathExists(baseDir->chars, FF_PATHTYPE_FILE)) { + ++result; // `am` itself is counted as a package too + ffStrbufSubstrBefore(baseDir, optDirLength); + result = getAMPackages(baseDir); + } + + ffStrbufSubstrBefore(baseDir, baseDirLength); + return result; +} + +static uint32_t getAMUser(void) { + if (instance.state.platform.configDirs.length == 0) { + return 0; + } + + // check if $XDG_CONFIG_HOME/appman/appman-config exists + FFstrbuf* baseDir = FF_LIST_FIRST(FFstrbuf, instance.state.platform.configDirs); + uint32_t baseLen = baseDir->length; + ffStrbufAppendS(baseDir, "appman/appman-config"); + FF_STRBUF_AUTO_DESTROY packagesPath = ffStrbufCreate(); + if (ffReadFileBuffer(baseDir->chars, &packagesPath)) { + ffStrbufTrimRightSpace(&packagesPath); + } + ffStrbufSubstrBefore(baseDir, baseLen); + + return packagesPath.length > 0 ? getAMPackages(&packagesPath) : 0; +} + +static int compareHash(const void* a, const void* b) { + return memcmp(a, b, 32); +} + +static uint32_t getGuixPackagesImpl(char* filename) { + FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); + if (!ffAppendFileBuffer(filename, &content)) { + return 0; + } + + // Count number of unique /gnu/store/ paths in PROFILE/manifest based on their hash value. + // Contains packages explicitly installed and their propagated inputs. + char* pend = content.chars; + + for (const char* pattern = content.chars; (pattern = strstr(pattern, "/gnu/store/")); pattern += 32) { + pattern += strlen("/gnu/store/"); + memmove(pend, pattern, 32); + pend += 32; + } + + if (pend == content.chars) { + return 0; + } + + qsort(content.chars, (size_t) (pend - content.chars) / 32, 32, compareHash); + + uint32_t count = 1; + for (const char* p = content.chars + 32; p < pend; p += 32) { + count += compareHash(p - 32, p) != 0; + } + + return count; +} + +static uint32_t getGuixPackages(FFstrbuf* baseDir, const char* dirname) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + ffStrbufAppendS(baseDir, "/manifest"); + uint32_t num_elements = getGuixPackagesImpl(baseDir->chars); + ffStrbufSubstrBefore(baseDir, baseDirLength); + return num_elements; +} + +static inline uint32_t getFlatpakRuntimePackagesArch(FFstrbuf* baseDir) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars); + if (dirp == NULL) { + return 0; + } + + uint32_t num_elements = 0; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_type == DT_DIR && entry->d_name[0] != '.') { + num_elements += getNumElements(baseDir, entry->d_name, true); + } + } + + return num_elements; +} + +static inline uint32_t getFlatpakRuntimePackages(FFstrbuf* baseDir) { + ffStrbufAppendS(baseDir, "runtime/"); + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars); + if (dirp == NULL) { + return 0; + } + + uint32_t runtimeDirLength = baseDir->length; + uint32_t num_elements = 0; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_type == DT_DIR && entry->d_name[0] != '.') { + // `flatpak list` ignores `.Locale` and `.Debug` packages, and maybe others + const char* dot = strrchr(entry->d_name, '.'); + if (__builtin_expect(!dot, false)) { + continue; + } + dot++; + + if (ffStrEquals(dot, "Locale") || ffStrEquals(dot, "Debug")) { + continue; + } + + ffStrbufAppendS(baseDir, entry->d_name); + ffStrbufAppendC(baseDir, '/'); + num_elements += getFlatpakRuntimePackagesArch(baseDir); + ffStrbufSubstrBefore(baseDir, runtimeDirLength); + } + } + + return num_elements; +} + +static inline uint32_t getFlatpakAppPackages(FFstrbuf* baseDir) { + ffStrbufAppendS(baseDir, "app/"); + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars); + if (dirp == NULL) { + return 0; + } + + uint32_t appDirLength = baseDir->length; + uint32_t num_elements = 0; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_type == DT_DIR && entry->d_name[0] != '.') { + ffStrbufAppendS(baseDir, entry->d_name); + ffStrbufAppendS(baseDir, "/current"); + if (ffPathExists(baseDir->chars, FF_PATHTYPE_ANY)) { // Exclude deleted apps, #1856 + ++num_elements; + } + ffStrbufSubstrBefore(baseDir, appDirLength); + } + } + return num_elements; +} + +static uint32_t getFlatpakPackages(FFstrbuf* baseDir, const char* dirname) { + uint32_t num_elements = 0; + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + ffStrbufAppendS(baseDir, "/flatpak/"); + uint32_t flatpakDirLength = baseDir->length; + + num_elements += getFlatpakAppPackages(baseDir); + ffStrbufSubstrBefore(baseDir, flatpakDirLength); + + num_elements += getFlatpakRuntimePackages(baseDir); + + ffStrbufSubstrBefore(baseDir, baseDirLength); + + return num_elements; +} + +static uint32_t getPacmanPackages(FFstrbuf* baseDir) { + FF_STRBUF_AUTO_DESTROY dbPath = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY rootDir = ffStrbufCreate(); + + // Get path to pacman.conf + uint32_t baseDirLen = baseDir->length; + ffStrbufAppendS(baseDir, "/etc/pacman.conf"); + + bool confFound = ffParsePropFileValues(baseDir->chars, 2, (FFpropquery[]) { + { "DBPath =", &dbPath }, + { "RootDir =", &rootDir }, + }); + ffStrbufSubstrBefore(baseDir, baseDirLen); + + if (confFound) { + if (dbPath.length > 0) { + // If DBPath is specified, use it + ffStrbufEnsureEndsWithC(&dbPath, '/'); + ffStrbufAppendS(&dbPath, "local"); + } else if (rootDir.length > 0) { + // ... otherwise, use RootDir + ffStrbufDestroy(&dbPath); + ffStrbufInitMove(&dbPath, &rootDir); + ffStrbufEnsureEndsWithC(&dbPath, '/'); + ffStrbufAppendS(&dbPath, "var/lib/pacman/local"); + } + } + + if (dbPath.length == 0) { + ffStrbufSetStatic(&dbPath, "/var/lib/pacman/local"); + } + + return getNumElements(baseDir, dbPath.chars, true); +} + +static void getPackageCounts(FFstrbuf* baseDir, FFPackagesResult* packageCounts, FFPackagesOptions* options) { + if (FF_PACKAGES_IS_ENABLED(options, APK)) { + packageCounts->apk += getNumStrings(baseDir, "/lib/apk/db/installed", "C:Q", "apk"); + } + if (FF_PACKAGES_IS_ENABLED(options, DPKG)) { + packageCounts->dpkg += getNumStrings(baseDir, "/var/lib/dpkg/status", "Status: install ok installed", "dpkg"); + } + if (FF_PACKAGES_IS_ENABLED(options, LPKG)) { + packageCounts->lpkg += getNumStrings(baseDir, "/opt/Loc-OS-LPKG/installed-lpkg/Listinstalled-lpkg.list", "\n", "lpkg"); + } + if (FF_PACKAGES_IS_ENABLED(options, EMERGE)) { + packageCounts->emerge += countFilesRecursive(baseDir, "/var/db/pkg", "SIZE"); + } + if (FF_PACKAGES_IS_ENABLED(options, EOPKG)) { + packageCounts->eopkg += getNumElements(baseDir, "/var/lib/eopkg/package", true); + } + if (FF_PACKAGES_IS_ENABLED(options, FLATPAK)) { + packageCounts->flatpakSystem += getFlatpakPackages(baseDir, "/var/lib"); + } + if (FF_PACKAGES_IS_ENABLED(options, KISS)) { + packageCounts->kiss += getNumElements(baseDir, "/var/db/kiss/installed", true); + } + if (FF_PACKAGES_IS_ENABLED(options, NIX)) { + packageCounts->nixDefault += ffPackagesGetNix(baseDir, "/nix/var/nix/profiles/default"); + packageCounts->nixSystem += ffPackagesGetNix(baseDir, "/run/current-system"); + } + if (FF_PACKAGES_IS_ENABLED(options, PACMAN)) { + packageCounts->pacman += getPacmanPackages(baseDir); + } + if (FF_PACKAGES_IS_ENABLED(options, LPKGBUILD)) { + packageCounts->lpkgbuild += getNumElements(baseDir, "/opt/Loc-OS-LPKG/lpkgbuild/remove", false); + } + if (FF_PACKAGES_IS_ENABLED(options, PKGTOOL)) { + packageCounts->pkgtool += getNumElements(baseDir, "/var/log/packages", false); + } + if (FF_PACKAGES_IS_ENABLED(options, RPM)) { + // `Sigmd5` is the only table that doesn't contain the virtual `gpg-pubkey` package + packageCounts->rpm += getSQLite3Int(baseDir, "/var/lib/rpm/rpmdb.sqlite", "SELECT count(*) FROM Sigmd5", "rpm"); + } + if (FF_PACKAGES_IS_ENABLED(options, SNAP)) { + packageCounts->snap += getSnap(baseDir); + } + if (FF_PACKAGES_IS_ENABLED(options, XBPS)) { + packageCounts->xbps += getXBPS(baseDir, "/var/db/xbps"); + } + if (FF_PACKAGES_IS_ENABLED(options, BREW)) { + packageCounts->brewCask += getNumElements(baseDir, "/home/linuxbrew/.linuxbrew/Caskroom", true); + packageCounts->brew += getNumElements(baseDir, "/home/linuxbrew/.linuxbrew/Cellar", true); + } + if (FF_PACKAGES_IS_ENABLED(options, PALUDIS)) { + packageCounts->paludis += countFilesRecursive(baseDir, "/var/db/paludis/repositories", "environment.bz2"); + } + if (FF_PACKAGES_IS_ENABLED(options, OPKG)) { + packageCounts->opkg += getNumStrings(baseDir, "/usr/lib/opkg/status", "Package:", "opkg"); // openwrt + } + if (FF_PACKAGES_IS_ENABLED(options, AM)) { + packageCounts->amSystem = getAMSystem(baseDir); + } + if (FF_PACKAGES_IS_ENABLED(options, SORCERY)) { + packageCounts->sorcery += getNumStrings(baseDir, "/var/state/sorcery/packages", ":installed:", "sorcery"); + } + if (FF_PACKAGES_IS_ENABLED(options, GUIX)) { + packageCounts->guixSystem += getGuixPackages(baseDir, "/run/current-system/profile"); + } + if (FF_PACKAGES_IS_ENABLED(options, LINGLONG)) { + packageCounts->linglong += getNumElements(baseDir, "/var/lib/linglong/layers", true); + } + if (FF_PACKAGES_IS_ENABLED(options, PACSTALL)) { + packageCounts->pacstall += getNumElements(baseDir, "/var/lib/pacstall/metadata", false); + } + if (FF_PACKAGES_IS_ENABLED(options, PISI)) { + packageCounts->pisi += getNumElements(baseDir, "/var/lib/pisi/package", true); + } + if (FF_PACKAGES_IS_ENABLED(options, PKGSRC)) { + packageCounts->pkgsrc += getNumElements(baseDir, "/usr/pkg/pkgdb", DT_DIR); + } + if (FF_PACKAGES_IS_ENABLED(options, MOSS)) { + packageCounts->moss += getSQLite3Int(baseDir, "/.moss/db/state", "SELECT COUNT(*) FROM state_selections WHERE state_id = (SELECT MAX(id) FROM state)", "moss"); + } + if (FF_PACKAGES_IS_ENABLED(options, CARDS)) { + packageCounts->cards += getNumElements(baseDir, "/var/lib/pkg/DB", true); + } +} + +static void getPackageCountsRegular(FFstrbuf* baseDir, FFPackagesResult* packageCounts, FFPackagesOptions* options) { + getPackageCounts(baseDir, packageCounts, options); + + if (FF_PACKAGES_IS_ENABLED(options, PACMAN)) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, FASTFETCH_TARGET_DIR_ETC "/pacman-mirrors.conf"); + if (ffParsePropFile(baseDir->chars, "Branch =", &packageCounts->pacmanBranch) && packageCounts->pacmanBranch.length == 0) { + ffStrbufAppendS(&packageCounts->pacmanBranch, "stable"); + } + ffStrbufSubstrBefore(baseDir, baseDirLength); + } +} + +static void getPackageCountsBedrock(FFstrbuf* baseDir, FFPackagesResult* packageCounts, FFPackagesOptions* options) { + uint32_t baseDirLength = baseDir->length; + + ffStrbufAppendS(baseDir, "/bedrock/strata"); + + FF_AUTO_CLOSE_DIR DIR* dir = opendir(baseDir->chars); + if (dir == NULL) { + ffStrbufSubstrBefore(baseDir, baseDirLength); + return; + } + + ffStrbufAppendC(baseDir, '/'); + uint32_t baseDirLength2 = baseDir->length; + + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_type != DT_DIR) { + continue; + } + if (entry->d_name[0] == '.') { + continue; + } + + ffStrbufAppendS(baseDir, entry->d_name); + getPackageCounts(baseDir, packageCounts, options); + ffStrbufSubstrBefore(baseDir, baseDirLength2); + } + + ffStrbufSubstrBefore(baseDir, baseDirLength); +} + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + FF_STRBUF_AUTO_DESTROY baseDir = ffStrbufCreateA(512); + ffStrbufAppendS(&baseDir, FASTFETCH_TARGET_DIR_ROOT); + + if (ffStrbufIgnCaseEqualS(&ffDetectOS()->id, "bedrock")) { + getPackageCountsBedrock(&baseDir, result, options); + } else { + getPackageCountsRegular(&baseDir, result, options); + } + +// If SQL failed, we can still try with librpm. +// This is needed on openSUSE, which seems to use a proprietary database file +// This method doesn't work on bedrock, so we do it here. +#ifdef FF_HAVE_RPM + if (FF_PACKAGES_IS_ENABLED(options, RPM) && result->rpm == 0) { + result->rpm = getRpmFromLibrpm(); + } +#endif + + ffStrbufSet(&baseDir, &instance.state.platform.homeDir); + if (FF_PACKAGES_IS_ENABLED(options, NIX)) { + // Count packages from $HOME/.nix-profile + result->nixUser += ffPackagesGetNix(&baseDir, ".nix-profile"); + + // Check in $XDG_STATE_HOME/nix/profile + FF_STRBUF_AUTO_DESTROY stateHome = ffStrbufCreate(); + const char* stateHomeEnv = getenv("XDG_STATE_HOME"); + if (ffStrSet(stateHomeEnv)) { + ffStrbufSetS(&stateHome, stateHomeEnv); + ffStrbufEnsureEndsWithC(&stateHome, '/'); + } else { + ffStrbufSet(&stateHome, &instance.state.platform.homeDir); + ffStrbufAppendS(&stateHome, ".local/state/"); + } + result->nixUser += ffPackagesGetNix(&stateHome, "nix/profile"); + + // Check in /etc/profiles/per-user/$USER + FF_STRBUF_AUTO_DESTROY userPkgsDir = ffStrbufCreateStatic("/etc/profiles/per-user/"); + result->nixUser += ffPackagesGetNix(&userPkgsDir, instance.state.platform.userName.chars); + } + + if (FF_PACKAGES_IS_ENABLED(options, GUIX)) { + result->guixUser += getGuixPackages(&baseDir, ".guix-profile"); + result->guixHome += getGuixPackages(&baseDir, ".guix-home/profile"); + } + + if (FF_PACKAGES_IS_ENABLED(options, FLATPAK)) { + result->flatpakUser = getFlatpakPackages(&baseDir, "/.local/share"); + } + + if (FF_PACKAGES_IS_ENABLED(options, AM)) { + result->amUser = getAMUser(); + } + + if (FF_PACKAGES_IS_ENABLED(options, SOAR)) { + result->soar += getSQLite3Int(&baseDir, ".local/share/soar/db/soar.db", "SELECT COUNT(DISTINCT pkg_id || pkg_name) FROM packages WHERE is_installed = true", "soar"); + } + + if (FF_PACKAGES_IS_ENABLED(options, APPIMAGE)) { + result->appimage += getNumElementsBySuffix(&baseDir, "/AppImages", ".appimage"); + result->appimage += getNumElementsBySuffix(&baseDir, "/Applications", ".appimage"); + } +} diff --git a/src/detection/packages/packages_nbsd.c b/src/detection/packages/packages_nbsd.c new file mode 100644 index 0000000..2f79126 --- /dev/null +++ b/src/detection/packages/packages_nbsd.c @@ -0,0 +1,9 @@ +#include "packages.h" + +#include "common/io.h" + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + if (FF_PACKAGES_IS_ENABLED(options, PKGSRC)) { + result->pkgsrc = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/usr/pkg/pkgdb", true); + } +} diff --git a/src/detection/packages/packages_nix.c b/src/detection/packages/packages_nix.c new file mode 100644 index 0000000..d1cc264 --- /dev/null +++ b/src/detection/packages/packages_nix.c @@ -0,0 +1,149 @@ +#include "packages.h" +#include "common/io.h" +#include "common/processing.h" +#include "common/strutil.h" + +static bool isValidNixPkg(FFstrbuf* pkg) { + if (!ffPathExists(pkg->chars, FF_PATHTYPE_DIRECTORY)) { + return false; + } + + ffStrbufSubstrAfterLastC(pkg, '/'); + if ( + ffStrbufStartsWithS(pkg, "nixos-system-nixos-") || + ffStrbufEndsWithS(pkg, "-doc") || + ffStrbufEndsWithS(pkg, "-man") || + ffStrbufEndsWithS(pkg, "-info") || + ffStrbufEndsWithS(pkg, "-dev") || + ffStrbufEndsWithS(pkg, "-bin")) { + return false; + } + + enum { START, + DIGIT, + DOT, + MATCH } state = START; + + for (uint32_t i = 0; i < pkg->length; i++) { + char c = pkg->chars[i]; + switch (state) { + case START: + if (ffCharIsDigit(c)) { + state = DIGIT; + } + break; + case DIGIT: + if (ffCharIsDigit(c)) { + continue; + } + if (c == '.') { + state = DOT; + } else { + state = START; + } + break; + case DOT: + if (ffCharIsDigit(c)) { + state = MATCH; + } else { + state = START; + } + break; + case MATCH: + break; + } + } + + return state == MATCH; +} + +static bool checkNixCache(FFstrbuf* cacheDir, FFstrbuf* hash, uint32_t* count) { + if (!ffPathExists(cacheDir->chars, FF_PATHTYPE_FILE)) { + return false; + } + + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate(); + if (!ffReadFileBuffer(cacheDir->chars, &cacheContent)) { + return false; + } + + // Format: <hash>\n<count> + uint32_t split = ffStrbufFirstIndexC(&cacheContent, '\n'); + if (split == cacheContent.length) { + return false; + } + + ffStrbufSetNS(hash, split, cacheContent.chars); + *count = (uint32_t) atoi(cacheContent.chars + split + 1); + + return true; +} + +static bool writeNixCache(FFstrbuf* cacheDir, FFstrbuf* hash, uint32_t count) { + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreateCopy(hash); + ffStrbufAppendF(&cacheContent, "\n%u", count); + return ffWriteFileBuffer(cacheDir->chars, &cacheContent); +} + +static uint32_t getNixPackagesImpl(char* path) { + // Nix detection is kinda slow, so we only do it if the dir exists + if (!ffPathExists(path, FF_PATHTYPE_DIRECTORY)) { + return 0; + } + + FF_STRBUF_AUTO_DESTROY cacheDir = ffStrbufCreateCopy(&instance.state.platform.cacheDir); + ffStrbufEnsureEndsWithC(&cacheDir, '/'); + ffStrbufAppendS(&cacheDir, "fastfetch/packages/nix"); + ffStrbufAppendS(&cacheDir, path); + + // Check the hash first to determine if we need to recompute the count + FF_STRBUF_AUTO_DESTROY hash = ffStrbufCreateA(64); + FF_STRBUF_AUTO_DESTROY cacheHash = ffStrbufCreateA(64); + uint32_t count = 0; + + ffProcessAppendStdOut(&hash, (char* const[]) { "nix-store", "--query", "--hash", path, NULL }); + + if (checkNixCache(&cacheDir, &cacheHash, &count) && ffStrbufEqual(&hash, &cacheHash)) { + return count; + } + + // Cache is invalid, recompute the count + count = 0; + + // Implementation based on bash script from here: + // https://github.com/fastfetch-cli/fastfetch/issues/195#issuecomment-1191748222 + + FF_STRBUF_AUTO_DESTROY output = ffStrbufCreateA(1024); + + ffProcessAppendStdOut(&output, (char* const[]) { "nix-store", "--query", "--requisites", path, NULL }); + + uint32_t lineLength = 0; + for (uint32_t i = 0; i < output.length; i++) { + if (output.chars[i] != '\n') { + lineLength++; + continue; + } + + output.chars[i] = '\0'; + FFstrbuf line = { + .allocated = 0, + .length = lineLength, + .chars = output.chars + i - lineLength + }; + if (isValidNixPkg(&line)) { + count++; + } + lineLength = 0; + } + + writeNixCache(&cacheDir, &hash, count); + return count; +} + +uint32_t ffPackagesGetNix(FFstrbuf* baseDir, const char* dirname) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + uint32_t num_elements = getNixPackagesImpl(baseDir->chars); + ffStrbufSubstrBefore(baseDir, baseDirLength); + return num_elements; +} diff --git a/src/detection/packages/packages_nosupport.c b/src/detection/packages/packages_nosupport.c new file mode 100644 index 0000000..40300fa --- /dev/null +++ b/src/detection/packages/packages_nosupport.c @@ -0,0 +1,4 @@ +#include "packages.h" + +void ffDetectPackagesImpl(FF_A_UNUSED FFPackagesResult* result, FF_A_UNUSED FFPackagesOptions* options) { +} diff --git a/src/detection/packages/packages_obsd.c b/src/detection/packages/packages_obsd.c new file mode 100644 index 0000000..c7b098e --- /dev/null +++ b/src/detection/packages/packages_obsd.c @@ -0,0 +1,9 @@ +#include "packages.h" + +#include "common/io.h" + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + if (FF_PACKAGES_IS_ENABLED(options, PKG)) { + result->pkg = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/var/db/pkg", true); + } +} diff --git a/src/detection/packages/packages_sunos.c b/src/detection/packages/packages_sunos.c new file mode 100644 index 0000000..16b8cd7 --- /dev/null +++ b/src/detection/packages/packages_sunos.c @@ -0,0 +1,17 @@ +#include "packages.h" +#include <dirent.h> + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + if (FF_PACKAGES_IS_ENABLED(options, PKG)) { + yyjson_doc* doc = yyjson_read_file(FASTFETCH_TARGET_DIR_ROOT "/var/pkg/state/installed/catalog.attrs", YYJSON_READ_NOFLAG, NULL, NULL); + if (doc) { + yyjson_val* packageCount = yyjson_obj_get(yyjson_doc_get_root(doc), "package-count"); + if (packageCount) { + result->pkg = (uint32_t) yyjson_get_uint(packageCount); + } + } + } + if (FF_PACKAGES_IS_ENABLED(options, PKGSRC)) { + result->pkgsrc = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/usr/pkg/pkgdb", true); + } +} diff --git a/src/detection/packages/packages_windows.c b/src/detection/packages/packages_windows.c new file mode 100644 index 0000000..a3faee5 --- /dev/null +++ b/src/detection/packages/packages_windows.c @@ -0,0 +1,203 @@ +#include "packages.h" +#include "common/processing.h" +#include "common/strutil.h" +#include "common/path.h" +#include "common/windows/unicode.h" +#include "common/mallocHelper.h" +#include "common/io.h" + +#include <stdalign.h> +#include <windows.h> +#include "common/windows/nt.h" +#include <ntstatus.h> +#include <shlobj.h> + +static uint32_t getNumElements(const char* searchPath, DWORD type, const wchar_t* ignore) { + FF_AUTO_CLOSE_FD HANDLE dfd = CreateFileA(searchPath, FILE_LIST_DIRECTORY | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (dfd == INVALID_HANDLE_VALUE) { + return 0; + } + + bool flag = ignore == NULL; + uint32_t counter = 0; + alignas(8) uint8_t buffer[64 * 1024]; + BOOLEAN firstScan = TRUE; + + size_t ignoreLen = ignore ? wcslen(ignore) : 0; + + while (true) { + IO_STATUS_BLOCK ioStatus = {}; + NTSTATUS status = NtQueryDirectoryFile( + dfd, + NULL, + NULL, + NULL, + &ioStatus, + buffer, + ARRAY_SIZE(buffer), + FileDirectoryInformation, + FALSE, + NULL, + firstScan); + firstScan = FALSE; + + if (!NT_SUCCESS(status) && status != STATUS_BUFFER_OVERFLOW) { + break; + } + + for (FILE_DIRECTORY_INFORMATION* entry = (FILE_DIRECTORY_INFORMATION*) buffer; + ; + entry = (FILE_DIRECTORY_INFORMATION*) ((uint8_t*) entry + entry->NextEntryOffset)) { + if (!(entry->FileAttributes & type)) { + continue; + } + + if (!flag && + ignoreLen == entry->FileNameLength / sizeof(*entry->FileName) && + _wcsnicmp(entry->FileName, ignore, ignoreLen) == 0) { + flag = true; + continue; + } + + counter++; + + if (entry->NextEntryOffset == 0) { + break; + } + } + + if (status == STATUS_SUCCESS) { + break; // No next page + } + } + + if (type == FILE_ATTRIBUTE_DIRECTORY && counter >= 2) { + counter -= 2; // accounting for . and .. + } + + return counter; +} + +static inline void wrapYyjsonFree(yyjson_doc** doc) { + assert(doc); + if (*doc) { + yyjson_doc_free(*doc); + } +} + +static void detectScoop(FFPackagesResult* result) { + FF_STRBUF_AUTO_DESTROY scoopPath = ffStrbufCreateA(MAX_PATH + 3); + ffStrbufAppend(&scoopPath, &instance.state.platform.homeDir); + ffStrbufAppendS(&scoopPath, ".config/scoop/config.json"); + + yyjson_val* root = NULL; + + yyjson_doc* FF_A_CLEANUP(wrapYyjsonFree) doc = yyjson_read_file(scoopPath.chars, 0, NULL, NULL); + if (doc) { + root = yyjson_doc_get_root(doc); + if (!yyjson_is_obj(root)) { + root = NULL; + } + } + + { + ffStrbufClear(&scoopPath); + if (root) { + ffStrbufSetJsonVal(&scoopPath, yyjson_obj_get(root, "root_path")); + } + if (scoopPath.length == 0) { + ffStrbufSet(&scoopPath, &instance.state.platform.homeDir); + ffStrbufAppendS(&scoopPath, "/scoop"); + } + ffStrbufAppendS(&scoopPath, "/apps/"); + result->scoopUser = getNumElements(scoopPath.chars, FILE_ATTRIBUTE_DIRECTORY, L"scoop"); + } + + { + ffStrbufClear(&scoopPath); + if (root) { + ffStrbufSetJsonVal(&scoopPath, yyjson_obj_get(root, "global_path")); + } + if (scoopPath.length == 0) { + PWSTR pPath = NULL; + if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_ProgramData, KF_FLAG_DEFAULT, NULL, &pPath))) { + ffStrbufSetWS(&scoopPath, pPath); + CoTaskMemFree(pPath); + } + ffStrbufAppendS(&scoopPath, "/scoop"); + } + ffStrbufAppendS(&scoopPath, "/apps/"); + result->scoopGlobal = getNumElements(scoopPath.chars, FILE_ATTRIBUTE_DIRECTORY, L"scoop"); + } +} + +static void detectChoco(FF_A_UNUSED FFPackagesResult* result) { + const char* chocoInstall = getenv("ChocolateyInstall"); + if (!chocoInstall || chocoInstall[0] == '\0') { + return; + } + + char chocoPath[MAX_PATH + 3]; + char* pend = ffStrCopy(chocoPath, chocoInstall, ARRAY_SIZE(chocoPath)); + ffStrCopy(pend, "/lib/", ARRAY_SIZE(chocoPath) - (size_t) (pend - chocoPath)); + result->choco = getNumElements(chocoPath, FILE_ATTRIBUTE_DIRECTORY, L"choco"); +} + +static void detectPacman(FFPackagesResult* result) { + const char* msystemPrefix = getenv("MSYSTEM_PREFIX"); + if (!msystemPrefix) { + return; + } + + // MSYS2 + char pacmanPath[MAX_PATH + 3]; + char* pend = ffStrCopy(pacmanPath, msystemPrefix, ARRAY_SIZE(pacmanPath)); + ffStrCopy(pend, "/../var/lib/pacman/local/", ARRAY_SIZE(pacmanPath) - (size_t) (pend - pacmanPath)); + result->pacman = getNumElements(pacmanPath, FILE_ATTRIBUTE_DIRECTORY, NULL); +} + +static void detectWinget(FFPackagesResult* result) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + if (ffProcessAppendStdOut(&buffer, (char*[]) { + "winget.exe", + "list", + "--disable-interactivity", + NULL, + })) { + return; + } + + uint32_t index = ffStrbufFirstIndexS(&buffer, "--\r\n"); // Ignore garbage and table headers + if (index == buffer.length) { + return; + } + + uint32_t count = 0; + for ( + index += strlen("--\r\n"); + (index = ffStrbufNextIndexC(&buffer, index, '\n')) < buffer.length; + ++index) { + ++count; + } + + if (buffer.chars[buffer.length - 1] != '\n') { // count last line + ++count; + } + + result->winget = count; +} + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + if (FF_PACKAGES_IS_ENABLED(options, SCOOP)) { + detectScoop(result); + } + if (FF_PACKAGES_IS_ENABLED(options, CHOCO)) { + detectChoco(result); + } + if (FF_PACKAGES_IS_ENABLED(options, PACMAN)) { + detectPacman(result); + } + if (FF_PACKAGES_IS_ENABLED(options, WINGET)) { + detectWinget(result); + } +} diff --git a/src/detection/physicaldisk/physicaldisk.h b/src/detection/physicaldisk/physicaldisk.h new file mode 100644 index 0000000..3481063 --- /dev/null +++ b/src/detection/physicaldisk/physicaldisk.h @@ -0,0 +1,17 @@ +#include "fastfetch.h" +#include "modules/physicaldisk/option.h" + +#define FF_PHYSICALDISK_TEMP_UNSET (-DBL_MAX) + +typedef struct FFPhysicalDiskResult { + FFstrbuf name; + FFstrbuf interconnect; + FFstrbuf serial; + FFstrbuf devPath; + FFstrbuf revision; + FFPhysicalDiskType type; + uint64_t size; + double temperature; +} FFPhysicalDiskResult; + +const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options); diff --git a/src/detection/physicaldisk/physicaldisk_apple.c b/src/detection/physicaldisk/physicaldisk_apple.c new file mode 100644 index 0000000..f4c5cd6 --- /dev/null +++ b/src/detection/physicaldisk/physicaldisk_apple.c @@ -0,0 +1,191 @@ +#include "physicaldisk.h" +#include "common/apple/cf_helpers.h" + +#include <IOKit/IOKitLib.h> +#include <IOKit/IOBSD.h> +#include <IOKit/storage/IOMedia.h> +#include <IOKit/storage/IOBlockStorageDriver.h> +#include <IOKit/storage/IOStorageDeviceCharacteristics.h> +#include <IOKit/storage/IOStorageProtocolCharacteristics.h> +#ifdef MAC_OS_X_VERSION_10_15 + #include <IOKit/storage/nvme/NVMeSMARTLibExternal.h> +#endif + +#ifdef MAC_OS_X_VERSION_10_15 +static inline void wrapIoDestroyPlugInInterface(IOCFPlugInInterface*** pluginInf) { + assert(pluginInf); + if (*pluginInf) { + IODestroyPlugInInterface(*pluginInf); + } +} +#endif + +static const char* detectSsdTemp(io_service_t entryPhysical, double* temp) { +#ifdef MAC_OS_X_VERSION_10_15 + FF_A_CLEANUP(wrapIoDestroyPlugInInterface) IOCFPlugInInterface** pluginInf = NULL; + int32_t score; + if (IOCreatePlugInInterfaceForService(entryPhysical, kIONVMeSMARTUserClientTypeID, kIOCFPlugInInterfaceID, &pluginInf, &score) != kIOReturnSuccess) { + return "IOCreatePlugInInterfaceForService() failed"; + } + + IONVMeSMARTInterface** smartInf = NULL; + if ((*pluginInf)->QueryInterface(pluginInf, CFUUIDGetUUIDBytes(kIONVMeSMARTInterfaceID), (LPVOID) &smartInf) != kIOReturnSuccess) { + return "QueryInterface() failed"; + } + + NVMeSMARTData smartData; + const char* error = NULL; + if ((*smartInf)->SMARTReadData(smartInf, &smartData) == kIOReturnSuccess) { + *temp = smartData.TEMPERATURE - 273; + } else { + error = "SMARTReadData() failed"; + } + + (*pluginInf)->Release(smartInf); + return error; +#else + return "No support for old MacOS version"; +#endif +} + +const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { + FF_IOOBJECT_AUTO_RELEASE io_iterator_t iterator = 0; + if (IOServiceGetMatchingServices(MACH_PORT_NULL, IOServiceMatching(kIOBlockStorageDriverClass), &iterator) != KERN_SUCCESS) { + return "IOServiceGetMatchingServices() failed"; + } + + io_registry_entry_t registryEntry; + while ((registryEntry = IOIteratorNext(iterator)) != IO_OBJECT_NULL) { + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryDriver = registryEntry; + + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryMedia = IO_OBJECT_NULL; + IORegistryEntryGetChildEntry(entryDriver, kIOServicePlane, &entryMedia); + + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryPhysical = 0; + if (IORegistryEntryGetParentEntry(entryDriver, kIOServicePlane, &entryPhysical) != KERN_SUCCESS) { + continue; + } + + FF_STRBUF_AUTO_DESTROY deviceName = ffStrbufCreate(); + + if (entryMedia != IO_OBJECT_NULL) { + io_name_t name; + if (IORegistryEntryGetName(entryMedia, name) == KERN_SUCCESS) { + ffStrbufSetS(&deviceName, name); + } + } + + if (deviceName.length == 0) { + FF_CFTYPE_AUTO_RELEASE CFDictionaryRef deviceCharacteristics = IORegistryEntryCreateCFProperty(entryPhysical, CFSTR(kIOPropertyDeviceCharacteristicsKey), NULL, kNilOptions); + if (deviceCharacteristics) { + if (ffCfDictGetString(deviceCharacteristics, CFSTR(kIOPropertyVendorNameKey), &deviceName) == NULL && deviceName.length > 0) { + ffStrbufAppendC(&deviceName, ' '); + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + if (ffCfDictGetString(deviceCharacteristics, CFSTR(kIOPropertyProductNameKey), &name) == NULL) { + ffStrbufAppend(&deviceName, &name); + } + } + } + + if (options->namePrefix.length && ffStrbufStartsWith(&deviceName, &options->namePrefix) != 0) { + continue; + } + + FFPhysicalDiskType type = FF_PHYSICALDISK_TYPE_NONE; + + FF_STRBUF_AUTO_DESTROY interconnect = ffStrbufCreate(); + FF_CFTYPE_AUTO_RELEASE CFDictionaryRef protocolCharacteristics = IORegistryEntryCreateCFProperty(entryPhysical, CFSTR(kIOPropertyProtocolCharacteristicsKey), NULL, kNilOptions); + if (protocolCharacteristics) { + if (ffCfDictGetString(protocolCharacteristics, CFSTR(kIOPropertyPhysicalInterconnectTypeKey), &interconnect) == NULL) { + if (ffStrbufEqualS(&interconnect, kIOPropertyPhysicalInterconnectTypeVirtual)) { + if (options->hideType & FF_PHYSICALDISK_TYPE_VIRTUAL) { + continue; + } + + type |= FF_PHYSICALDISK_TYPE_VIRTUAL; + FF_STRBUF_AUTO_DESTROY location = ffStrbufCreate(); + if (ffCfDictGetString(protocolCharacteristics, CFSTR(kIOPropertyPhysicalInterconnectLocationKey), &location) == NULL) { + ffStrbufAppendS(&interconnect, " - "); + ffStrbufAppend(&interconnect, &location); + } + } + } + } + + uint64_t size = 0; + if (entryMedia != IO_OBJECT_NULL) { + FF_CFTYPE_AUTO_RELEASE CFNumberRef mediaSize = IORegistryEntryCreateCFProperty(entryMedia, CFSTR(kIOMediaSizeKey), NULL, kNilOptions); + if (mediaSize) { + ffCfNumGetInt64(mediaSize, (int64_t*) &size); + } + } + + if (size == 0) { + if (options->hideType & FF_PHYSICALDISK_TYPE_UNUSED) { + continue; + } + + type |= FF_PHYSICALDISK_TYPE_UNUSED; + } + + FFPhysicalDiskResult* device = FF_LIST_ADD(FFPhysicalDiskResult, *result); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->revision); + ffStrbufInitMove(&device->name, &deviceName); + ffStrbufInit(&device->devPath); + ffStrbufInitMove(&device->interconnect, &interconnect); + device->type = type; + device->size = size; + device->temperature = FF_PHYSICALDISK_TEMP_UNSET; + + if (entryMedia != IO_OBJECT_NULL) { + FF_CFTYPE_AUTO_RELEASE CFBooleanRef removable = IORegistryEntryCreateCFProperty(entryMedia, CFSTR(kIOMediaRemovableKey), NULL, kNilOptions); + if (removable) { + device->type |= CFBooleanGetValue(removable) ? FF_PHYSICALDISK_TYPE_REMOVABLE : FF_PHYSICALDISK_TYPE_FIXED; + } + + FF_CFTYPE_AUTO_RELEASE CFBooleanRef writable = IORegistryEntryCreateCFProperty(entryMedia, CFSTR(kIOMediaWritableKey), NULL, kNilOptions); + if (writable) { + device->type |= CFBooleanGetValue(writable) ? FF_PHYSICALDISK_TYPE_READWRITE : FF_PHYSICALDISK_TYPE_READONLY; + } + + FF_CFTYPE_AUTO_RELEASE CFStringRef bsdName = IORegistryEntryCreateCFProperty(entryMedia, CFSTR(kIOBSDNameKey), NULL, kNilOptions); + if (bsdName) { + ffCfStrGetString(bsdName, &device->devPath); + ffStrbufPrependS(&device->devPath, "/dev/"); + } + } + + FF_CFTYPE_AUTO_RELEASE CFDictionaryRef deviceCharacteristics = IORegistryEntryCreateCFProperty(entryPhysical, CFSTR(kIOPropertyDeviceCharacteristicsKey), NULL, kNilOptions); + if (deviceCharacteristics) { + ffCfDictGetString(deviceCharacteristics, CFSTR(kIOPropertyProductSerialNumberKey), &device->serial); + ffStrbufTrimSpace(&device->serial); + ffCfDictGetString(deviceCharacteristics, CFSTR(kIOPropertyProductRevisionLevelKey), &device->revision); + ffStrbufTrimRightSpace(&device->revision); + + if (!(device->type & FF_PHYSICALDISK_TYPE_VIRTUAL)) { + CFStringRef mediumType = (CFStringRef) CFDictionaryGetValue(deviceCharacteristics, CFSTR(kIOPropertyMediumTypeKey)); + if (mediumType) { + if (CFStringCompare(mediumType, CFSTR(kIOPropertyMediumTypeSolidStateKey), 0) == 0) { + device->type |= FF_PHYSICALDISK_TYPE_SSD; + } else if (CFStringCompare(mediumType, CFSTR(kIOPropertyMediumTypeRotationalKey), 0) == 0) { + device->type |= FF_PHYSICALDISK_TYPE_HDD; + } + } + } + } + +#ifdef MAC_OS_X_VERSION_10_15 + if (!(device->type & FF_PHYSICALDISK_TYPE_VIRTUAL) && options->temp) { + FF_CFTYPE_AUTO_RELEASE CFBooleanRef nvmeSMARTCapable = IORegistryEntryCreateCFProperty(entryPhysical, CFSTR(kIOPropertyNVMeSMARTCapableKey), NULL, kNilOptions); + if (nvmeSMARTCapable && CFBooleanGetValue(nvmeSMARTCapable)) { + detectSsdTemp(entryPhysical, &device->temperature); + } + } +#endif + } + + return NULL; +} diff --git a/src/detection/physicaldisk/physicaldisk_bsd.c b/src/detection/physicaldisk/physicaldisk_bsd.c new file mode 100644 index 0000000..5a87768 --- /dev/null +++ b/src/detection/physicaldisk/physicaldisk_bsd.c @@ -0,0 +1,127 @@ +#include "physicaldisk.h" + +#if __has_include(<libgeom.h>) + + #include "common/strutil.h" + + #include <devstat.h> + #include <memory.h> + #include <fcntl.h> + #include <sys/ioctl.h> + #include <sys/disk.h> + #include <libgeom.h> + +const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { + struct gmesh geomTree; + if (geom_gettree(&geomTree) < 0) { + return "geom_gettree() failed"; + } + + if (geom_stats_open() < 0) { + return "geom_stats_open() failed"; + } + + void* snap = geom_stats_snapshot_get(); + struct devstat* snapIter; + while ((snapIter = geom_stats_snapshot_next(snap)) != NULL) { + if (snapIter->device_type & DEVSTAT_TYPE_PASS) { + continue; + } + struct gident* geomId = geom_lookupid(&geomTree, snapIter->id); + if (geomId == NULL) { + continue; + } + if (geomId->lg_what != ISPROVIDER) { + continue; + } + struct gprovider* provider = (struct gprovider*) geomId->lg_ptr; + if (provider->lg_geom->lg_rank != 1) { + continue; + } + + FFPhysicalDiskType type = FF_PHYSICALDISK_TYPE_NONE; + if (!ffStrEquals(provider->lg_geom->lg_class->lg_name, "DISK")) { + if (options->hideType & FF_PHYSICALDISK_TYPE_VIRTUAL) { + continue; + } + + type |= FF_PHYSICALDISK_TYPE_VIRTUAL; + } + uint64_t size = (uint64_t) provider->lg_mediasize; + if (size == 0) { + if (options->hideType & FF_PHYSICALDISK_TYPE_UNUSED) { + continue; + } + + type |= FF_PHYSICALDISK_TYPE_UNUSED; + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateS(provider->lg_name); + FF_STRBUF_AUTO_DESTROY identifier = ffStrbufCreate(); + for (struct gconfig* ptr = provider->lg_config.lh_first; ptr; ptr = ptr->lg_config.le_next) { + if (ffStrEquals(ptr->lg_name, "descr")) { + ffStrbufSetS(&name, ptr->lg_val); + } else if (ffStrEquals(ptr->lg_name, "rotationrate") && !ffStrEquals(ptr->lg_val, "unknown")) { + type |= ffStrEquals(ptr->lg_val, "0") ? FF_PHYSICALDISK_TYPE_SSD : FF_PHYSICALDISK_TYPE_HDD; + } else if (ffStrEquals(ptr->lg_name, "ident")) { + ffStrbufSetS(&identifier, ptr->lg_val); + } else if (ffStrEquals(ptr->lg_name, "access")) { + if (ffStrEquals(ptr->lg_val, "read-only")) { + type |= FF_PHYSICALDISK_TYPE_READONLY; + } else if (ffStrEquals(ptr->lg_val, "read-write")) { + type |= FF_PHYSICALDISK_TYPE_READWRITE; + } + } + } + + if (options->namePrefix.length && !ffStrbufStartsWith(&name, &options->namePrefix)) { + continue; + } + + FFPhysicalDiskResult* device = FF_LIST_ADD(FFPhysicalDiskResult, *result); + ffStrbufInitF(&device->devPath, "/dev/%s", provider->lg_name); + ffStrbufInitMove(&device->serial, &identifier); + ffStrbufTrimSpace(&device->serial); + ffStrbufInit(&device->revision); + ffStrbufInit(&device->interconnect); + ffStrbufInitMove(&device->name, &name); + device->size = size; + device->temperature = FF_PHYSICALDISK_TEMP_UNSET; + + switch (snapIter->device_type & DEVSTAT_TYPE_IF_MASK) { + case DEVSTAT_TYPE_IF_SCSI: + ffStrbufSetStatic(&device->interconnect, "SCSI"); + break; + case DEVSTAT_TYPE_IF_IDE: + ffStrbufSetStatic(&device->interconnect, "IDE"); + break; + case DEVSTAT_TYPE_IF_OTHER: + ffStrbufSetStatic(&device->interconnect, "OTHER"); + break; + + // https://github.com/freebsd/freebsd-src/commit/d282baddb0b029ca8466d23ac51e95c918442535 + case 0x040 /*DEVSTAT_TYPE_IF_NVME*/: + ffStrbufSetStatic(&device->interconnect, "NVMe"); + break; + } + + if (!(device->type & FF_PHYSICALDISK_TYPE_READONLY) && !(device->type & FF_PHYSICALDISK_TYPE_READWRITE)) { + int acr = 1, acw = 1; // Number of partitions mounted for reading or writing + if (sscanf(provider->lg_mode, "r%dw%de%*d", &acr, &acw) == 2 && acr) { + type |= acw ? FF_PHYSICALDISK_TYPE_READWRITE : FF_PHYSICALDISK_TYPE_READONLY; + } + } + + device->type = type; + } + + geom_stats_snapshot_free(snap); + geom_stats_close(); + + return NULL; +} +#else +const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { + return "Fastfetch was compiled without libgeom support"; +} +#endif diff --git a/src/detection/physicaldisk/physicaldisk_haiku.c b/src/detection/physicaldisk/physicaldisk_haiku.c new file mode 100644 index 0000000..e44ddce --- /dev/null +++ b/src/detection/physicaldisk/physicaldisk_haiku.c @@ -0,0 +1,104 @@ +#include "physicaldisk.h" + +#include "common/io.h" +#include "common/strutil.h" + +#include <OS.h> +#include <StorageDefs.h> +#include <Drivers.h> +#include <sys/ioctl.h> + +static const char* searchRawDeviceFile(FFstrbuf* path, const char* diskType, FFlist* result, FFPhysicalDiskOptions* options) { + FF_AUTO_CLOSE_DIR DIR* dir = opendir(path->chars); + if (!dir) { + return "detectDiskType: opendir() failed"; + } + uint32_t baseLen = path->length; + + struct dirent* entry; + while ((entry = readdir(dir))) { + if (entry->d_name[0] == '.') { + continue; + } + ffStrbufAppendC(path, '/'); + ffStrbufAppendS(path, entry->d_name); + + struct stat st; + if (stat(path->chars, &st) != 0) { + ffStrbufSubstrBefore(path, baseLen); + continue; + } + + if (S_ISDIR(st.st_mode)) { + searchRawDeviceFile(path, diskType, result, options); + } else if (ffStrEquals(entry->d_name, "raw")) { + FF_AUTO_CLOSE_FD int rawfd = open(path->chars, O_RDONLY | O_CLOEXEC); + if (rawfd < 0) { + continue; + } + + device_geometry geometry; + if (ioctl(rawfd, B_GET_GEOMETRY, &geometry, sizeof(geometry)) < 0) { + continue; + } + + char name[B_OS_NAME_LENGTH]; + if (ioctl(rawfd, B_GET_DEVICE_NAME, name, sizeof(name)) != 0) { + // ioctl reports `not a tty` for NVME drives for some reason + snprintf(name, sizeof(name), "Unknown %s drive", diskType); + } + + if (options->namePrefix.length && strncmp(name, options->namePrefix.chars, options->namePrefix.length) != 0) { + continue; + } + + FFPhysicalDiskType type = FF_PHYSICALDISK_TYPE_NONE; + uint64_t size = (uint64_t) geometry.cylinder_count * geometry.head_count * geometry.sectors_per_track * geometry.bytes_per_sector; + if (size == 0) { + if (options->hideType & FF_PHYSICALDISK_TYPE_UNUSED) { + continue; + } + + type |= FF_PHYSICALDISK_TYPE_UNUSED; + } + + FFPhysicalDiskResult* device = FF_LIST_ADD(FFPhysicalDiskResult, *result); + ffStrbufInitS(&device->name, name); + ffStrbufInitCopy(&device->devPath, path); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->revision); + ffStrbufInitS(&device->interconnect, diskType); + device->temperature = FF_PHYSICALDISK_TEMP_UNSET; + device->type = type | + (geometry.read_only ? FF_PHYSICALDISK_TYPE_READONLY : FF_PHYSICALDISK_TYPE_READWRITE) | + (geometry.removable ? FF_PHYSICALDISK_TYPE_REMOVABLE : FF_PHYSICALDISK_TYPE_FIXED); + device->size = size; + } + + ffStrbufSubstrBefore(path, baseLen); + } + return NULL; +} + +const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { + FF_AUTO_CLOSE_DIR DIR* dir = opendir("/dev/disk"); + if (!dir) { + return "opendir(/dev/disk) failed"; + } + + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateA(64); + ffStrbufAppendS(&path, "/dev/disk/"); + uint32_t baseLen = path.length; + + struct dirent* entry; + while ((entry = readdir(dir))) { + if (entry->d_name[0] == '.' || ffStrEquals(entry->d_name, "virtual")) { + continue; + } + ffStrbufAppendS(&path, entry->d_name); + searchRawDeviceFile(&path, entry->d_name, result, options); + ffStrbufSubstrBefore(&path, baseLen); + } + + return NULL; +} diff --git a/src/detection/physicaldisk/physicaldisk_linux.c b/src/detection/physicaldisk/physicaldisk_linux.c new file mode 100644 index 0000000..f8126b9 --- /dev/null +++ b/src/detection/physicaldisk/physicaldisk_linux.c @@ -0,0 +1,202 @@ +#include "physicaldisk.h" +#include "common/io.h" +#include "common/properties.h" +#include "common/strutil.h" + +#include <ctype.h> +#include <limits.h> +#include <unistd.h> +#include <fcntl.h> + +static double detectNvmeTemp(int devfd) { + char pathHwmon[] = "hwmon$/temp1_input"; + + for (char c = '0'; c <= '9'; c++) // hopefully there's only one digit + { + pathHwmon[strlen("hwmon")] = c; + char buffer[64]; + ssize_t size = ffReadFileDataRelative(devfd, pathHwmon, ARRAY_SIZE(buffer), buffer); + if (size > 0) { + buffer[size] = '\0'; + double temp = strtod(buffer, NULL); + return temp > 0 && temp < 10000000 /*VMware*/ ? temp / 1000 : FF_PHYSICALDISK_TEMP_UNSET; + } + } + + return FF_PHYSICALDISK_TEMP_UNSET; +} + +static void parsePhysicalDisk(int dfd, const char* devName, FFPhysicalDiskOptions* options, FFlist* result) { + uint64_t size = 0; + + { + char blkSize[32]; + ssize_t fileSize = ffReadFileDataRelative(dfd, "size", ARRAY_SIZE(blkSize) - 1, blkSize); + if (fileSize > 0) { + blkSize[fileSize] = 0; + size = (uint64_t) strtoul(blkSize, NULL, 10) * 512; + } + } + + FFPhysicalDiskType type = FF_PHYSICALDISK_TYPE_NONE; + if (size == 0) { + if (options->hideType & FF_PHYSICALDISK_TYPE_UNUSED) { + return; + } + + type |= FF_PHYSICALDISK_TYPE_UNUSED; + } + + int devfd = openat(dfd, "device", O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY); + + if (devfd < 0) { + if (options->hideType & FF_PHYSICALDISK_TYPE_VIRTUAL) { + return; + } + + type |= FF_PHYSICALDISK_TYPE_VIRTUAL; + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + + if (devfd > 0) { + if (ffAppendFileBufferRelative(devfd, "vendor", &name)) { + ffStrbufTrimRightSpace(&name); + if (name.length > 0) { + ffStrbufAppendC(&name, ' '); + } + } + + ffAppendFileBufferRelative(devfd, "model", &name); + ffStrbufTrimRightSpace(&name); + + if (name.length == 0) { + ffStrbufSetS(&name, devName); + } + + if (ffStrStartsWith(devName, "nvme")) { + int devid, nsid; + if (sscanf(devName, "nvme%dn%d", &devid, &nsid) == 2) { + bool multiNs = nsid > 1; + if (!multiNs) { + char pathSysBlock[32]; + snprintf(pathSysBlock, ARRAY_SIZE(pathSysBlock), "/dev/nvme%dn2", devid); + multiNs = access(pathSysBlock, F_OK) == 0; + } + if (multiNs) { + // In Asahi Linux, there are multiple namespaces for the same NVMe drive. + ffStrbufAppendF(&name, " - %d", nsid); + } + } + } + + if (options->namePrefix.length && !ffStrbufStartsWith(&name, &options->namePrefix)) { + return; + } + } else { + ffStrbufSetS(&name, devName); + } + + FFPhysicalDiskResult* device = FF_LIST_ADD(FFPhysicalDiskResult, *result); + ffStrbufInitMove(&device->name, &name); + ffStrbufInitF(&device->devPath, "/dev/%s", devName); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->revision); + ffStrbufInit(&device->interconnect); + device->type = type; + device->size = size; + device->temperature = FF_PHYSICALDISK_TEMP_UNSET; + + bool isVirtio = false; + if (devfd > 0) { + if (ffStrStartsWith(devName, "nvme")) { + ffStrbufSetStatic(&device->interconnect, "NVMe"); + } else if (ffStrStartsWith(devName, "mmcblk")) { + ffStrbufSetStatic(&device->interconnect, "MMC"); + } else { + char pathSysDeviceLink[64]; + snprintf(pathSysDeviceLink, ARRAY_SIZE(pathSysDeviceLink), "/sys/block/%s/device", devName); + char pathSysDeviceReal[PATH_MAX]; + if (realpath(pathSysDeviceLink, pathSysDeviceReal)) { + if (strstr(pathSysDeviceReal, "/usb") != NULL) { + ffStrbufSetStatic(&device->interconnect, "USB"); + } else if (strstr(pathSysDeviceReal, "/ata") != NULL) { + ffStrbufSetStatic(&device->interconnect, "ATA"); + } else if (strstr(pathSysDeviceReal, "/scsi") != NULL) { + ffStrbufSetStatic(&device->interconnect, "SCSI"); + } else if (strstr(pathSysDeviceReal, "/nvme") != NULL) { + ffStrbufSetStatic(&device->interconnect, "NVMe"); + } else if (strstr(pathSysDeviceReal, "/virtio") != NULL) { + ffStrbufSetStatic(&device->interconnect, "VirtIO"); + isVirtio = true; // VirtIO devices are virtual, but we still want to report it + } else { + if (ffAppendFileBufferRelative(devfd, "transport", &device->interconnect)) { + ffStrbufTrimRightSpace(&device->interconnect); + } + } + } + } + } else { + ffStrbufSetStatic(&device->interconnect, "Virtual"); + } + + if (devfd > 0 && !isVirtio) { + char isRotationalChar = '1'; + if (ffReadFileDataRelative(dfd, "queue/rotational", 1, &isRotationalChar) > 0) { + device->type |= isRotationalChar == '1' ? FF_PHYSICALDISK_TYPE_HDD : FF_PHYSICALDISK_TYPE_SSD; + } + + if (ffReadFileBufferRelative(devfd, "serial", &device->serial)) { + ffStrbufTrimSpace(&device->serial); + } + + if (ffReadFileBufferRelative(devfd, "firmware_rev", &device->revision) || + ffReadFileBufferRelative(devfd, "rev", &device->revision)) { + ffStrbufTrimRightSpace(&device->revision); + } + + if (options->temp) { + device->temperature = detectNvmeTemp(devfd); + } + } + + { + char removableChar = '0'; + if (ffReadFileDataRelative(dfd, "removable", 1, &removableChar) > 0) { + device->type |= removableChar == '1' ? FF_PHYSICALDISK_TYPE_REMOVABLE : FF_PHYSICALDISK_TYPE_FIXED; + } + } + + { + char roChar = '0'; + if (ffReadFileDataRelative(dfd, "ro", 1, &roChar) > 0) { + device->type |= roChar == '1' ? FF_PHYSICALDISK_TYPE_READONLY : FF_PHYSICALDISK_TYPE_READWRITE; + } + } +} + +const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { + FF_AUTO_CLOSE_DIR DIR* sysBlockDirp = opendir("/sys/block/"); + if (sysBlockDirp == NULL) { + return "opendir(\"/sys/block/\") == NULL"; + } + + struct dirent* sysBlockEntry; + while ((sysBlockEntry = readdir(sysBlockDirp)) != NULL) { + const char* const devName = sysBlockEntry->d_name; + + if (devName[0] == '.') { + continue; + } + + char pathSysBlock[sizeof("/sys/block/") + sizeof(sysBlockEntry->d_name)]; + snprintf(pathSysBlock, ARRAY_SIZE(pathSysBlock), "/sys/block/%s", devName); + + int dfd = openat(dirfd(sysBlockDirp), devName, O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY); + if (dfd > 0) { + parsePhysicalDisk(dfd, devName, options, result); + } + } + + return NULL; +} diff --git a/src/detection/physicaldisk/physicaldisk_nbsd.c b/src/detection/physicaldisk/physicaldisk_nbsd.c new file mode 100644 index 0000000..0733f3b --- /dev/null +++ b/src/detection/physicaldisk/physicaldisk_nbsd.c @@ -0,0 +1,324 @@ +#define DKTYPENAMES +#include "physicaldisk.h" +#include "common/io.h" +#include "common/sysctl.h" +#include "common/debug.h" + +#include <util.h> +#include <prop/proplib.h> +#include <sys/ioctl.h> +#include <sys/disklabel.h> +#include <sys/dkio.h> +#include <sys/scsiio.h> +#include <dev/scsipi/scsipi_all.h> +#include <dev/scsipi/scsi_spc.h> +#include <fcntl.h> +#include <errno.h> + +static inline const char* retstsToStr(uint8_t retsts) { + switch (retsts) { + case SCCMD_OK: + return "OK"; + case SCCMD_TIMEOUT: + return "TIMEOUT"; + case SCCMD_BUSY: + return "BUSY"; + case SCCMD_SENSE: + return "SENSE"; + case SCCMD_UNKNOWN: + return "UNKNOWN"; + default: + return "?"; + } +} + +#ifndef NDEBUG +static inline const char* senseKeyToStr(uint8_t key) { + switch (key) { + case 0x0: + return "No Sense"; + case 0x1: + return "Recovered Error"; + case 0x2: + return "Not Ready"; + case 0x3: + return "Medium Error"; + case 0x4: + return "Hardware Error"; + case 0x5: + return "Illegal Request"; + case 0x6: + return "Unit Attention"; + case 0x7: + return "Data Protect"; + case 0x8: + return "Blank Check"; + case 0x9: + return "Vendor Specific"; + case 0xA: + return "Copy Aborted"; + case 0xB: + return "Aborted Command"; + case 0xC: + return "Equal"; + case 0xD: + return "Volume Overflow"; + case 0xE: + return "Miscompare"; + case 0xF: + return "Completed"; + default: + return "Unknown"; + } +} + +static void logScsiSense(const char* diskName, const char* operation, const scsireq_t* req) { + uint8_t responseCode = req->sense[0] & 0x7F; + uint8_t senseKey; + uint8_t asc; + uint8_t ascq; + + if (responseCode == 0x72 || responseCode == 0x73) { + senseKey = req->sense[1] & 0x0F; + asc = req->sense[2]; + ascq = req->sense[3]; + } else { + senseKey = req->sense[2] & 0x0F; + asc = req->sense[12]; + ascq = req->sense[13]; + } + + FF_STRBUF_AUTO_DESTROY rawSense = ffStrbufCreate(); + for (size_t i = 0; i < req->senselen_used; ++i) { + if (i) { + ffStrbufAppendC(&rawSense, ' '); + } + ffStrbufAppendF(&rawSense, "%02X", req->sense[i]); + } + + FF_DEBUG( + "%s for %s reported SENSE: response=0x%02X, key=0x%X (%s), ASC=0x%02X, ASCQ=0x%02X, raw=[%s]", + operation, + diskName, + responseCode, + senseKey, + senseKeyToStr(senseKey), + asc, + ascq, + rawSense.length ? rawSense.chars : "empty"); +} +#else + #define logScsiSense(...) ((void) 0) +#endif + +const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { + FF_STRBUF_AUTO_DESTROY diskNames = ffStrbufCreate(); + FF_DEBUG("Querying disk names via sysctl hw.disknames"); + const char* error = ffSysctlGetString("hw.disknames", &diskNames); + if (error) { + FF_DEBUG("ffSysctlGetString(hw.disknames) failed: %s", error); + return error; + } + + FF_DEBUG("Disk names: %s", diskNames.chars); + + char* diskName = NULL; + size_t len = 0; + while (ffStrbufGetdelim(&diskName, &len, ' ', &diskNames)) { + FF_DEBUG("Probing disk: %s", diskName); + + if (options->namePrefix.length && !ffStrbufStartsWith(&(FFstrbuf) { + .chars = diskName, + .length = (uint32_t) len, + }, + &options->namePrefix)) { + FF_DEBUG("Skipping %s due to namePrefix filter", diskName); + continue; + } + + char devPath[256]; + FF_AUTO_CLOSE_FD int f = opendisk(diskName, O_RDONLY, devPath, ARRAY_SIZE(devPath), 0); + if (f < 0) { + if (errno == EACCES) { + FF_DEBUG("opendisk(%s) failed: permission denied", diskName); + return "Permission denied; root required"; + } else { + FF_DEBUG("opendisk(%s) failed: %s", diskName, strerror(errno)); + continue; + } + } + + FF_DEBUG("Opened %s as %s", diskName, devPath); + + struct disklabel dl; + if (ioctl(f, DIOCGDINFO, &dl) < 0) { + FF_DEBUG("ioctl(DIOCGDINFO) failed for %s: %s", diskName, strerror(errno)); + continue; + } + + unsigned long sectorsPerUnit, sectorSize; + + const char* devType = NULL; + + prop_dictionary_t dict = NULL; + if (prop_dictionary_recv_ioctl(f, DIOCGDISKINFO, &dict) == 0) { + FF_DEBUG("DIOCGDISKINFO succeeded for %s", diskName); + prop_dictionary_get_string(dict, "type", &devType); + + prop_dictionary_t geometry; + if (prop_dictionary_get_dict(dict, "geometry", &geometry)) { + prop_dictionary_get_ulong(geometry, "sectors-per-unit", §orsPerUnit); + prop_dictionary_get_ulong(geometry, "sector-size", §orSize); + } else { + FF_DEBUG("No geometry in diskinfo for %s, falling back to disklabel", diskName); + sectorsPerUnit = dl.d_secperunit; + sectorSize = dl.d_secsize; + } + } else { + FF_DEBUG("DIOCGDISKINFO failed for %s, falling back to disklabel", diskName); + sectorsPerUnit = dl.d_secperunit; + sectorSize = dl.d_secsize; + } + + FFPhysicalDiskType type = dl.d_flags & D_REMOVABLE ? FF_PHYSICALDISK_TYPE_REMOVABLE : FF_PHYSICALDISK_TYPE_FIXED; + + switch (dl.d_type) { + case DKTYPE_VND: + case DKTYPE_LD: + case DKTYPE_RAID: + case DKTYPE_CGD: + case DKTYPE_VINUM: + case DKTYPE_DM: + case DKTYPE_RUMPD: + case DKTYPE_MD: { + if (options->hideType & FF_PHYSICALDISK_TYPE_VIRTUAL) { + FF_DEBUG("Skipping virtual disk %s due to hideType", diskName); + continue; + } + + type |= FF_PHYSICALDISK_TYPE_VIRTUAL; + break; + } + } + + uint64_t size = sectorsPerUnit * sectorSize; + if (size == 0) { + if (options->hideType & FF_PHYSICALDISK_TYPE_UNUSED) { + FF_DEBUG("Skipping unused disk %s due to hideType", diskName); + continue; + } + + type |= FF_PHYSICALDISK_TYPE_UNUSED; + } + + FFPhysicalDiskResult* device = FF_LIST_ADD(FFPhysicalDiskResult, *result); + ffStrbufInitS(&device->name, devType ?: dl.d_packname); + ffStrbufInitS(&device->devPath, devPath); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->revision); + ffStrbufInitS(&device->interconnect, dktypenames[dl.d_type]); + device->type = type; + device->size = size; + device->temperature = FF_PHYSICALDISK_TEMP_UNSET; + FF_DEBUG("Added disk entry: name='%s', devPath='%s', interconnect='%s', size=%llu", + device->name.chars, + device->devPath.chars, + device->interconnect.chars, + (unsigned long long) device->size); + + if (dict) { + prop_object_release(dict); + dict = NULL; + } + + struct scsipi_inquiry_data inquiry = {}; + scsireq_t req = { + .cmd = { [0] = INQUIRY, [4] = sizeof(inquiry) }, + .cmdlen = 6, + .databuf = (caddr_t) &inquiry, + .datalen = sizeof(inquiry), + .timeout = 1000, + .flags = SCCMD_READ, + }; + if (ioctl(f, SCIOCCOMMAND, &req) == 0) { + if (req.retsts == SCCMD_OK) { + ffStrbufClear(&device->name); + ffStrbufAppendNS(&device->name, (uint32_t) ARRAY_SIZE(inquiry.vendor), inquiry.vendor); + ffStrbufTrimRight(&device->name, '\0'); + ffStrbufTrimRight(&device->name, ' '); + ffStrbufTrimRight(&device->name, ','); + ffStrbufAppendC(&device->name, ' '); + ffStrbufAppendNS(&device->name, (uint32_t) ARRAY_SIZE(inquiry.product), inquiry.product); + ffStrbufTrimRight(&device->name, '\0'); + ffStrbufTrimRight(&device->name, ' '); + + ffStrbufSetNS(&device->revision, (uint32_t) ARRAY_SIZE(inquiry.revision), inquiry.revision); + ffStrbufTrimRight(&device->name, '\0'); + ffStrbufTrimRight(&device->revision, ' '); + FF_DEBUG("SCSI inquiry for %s: name='%s', revision='%s'", diskName, device->name.chars, device->revision.chars); + } else { + FF_DEBUG("SCSI inquiry retsts != SCCMD_OK for %s (%d)", diskName, req.retsts); + continue; + } + } else { + FF_DEBUG("ioctl(SCIOCCOMMAND) failed for SCSI inquiry on %s: %s", diskName, strerror(errno)); + continue; + } + +#ifdef SINQ_EVPD // Available since NetBSD 11.0 + struct { + struct scsipi_inquiry_evpd_header header; + struct scsipi_inquiry_evpd_serial body; + } evpd = {}; + req = (scsireq_t) { + .cmd = { [0] = INQUIRY, [1] = SINQ_EVPD, [2] = SINQ_VPD_UNIT_SERIAL, [4] = sizeof(evpd) }, + .cmdlen = 6, + .databuf = (caddr_t) &evpd, + .datalen = sizeof(evpd), + .timeout = 1000, + .flags = SCCMD_READ, + }; + if (ioctl(f, SCIOCCOMMAND, &req) == 0) { + if (req.retsts == SCCMD_OK && evpd.header.pagecode == SINQ_VPD_UNIT_SERIAL) { + for (uint8_t i = 0; i < evpd.header.length[1]; ++i) { + ffStrbufAppendF(&device->serial, "%02X", evpd.body.serial_number[i]); + } + ffStrbufTrimRight(&device->serial, ' '); + FF_DEBUG("SCSI serial for %s: %s", diskName, device->serial.chars); + } else if (req.retsts == SCCMD_SENSE) { + logScsiSense(diskName, "SCSI serial page", &req); + } else { + FF_DEBUG("SCSI serial page unavailable for %s (retsts=%s, pagecode=%u)", diskName, retstsToStr(req.retsts), (unsigned) evpd.header.pagecode); + } + } else { + FF_DEBUG("ioctl(SCIOCCOMMAND) failed for SCSI serial on %s: %s", diskName, strerror(errno)); + } +#endif + + struct scsi_mode_parameter_header_6 mode = {}; + req = (scsireq_t) { + .cmd = { [0] = SCSI_MODE_SENSE_6, [2] = SMS_PAGE_MASK, [4] = sizeof(mode) }, + .cmdlen = 6, + .databuf = (caddr_t) &mode, + .datalen = sizeof(mode), + .timeout = 1000, + .flags = SCCMD_READ, + }; + if (ioctl(f, SCIOCCOMMAND, &req) == 0) { + if (req.retsts == SCCMD_OK && mode.data_length > 0) { + device->type |= mode.dev_spec & 0x80 ? FF_PHYSICALDISK_TYPE_READONLY : FF_PHYSICALDISK_TYPE_READWRITE; + FF_DEBUG("SCSI mode for %s indicates: %s", diskName, mode.dev_spec & 0x80 ? "readonly" : "readwrite"); + } else if (req.retsts == SCCMD_SENSE) { + logScsiSense(diskName, "SCSI mode sense", &req); + } else { + FF_DEBUG("SCSI mode sense unavailable for %s (retsts=%s, data_length=%u)", diskName, retstsToStr(req.retsts), (unsigned) mode.data_length); + } + } else { + FF_DEBUG("ioctl(SCIOCCOMMAND) failed for SCSI mode sense on %s: %s", diskName, strerror(errno)); + } + + FF_DEBUG("Detected disk '%s' (%s), type=%u", device->name.chars, diskName, (unsigned) device->type); + } + + return NULL; +} diff --git a/src/detection/physicaldisk/physicaldisk_nosupport.c b/src/detection/physicaldisk/physicaldisk_nosupport.c new file mode 100644 index 0000000..26e0ce8 --- /dev/null +++ b/src/detection/physicaldisk/physicaldisk_nosupport.c @@ -0,0 +1,5 @@ +#include "physicaldisk.h" + +const char* ffDetectPhysicalDisk(FF_A_UNUSED FFlist* result, FF_A_UNUSED FFPhysicalDiskOptions* options) { + return "Not supported on this platform"; +} diff --git a/src/detection/physicaldisk/physicaldisk_obsd.c b/src/detection/physicaldisk/physicaldisk_obsd.c new file mode 100644 index 0000000..6e89a99 --- /dev/null +++ b/src/detection/physicaldisk/physicaldisk_obsd.c @@ -0,0 +1,279 @@ +#include "physicaldisk.h" +#include "common/io.h" +#include "common/sysctl.h" +#include "common/debug.h" + +#include <util.h> +#include <sys/ioctl.h> +#include <sys/disklabel.h> +#include <sys/dkio.h> +#include <sys/scsiio.h> +#include <scsi/scsi_all.h> +#include <fcntl.h> +#include <errno.h> + +static inline const char* retstsToStr(uint8_t retsts) { + switch (retsts) { + case SCCMD_OK: + return "OK"; + case SCCMD_TIMEOUT: + return "TIMEOUT"; + case SCCMD_BUSY: + return "BUSY"; + case SCCMD_SENSE: + return "SENSE"; + case SCCMD_UNKNOWN: + return "UNKNOWN"; + default: + return "?"; + } +} + +#ifndef NDEBUG +static inline const char* senseKeyToStr(uint8_t key) { + switch (key) { + case 0x0: + return "No Sense"; + case 0x1: + return "Recovered Error"; + case 0x2: + return "Not Ready"; + case 0x3: + return "Medium Error"; + case 0x4: + return "Hardware Error"; + case 0x5: + return "Illegal Request"; + case 0x6: + return "Unit Attention"; + case 0x7: + return "Data Protect"; + case 0x8: + return "Blank Check"; + case 0x9: + return "Vendor Specific"; + case 0xA: + return "Copy Aborted"; + case 0xB: + return "Aborted Command"; + case 0xC: + return "Equal"; + case 0xD: + return "Volume Overflow"; + case 0xE: + return "Miscompare"; + case 0xF: + return "Completed"; + default: + return "Unknown"; + } +} + +static void logScsiSense(const char* diskName, const char* operation, const scsireq_t* req) { + uint8_t responseCode = req->sense[0] & 0x7F; + uint8_t senseKey; + uint8_t asc; + uint8_t ascq; + + if (responseCode == 0x72 || responseCode == 0x73) { + senseKey = req->sense[1] & 0x0F; + asc = req->sense[2]; + ascq = req->sense[3]; + } else { + senseKey = req->sense[2] & 0x0F; + asc = req->sense[12]; + ascq = req->sense[13]; + } + + FF_STRBUF_AUTO_DESTROY rawSense = ffStrbufCreate(); + for (size_t i = 0; i < req->senselen_used; ++i) { + if (i) { + ffStrbufAppendC(&rawSense, ' '); + } + ffStrbufAppendF(&rawSense, "%02X", req->sense[i]); + } + + FF_DEBUG( + "%s for %s reported SENSE: response=0x%02X, key=0x%X (%s), ASC=0x%02X, ASCQ=0x%02X, raw=[%s]", + operation, + diskName, + responseCode, + senseKey, + senseKeyToStr(senseKey), + asc, + ascq, + rawSense.length ? rawSense.chars : "empty"); +} +#else + #define logScsiSense(...) ((void) 0) +#endif + +const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { + FF_STRBUF_AUTO_DESTROY diskNames = ffStrbufCreate(); + FF_DEBUG("Querying disk names via sysctl hw.disknames"); + const char* error = ffSysctlGetString(CTL_HW, HW_DISKNAMES, &diskNames); + if (error) { + FF_DEBUG("ffSysctlGetString(HW_DISKNAMES) failed: %s", error); + return error; + } + + FF_DEBUG("Disk names: %s", diskNames.chars); + + char* diskName = NULL; + size_t len = 0; + while (ffStrbufGetdelim(&diskName, &len, ',', &diskNames)) { + char* colon = strchr(diskName, ':'); + if (colon) { + *colon = '\0'; + } + + FF_DEBUG("Probing disk: %s", diskName); + + if (options->namePrefix.length && !ffStrbufStartsWith(&(FFstrbuf) { + .chars = diskName, + .length = (uint32_t) len, + }, + &options->namePrefix)) { + FF_DEBUG("Skipping %s due to namePrefix filter", diskName); + continue; + } + + char* devPath = NULL; + FF_AUTO_CLOSE_FD int f = opendev(diskName, O_RDONLY, OPENDEV_PART, &devPath); + if (f < 0) { + if (errno == EACCES) { + FF_DEBUG("opendev(%s) failed: permission denied", diskName); + return "Permission denied; root required"; + } else { + FF_DEBUG("opendev(%s) failed: %s", diskName, strerror(errno)); + continue; // Unknown error + } + } + + FF_DEBUG("Opened %s as %s", diskName, devPath); + + struct disklabel dl; + if (ioctl(f, DIOCGPDINFO, &dl) < 0) { + FF_DEBUG("ioctl(DIOCGPDINFO) failed for %s: %s", diskName, strerror(errno)); + continue; + } + + FFPhysicalDiskType type = FF_PHYSICALDISK_TYPE_NONE; + if (dl.d_type == DTYPE_VND || dl.d_type == DTYPE_RDROOT) { + if (options->hideType & FF_PHYSICALDISK_TYPE_VIRTUAL) { + FF_DEBUG("Skipping virtual disk %s due to hideType", diskName); + continue; + } + + type |= FF_PHYSICALDISK_TYPE_VIRTUAL; + } + + uint64_t size = DL_GETDSIZE(&dl) * dl.d_secsize; + if (size == 0) { + if (options->hideType & FF_PHYSICALDISK_TYPE_UNUSED) { + FF_DEBUG("Skipping unused disk %s due to hideType", diskName); + continue; + } + + type |= FF_PHYSICALDISK_TYPE_UNUSED; + } + + FFPhysicalDiskResult* device = FF_LIST_ADD(FFPhysicalDiskResult, *result); + ffStrbufInitS(&device->name, dl.d_packname); + ffStrbufInitS(&device->devPath, devPath); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->revision); + ffStrbufInitS(&device->interconnect, dl.d_typename); + device->type = type; + device->size = size; + device->temperature = FF_PHYSICALDISK_TEMP_UNSET; + FF_DEBUG("Added disk entry: name='%s', devPath='%s', interconnect='%s', size=%llu", + device->name.chars, + device->devPath.chars, + device->interconnect.chars, + (unsigned long long) device->size); + + struct scsi_inquiry_data inquiry = {}; + struct scsireq req = { + .cmd = { [0] = INQUIRY, [4] = sizeof(inquiry) }, + .cmdlen = 6, + .databuf = (caddr_t) &inquiry, + .datalen = sizeof(inquiry), + .timeout = 1000, + .flags = SCCMD_READ, + }; + if (ioctl(f, SCIOCCOMMAND, &req) == 0) { + if (req.retsts == SCCMD_OK) { + ffStrbufClear(&device->name); + ffStrbufAppendNS(&device->name, (uint32_t) ARRAY_SIZE(inquiry.vendor), inquiry.vendor); + ffStrbufTrimRight(&device->name, '\0'); + ffStrbufTrimRight(&device->name, ' '); + ffStrbufAppendC(&device->name, ' '); + ffStrbufAppendNS(&device->name, (uint32_t) ARRAY_SIZE(inquiry.product), inquiry.product); + ffStrbufTrimRight(&device->name, '\0'); + ffStrbufTrimRight(&device->name, ' '); + + ffStrbufSetNS(&device->revision, (uint32_t) ARRAY_SIZE(inquiry.revision), inquiry.revision); + ffStrbufTrimRight(&device->revision, '\0'); + ffStrbufTrimRight(&device->revision, ' '); + device->type |= inquiry.dev_qual2 & SID_REMOVABLE ? FF_PHYSICALDISK_TYPE_REMOVABLE : FF_PHYSICALDISK_TYPE_FIXED; + FF_DEBUG("SCSI inquiry for %s: name='%s', revision='%s'", diskName, device->name.chars, device->revision.chars); + } else { + FF_DEBUG("SCSI inquiry retsts != SCCMD_OK for %s (%d)", diskName, req.retsts); + } + } else { + FF_DEBUG("ioctl(SCIOCCOMMAND) failed for SCSI inquiry on %s: %s", diskName, strerror(errno)); + } + + struct scsi_vpd_serial evpd = {}; + req = (struct scsireq) { + .cmd = { [0] = INQUIRY, [1] = SI_EVPD, [2] = SI_PG_SERIAL, [4] = sizeof(evpd) }, + .cmdlen = 6, + .databuf = (caddr_t) &evpd, + .datalen = sizeof(evpd), + .timeout = 1000, + .flags = SCCMD_READ, + }; + if (ioctl(f, SCIOCCOMMAND, &req) == 0) { + if (req.retsts == SCCMD_OK && evpd.hdr.page_code == SI_PG_SERIAL) { + for (uint8_t i = 0; i < evpd.hdr.page_length[1]; ++i) { + ffStrbufAppendF(&device->serial, "%02X", evpd.serial[i]); + } + ffStrbufTrimSpace(&device->serial); + FF_DEBUG("SCSI serial for %s: %s", diskName, device->serial.chars); + } else if (req.retsts == SCCMD_SENSE) { + logScsiSense(diskName, "SCSI serial page", &req); + } else { + FF_DEBUG("SCSI serial page unavailable for %s (retsts=%s, pagecode=%u)", diskName, retstsToStr(req.retsts), (unsigned) evpd.hdr.page_code); + } + } else { + FF_DEBUG("ioctl(SCIOCCOMMAND) failed for SCSI serial on %s: %s", diskName, strerror(errno)); + } + + struct scsi_mode_header mode = {}; + req = (scsireq_t) { + .cmd = { [0] = MODE_SENSE, [2] = SMS_PAGE_CODE, [4] = sizeof(mode) }, + .cmdlen = 6, + .databuf = (caddr_t) &mode, + .datalen = sizeof(mode), + .timeout = 1000, + .flags = SCCMD_READ, + }; + if (ioctl(f, SCIOCCOMMAND, &req) == 0) { + if (req.retsts == SCCMD_OK) { + device->type |= mode.dev_spec & 0x80 ? FF_PHYSICALDISK_TYPE_READONLY : FF_PHYSICALDISK_TYPE_READWRITE; + FF_DEBUG("SCSI mode for %s indicates: %s", diskName, mode.dev_spec & 0x80 ? "readonly" : "readwrite"); + } else if (req.retsts == SCCMD_SENSE) { + logScsiSense(diskName, "SCSI mode sense", &req); + } else { + FF_DEBUG("SCSI mode sense unavailable for %s (retsts=%s, data_length=%u)", diskName, retstsToStr(req.retsts), (unsigned) mode.data_length); + } + } else { + FF_DEBUG("ioctl(SCIOCCOMMAND) failed for SCSI mode sense on %s: %s", diskName, strerror(errno)); + } + + FF_DEBUG("Detected disk '%s' (%s), type=%u", device->name.chars, diskName, (unsigned) device->type); + } + + return NULL; +} diff --git a/src/detection/physicaldisk/physicaldisk_sunos.c b/src/detection/physicaldisk/physicaldisk_sunos.c new file mode 100644 index 0000000..d929d5a --- /dev/null +++ b/src/detection/physicaldisk/physicaldisk_sunos.c @@ -0,0 +1,107 @@ +#include "physicaldisk.h" +#include "common/strutil.h" +#include "sys/scsi/generic/inquiry.h" + +#include <libdevinfo.h> +#include <sys/stat.h> + +struct FFWalkTreeBundle { + FFPhysicalDiskOptions* options; + FFlist* disks; +}; + +static int walkDevTree(di_node_t node, di_minor_t minor, struct FFWalkTreeBundle* bundle) { + FFPhysicalDiskOptions* options = bundle->options; + FFlist* result = bundle->disks; + + if (di_minor_spectype(minor) != S_IFCHR || !ffStrEquals(di_minor_name(minor), "a,raw")) { + return DI_WALK_CONTINUE; + } + + char* productId; + char* vendorId; + if (di_prop_lookup_strings(DDI_DEV_T_ANY, node, "inquiry-product-id", &productId) > 0 && di_prop_lookup_strings(DDI_DEV_T_ANY, node, "inquiry-vendor-id", &vendorId) > 0) { + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreateF("%s %s", vendorId, productId); + if (options->namePrefix.length && !ffStrbufStartsWithIgnCase(&name, &options->namePrefix)) { + return DI_WALK_CONTINUE; + } + + int* value; + + FFPhysicalDiskType type = FF_PHYSICALDISK_TYPE_NONE; + uint64_t size = 0; + int64_t* nblocks; + if (di_prop_lookup_int64(DDI_DEV_T_ANY, node, "device-nblocks", &nblocks) > 0) { + if (*nblocks == 0) { + if (options->hideType & FF_PHYSICALDISK_TYPE_UNUSED) { + return DI_WALK_CONTINUE; + } + + type |= FF_PHYSICALDISK_TYPE_UNUSED; + } else if (di_prop_lookup_ints(DDI_DEV_T_ANY, node, "device-blksize", &value) > 0) { + size = (uint64_t) ((uint64_t) *nblocks * (uint64_t) *value); + } + } + + FFPhysicalDiskResult* device = FF_LIST_ADD(FFPhysicalDiskResult, *result); + ffStrbufInitMove(&device->name, &name); + ffStrbufInitF(&device->devPath, "/devices%s", di_devfs_path(node)); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->revision); + ffStrbufInit(&device->interconnect); + device->temperature = FF_PHYSICALDISK_TEMP_UNSET; + device->type = type; + device->size = size; + + char* buf; + bool usb = false; + if (di_prop_lookup_strings(DDI_DEV_T_ANY, node, "inquiry-serial-no", &buf) > 0) { + ffStrbufSetS(&device->serial, buf); + ffStrbufTrimSpace(&device->serial); + } else { + di_node_t parent = di_parent_node(node); + if (parent != DI_NODE_NIL && di_prop_lookup_strings(DDI_DEV_T_ANY, parent, "usb-serialno", &buf) > 0) { + ffStrbufSetS(&device->serial, buf); + usb = true; + } + } + if (di_prop_lookup_strings(DDI_DEV_T_ANY, node, "inquiry-revision-id", &buf) > 0) { + ffStrbufSetS(&device->revision, buf); + ffStrbufTrimRightSpace(&device->revision); + } + + if (usb) { + ffStrbufSetStatic(&device->interconnect, "USB"); + } else if (di_prop_lookup_strings(DDI_DEV_T_ANY, node, "class", &buf) > 0) { + ffStrbufSetS(&device->interconnect, buf); + } else { + di_node_t parent = di_parent_node(node); + if (parent != DI_NODE_NIL && di_prop_lookup_strings(DDI_DEV_T_ANY, parent, "model", &buf) > 0) { + ffStrbufSetS(&device->interconnect, buf); + } + } + + device->type |= di_prop_find(DDI_DEV_T_ANY, node, "removable-media") ? FF_PHYSICALDISK_TYPE_REMOVABLE : FF_PHYSICALDISK_TYPE_FIXED; + + if (di_prop_lookup_ints(DDI_DEV_T_ANY, node, "device-solid-state", &value) > 0) { + device->type |= *value ? FF_PHYSICALDISK_TYPE_SSD : FF_PHYSICALDISK_TYPE_HDD; + } + if (di_prop_lookup_ints(DDI_DEV_T_ANY, node, "inquiry-device-type", &value) > 0) { + device->type |= *value == DTYPE_DIRECT ? FF_PHYSICALDISK_TYPE_READWRITE : *value == DTYPE_RODIRECT ? FF_PHYSICALDISK_TYPE_READONLY + : 0; + } + } + + return DI_WALK_CONTINUE; +} + +const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { + di_node_t rootNode = di_init("/", DINFOCPYALL); + if (rootNode == DI_NODE_NIL) { + return "di_init() failed"; + } + di_walk_minor(rootNode, DDI_NT_BLOCK, DI_WALK_CLDFIRST, &(struct FFWalkTreeBundle) { options, result }, (void*) walkDevTree); + di_fini(rootNode); + + return NULL; +} diff --git a/src/detection/physicaldisk/physicaldisk_windows.c b/src/detection/physicaldisk/physicaldisk_windows.c new file mode 100644 index 0000000..5d4f7f7 --- /dev/null +++ b/src/detection/physicaldisk/physicaldisk_windows.c @@ -0,0 +1,349 @@ +#include "physicaldisk.h" +#include "common/io.h" +#include "common/windows/unicode.h" +#include "common/mallocHelper.h" +#include "common/debug.h" + +#include <stdalign.h> +#include <windows.h> +#include <winioctl.h> +#include <cfgmgr32.h> + +static const char* detectPhysicalDisk(const char* physicalType, const wchar_t* szDevice, FFlist* result, FFPhysicalDiskOptions* options) { + FF_AUTO_CLOSE_FD HANDLE hDevice = CreateFileW(szDevice, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + if (hDevice == INVALID_HANDLE_VALUE) { + return "CreateFileW() failed"; + } + + DWORD retSize; + FFPhysicalDiskType type = FF_PHYSICALDISK_TYPE_NONE; + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + + const char* interconnect = NULL; + uint64_t size = 0; + { + alignas(DISK_GEOMETRY_EX) uint8_t dgeBuffer[4096]; + if (DeviceIoControl( + hDevice, + IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, + NULL, + 0, + dgeBuffer, + sizeof(dgeBuffer), + &retSize, + NULL)) { + const DISK_GEOMETRY_EX* dge = (const DISK_GEOMETRY_EX*) dgeBuffer; + size = (uint64_t) dge->DiskSize.QuadPart; + } else if (DeviceIoControl( + hDevice, + IOCTL_DISK_GET_DRIVE_GEOMETRY, + NULL, + 0, + dgeBuffer, + sizeof(dgeBuffer), + &retSize, + NULL) && + retSize >= sizeof(DISK_GEOMETRY)) { + const DISK_GEOMETRY* dg = (const DISK_GEOMETRY*) dgeBuffer; + size = (uint64_t) dg->BytesPerSector * dg->SectorsPerTrack * dg->TracksPerCylinder * (uint64_t) dg->Cylinders.QuadPart; + switch (dg->MediaType) { + case F3_1Pt44_512: + case F3_2Pt88_512: + case F3_20Pt8_512: + case F3_720_512: + case F3_120M_512: + case F3_640_512: + case F3_1Pt2_512: + case F3_1Pt23_1024: + case F3_128Mb_512: + case F3_230Mb_512: + case F3_200Mb_512: + case F3_240M_512: + case F3_32M_512: + ffStrbufSetStatic(&name, "3.5-inch Floppy Disk"); + break; + case F5_1Pt2_512: + case F5_360_512: + case F5_320_512: + case F5_320_1024: + case F5_180_512: + case F5_160_512: + case F5_640_512: + case F5_720_512: + case F5_1Pt23_1024: + ffStrbufSetStatic(&name, "5.25-inch Floppy Disk"); + break; + case F8_256_128: + ffStrbufSetStatic(&name, "8-inch Floppy Disk"); + break; + default: + return "Unsupported media type"; + } + interconnect = "Floppy Controller"; + type |= FF_PHYSICALDISK_TYPE_HDD | FF_PHYSICALDISK_TYPE_REMOVABLE; + } + } + if (size == 0) { + if (options->hideType & FF_PHYSICALDISK_TYPE_UNUSED) { + return "Skipping unknown disk with size 0"; + } + + type |= FF_PHYSICALDISK_TYPE_UNUSED; + } + + const STORAGE_DEVICE_DESCRIPTOR* sdd = NULL; + alignas(STORAGE_DEVICE_DESCRIPTOR) uint8_t sddBuffer[4096]; + if (!interconnect) { + if (DeviceIoControl( + hDevice, + IOCTL_STORAGE_QUERY_PROPERTY, + &(STORAGE_PROPERTY_QUERY) { + .PropertyId = StorageDeviceProperty, + .QueryType = PropertyStandardQuery, + }, + sizeof(STORAGE_PROPERTY_QUERY), + &sddBuffer, + sizeof(sddBuffer), + &retSize, + NULL) || + retSize == 0) { + sdd = (const STORAGE_DEVICE_DESCRIPTOR*) sddBuffer; + + switch (sdd->BusType) { + case BusTypeScsi: + interconnect = "SCSI"; + break; + case BusTypeAtapi: + interconnect = "ATAPI"; + break; + case BusTypeAta: + interconnect = "ATA"; + break; + case BusType1394: + interconnect = "IEEE 1394"; + break; + case BusTypeSsa: + interconnect = "SSA"; + break; + case BusTypeFibre: + interconnect = "Fibre"; + break; + case BusTypeUsb: + interconnect = "USB"; + break; + case BusTypeRAID: + interconnect = "RAID"; + break; + case BusTypeiScsi: + interconnect = "iSCSI"; + break; + case BusTypeSas: + interconnect = "SAS"; + break; + case BusTypeSata: + interconnect = "SATA"; + break; + case BusTypeSd: + interconnect = "SD"; + break; + case BusTypeMmc: + interconnect = "MMC"; + break; + case BusTypeVirtual: + interconnect = "Virtual"; + type |= FF_PHYSICALDISK_TYPE_VIRTUAL; + break; + case BusTypeFileBackedVirtual: + interconnect = "File Backed Virtual"; + type |= FF_PHYSICALDISK_TYPE_VIRTUAL; + break; + case BusTypeSpaces: + interconnect = "Storage Spaces"; + type |= FF_PHYSICALDISK_TYPE_VIRTUAL; + break; + case BusTypeNvme: + interconnect = "NVMe"; + break; + case BusTypeSCM: + interconnect = "SCM"; + break; + case BusTypeUfs: + interconnect = "UFS"; + break; + case 0x14 /*BusTypeNvmeof*/: + interconnect = "NVMe-oF"; + break; + default: + interconnect = "Unknown"; + break; + } + + if (type & FF_PHYSICALDISK_TYPE_VIRTUAL && options->hideType & FF_PHYSICALDISK_TYPE_VIRTUAL) { + return "Skipping virtual disk"; + } + + if (sdd->VendorIdOffset != 0) { + ffStrbufSetS(&name, (const char*) sdd + sdd->VendorIdOffset); + ffStrbufTrim(&name, ' '); + } + if (sdd->ProductIdOffset != 0) { + if (name.length) { + ffStrbufAppendC(&name, ' '); + } + + ffStrbufAppendS(&name, (const char*) sdd + sdd->ProductIdOffset); + ffStrbufTrimRight(&name, ' '); + } + } + if (!name.length) { + ffStrbufSetStatic(&name, physicalType); + } + } + + if (options->namePrefix.length && !ffStrbufStartsWith(&name, &options->namePrefix)) { + return "Name prefix mismatch"; + } + + FFPhysicalDiskResult* device = FF_LIST_ADD(FFPhysicalDiskResult, *result); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->revision); + ffStrbufInitMove(&device->name, &name); + ffStrbufInit(&device->devPath); + ffStrbufInitStatic(&device->interconnect, interconnect); + device->type = type; + device->size = size; + device->temperature = FF_PHYSICALDISK_TEMP_UNSET; + + ffStrbufSetWS(&device->devPath, szDevice); + + if (sdd) { + if (sdd->SerialNumberOffset != 0) { + ffStrbufSetS(&device->serial, (const char*) sdd + sdd->SerialNumberOffset); + ffStrbufTrimSpace(&device->serial); + } + + if (sdd->ProductRevisionOffset != 0) { + ffStrbufSetS(&device->revision, (const char*) sdd + sdd->ProductRevisionOffset); + ffStrbufTrimRightSpace(&device->revision); + } + + device->type |= sdd->RemovableMedia ? FF_PHYSICALDISK_TYPE_REMOVABLE : FF_PHYSICALDISK_TYPE_FIXED; + } + + { + alignas(GET_MEDIA_TYPES) uint8_t buffer[4096]; + GET_MEDIA_TYPES* gmt = (GET_MEDIA_TYPES*) buffer; + if (DeviceIoControl( + hDevice, + IOCTL_STORAGE_GET_MEDIA_TYPES_EX, + NULL, + 0, + gmt, + sizeof(buffer), + &retSize, + NULL) && + gmt->MediaInfoCount > 0) { + // DiskInfo and RemovableDiskInfo have the same structures. TapeInfo doesn't. + if (gmt->DeviceType != FILE_DEVICE_TAPE) { + __auto_type diskInfo = &gmt->MediaInfo[0].DeviceSpecific.DiskInfo; + if (diskInfo->MediaCharacteristics & MEDIA_READ_ONLY) { + device->type |= FF_PHYSICALDISK_TYPE_READONLY; + } else if (diskInfo->MediaCharacteristics & MEDIA_READ_WRITE) { + device->type |= FF_PHYSICALDISK_TYPE_READWRITE; + } + } else { + __auto_type tapeInfo = &gmt->MediaInfo[0].DeviceSpecific.TapeInfo; + if (tapeInfo->MediaCharacteristics & MEDIA_READ_ONLY) { + device->type |= FF_PHYSICALDISK_TYPE_READONLY; + } else if (tapeInfo->MediaCharacteristics & MEDIA_READ_WRITE) { + device->type |= FF_PHYSICALDISK_TYPE_READWRITE; + } + } + } + } + + if (!(device->type & FF_PHYSICALDISK_TYPE_VIRTUAL) && !(device->type & FF_PHYSICALDISK_TYPE_HDD)) { + DEVICE_SEEK_PENALTY_DESCRIPTOR dspd = {}; + if (DeviceIoControl( + hDevice, + IOCTL_STORAGE_QUERY_PROPERTY, + &(STORAGE_PROPERTY_QUERY) { + .PropertyId = StorageDeviceSeekPenaltyProperty, + .QueryType = PropertyStandardQuery, + }, + sizeof(STORAGE_PROPERTY_QUERY), + &dspd, + sizeof(dspd), + &retSize, + NULL) && + retSize == sizeof(dspd)) { + device->type |= dspd.IncursSeekPenalty ? FF_PHYSICALDISK_TYPE_HDD : FF_PHYSICALDISK_TYPE_SSD; + } + + if (options->temp) { + STORAGE_TEMPERATURE_DATA_DESCRIPTOR stdd = {}; + if (DeviceIoControl( + hDevice, + IOCTL_STORAGE_QUERY_PROPERTY, + &(STORAGE_PROPERTY_QUERY) { + .PropertyId = StorageDeviceTemperatureProperty, + .QueryType = PropertyStandardQuery, + }, + sizeof(STORAGE_PROPERTY_QUERY), + &stdd, + sizeof(stdd), + &retSize, + NULL) && + retSize == sizeof(stdd)) { + device->temperature = stdd.TemperatureInfo[0].Temperature; + } + } + } + + return NULL; +} + +static void detectPhysicalDisksByInterfaceClass(const char* type, const GUID* interfaceClassGuid, FFlist* result, FFPhysicalDiskOptions* options) { + ULONG cchDeviceInterfaces = 0; + if (CM_Get_Device_Interface_List_SizeW( + &cchDeviceInterfaces, + (LPGUID) interfaceClassGuid, + NULL, + CM_GET_DEVICE_INTERFACE_LIST_PRESENT) != CR_SUCCESS || + cchDeviceInterfaces <= 1) { + return; + } + + wchar_t* FF_AUTO_FREE mszDeviceInterfaces = (wchar_t*) malloc(cchDeviceInterfaces * sizeof(wchar_t)); + if (!mszDeviceInterfaces) { + return; + } + + if (CM_Get_Device_Interface_ListW( + (LPGUID) interfaceClassGuid, + NULL, + mszDeviceInterfaces, + cchDeviceInterfaces, + CM_GET_DEVICE_INTERFACE_LIST_PRESENT) != CR_SUCCESS) { + return; + } + + // MULTI_SZ: "str1\0str2\0...\0\0" + for (const wchar_t* p = mszDeviceInterfaces; *p; p += wcslen(p) + 1) { + FF_DEBUG("Probing %s: %ls", type, p); + FF_A_UNUSED const char* error = detectPhysicalDisk(type, p, result, options); + if (error == NULL) { + FF_DEBUG("Detected device \"%s\"", FF_LIST_LAST(FFPhysicalDiskResult, *result)->name.chars); + } else { + FF_DEBUG("Failed to detect device %s: %s", type, error); + } + } +} + +const char* ffDetectPhysicalDisk(FFlist* result, FFPhysicalDiskOptions* options) { + detectPhysicalDisksByInterfaceClass("Floppy", &GUID_DEVINTERFACE_FLOPPY, result, options); + detectPhysicalDisksByInterfaceClass("Disk", &GUID_DEVINTERFACE_DISK, result, options); + detectPhysicalDisksByInterfaceClass("CD-ROM", &GUID_DEVINTERFACE_CDROM, result, options); + detectPhysicalDisksByInterfaceClass("Tape", &GUID_DEVINTERFACE_TAPE, result, options); + return NULL; +} diff --git a/src/detection/physicalmemory/physicalmemory.c b/src/detection/physicalmemory/physicalmemory.c new file mode 100644 index 0000000..0188e05 --- /dev/null +++ b/src/detection/physicalmemory/physicalmemory.c @@ -0,0 +1,73 @@ +#include "physicalmemory.h" + +static inline const char* getVendorString(unsigned vendorId) { + switch (vendorId) { + case 0x017A: + return "Apacer"; + case 0x0198: + return "Kingston"; + case 0x029E: + return "Corsair"; + case 0x04CB: + return "A-DATA"; + case 0x04CD: + return "G-Skill"; + case 0x059B: + case 0x859B: + return "Crucial"; + case 0x00CE: + case 0x80CE: + case 0xCE00: + return "Samsung"; + case 0x014F: + return "Transcend"; + case 0x2C00: + case 0x802C: + return "Micron"; + case 0xAD00: + case 0x80AD: + return "SK Hynix"; + case 0x5105: + case 0x8551: + return "Qimonda"; + case 0x02FE: + return "Elpida"; + case 0x0467: + return "Ramaxel"; + default: + return NULL; + } +} + +void FFPhysicalMemoryUpdateVendorString(FFPhysicalMemoryResult* device) { + if (device->vendor.length == 0) { + return; + } + if (ffStrbufEqualS(&device->vendor, "Unknown")) { + ffStrbufClear(&device->vendor); + return; + } + + char vendorIdStr[5]; + if (ffStrbufStartsWithS(&device->vendor, "0x")) { + if (device->vendor.length < 6) { + return; + } + memcpy(vendorIdStr, device->vendor.chars + 2, 4); + } else { + if (device->vendor.length < 4) { + return; + } + memcpy(vendorIdStr, device->vendor.chars, 4); + } + vendorIdStr[4] = '\0'; + char* pEnd = NULL; + uint32_t vendorId = (uint32_t) strtoul(vendorIdStr, &pEnd, 16); + if (*pEnd != '\0') { + return; + } + const char* vendorStr = getVendorString(vendorId); + if (vendorStr) { + ffStrbufSetStatic(&device->vendor, vendorStr); + } +} diff --git a/src/detection/physicalmemory/physicalmemory.h b/src/detection/physicalmemory/physicalmemory.h new file mode 100644 index 0000000..d4c8511 --- /dev/null +++ b/src/detection/physicalmemory/physicalmemory.h @@ -0,0 +1,22 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/physicalmemory/option.h" + +typedef struct FFPhysicalMemoryResult { + uint64_t size; // B + uint32_t maxSpeed; // MT/s + uint32_t runningSpeed; // MT/s + bool installed; + FFstrbuf type; + FFstrbuf formFactor; + FFstrbuf locator; + FFstrbuf partNumber; + FFstrbuf vendor; + FFstrbuf serial; + bool ecc; +} FFPhysicalMemoryResult; + +const char* ffDetectPhysicalMemory(FFPhysicalMemoryOptions* options, FFlist* result); // list of FFPhysicalMemoryResult + +void FFPhysicalMemoryUpdateVendorString(FFPhysicalMemoryResult* device); diff --git a/src/detection/physicalmemory/physicalmemory_apple.m b/src/detection/physicalmemory/physicalmemory_apple.m new file mode 100644 index 0000000..e35f538 --- /dev/null +++ b/src/detection/physicalmemory/physicalmemory_apple.m @@ -0,0 +1,163 @@ +#include "physicalmemory.h" +#include "common/processing.h" +#include "common/smbios.h" +#include "common/strutil.h" +#include "common/apple/cf_helpers.h" + +#import <Foundation/Foundation.h> + +static void appendDevice( + FFlist* result, + NSString* type, + NSString* vendor, + NSString* size, + + // Intel only + NSString* locator, + NSString* serial, + NSString* partNumber, + NSString* speed, + bool ecc) +{ + FFPhysicalMemoryResult* device = FF_LIST_ADD(FFPhysicalMemoryResult, *result); + ffStrbufInitS(&device->type, type.UTF8String); + ffStrbufInit(&device->formFactor); + ffStrbufInitS(&device->locator, locator.UTF8String); + ffStrbufInitS(&device->vendor, vendor.UTF8String); + FFPhysicalMemoryUpdateVendorString(device); + ffStrbufInitS(&device->serial, serial.UTF8String); + ffCleanUpSmbiosValue(&device->serial); + ffStrbufInitS(&device->partNumber, partNumber.UTF8String); + ffCleanUpSmbiosValue(&device->partNumber); + device->size = 0; + device->maxSpeed = 0; + device->runningSpeed = 0; + device->installed = true; + device->ecc = ecc; + + if (size) + { + char* unit = NULL; + device->size = strtoul(size.UTF8String, &unit, 10); + if (*unit == ' ') ++unit; + + switch (*unit) + { + case 'G': device->size *= 1024ULL * 1024 * 1024; break; + case 'M': device->size *= 1024ULL * 1024; break; + case 'K': device->size *= 1024ULL; break; + case 'T': device->size *= 1024ULL * 1024 * 1024 * 1024; break; + } + } + + if (speed) + { + char* unit = NULL; + device->maxSpeed = (uint32_t) strtoul(speed.UTF8String, &unit, 10); + if (*unit == ' ') ++unit; + + switch (*unit) + { + case 'T': device->maxSpeed *= 1000 * 1000; break; + case 'G': device->maxSpeed *= 1000; break; + case 'K': device->maxSpeed /= 1000; break; + } + } +} + +static const char* detectFromSystemProfiler(FFlist* result) +{ + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + if (ffProcessAppendStdOut(&buffer, (char* const[]) { + "system_profiler", + "SPMemoryDataType", + "-xml", + "-detailLevel", + "full", + NULL + }) != NULL) + return "Starting `system_profiler SPMemoryDataType -xml -detailLevel full` failed"; + + NSArray* arr = [NSPropertyListSerialization propertyListWithData:[NSData dataWithBytes:buffer.chars length:buffer.length] + options:NSPropertyListImmutable + format:nil + error:nil]; + if (!arr || !arr.count) + return "system_profiler SPMemoryDataType returned an empty array"; + + for (NSDictionary* data in arr[0][@"_items"]) + { + if (data[@"_items"]) + { + // for Intel + for (NSDictionary* item in data[@"_items"]) + { + appendDevice(result, + item[@"dimm_type"], + item[@"dimm_manufacturer"], + item[@"dimm_size"], + item[@"_name"], + item[@"dimm_serial_number"], + item[@"dimm_part_number"], + item[@"dimm_speed"], + !![data[@"global_ecc_state"] isEqualToString:@"ecc_enabled"]); + } + } + else + { + // for Apple Silicon + appendDevice(result, + data[@"dimm_type"], + data[@"dimm_manufacturer"], + data[@"SPMemoryDataType"], + nil, + nil, + nil, + nil, + false); + } + } + + return NULL; +} + +FF_A_UNUSED static const char* detectFromIokit(FFlist* result) +{ + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t entryDevice = IORegistryEntryFromPath(MACH_PORT_NULL, "IODeviceTree:/chosen"); + if (!entryDevice) + return "IORegistryEntryFromPath() failed"; + + FF_CFTYPE_AUTO_RELEASE CFTypeRef dramType = IORegistryEntryCreateCFProperty(entryDevice, CFSTR("dram-type"), kCFAllocatorDefault, 0); + FF_CFTYPE_AUTO_RELEASE CFTypeRef dramSize = IORegistryEntryCreateCFProperty(entryDevice, CFSTR("dram-size"), kCFAllocatorDefault, 0); + FF_CFTYPE_AUTO_RELEASE CFTypeRef dramVendor = IORegistryEntryCreateCFProperty(entryDevice, CFSTR("dram-vendor"), kCFAllocatorDefault, 0); + if (!dramType || !dramSize || !dramVendor) + return "IORegistryEntryCreateCFProperty() failed"; + + FFPhysicalMemoryResult* device = FF_LIST_ADD(FFPhysicalMemoryResult, *result); + ffStrbufInit(&device->type); + ffStrbufInit(&device->formFactor); + ffStrbufInit(&device->locator); + ffStrbufInit(&device->vendor); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->partNumber); + device->size = 0; + device->maxSpeed = 0; + device->runningSpeed = 0; + device->installed = true; + device->ecc = false; + + ffCfStrGetString(dramType, &device->type); + ffCfStrGetString(dramVendor, &device->vendor); + ffCfNumGetInt64(dramSize, (int64_t*) &device->size); + return NULL; +} + +const char* ffDetectPhysicalMemory(FF_A_UNUSED FFPhysicalMemoryOptions* options, FFlist* result) +{ + #if __aarch64__ + if (detectFromIokit(result) == NULL) + return NULL; + #endif + + return detectFromSystemProfiler(result); +} diff --git a/src/detection/physicalmemory/physicalmemory_linux.c b/src/detection/physicalmemory/physicalmemory_linux.c new file mode 100644 index 0000000..02df789 --- /dev/null +++ b/src/detection/physicalmemory/physicalmemory_linux.c @@ -0,0 +1,235 @@ +#include "physicalmemory.h" +#include "common/smbios.h" + +// 7.18 +typedef struct FFSmbiosMemoryDevice { + FFSmbiosHeader Header; + + // 2.1+ + uint16_t PhysicalMemoryArrayHandle; // varies + uint16_t MemoryErrorInformationHandle; // varies + uint16_t TotalWidth; // varies + uint16_t DataWidth; // varies + uint16_t Size; // varies + uint8_t FormFactor; // enum + uint8_t DeviceSet; // varies + uint8_t DeviceLocator; // string + uint8_t BankLocator; // string + uint8_t MemoryType; // enum + uint16_t TypeDetail; // bit field + + // 2.3+ + uint16_t Speed; // varies + uint8_t Manufacturer; // string + uint8_t SerialNumber; // string + uint8_t AssetTag; // string + uint8_t PartNumber; // string + + // 2.6+ + uint8_t Attributes; // varies + + // 2.7+ + uint32_t ExtendedSize; // varies + uint16_t ConfiguredMemorySpeed; // varies + + // 2.8+ + uint16_t MinimumVoltage; // varies + uint16_t MaximumVoltage; // varies + uint16_t ConfiguredVoltage; // varies + + // 3.2+ + uint8_t MemoryTechnology; // varies + uint16_t MemoryOperatingMode; // bit field + uint8_t FirmwareVersion; // string + uint16_t ModuleManufacturerID; // varies + uint16_t ModuleProductID; // varies + uint16_t MemorySubsystemControllerManufacturerID; // vaies + uint16_t MemorySubsystemControllerProductID; // varies + uint64_t NonVolatileSize; // varies + uint64_t VolatileSize; // varies + uint64_t CacheSize; // varies + uint64_t LogicalSize; // varies + + // 3.3+ + uint32_t ExtendedSpeed; // varies + uint32_t ExtendedConfiguredSpeed; // varies + + // 3.7+ + uint16_t Pmic0ManufacturerID; // varies + uint16_t Pmic0RevisionNumber; // varies + uint16_t RcdManufacturerID; // varies + uint16_t RcdRevisionNumber; // varies +} FF_A_PACKED FFSmbiosMemoryDevice; + +static_assert(offsetof(FFSmbiosMemoryDevice, RcdRevisionNumber) == 0x62, + "FFSmbiosMemoryDevice: Wrong struct alignment"); + +const char* ffDetectPhysicalMemory(FFPhysicalMemoryOptions* options, FFlist* result) { + const FFSmbiosHeaderTable* smbiosTable = ffGetSmbiosHeaderTable(); + if (!smbiosTable) { + return "Failed to get SMBIOS data"; + } + + const FFSmbiosMemoryDevice* data = (const FFSmbiosMemoryDevice*) (*smbiosTable)[FF_SMBIOS_TYPE_MEMORY_DEVICE]; + if (!data) { + return "Memory device is not found in SMBIOS data"; + } + + const FFSmbiosMemoryDevice* endOfTable = (const FFSmbiosMemoryDevice*) (*smbiosTable)[FF_SMBIOS_TYPE_END_OF_TABLE]; + for (; data != endOfTable; data = (const FFSmbiosMemoryDevice*) ffSmbiosNextEntry(&data->Header)) { + if (data->Header.Type != FF_SMBIOS_TYPE_MEMORY_DEVICE) { + continue; + } + + const char* strings = (const char*) data + data->Header.Length; + bool installed = data->Size != 0; + + if (!installed && !options->showEmptySlots) { + continue; + } + + FFPhysicalMemoryResult* device = FF_LIST_ADD(FFPhysicalMemoryResult, *result); + ffStrbufInit(&device->type); + ffStrbufInit(&device->formFactor); + ffStrbufInit(&device->locator); + ffStrbufInit(&device->vendor); + ffStrbufInit(&device->serial); + ffStrbufInit(&device->partNumber); + device->size = 0; + device->maxSpeed = 0; + device->runningSpeed = 0; + device->installed = installed; + device->ecc = false; + + if (installed && data->TotalWidth != 0xFFFF && data->DataWidth != 0xFFFF) { + device->ecc = data->TotalWidth > data->DataWidth; + } + + if (installed && data->Size != 0xFFFF) { + if (data->Size == 0x7FFF) { + device->size = (data->ExtendedSize & ~(1ULL << 31)) * 1024ULL * 1024ULL; + } else if (data->Size & (1 << 15)) { + // in kB + device->size = (data->Size & ~(1ULL << 15)) * 1024ULL; + } else { + // in MB + device->size = data->Size * 1024ULL * 1024ULL; + } + } + + // https://github.com/fastfetch-cli/fastfetch/issues/1051#issuecomment-2206687345 + const char* lbank = ffSmbiosLocateString(strings, data->BankLocator); + const char* ldevice = ffSmbiosLocateString(strings, data->DeviceLocator); + if (lbank && ldevice) { + ffStrbufSetF(&device->locator, "%s/%s", lbank, ldevice); + } else if (lbank) { + ffStrbufSetS(&device->locator, lbank); + } else if (ldevice) { + ffStrbufSetS(&device->locator, ldevice); + } + + const char* formFactorNames[] = { + NULL, // 0x00 (Placeholder for indexing) + "Other", // 0x01 + "Unknown", // 0x02 + "SIMM", // 0x03 + "SIP", // 0x04 + "Chip", // 0x05 + "DIP", // 0x06 + "ZIP", // 0x07 + "Proprietary Card", // 0x08 + "DIMM", // 0x09 + "TSOP", // 0x0A + "Row of chips", // 0x0B + "RIMM", // 0x0C + "SODIMM", // 0x0D + "SRIMM", // 0x0E + "FBDIMM", // 0x0F + "Die", // 0x10 + "CAMM", // 0x11 + "CUDIMM", // 0x12 + "CSODIMM", // 0x13 + }; + if (data->FormFactor > 0 && data->FormFactor < ARRAY_SIZE(formFactorNames)) { + ffStrbufSetS(&device->formFactor, formFactorNames[data->FormFactor]); + } else { + ffStrbufSetF(&device->formFactor, "Unknown (%d)", (int) data->FormFactor); + } + + const char* memoryTypeNames[] = { + NULL, // 0x00 (Placeholder for indexing) + "Other", // 0x01 + "Unknown", // 0x02 + "DRAM", // 0x03 + "EDRAM", // 0x04 + "VRAM", // 0x05 + "SRAM", // 0x06 + "RAM", // 0x07 + "ROM", // 0x08 + "FLASH", // 0x09 + "EEPROM", // 0x0A + "FEPROM", // 0x0B + "EPROM", // 0x0C + "CDRAM", // 0x0D + "3DRAM", // 0x0E + "SDRAM", // 0x0F + "SGRAM", // 0x10 + "RDRAM", // 0x11 + "DDR", // 0x12 + "DDR2", // 0x13 + "DDR2 FB-DIMM", // 0x14 + "Reserved", // 0x15 + "Reserved", // 0x16 + "Reserved", // 0x17 + "DDR3", // 0x18 + "FBD2", // 0x19 + "DDR4", // 0x1A + "LPDDR", // 0x1B + "LPDDR2", // 0x1C + "LPDDR3", // 0x1D + "LPDDR4", // 0x1E + "Logical non-volatile device", // 0x1F + "HBM", // 0x20 + "HBM2", // 0x21 + "DDR5", // 0x22 + "LPDDR5", // 0x23 + "HBM3", // 0x24 + "MRDIMM", // 0x25 + }; + if (!installed) { + ffStrbufSetStatic(&device->type, "Empty"); + } else if (data->MemoryType > 0 && data->MemoryType < ARRAY_SIZE(memoryTypeNames)) { + ffStrbufSetStatic(&device->type, memoryTypeNames[data->MemoryType]); + } else { + ffStrbufSetF(&device->type, "Unknown (%d)", (int) data->MemoryType); + } + + if (installed && data->Header.Length > offsetof(FFSmbiosMemoryDevice, Speed)) // 2.3+ + { + if (data->Speed) { + device->maxSpeed = data->Speed == 0xFFFF ? data->ExtendedSpeed : data->Speed; + } + + ffStrbufSetStatic(&device->vendor, ffSmbiosLocateString(strings, data->Manufacturer)); + ffCleanUpSmbiosValue(&device->vendor); + FFPhysicalMemoryUpdateVendorString(device); + + ffStrbufSetStatic(&device->serial, ffSmbiosLocateString(strings, data->SerialNumber)); + ffCleanUpSmbiosValue(&device->serial); + + ffStrbufSetStatic(&device->partNumber, ffSmbiosLocateString(strings, data->PartNumber)); + ffCleanUpSmbiosValue(&device->partNumber); + } + + if (installed && data->Header.Length > offsetof(FFSmbiosMemoryDevice, ConfiguredMemorySpeed)) // 2.7+ + { + if (data->ConfiguredMemorySpeed) { + device->runningSpeed = data->ConfiguredMemorySpeed == 0xFFFF + ? data->ExtendedConfiguredSpeed + : data->ConfiguredMemorySpeed; + } + } + } + + return NULL; +} diff --git a/src/detection/physicalmemory/physicalmemory_nosupport.c b/src/detection/physicalmemory/physicalmemory_nosupport.c new file mode 100644 index 0000000..2edb3a7 --- /dev/null +++ b/src/detection/physicalmemory/physicalmemory_nosupport.c @@ -0,0 +1,6 @@ +#include "physicalmemory.h" + +const char* ffDetectPhysicalMemory(FFPhysicalMemoryOptions* options, FFlist* result) { + FF_UNUSED(options, result); + return "Not supported on this platform"; +} diff --git a/src/detection/poweradapter/poweradapter.h b/src/detection/poweradapter/poweradapter.h new file mode 100644 index 0000000..2d889b7 --- /dev/null +++ b/src/detection/poweradapter/poweradapter.h @@ -0,0 +1,15 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/poweradapter/option.h" + +typedef struct FFPowerAdapterResult { + FFstrbuf description; + FFstrbuf name; + FFstrbuf modelName; + FFstrbuf manufacturer; + FFstrbuf serial; + int watts; +} FFPowerAdapterResult; + +const char* ffDetectPowerAdapter(FFlist* results); diff --git a/src/detection/poweradapter/poweradapter_apple.c b/src/detection/poweradapter/poweradapter_apple.c new file mode 100644 index 0000000..9a5008a --- /dev/null +++ b/src/detection/poweradapter/poweradapter_apple.c @@ -0,0 +1,39 @@ +#include "fastfetch.h" +#include "poweradapter.h" +#include "common/apple/cf_helpers.h" + +#include <IOKit/ps/IOPowerSources.h> +#include <IOKit/ps/IOPSKeys.h> + +const char* ffDetectPowerAdapter(FFlist* results) { + FF_CFTYPE_AUTO_RELEASE CFDictionaryRef details = IOPSCopyExternalPowerAdapterDetails(); + if (details && CFDictionaryContainsKey(details, CFSTR(kIOPSPowerAdapterWattsKey))) { + FFPowerAdapterResult* adapter = FF_LIST_ADD(FFPowerAdapterResult, *results); + + ffStrbufInit(&adapter->name); + ffStrbufInit(&adapter->description); + ffStrbufInit(&adapter->manufacturer); + ffStrbufInit(&adapter->modelName); + ffStrbufInit(&adapter->serial); + adapter->watts = 0; + + ffCfDictGetString(details, CFSTR(kIOPSNameKey), &adapter->name); + if (ffCfDictGetString(details, CFSTR("Model"), &adapter->modelName) != NULL) { + int adapterId; + if (ffCfDictGetInt(details, CFSTR(kIOPSPowerAdapterIDKey), &adapterId) == 0) { + ffStrbufSetF(&adapter->modelName, "%d", adapterId); + } + } + ffCfDictGetString(details, CFSTR("Manufacturer"), &adapter->manufacturer); + ffCfDictGetString(details, CFSTR("Description"), &adapter->description); + if (ffCfDictGetString(details, CFSTR("SerialString"), &adapter->serial) != NULL) { + int serialNumber; + if (ffCfDictGetInt(details, CFSTR(kIOPSPowerAdapterSerialNumberKey), &serialNumber) == 0) { + ffStrbufSetF(&adapter->serial, "%X", serialNumber); + } + } + ffCfDictGetInt(details, CFSTR(kIOPSPowerAdapterWattsKey), &adapter->watts); + } + + return NULL; +} diff --git a/src/detection/poweradapter/poweradapter_linux.c b/src/detection/poweradapter/poweradapter_linux.c new file mode 100644 index 0000000..fb555d5 --- /dev/null +++ b/src/detection/poweradapter/poweradapter_linux.c @@ -0,0 +1,91 @@ +#include "poweradapter.h" +#include "common/io.h" +#include "common/strutil.h" + +#include <dirent.h> +#include <unistd.h> +#include <fcntl.h> + +static void parsePowerAdapter(int dfd, const char* id, FFlist* results) { + FF_STRBUF_AUTO_DESTROY tmpBuffer = ffStrbufCreate(); + + { + char present = '\0'; + if (ffReadFileDataRelative(dfd, "present", 1, &present) && present == '0') { + return; + } + } + + // type must exist and be "Mains" + if (ffReadFileBufferRelative(dfd, "type", &tmpBuffer)) { + ffStrbufTrimRightSpace(&tmpBuffer); + } + + if (!ffStrbufEqualS(&tmpBuffer, "Mains")) { + return; + } + + // scope may not exist or must not be "Device" (?) + if (ffReadFileBufferRelative(dfd, "scope", &tmpBuffer)) { + ffStrbufTrimRightSpace(&tmpBuffer); + + if (ffStrbufEqualS(&tmpBuffer, "Device")) { + return; + } + } + + char online = '\0'; + ffReadFileDataRelative(dfd, "online", sizeof(online), &online); + + if (online != '1') { + return; + } + + // input_power_limit must exist and be not empty + if (!ffReadFileBufferRelative(dfd, "input_power_limit", &tmpBuffer) || tmpBuffer.length == 0) { + return; + } + + FFPowerAdapterResult* result = FF_LIST_ADD(FFPowerAdapterResult, *results); + ffStrbufInit(&result->name); + ffStrbufInit(&result->description); + result->watts = (int) (ffStrbufToDouble(&tmpBuffer, 0) / 1e6 + 0.5); + ffStrbufInit(&result->manufacturer); + ffStrbufInit(&result->modelName); + ffStrbufInit(&result->serial); + + if (ffReadFileBufferRelative(dfd, "manufacturer", &result->manufacturer)) { + ffStrbufTrimRightSpace(&result->manufacturer); + } else if (ffStrEquals(id, "macsmc-ac")) { // asahi + ffStrbufSetStatic(&result->manufacturer, "Apple Inc."); + } + + if (ffReadFileBufferRelative(dfd, "model_name", &result->modelName)) { + ffStrbufTrimRightSpace(&result->modelName); + } + + if (ffReadFileBufferRelative(dfd, "serial_number", &result->serial)) { + ffStrbufTrimRightSpace(&result->serial); + } +} + +const char* ffDetectPowerAdapter(FFlist* results) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir("/sys/class/power_supply/"); + if (dirp == NULL) { + return "opendir(\"/sys/class/power_supply/\") == NULL"; + } + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + + FF_AUTO_CLOSE_FD int dfd = openat(dirfd(dirp), entry->d_name, O_RDONLY | O_CLOEXEC); + if (dfd > 0) { + parsePowerAdapter(dfd, entry->d_name, results); + } + } + + return NULL; +} diff --git a/src/detection/poweradapter/poweradapter_nosupport.c b/src/detection/poweradapter/poweradapter_nosupport.c new file mode 100644 index 0000000..d5e725b --- /dev/null +++ b/src/detection/poweradapter/poweradapter_nosupport.c @@ -0,0 +1,5 @@ +#include "poweradapter.h" + +const char* ffDetectPowerAdapter(FF_A_UNUSED FFlist* results) { + return "Not supported on this platform"; +} diff --git a/src/detection/processes/processes.h b/src/detection/processes/processes.h new file mode 100644 index 0000000..69cf463 --- /dev/null +++ b/src/detection/processes/processes.h @@ -0,0 +1,5 @@ +#pragma once + +#include "fastfetch.h" + +const char* ffDetectProcesses(uint32_t* result); diff --git a/src/detection/processes/processes_bsd.c b/src/detection/processes/processes_bsd.c new file mode 100644 index 0000000..e3f02b8 --- /dev/null +++ b/src/detection/processes/processes_bsd.c @@ -0,0 +1,23 @@ +#include "processes.h" + +#include <sys/sysctl.h> +#ifdef __FreeBSD__ + #include <sys/types.h> + #include <sys/user.h> +#endif + +#ifndef KERN_PROC_PROC + #define KERN_PROC_PROC KERN_PROC_ALL // Apple +#endif + +const char* ffDetectProcesses(uint32_t* result) { + int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_PROC }; + size_t length; + + if (sysctl(request, ARRAY_SIZE(request), NULL, &length, NULL, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC, KERN_PROC_PROC}) failed"; + } + + *result = (uint32_t) (length / sizeof(struct kinfo_proc)); + return NULL; +} diff --git a/src/detection/processes/processes_haiku.c b/src/detection/processes/processes_haiku.c new file mode 100644 index 0000000..c50e62d --- /dev/null +++ b/src/detection/processes/processes_haiku.c @@ -0,0 +1,14 @@ +#include "processes.h" + +#include <OS.h> + +const char* ffDetectProcesses(uint32_t* result) { + system_info info; + if (get_system_info(&info) != B_OK) { + return "Error getting system info"; + } + + *result = info.used_teams; + + return NULL; +} diff --git a/src/detection/processes/processes_linux.c b/src/detection/processes/processes_linux.c new file mode 100644 index 0000000..dee8c09 --- /dev/null +++ b/src/detection/processes/processes_linux.c @@ -0,0 +1,27 @@ +#include "processes.h" + +#include "common/io.h" +#include "common/strutil.h" + +const char* ffDetectProcesses(uint32_t* result) { + FF_AUTO_CLOSE_DIR DIR* dir = opendir("/proc"); + if (dir == NULL) { + return "opendir(\"/proc\") failed"; + } + + uint32_t num = 0; + + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) { + if ( +#ifdef _DIRENT_HAVE_D_TYPE + (entry->d_type == DT_DIR || entry->d_type == DT_UNKNOWN) && +#endif + ffCharIsDigit(entry->d_name[0])) + ++num; + } + + *result = num; + + return NULL; +} diff --git a/src/detection/processes/processes_nbsd.c b/src/detection/processes/processes_nbsd.c new file mode 100644 index 0000000..efd3d6f --- /dev/null +++ b/src/detection/processes/processes_nbsd.c @@ -0,0 +1,15 @@ +#include "processes.h" + +#include <sys/sysctl.h> + +const char* ffDetectProcesses(uint32_t* result) { + int request[] = { CTL_KERN, KERN_PROC2, KERN_PROC_ALL, -1, sizeof(struct kinfo_proc2), 0 }; + size_t length = 0; + + if (sysctl(request, ARRAY_SIZE(request), NULL, &length, NULL, 0) != 0) { + return "sysctl({CTL_KERN, KERN_PROC2, KERN_PROC_ALL}) failed"; + } + + *result = (uint32_t) (length / sizeof(struct kinfo_proc2)); + return NULL; +} diff --git a/src/detection/processes/processes_nosupport.c b/src/detection/processes/processes_nosupport.c new file mode 100644 index 0000000..7da208e --- /dev/null +++ b/src/detection/processes/processes_nosupport.c @@ -0,0 +1,5 @@ +#include "processes.h" + +const char* ffDetectProcesses(uint32_t* result) { + return "Not supported on this platform"; +} diff --git a/src/detection/processes/processes_obsd.c b/src/detection/processes/processes_obsd.c new file mode 100644 index 0000000..6246d8f --- /dev/null +++ b/src/detection/processes/processes_obsd.c @@ -0,0 +1,15 @@ +#include "processes.h" + +#include <sys/param.h> +#include <sys/sysctl.h> +#include <kvm.h> + +const char* ffDetectProcesses(uint32_t* result) { + kvm_t* kd = kvm_open(NULL, NULL, NULL, KVM_NO_FILES, NULL); + const void* ret = kvm_getprocs(kd, KERN_PROC_ALL, 0, 1, result); + kvm_close(kd); + if (!ret) { + return "kvm_getprocs() failed"; + } + return NULL; +} diff --git a/src/detection/processes/processes_windows.c b/src/detection/processes/processes_windows.c new file mode 100644 index 0000000..03b34db --- /dev/null +++ b/src/detection/processes/processes_windows.c @@ -0,0 +1,34 @@ +#include "processes.h" +#include "common/mallocHelper.h" + +#include <ntstatus.h> +#include <winternl.h> + +const char* ffDetectProcesses(uint32_t* result) { + SYSTEM_PROCESS_INFORMATION* FF_AUTO_FREE pstart = NULL; + + // Multiple attempts in case processes change while + // we are in the middle of querying them. + ULONG size = 0; + for (int attempts = 0;; ++attempts) { + if (size) { + pstart = (SYSTEM_PROCESS_INFORMATION*) realloc(pstart, size); + assert(pstart); + } + NTSTATUS status = NtQuerySystemInformation(SystemProcessInformation, pstart, size, &size); + if (NT_SUCCESS(status)) { + break; + } else if (status == STATUS_INFO_LENGTH_MISMATCH && attempts < 4) { + size += sizeof(SYSTEM_PROCESS_INFORMATION) * 5; + } else { + return "NtQuerySystemInformation(SystemProcessInformation) failed"; + } + } + + *result = 1; // Init with 1 because we test for ptr->NextEntryOffset + for (SYSTEM_PROCESS_INFORMATION* ptr = pstart; ptr->NextEntryOffset; ptr = (SYSTEM_PROCESS_INFORMATION*) ((uint8_t*) ptr + ptr->NextEntryOffset)) { + ++*result; + } + + return NULL; +} diff --git a/src/detection/publicip/publicip.c b/src/detection/publicip/publicip.c new file mode 100644 index 0000000..abca57c --- /dev/null +++ b/src/detection/publicip/publicip.c @@ -0,0 +1,95 @@ +#include "publicip.h" +#include "common/networking.h" + +#define FF_UNINITIALIZED ((const char*) (uintptr_t) -1) +static FFNetworkingState states[2]; +static const char* statuses[2] = { FF_UNINITIALIZED, FF_UNINITIALIZED }; + +void ffPreparePublicIp(FFPublicIPOptions* options) { + FFNetworkingState* state = &states[options->ipv6]; + const char** status = &statuses[options->ipv6]; + if (*status != FF_UNINITIALIZED) { + fputs("Error: PublicIp module can only be used once due to internal limitations\n", stderr); + exit(1); + } + + state->timeout = options->timeout; + state->ipv6 = options->ipv6; + + if (options->url.length == 0) { + state->compression = true; + state->tfo = true; + *status = ffNetworkingSendHttpRequest(state, options->ipv6 ? "v6.ipinfo.io" : "ipinfo.io", "/json", NULL); + } else { + FF_STRBUF_AUTO_DESTROY host = ffStrbufCreateCopy(&options->url); + uint32_t hostStartIndex = ffStrbufFirstIndexS(&host, "://"); + if (hostStartIndex < host.length) { + if (hostStartIndex != 4 || !ffStrbufStartsWithIgnCaseS(&host, "http")) { + fputs("Error: only http: protocol is supported. Use `Command` module with `curl` if needed\n", stderr); + exit(1); + } + ffStrbufSubstrAfter(&host, hostStartIndex + (uint32_t) (strlen("://") - 1)); + } + uint32_t pathStartIndex = ffStrbufFirstIndexC(&host, '/'); + + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + if (pathStartIndex != host.length) { + ffStrbufAppendNS(&path, pathStartIndex, host.chars + (host.length - pathStartIndex)); + host.length = pathStartIndex; + host.chars[pathStartIndex] = '\0'; + } + + *status = ffNetworkingSendHttpRequest(state, host.chars, path.length == 0 ? "/" : path.chars, NULL); + } +} + +static inline void wrapYyjsonFree(yyjson_doc** doc) { + assert(doc); + if (*doc) { + yyjson_doc_free(*doc); + } +} + +const char* ffDetectPublicIp(FFPublicIPOptions* options, FFPublicIpResult* result) { + FFNetworkingState* state = &states[options->ipv6]; + const char** status = &statuses[options->ipv6]; + if (*status == FF_UNINITIALIZED) { + ffPreparePublicIp(options); + } + + if (*status != NULL) { + return *status; + } + + FF_STRBUF_AUTO_DESTROY response = ffStrbufCreateA(4096); + const char* error = ffNetworkingRecvHttpResponse(state, &response); + + *state = (FFNetworkingState) {}; + *status = FF_UNINITIALIZED; + + if (error == NULL) { + ffStrbufSubstrAfterFirstS(&response, "\r\n\r\n"); + } else { + return error; + } + + if (response.length == 0) { + return "Empty server response received"; + } + + if (options->url.length == 0) { + yyjson_doc* FF_A_CLEANUP(wrapYyjsonFree) doc = yyjson_read_opts(response.chars, response.length, 0, NULL, NULL); + if (doc) { + yyjson_val* root = yyjson_doc_get_root(doc); + ffStrbufAppendJsonVal(&result->ip, yyjson_obj_get(root, "ip")); + ffStrbufDestroy(&result->location); + ffStrbufInitF(&result->location, "%s, %s", yyjson_get_str(yyjson_obj_get(root, "city")), yyjson_get_str(yyjson_obj_get(root, "country"))); + return NULL; + } + } + + ffStrbufDestroy(&result->ip); + ffStrbufInitMove(&result->ip, &response); + ffStrbufTrimRightSpace(&result->ip); + return NULL; +} diff --git a/src/detection/publicip/publicip.h b/src/detection/publicip/publicip.h new file mode 100644 index 0000000..e8f9702 --- /dev/null +++ b/src/detection/publicip/publicip.h @@ -0,0 +1,12 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/publicip/option.h" + +typedef struct FFPublicIpResult { + FFstrbuf ip; + FFstrbuf location; +} FFPublicIpResult; + +void ffPreparePublicIp(FFPublicIPOptions* options); +const char* ffDetectPublicIp(FFPublicIPOptions* options, FFPublicIpResult* result); diff --git a/src/detection/sound/audio_oss_sunos.h b/src/detection/sound/audio_oss_sunos.h new file mode 100644 index 0000000..4e0e760 --- /dev/null +++ b/src/detection/sound/audio_oss_sunos.h @@ -0,0 +1,898 @@ +/* + * CDDL HEADER START + * + * The contents of this file are subject to the terms of the + * Common Development and Distribution License (the "License"). + * You may not use this file except in compliance with the License. + * + * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE + * or http://www.opensolaris.org/os/licensing. + * See the License for the specific language governing permissions + * and limitations under the License. + * + * When distributing Covered Code, include this CDDL HEADER in each + * file and include the License file at usr/src/OPENSOLARIS.LICENSE. + * If applicable, add the following below this CDDL HEADER, with the + * fields enclosed by brackets "[]" replaced with your own identifying + * information: Portions Copyright [yyyy] [name of copyright owner] + * + * CDDL HEADER END + */ +/* + * Copyright (C) 4Front Technologies 1996-2008. + * + * Copyright 2009 Sun Microsystems, Inc. All rights reserved. + * Use is subject to license terms. + */ + +#ifndef _SYS_AUDIO_OSS_H +#define _SYS_AUDIO_OSS_H + +#include <sys/types.h> +#include <sys/time.h> + +/* + * These are the ioctl calls for all Solaris /dev/dsp and /dev/mixer audio + * devices. + * + * Note that the contents of this file include definitions which exist + * primarily for compatibility. Many of the defines here are not + * actually implemented, but exist solely to facilitate compilation of + * programs from other operating systems. Other definitions here may + * not be fully supported or may otherwise be obsolete. There are many + * things in this file which should not be used on SunOS. + * + * Please read the documentation to determine which portions of the + * API are fully supported and recommended for use in new + * applications. + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Buffer status queries. + * SNDCTL_DSP_GETOSPACE and SNDCTL_DSP_GETISPACE + */ +typedef struct audio_buf_info { + int fragments; /* # of available fragments */ + int fragstotal; /* Total # of fragments allocated */ + int fragsize; /* Size of a fragment in bytes */ + int bytes; /* Available space in bytes */ + /* Note! 'bytes' could be more than fragments*fragsize */ +} audio_buf_info; + +/* + * Sync groups for audio devices. + * SNDCTL_DSP_SYNCGROUP and SNDCTL_DSP_SYNCSTART + */ +typedef struct oss_syncgroup { + int id; + int mode; + int filler[16]; +} oss_syncgroup; + +/* + * SNDCTL_DSP_GETERROR + */ +typedef struct audio_errinfo { + int play_underruns; + int rec_overruns; + unsigned int play_ptradjust; + unsigned int rec_ptradjust; + int play_errorcount; + int rec_errorcount; + int play_lasterror; + int rec_lasterror; + int play_errorparm; + int rec_errorparm; + int filler[16]; +} audio_errinfo; + +/* + * SNDCTL_DSP_GETIPTR and SNDCTL_DSP_GETOPTR + */ +typedef struct count_info { + unsigned int bytes; /* Total # of bytes processed */ + int blocks; /* # of fragment transitions since last time */ + int ptr; /* Current DMA pointer value */ +} count_info; + +/* + * SNDCTL_DSP_CURENT_IPTR and SNDCTL_DSP_CURRENT_OPTR + */ +typedef struct { + long long samples; /* Total # of samples */ + int fifo_samples; /* Samples in device FIFO */ + int filler[32]; /* For future use */ +} oss_count_t; + +/* + * SNDCTL_DSP_GET_RECSRC_NAMES and SNDCTL_DSP_GET_PLAYTGT_NAMES + */ +#define OSS_ENUM_MAXVALUE 255 +typedef struct oss_mixer_enuminfo { + int dev; + int ctrl; + int nvalues; + int version; + short strindex[OSS_ENUM_MAXVALUE]; + char strings[3000]; +} oss_mixer_enuminfo; + +/* + * Digital interface (S/PDIF) control interface + * SNDCTL_DSP_READCTL and SNDCTL_DSP_WRITECTL + */ +typedef struct oss_digital_control { + unsigned int caps; +#define DIG_CBITIN_NONE 0x00000000 +#define DIG_CBITIN_LIMITED 0x00000001 +#define DIG_CBITIN_DATA 0x00000002 +#define DIG_CBITIN_BYTE0 0x00000004 +#define DIG_CBITIN_FULL 0x00000008 +#define DIG_CBITIN_MASK 0x0000000f +#define DIG_CBITOUT_NONE 0x00000000 +#define DIG_CBITOUT_LIMITED 0x00000010 +#define DIG_CBITOUT_BYTE0 0x00000020 +#define DIG_CBITOUT_FULL 0x00000040 +#define DIG_CBITOUT_DATA 0x00000080 +#define DIG_CBITOUT_MASK 0x000000f0 +#define DIG_UBITIN 0x00000100 +#define DIG_UBITOUT 0x00000200 +#define DIG_VBITOUT 0x00000400 +#define DIG_OUTRATE 0x00000800 +#define DIG_INRATE 0x00001000 +#define DIG_INBITS 0x00002000 +#define DIG_OUTBITS 0x00004000 +#define DIG_EXACT 0x00010000 +#define DIG_PRO 0x00020000 +#define DIG_CONSUMER 0x00040000 +#define DIG_PASSTHROUGH 0x00080000 +#define DIG_OUTSEL 0x00100000 + + unsigned int valid; +#define VAL_CBITIN 0x00000001 +#define VAL_UBITIN 0x00000002 +#define VAL_CBITOUT 0x00000004 +#define VAL_UBITOUT 0x00000008 +#define VAL_ISTATUS 0x00000010 +#define VAL_IRATE 0x00000020 +#define VAL_ORATE 0x00000040 +#define VAL_INBITS 0x00000080 +#define VAL_OUTBITS 0x00000100 +#define VAL_REQUEST 0x00000200 +#define VAL_OUTSEL 0x00000400 + +#define VAL_OUTMASK (VAL_CBITOUT | VAL_UBITOUT | VAL_ORATE | VAL_OUTBITS | VAL_OUTSEL) + + unsigned int request; + unsigned int param; +#define SPD_RQ_PASSTHROUGH 1 + + unsigned char cbitin[24]; + unsigned char ubitin[24]; + unsigned char cbitout[24]; + unsigned char ubitout[24]; + + unsigned int outsel; +#define OUTSEL_DIGITAL 1 +#define OUTSEL_ANALOG 2 +#define OUTSEL_BOTH (OUTSEL_DIGITAL | OUTSEL_ANALOG) + + int in_data; /* Audio/data if autodetectable by receiver */ +#define IND_UNKNOWN 0 +#define IND_AUDIO 1 +#define IND_DATA 2 + + int in_locked; /* Receiver locked */ +#define LOCK_NOT_INDICATED 0 +#define LOCK_UNLOCKED 1 +#define LOCK_LOCKED 2 + + int in_quality; /* Input signal quality */ +#define IN_QUAL_NOT_INDICATED 0 +#define IN_QUAL_POOR 1 +#define IN_QUAL_GOOD 2 + + int in_vbit; + int out_vbit; /* V bits */ +#define VBIT_NOT_INDICATED 0 +#define VBIT_OFF 1 +#define VBIT_ON 2 + + unsigned int in_errors; /* Various input error conditions */ +#define INERR_CRC 0x0001 +#define INERR_QCODE_CRC 0x0002 +#define INERR_PARITY 0x0004 +#define INERR_BIPHASE 0x0008 + + int srate_in; + int srate_out; + int bits_in; + int bits_out; + + int filler[32]; +} oss_digital_control; + +/* + * The "new" mixer API. + * + * This improved mixer API makes it possible to access every possible feature + * of every possible device. However you should read the mixer programming + * section of the OSS API Developer's Manual. There is no chance that you + * could use this interface correctly just by examining this header. + */ +#define OSS_VERSION 0x040003 +#define SOUND_VERSION OSS_VERSION + +typedef struct oss_sysinfo { + char product[32]; /* E.g. SunOS Audio */ + char version[32]; /* E.g. 4.0a */ + int versionnum; /* See OSS_GETVERSION */ + char options[128]; /* NOT SUPPORTED */ + + int numaudios; /* # of audio/dsp devices */ + int openedaudio[8]; /* Mask of audio devices are busy */ + + int numsynths; /* NOT SUPPORTED, always 0 */ + int nummidis; /* NOT SUPPORTED, always 0 */ + int numtimers; /* NOT SUPPORTED, always 0 */ + int nummixers; /* # of mixer devices */ + + int openedmidi[8]; /* Mask of midi devices are busy */ + int numcards; /* Number of sound cards in the system */ + int numaudioengines; /* Number of audio engines in the system */ + char license[16]; /* E.g. "GPL" or "CDDL" */ + char revision_info[256]; /* For internal use */ + int filler[172]; /* For future expansion */ +} oss_sysinfo; + +typedef struct oss_mixext { + int dev; /* Mixer device number */ + int ctrl; /* Extension number */ + int type; /* Entry type */ +#define MIXT_DEVROOT 0 /* Device root entry */ +#define MIXT_GROUP 1 /* Controller group */ +#define MIXT_ONOFF 2 /* OFF (0) or ON (1) */ +#define MIXT_ENUM 3 /* Enumerated (0 to maxvalue) */ +#define MIXT_MONOSLIDER 4 /* Mono slider (0 to 255) */ +#define MIXT_STEREOSLIDER 5 /* Stereo slider (dual 0 to 255) */ +#define MIXT_MESSAGE 6 /* (Readable) textual message */ +#define MIXT_MONOVU 7 /* VU meter value (mono) */ +#define MIXT_STEREOVU 8 /* VU meter value (stereo) */ +#define MIXT_MONOPEAK 9 /* VU meter peak value (mono) */ +#define MIXT_STEREOPEAK 10 /* VU meter peak value (stereo) */ +#define MIXT_RADIOGROUP 11 /* Radio button group */ +#define MIXT_MARKER 12 /* Separator between entries */ +#define MIXT_VALUE 13 /* Decimal value entry */ +#define MIXT_HEXVALUE 14 /* Hexadecimal value entry */ +#define MIXT_MONODB 15 /* OBSOLETE */ +#define MIXT_STEREODB 16 /* OBSOLETE */ +#define MIXT_SLIDER 17 /* Slider (mono, 31 bit int range) */ +#define MIXT_3D 18 +#define MIXT_MONOSLIDER16 19 /* Mono slider (0-32767) */ +#define MIXT_STEREOSLIDER16 20 /* Stereo slider (dual 0-32767) */ +#define MIXT_MUTE 21 /* Mute=1, unmute=0 */ + + /* Possible value range (minvalue to maxvalue) */ + /* Note that maxvalue may also be smaller than minvalue */ + int maxvalue; + int minvalue; + + int flags; +#define MIXF_READABLE 0x00000001 /* Has readable value */ +#define MIXF_WRITEABLE 0x00000002 /* Has writeable value */ +#define MIXF_POLL 0x00000004 /* May change itself */ +#define MIXF_HZ 0x00000008 /* Hertz scale */ +#define MIXF_STRING 0x00000010 /* Use dynamic extensions for value */ +#define MIXF_DYNAMIC 0x00000010 /* Supports dynamic extensions */ +#define MIXF_OKFAIL 0x00000020 /* Interpret value as 1=OK, 0=FAIL */ +#define MIXF_FLAT 0x00000040 /* NOT SUPPORTED */ +#define MIXF_LEGACY 0x00000080 /* NOT SUPPORTED */ +#define MIXF_CENTIBEL 0x00000100 /* Centibel (0.1 dB) step size */ +#define MIXF_DECIBEL 0x00000200 /* Step size of 1 dB */ +#define MIXF_MAINVOL 0x00000400 /* Main volume control */ +#define MIXF_PCMVOL 0x00000800 /* PCM output volume control */ +#define MIXF_RECVOL 0x00001000 /* PCM recording volume control */ +#define MIXF_MONVOL 0x00002000 /* Input->output monitor volume */ +#define MIXF_WIDE 0x00004000 /* NOT SUPPORTED */ +#define MIXF_DESCR 0x00008000 /* NOT SUPPORTED */ +#define MIXF_DISABLE 0x00010000 /* Control has been disabled */ + + char id[16]; /* Mnemonic ID (internal use) */ + int parent; /* Entry# of parent (-1 if root) */ + + int dummy; /* NOT SUPPORTED */ + + int timestamp; + + char data[64]; /* Misc data (entry type dependent) */ + unsigned char enum_present[32]; /* Mask of allowed enum values */ + int control_no; /* NOT SUPPORTED, always -1 */ + + unsigned int desc; /* Scope flags, etc */ +#define MIXEXT_SCOPE_MASK 0x0000003f +#define MIXEXT_SCOPE_OTHER 0x00000000 +#define MIXEXT_SCOPE_INPUT 0x00000001 +#define MIXEXT_SCOPE_OUTPUT 0x00000002 +#define MIXEXT_SCOPE_MONITOR 0x00000003 +#define MIXEXT_SCOPE_RECSWITCH 0x00000004 + + char extname[32]; + int update_counter; +#ifdef _KERNEL + int filler[6]; + int enumbit; +#else + int filler[7]; +#endif +} oss_mixext; + +typedef struct oss_mixext_root { + char id[16]; + char name[48]; +} oss_mixext_root; + +typedef struct oss_mixer_value { + int dev; + int ctrl; + int value; + int flags; /* Reserved for future use. Initialize to 0 */ + int timestamp; /* Must be set to oss_mixext.timestamp */ + int filler[8]; /* Reserved for future use. Initialize to 0 */ +} oss_mixer_value; + +#define OSS_LONGNAME_SIZE 64 +#define OSS_LABEL_SIZE 16 +#define OSS_DEVNODE_SIZE 32 +typedef char oss_longname_t[OSS_LONGNAME_SIZE]; +typedef char oss_label_t[OSS_LABEL_SIZE]; +typedef char oss_devnode_t[OSS_DEVNODE_SIZE]; + +typedef struct oss_audioinfo { + int dev; /* Audio device number */ + char name[64]; + int busy; /* 0, OPEN_READ, OPEN_WRITE, OPEN_READWRITE */ + int pid; /* Process ID, not used in SunOS */ + int caps; /* PCM_CAP_INPUT, PCM_CAP_OUTPUT */ + int iformats; /* Supported input formats */ + int oformats; /* Supported output formats */ + int magic; /* Internal use only */ + char cmd[64]; /* Command using the device (if known) */ + int card_number; + int port_number; + int mixer_dev; + int legacy_device; /* Obsolete field. Replaced by devnode */ + int enabled; /* 1=enabled, 0=device not ready */ + int flags; /* internal use only - no practical meaning */ + int min_rate; /* Minimum sample rate */ + int max_rate; /* Maximum sample rate */ + int min_channels; /* Minimum number of channels */ + int max_channels; /* Maximum number of channels */ + int binding; /* DSP_BIND_FRONT, etc. 0 means undefined */ + int rate_source; + char handle[32]; +#define OSS_MAX_SAMPLE_RATES 20 /* Cannot be changed */ + unsigned int nrates; /* Array of supported sample rates */ + unsigned int rates[OSS_MAX_SAMPLE_RATES]; + oss_longname_t song_name; /* Song name (if given) */ + oss_label_t label; /* Device label (if given) */ + int latency; /* In usecs, -1=unknown */ + oss_devnode_t devnode; /* Device special file name (absolute path) */ + int next_play_engine; + int next_rec_engine; + int filler[184]; +} oss_audioinfo; + +typedef struct oss_mixerinfo { + int dev; + char id[16]; + char name[32]; + int modify_counter; + int card_number; + int port_number; + char handle[32]; + int magic; /* Reserved */ + int enabled; /* Reserved */ + int caps; +#define MIXER_CAP_VIRTUAL 0x00000001 +#define MIXER_CAP_LAYOUT_B 0x00000002 /* For internal use only */ +#define MIXER_CAP_NARROW 0x00000004 /* Conserve horiz space */ + int flags; /* Reserved */ + int nrext; + /* + * The priority field can be used to select the default + * (motherboard) mixer device. The mixer with the highest + * priority is the most preferred one. -2 or less means that + * this device cannot be used as the default mixer. + */ + int priority; + oss_devnode_t devnode; /* Device special file name (absolute path) */ + int legacy_device; + int filler[245]; /* Reserved */ +} oss_mixerinfo; + +typedef struct oss_card_info { + int card; + char shortname[16]; + char longname[128]; + int flags; + char hw_info[400]; + int intr_count; + int ack_count; + int filler[154]; +} oss_card_info; + +typedef struct mixer_info { /* OBSOLETE */ + char id[16]; + char name[32]; + int modify_counter; + int card_number; + int port_number; + char handle[32]; +} mixer_info; + +#define MAX_PEAK_CHANNELS 128 +typedef unsigned short oss_peaks_t[MAX_PEAK_CHANNELS]; + +/* For use with SNDCTL_DSP_GET_CHNORDER */ +#define CHID_UNDEF 0 +#define CHID_L 1 +#define CHID_R 2 +#define CHID_C 3 +#define CHID_LFE 4 +#define CHID_LS 5 +#define CHID_RS 6 +#define CHID_LR 7 +#define CHID_RR 8 +#define CHNORDER_UNDEF 0x0000000000000000ULL +#define CHNORDER_NORMAL 0x0000000087654321ULL + +#define OSSIOCPARM_MASK 0x1fff /* parameters must be < 8192 bytes */ +#define OSSIOC_VOID 0x00000000 /* no parameters */ +#define OSSIOC_OUT 0x20000000 /* copy out parameters */ +#define OSSIOC_IN 0x40000000 /* copy in parameters */ +#define OSSIOC_INOUT (OSSIOC_IN | OSSIOC_OUT) +#define OSSIOC_SZ(t) ((sizeof(t) & OSSIOCPARM_MASK) << 16) +#define OSSIOC_GETSZ(x) (((x) >> 16) & OSSIOCPARM_MASK) + +#define __OSSIO(x, y) ((int) (OSSIOC_VOID | (x << 8) | y)) +#define __OSSIOR(x, y, t) ((int) (OSSIOC_OUT | OSSIOC_SZ(t) | (x << 8) | y)) +#define __OSSIOW(x, y, t) ((int) (OSSIOC_IN | OSSIOC_SZ(t) | (x << 8) | y)) +#define __OSSIOWR(x, y, t) ((int) (OSSIOC_INOUT | OSSIOC_SZ(t) | (x << 8) | y)) + +#define SNDCTL_SYSINFO __OSSIOR('X', 1, oss_sysinfo) +#define OSS_SYSINFO SNDCTL_SYSINFO /* Old name */ + +#define SNDCTL_MIX_NRMIX __OSSIOR('X', 2, int) +#define SNDCTL_MIX_NREXT __OSSIOWR('X', 3, int) +#define SNDCTL_MIX_EXTINFO __OSSIOWR('X', 4, oss_mixext) +#define SNDCTL_MIX_READ __OSSIOWR('X', 5, oss_mixer_value) +#define SNDCTL_MIX_WRITE __OSSIOWR('X', 6, oss_mixer_value) + +#define SNDCTL_AUDIOINFO __OSSIOWR('X', 7, oss_audioinfo) +#define SNDCTL_MIX_ENUMINFO __OSSIOWR('X', 8, oss_mixer_enuminfo) +#define SNDCTL_MIDIINFO __OSSIO('X', 9) +#define SNDCTL_MIXERINFO __OSSIOWR('X', 10, oss_mixerinfo) +#define SNDCTL_CARDINFO __OSSIOWR('X', 11, oss_card_info) +#define SNDCTL_ENGINEINFO __OSSIOWR('X', 12, oss_audioinfo) +#define SNDCTL_AUDIOINFO_EX __OSSIOWR('X', 13, oss_audioinfo) +#define SNDCTL_MIX_DESCRIPTION __OSSIOWR('X', 14, oss_mixer_enuminfo) + +/* ioctl codes 'X', 200-255 are reserved for internal use */ + +/* + * Few more "globally" available ioctl calls. + */ +#define SNDCTL_SETSONG __OSSIOW('Y', 2, oss_longname_t) +#define SNDCTL_GETSONG __OSSIOR('Y', 2, oss_longname_t) +#define SNDCTL_SETNAME __OSSIOW('Y', 3, oss_longname_t) +#define SNDCTL_SETLABEL __OSSIOW('Y', 4, oss_label_t) +#define SNDCTL_GETLABEL __OSSIOR('Y', 4, oss_label_t) + +/* + * IOCTL commands for /dev/dsp + */ +#define SNDCTL_DSP_HALT __OSSIO('P', 0) +#define SNDCTL_DSP_RESET SNDCTL_DSP_HALT /* Old name */ +#define SNDCTL_DSP_SYNC __OSSIO('P', 1) +#define SNDCTL_DSP_SPEED __OSSIOWR('P', 2, int) + +#define SNDCTL_DSP_STEREO __OSSIOWR('P', 3, int) /* OBSOLETE */ + +#define SNDCTL_DSP_GETBLKSIZE __OSSIOWR('P', 4, int) +#define SNDCTL_DSP_SAMPLESIZE SNDCTL_DSP_SETFMT +#define SNDCTL_DSP_CHANNELS __OSSIOWR('P', 6, int) +#define SNDCTL_DSP_POST __OSSIO('P', 8) +#define SNDCTL_DSP_SUBDIVIDE __OSSIOWR('P', 9, int) +#define SNDCTL_DSP_SETFRAGMENT __OSSIOWR('P', 10, int) + +#define SNDCTL_DSP_GETFMTS __OSSIOR('P', 11, int) /* Returns a mask */ +#define SNDCTL_DSP_SETFMT __OSSIOWR('P', 5, int) /* Selects ONE fmt */ + +#define SNDCTL_DSP_GETOSPACE __OSSIOR('P', 12, audio_buf_info) +#define SNDCTL_DSP_GETISPACE __OSSIOR('P', 13, audio_buf_info) +#define SNDCTL_DSP_NONBLOCK __OSSIO('P', 14) /* Obsolete */ +#define SNDCTL_DSP_GETCAPS __OSSIOR('P', 15, int) + +#define SNDCTL_DSP_GETTRIGGER __OSSIOR('P', 16, int) +#define SNDCTL_DSP_SETTRIGGER __OSSIOW('P', 16, int) + +#define SNDCTL_DSP_GETIPTR __OSSIOR('P', 17, count_info) +#define SNDCTL_DSP_GETOPTR __OSSIOR('P', 18, count_info) + +#define SNDCTL_DSP_SETSYNCRO __OSSIO('P', 21) +#define SNDCTL_DSP_SETDUPLEX __OSSIO('P', 22) + +#define SNDCTL_DSP_PROFILE __OSSIOW('P', 23, int) /* OBSOLETE */ +#define APF_NORMAL 0 /* Normal applications */ +#define APF_NETWORK 1 /* Underruns caused by "external" delay */ +#define APF_CPUINTENS 2 /* Underruns caused by "overheating" the CPU */ + +#define SNDCTL_DSP_GETODELAY __OSSIOR('P', 23, int) + +#define SNDCTL_DSP_GETPLAYVOL __OSSIOR('P', 24, int) +#define SNDCTL_DSP_SETPLAYVOL __OSSIOWR('P', 24, int) +#define SNDCTL_DSP_GETERROR __OSSIOR('P', 25, audio_errinfo) + +#define SNDCTL_DSP_READCTL __OSSIOWR('P', 26, oss_digital_control) +#define SNDCTL_DSP_WRITECTL __OSSIOWR('P', 27, oss_digital_control) + +#define SNDCTL_DSP_SYNCGROUP __OSSIOWR('P', 28, oss_syncgroup) +#define SNDCTL_DSP_SYNCSTART __OSSIOW('P', 29, int) + +#define SNDCTL_DSP_COOKEDMODE __OSSIOW('P', 30, int) + +#define SNDCTL_DSP_SILENCE __OSSIO('P', 31) +#define SNDCTL_DSP_SKIP __OSSIO('P', 32) + +#define SNDCTL_DSP_HALT_INPUT __OSSIO('P', 33) +#define SNDCTL_DSP_RESET_INPUT SNDCTL_DSP_HALT_INPUT /* Old name */ +#define SNDCTL_DSP_HALT_OUTPUT __OSSIO('P', 34) +#define SNDCTL_DSP_RESET_OUTPUT SNDCTL_DSP_HALT_OUTPUT /* Old name */ + +#define SNDCTL_DSP_LOW_WATER __OSSIOW('P', 34, int) + +#define SNDCTL_DSP_CURRENT_IPTR __OSSIOR('P', 35, oss_count_t) +#define SNDCTL_DSP_CURRENT_OPTR __OSSIOR('P', 36, oss_count_t) + +#define SNDCTL_DSP_GET_RECSRC_NAMES __OSSIOR('P', 37, oss_mixer_enuminfo) +#define SNDCTL_DSP_GET_RECSRC __OSSIOR('P', 38, int) +#define SNDCTL_DSP_SET_RECSRC __OSSIOWR('P', 38, int) + +#define SNDCTL_DSP_GET_PLAYTGT_NAMES __OSSIOR('P', 39, oss_mixer_enuminfo) +#define SNDCTL_DSP_GET_PLAYTGT __OSSIOR('P', 40, int) +#define SNDCTL_DSP_SET_PLAYTGT __OSSIOWR('P', 40, int) +#define SNDCTL_DSP_GETRECVOL __OSSIOR('P', 41, int) +#define SNDCTL_DSP_SETRECVOL __OSSIOWR('P', 41, int) + +#define SNDCTL_DSP_GET_CHNORDER __OSSIOR('P', 42, unsigned long long) +#define SNDCTL_DSP_SET_CHNORDER __OSSIOWR('P', 42, unsigned long long) + +#define SNDCTL_DSP_GETIPEAKS __OSSIOR('P', 43, oss_peaks_t) +#define SNDCTL_DSP_GETOPEAKS __OSSIOR('P', 44, oss_peaks_t) + +#define SNDCTL_DSP_POLICY __OSSIOW('P', 45, int) /* See the manual */ + +#define SNDCTL_DSP_GETCHANNELMASK __OSSIOWR('P', 64, int) +#define SNDCTL_DSP_BIND_CHANNEL __OSSIOWR('P', 65, int) + +/* + * These definitions are here for the benefit of compiling application + * code. Most of these are NOT implemented in the Solaris code, + * however. This is the older 3.x OSS API, and only the master input and + * output levels are actually supported. + */ +#define SOUND_MIXER_NRDEVICES 28 +#define SOUND_MIXER_VOLUME 0 +#define SOUND_MIXER_BASS 1 +#define SOUND_MIXER_TREBLE 2 +#define SOUND_MIXER_SYNTH 3 +#define SOUND_MIXER_PCM 4 +#define SOUND_MIXER_SPEAKER 5 +#define SOUND_MIXER_LINE 6 +#define SOUND_MIXER_MIC 7 +#define SOUND_MIXER_CD 8 +#define SOUND_MIXER_IMIX 9 /* Recording monitor */ +#define SOUND_MIXER_ALTPCM 10 +#define SOUND_MIXER_RECLEV 11 /* Recording level */ +#define SOUND_MIXER_IGAIN 12 /* Input gain */ +#define SOUND_MIXER_OGAIN 13 /* Output gain */ +#define SOUND_MIXER_LINE1 14 /* Input source 1 (aux1) */ +#define SOUND_MIXER_LINE2 15 /* Input source 2 (aux2) */ +#define SOUND_MIXER_LINE3 16 /* Input source 3 (line) */ +#define SOUND_MIXER_DIGITAL1 17 /* Digital I/O 1 */ +#define SOUND_MIXER_DIGITAL2 18 /* Digital I/O 2 */ +#define SOUND_MIXER_DIGITAL3 19 /* Digital I/O 3 */ +#define SOUND_MIXER_PHONE 20 /* Phone */ +#define SOUND_MIXER_MONO 21 /* Mono Output */ +#define SOUND_MIXER_VIDEO 22 /* Video/TV (audio) in */ +#define SOUND_MIXER_RADIO 23 /* Radio in */ +#define SOUND_MIXER_DEPTH 24 /* Surround depth */ +#define SOUND_MIXER_REARVOL 25 /* Rear/Surround speaker vol */ +#define SOUND_MIXER_CENTERVOL 26 /* Center/LFE speaker vol */ +#define SOUND_MIXER_SIDEVOL 27 /* Side-Surround (8speaker) vol */ +#define SOUND_MIXER_SURRVOL SOUND_MIXER_SIDEVOL +#define SOUND_ONOFF_MIN 28 +#define SOUND_ONOFF_MAX 30 +#define SOUND_MIXER_NONE 31 + +#define SOUND_MIXER_RECSRC 0xff /* Recording sources */ +#define SOUND_MIXER_DEVMASK 0xfe /* Supported devices */ +#define SOUND_MIXER_RECMASK 0xfd /* Recording sources */ +#define SOUND_MIXER_CAPS 0xfc /* Mixer capabilities (do not use) */ +#define SOUND_MIXER_STEREODEVS 0xfb /* Mixer channels supporting stereo */ +#define SOUND_MIXER_OUTSRC 0xfa +#define SOUND_MIXER_OUTMASK 0xf9 + +#define SOUND_MIXER_ENHANCE SOUND_MIXER_NONE +#define SOUND_MIXER_MUTE SOUND_MIXER_NONE +#define SOUND_MIXER_LOUD SOUND_MIXER_NONE + +#define SOUND_MASK_VOLUME (1 << SOUND_MIXER_VOLUME) +#define SOUND_MASK_BASS (1 << SOUND_MIXER_BASS) +#define SOUND_MASK_TREBLE (1 << SOUND_MIXER_TREBLE) +#define SOUND_MASK_SYNTH (1 << SOUND_MIXER_SYNTH) +#define SOUND_MASK_PCM (1 << SOUND_MIXER_PCM) +#define SOUND_MASK_SPEAKER (1 << SOUND_MIXER_SPEAKER) +#define SOUND_MASK_LINE (1 << SOUND_MIXER_LINE) +#define SOUND_MASK_MIC (1 << SOUND_MIXER_MIC) +#define SOUND_MASK_CD (1 << SOUND_MIXER_CD) +#define SOUND_MASK_IMIX (1 << SOUND_MIXER_IMIX) +#define SOUND_MASK_ALTPCM (1 << SOUND_MIXER_ALTPCM) +#define SOUND_MASK_RECLEV (1 << SOUND_MIXER_RECLEV) +#define SOUND_MASK_IGAIN (1 << SOUND_MIXER_IGAIN) +#define SOUND_MASK_OGAIN (1 << SOUND_MIXER_OGAIN) +#define SOUND_MASK_LINE1 (1 << SOUND_MIXER_LINE1) +#define SOUND_MASK_LINE2 (1 << SOUND_MIXER_LINE2) +#define SOUND_MASK_LINE3 (1 << SOUND_MIXER_LINE3) +#define SOUND_MASK_DIGITAL1 (1 << SOUND_MIXER_DIGITAL1) +#define SOUND_MASK_DIGITAL2 (1 << SOUND_MIXER_DIGITAL2) +#define SOUND_MASK_DIGITAL3 (1 << SOUND_MIXER_DIGITAL3) +#define SOUND_MASK_MONO (1 << SOUND_MIXER_MONO) +#define SOUND_MASK_PHONE (1 << SOUND_MIXER_PHONE) +#define SOUND_MASK_RADIO (1 << SOUND_MIXER_RADIO) +#define SOUND_MASK_VIDEO (1 << SOUND_MIXER_VIDEO) +#define SOUND_MASK_DEPTH (1 << SOUND_MIXER_DEPTH) +#define SOUND_MASK_REARVOL (1 << SOUND_MIXER_REARVOL) +#define SOUND_MASK_CENTERVOL (1 << SOUND_MIXER_CENTERVOL) +#define SOUND_MASK_SIDEVOL (1 << SOUND_MIXER_SIDEVOL) +#define SOUND_MASK_SURRVOL SOUND_MASK_SIDEVOL +#define SOUND_MASK_MUTE (1 << SOUND_MIXER_MUTE) +#define SOUND_MASK_ENHANCE (1 << SOUND_MIXER_ENHANCE) +#define SOUND_MASK_LOUD (1 << SOUND_MIXER_LOUD) + +/* + * Again, DO NOT USE the following two macros. They are here for SOURCE + * COMPATIBILITY ONLY. + */ +#define SOUND_DEVICE_LABELS { \ + "Vol ", "Bass ", "Treble", "Synth", "Pcm ", "Speaker ", "Line ", "Mic ", "CD ", "Mix ", "Pcm2 ", "Rec ", "IGain", "OGain", "Aux1", "Aux2", "Aux3", "Digital1", "Digital2", "Digital3", "Phone", "Mono", "Video", "Radio", "Depth", "Rear", "Center", "Side" \ +} + +#define SOUND_DEVICE_NAMES { \ + "vol", "bass", "treble", "synth", "pcm", "speaker", "line", "mic", "cd", "mix", "pcm2", "rec", "igain", "ogain", "aux1", "aux2", "aux3", "dig1", "dig2", "dig3", "phone", "mono", "video", "radio", "depth", "rear", "center", "side" \ +} + +#define MIXER_READ(dev) __OSSIOR('M', dev, int) +#define MIXER_WRITE(dev) __OSSIOWR('M', dev, int) +#define SOUND_MIXER_INFO __OSSIOR('M', 101, mixer_info) +#define OSS_GETVERSION __OSSIOR('M', 118, int) + +/* + * These macros are useful for some applications. They are implemented + * as soft values for the application, and do not affect real hardware. + */ +#define SOUND_MIXER_READ_VOLUME MIXER_READ(SOUND_MIXER_VOLUME) +#define SOUND_MIXER_READ_OGAIN MIXER_READ(SOUND_MIXER_OGAIN) +#define SOUND_MIXER_READ_PCM MIXER_READ(SOUND_MIXER_PCM) +#define SOUND_MIXER_READ_IGAIN MIXER_READ(SOUND_MIXER_IGAIN) +#define SOUND_MIXER_READ_RECLEV MIXER_READ(SOUND_MIXER_RECLEV) +#define SOUND_MIXER_READ_RECSRC MIXER_READ(SOUND_MIXER_RECSRC) +#define SOUND_MIXER_READ_DEVMASK MIXER_READ(SOUND_MIXER_DEVMASK) +#define SOUND_MIXER_READ_RECMASK MIXER_READ(SOUND_MIXER_RECMASK) +#define SOUND_MIXER_READ_CAPS MIXER_READ(SOUND_MIXER_CAPS) +#define SOUND_MIXER_READ_STEREODEVS MIXER_READ(SOUND_MIXER_STEREODEVS) +#define SOUND_MIXER_READ_RECGAIN __OSSIOR('M', 119, int) +#define SOUND_MIXER_READ_MONGAIN __OSSIOR('M', 120, int) + +#define SOUND_MIXER_WRITE_VOLUME MIXER_WRITE(SOUND_MIXER_VOLUME) +#define SOUND_MIXER_WRITE_OGAIN MIXER_WRITE(SOUND_MIXER_OGAIN) +#define SOUND_MIXER_WRITE_PCM MIXER_WRITE(SOUND_MIXER_PCM) +#define SOUND_MIXER_WRITE_IGAIN MIXER_WRITE(SOUND_MIXER_IGAIN) +#define SOUND_MIXER_WRITE_RECLEV MIXER_WRITE(SOUND_MIXER_RECLEV) +#define SOUND_MIXER_WRITE_RECSRC MIXER_WRITE(SOUND_MIXER_RECSRC) +#define SOUND_MIXER_WRITE_RECGAIN __OSSIOWR('M', 119, int) +#define SOUND_MIXER_WRITE_MONGAIN __OSSIOWR('M', 120, int) + +/* + * These macros are here for source compatibility. They intentionally don't + * map to any real hardware. NOT SUPPORTED! + */ +#define SOUND_MIXER_READ_BASS MIXER_READ(SOUND_MIXER_BASS) +#define SOUND_MIXER_READ_TREBLE MIXER_READ(SOUND_MIXER_TREBLE) +#define SOUND_MIXER_READ_SYNTH MIXER_READ(SOUND_MIXER_SYNTH) +#define SOUND_MIXER_READ_SPEAKER MIXER_READ(SOUND_MIXER_SPEAKER) +#define SOUND_MIXER_READ_LINE MIXER_READ(SOUND_MIXER_LINE) +#define SOUND_MIXER_READ_MIC MIXER_READ(SOUND_MIXER_MIC) +#define SOUND_MIXER_READ_CD MIXER_READ(SOUND_MIXER_CD) +#define SOUND_MIXER_READ_IMIX MIXER_READ(SOUND_MIXER_IMIX) +#define SOUND_MIXER_READ_ALTPCM MIXER_READ(SOUND_MIXER_ALTPCM) +#define SOUND_MIXER_READ_LINE1 MIXER_READ(SOUND_MIXER_LINE1) +#define SOUND_MIXER_READ_LINE2 MIXER_READ(SOUND_MIXER_LINE2) +#define SOUND_MIXER_READ_LINE3 MIXER_READ(SOUND_MIXER_LINE3) + +#define SOUND_MIXER_WRITE_BASS MIXER_WRITE(SOUND_MIXER_BASS) +#define SOUND_MIXER_WRITE_TREBLE MIXER_WRITE(SOUND_MIXER_TREBLE) +#define SOUND_MIXER_WRITE_SYNTH MIXER_WRITE(SOUND_MIXER_SYNTH) +#define SOUND_MIXER_WRITE_SPEAKER MIXER_WRITE(SOUND_MIXER_SPEAKER) +#define SOUND_MIXER_WRITE_LINE MIXER_WRITE(SOUND_MIXER_LINE) +#define SOUND_MIXER_WRITE_MIC MIXER_WRITE(SOUND_MIXER_MIC) +#define SOUND_MIXER_WRITE_CD MIXER_WRITE(SOUND_MIXER_CD) +#define SOUND_MIXER_WRITE_IMIX MIXER_WRITE(SOUND_MIXER_IMIX) +#define SOUND_MIXER_WRITE_ALTPCM MIXER_WRITE(SOUND_MIXER_ALTPCM) +#define SOUND_MIXER_WRITE_LINE1 MIXER_WRITE(SOUND_MIXER_LINE1) +#define SOUND_MIXER_WRITE_LINE2 MIXER_WRITE(SOUND_MIXER_LINE2) +#define SOUND_MIXER_WRITE_LINE3 MIXER_WRITE(SOUND_MIXER_LINE3) + +/* + * Audio encoding types (Note! U8=8 and S16_LE=16 for compatibility) + */ +#define AFMT_QUERY 0x00000000 /* Return current fmt */ +#define AFMT_MU_LAW 0x00000001 +#define AFMT_A_LAW 0x00000002 +#define AFMT_IMA_ADPCM 0x00000004 +#define AFMT_U8 0x00000008 +#define AFMT_S16_LE 0x00000010 +#define AFMT_S16_BE 0x00000020 +#define AFMT_S8 0x00000040 +#define AFMT_U16_LE 0x00000080 +#define AFMT_U16_BE 0x00000100 +#define AFMT_MPEG 0x00000200 /* NOT SUPPORTED: MPEG (2) audio */ +#define AFMT_AC3 0x00000400 /* NOT SUPPORTED: AC3 compressed */ +#define AFMT_VORBIS 0x00000800 /* NOT SUPPORTED: Ogg Vorbis */ +#define AFMT_S32_LE 0x00001000 +#define AFMT_S32_BE 0x00002000 +#define AFMT_FLOAT 0x00004000 /* NOT SUPPORTED: IEEE double float */ +#define AFMT_S24_LE 0x00008000 /* LSB aligned in 32 bit word */ +#define AFMT_S24_BE 0x00010000 /* LSB aligned in 32 bit word */ +#define AFMT_SPDIF_RAW 0x00020000 /* NOT SUPPORTED: Raw S/PDIF frames */ +#define AFMT_S24_PACKED 0x00040000 /* 24 bit packed little endian */ +/* + * Some big endian/little endian handling macros (native endian and + * opposite endian formats). + */ +#if defined(_BIG_ENDIAN) + #define AFMT_S16_NE AFMT_S16_BE + #define AFMT_U16_NE AFMT_U16_BE + #define AFMT_S32_NE AFMT_S32_BE + #define AFMT_S24_NE AFMT_S24_BE + #define AFMT_S16_OE AFMT_S16_LE + #define AFMT_S32_OE AFMT_S32_LE + #define AFMT_S24_OE AFMT_S24_LE +#else + #define AFMT_S16_NE AFMT_S16_LE + #define AFMT_U16_NE AFMT_U16_LE + #define AFMT_S32_NE AFMT_S32_LE + #define AFMT_S24_NE AFMT_S24_LE + #define AFMT_S16_OE AFMT_S16_BE + #define AFMT_S32_OE AFMT_S32_BE + #define AFMT_S24_OE AFMT_S24_BE +#endif + +/* + * SNDCTL_DSP_GETCAPS bits + */ +#define PCM_CAP_REVISION 0x000000ff /* Revision level (0 to 255) */ +#define PCM_CAP_DUPLEX 0x00000100 /* Full duplex rec/play */ +#define PCM_CAP_REALTIME 0x00000200 /* NOT SUPPORTED */ +#define PCM_CAP_BATCH 0x00000400 /* NOT SUPPORTED */ +#define PCM_CAP_COPROC 0x00000800 /* NOT SUPPORTED */ +#define PCM_CAP_TRIGGER 0x00001000 /* Supports SETTRIGGER */ +#define PCM_CAP_MMAP 0x00002000 /* Supports mmap() */ +#define PCM_CAP_MULTI 0x00004000 /* Supports multiple open */ +#define PCM_CAP_BIND 0x00008000 /* Supports channel binding */ +#define PCM_CAP_INPUT 0x00010000 /* Supports recording */ +#define PCM_CAP_OUTPUT 0x00020000 /* Supports playback */ +#define PCM_CAP_VIRTUAL 0x00040000 /* Virtual device */ +#define PCM_CAP_ANALOGOUT 0x00100000 /* NOT SUPPORTED */ +#define PCM_CAP_ANALOGIN 0x00200000 /* NOT SUPPORTED */ +#define PCM_CAP_DIGITALOUT 0x00400000 /* NOT SUPPORTED */ +#define PCM_CAP_DIGITALIN 0x00800000 /* NOT SUPPORTED */ +#define PCM_CAP_ADMASK 0x00f00000 /* NOT SUPPORTED */ +#define PCM_CAP_SHADOW 0x01000000 /* "Shadow" device */ +#define PCM_CAP_CH_MASK 0x06000000 /* See DSP_CH_MASK below */ +#define PCM_CAP_HIDDEN 0x08000000 /* NOT SUPPORTED */ +#define PCM_CAP_FREERATE 0x10000000 +#define PCM_CAP_MODEM 0x20000000 /* NOT SUPPORTED */ +#define PCM_CAP_DEFAULT 0x40000000 /* "Default" device */ + +/* + * Preferred channel usage. These bits can be used to give + * recommendations to the application. Used by few drivers. For + * example if ((caps & DSP_CH_MASK) == DSP_CH_MONO) means that the + * device works best in mono mode. However it doesn't necessarily mean + * that the device cannot be used in stereo. These bits should only be + * used by special applications such as multi track hard disk + * recorders to find out the initial setup. However the user should be + * able to override this selection. + * + * To find out which modes are actually supported the application + * should try to select them using SNDCTL_DSP_CHANNELS. + */ +#define DSP_CH_MASK 0x06000000 /* Mask */ +#define DSP_CH_ANY 0x00000000 /* No preferred mode */ +#define DSP_CH_MONO 0x02000000 +#define DSP_CH_STEREO 0x04000000 +#define DSP_CH_MULTI 0x06000000 /* More than two channels */ + +/* + * The PCM_CAP_* capability names used to be known as DSP_CAP_*, so + * it's necessary to define the older names too. + */ +#define DSP_CAP_ADMASK PCM_CAP_ADMASK +#define DSP_CAP_ANALOGIN PCM_CAP_ANALOGIN +#define DSP_CAP_ANALOGOUT PCM_CAP_ANALOGOUT +#define DSP_CAP_BATCH PCM_CAP_BATCH +#define DSP_CAP_BIND PCM_CAP_BIND +#define DSP_CAP_COPROC PCM_CAP_COPROC +#define DSP_CAP_DEFAULT PCM_CAP_DEFAULT +#define DSP_CAP_DIGITALIN PCM_CAP_DIGITALIN +#define DSP_CAP_DIGITALOUT PCM_CAP_DIGITALOUT +#define DSP_CAP_DUPLEX PCM_CAP_DUPLEX +#define DSP_CAP_FREERATE PCM_CAP_FREERATE +#define DSP_CAP_HIDDEN PCM_CAP_HIDDEN +#define DSP_CAP_INPUT PCM_CAP_INPUT +#define DSP_CAP_MMAP PCM_CAP_MMAP +#define DSP_CAP_MODEM PCM_CAP_MODEM +#define DSP_CAP_MULTI PCM_CAP_MULTI +#define DSP_CAP_OUTPUT PCM_CAP_OUTPUT +#define DSP_CAP_REALTIME PCM_CAP_REALTIME +#define DSP_CAP_REVISION PCM_CAP_REVISION +#define DSP_CAP_SHADOW PCM_CAP_SHADOW +#define DSP_CAP_TRIGGER PCM_CAP_TRIGGER +#define DSP_CAP_VIRTUAL PCM_CAP_VIRTUAL + +/* + * SNDCTL_DSP_GETTRIGGER and SNDCTL_DSP_SETTRIGGER + */ +#define PCM_ENABLE_INPUT 0x00000001 +#define PCM_ENABLE_OUTPUT 0x00000002 + +/* + * SNDCTL_DSP_BIND_CHANNEL + */ +#define DSP_BIND_QUERY 0x00000000 +#define DSP_BIND_FRONT 0x00000001 +#define DSP_BIND_SURR 0x00000002 +#define DSP_BIND_CENTER_LFE 0x00000004 +#define DSP_BIND_HANDSET 0x00000008 +#define DSP_BIND_MIC 0x00000010 +#define DSP_BIND_MODEM1 0x00000020 +#define DSP_BIND_MODEM2 0x00000040 +#define DSP_BIND_I2S 0x00000080 +#define DSP_BIND_SPDIF 0x00000100 +#define DSP_BIND_REAR 0x00000200 + +/* + * SOUND_MIXER_READ_CAPS + */ +#define SOUND_CAP_EXCL_INPUT 0x00000001 +#define SOUND_CAP_NOLEGACY 0x00000004 +#define SOUND_CAP_NORECSRC 0x00000008 + +/* + * The following ioctl is for internal use only -- it is used to + * coordinate /dev/sndstat numbering with file names in /dev/sound. + * Applications must not use it. (This is duplicated in sys/audioio.h + * as well.) + */ +#define SNDCTL_SUN_SEND_NUMBER __OSSIOW('X', 200, int) + +#ifdef __cplusplus +} +#endif + +#endif /* _SYS_AUDIO_OSS_H */ diff --git a/src/detection/sound/sound.h b/src/detection/sound/sound.h new file mode 100644 index 0000000..041e6a1 --- /dev/null +++ b/src/detection/sound/sound.h @@ -0,0 +1,16 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/sound/option.h" + +#define FF_SOUND_VOLUME_UNKNOWN 255 + +typedef struct FFSoundDevice { + FFstrbuf identifier; + FFstrbuf name; + FFstrbuf platformApi; + uint8_t volume; // 0-100% + FFSoundType type; +} FFSoundDevice; + +const char* ffDetectSound(FFSoundOptions* options, FFlist* devices /* List of FFSoundDevice */); diff --git a/src/detection/sound/sound_apple.c b/src/detection/sound/sound_apple.c new file mode 100644 index 0000000..aaacf78 --- /dev/null +++ b/src/detection/sound/sound_apple.c @@ -0,0 +1,112 @@ +#include "sound.h" +#include "common/apple/cf_helpers.h" + +#include <CoreAudio/CoreAudio.h> +#include <AvailabilityMacros.h> + +#ifndef MAC_OS_VERSION_12_0 + #define kAudioObjectPropertyElementMain kAudioObjectPropertyElementMaster +#endif + +const char* ffDetectSound(FFSoundOptions* options, FFlist* devices /* List of FFSoundDevice */) { + AudioDeviceID mainDeviceId; + UInt32 dataSize = sizeof(mainDeviceId); + if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &(AudioObjectPropertyAddress) { kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, NULL, &dataSize, &mainDeviceId) != kAudioHardwareNoError) { + return "AudioObjectGetPropertyData(kAudioHardwarePropertyDefaultOutputDevice) failed"; + } + + AudioObjectID deviceIds[32] = {}; + if (options->soundType & FF_SOUND_TYPE_MAIN) { + deviceIds[0] = mainDeviceId; + dataSize = sizeof(mainDeviceId); + } else { + dataSize = sizeof(deviceIds); + if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &(AudioObjectPropertyAddress) { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, NULL, &dataSize, &deviceIds) != kAudioHardwareNoError) { + return "AudioObjectGetPropertyData(kAudioHardwarePropertyDevices) failed"; + } + } + + for (uint32_t index = 0, length = dataSize / sizeof(*deviceIds); index < length; ++index) { + AudioDeviceID deviceId = deviceIds[index]; + + // Ignore input devices + if (AudioObjectGetPropertyDataSize(deviceId, &(AudioObjectPropertyAddress) { kAudioDevicePropertyStreams, kAudioObjectPropertyScopeInput, kAudioObjectPropertyElementMain }, 0, NULL, &dataSize) == kAudioHardwareNoError && dataSize > 0) { + continue; + } + + uint32_t dataSource; + dataSize = sizeof(dataSource); + if (AudioObjectGetPropertyData(deviceId, &(AudioObjectPropertyAddress) { kAudioDevicePropertyDataSource, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, NULL, &dataSize, &dataSource) == kAudioHardwareNoError && dataSource == 'hdpn') { + uint32_t connected; + dataSize = sizeof(connected); + if (AudioObjectGetPropertyData(deviceId, &(AudioObjectPropertyAddress) { kAudioDevicePropertyJackIsConnected, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, NULL, &dataSize, &connected) == kAudioHardwareNoError) { + if (!connected) { + continue; + } + } + } + + uint32_t active = true; + dataSize = sizeof(active); + if (AudioObjectGetPropertyData(deviceId, &(AudioObjectPropertyAddress) { kAudioDevicePropertyDeviceIsAlive, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, NULL, &dataSize, &active) == kAudioHardwareNoError) { + if ((options->soundType & FF_SOUND_TYPE_ACTIVE) && !active) { + continue; + } + } + + FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *devices); + device->type = (deviceId == mainDeviceId ? FF_SOUND_TYPE_MAIN : FF_SOUND_TYPE_NONE) | + (active ? FF_SOUND_TYPE_ACTIVE : FF_SOUND_TYPE_NONE); + device->volume = FF_SOUND_VOLUME_UNKNOWN; + ffStrbufInit(&device->identifier); + ffStrbufInit(&device->name); + ffStrbufInitStatic(&device->platformApi, "Core Audio"); + + FF_CFTYPE_AUTO_RELEASE CFStringRef uid = NULL; + dataSize = sizeof(uid); + if (AudioObjectGetPropertyData(deviceId, &(AudioObjectPropertyAddress) { kAudioDevicePropertyDeviceUID, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, NULL, &dataSize, &uid) == kAudioHardwareNoError) { + ffCfStrGetString(uid, &device->identifier); + } else { + ffStrbufAppendF(&device->identifier, "ID-%u", (unsigned) deviceId); + } + + FF_CFTYPE_AUTO_RELEASE CFStringRef name = NULL; + dataSize = sizeof(name); + if (AudioObjectGetPropertyData(deviceId, &(AudioObjectPropertyAddress) { kAudioObjectPropertyName, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, NULL, &dataSize, &name) == kAudioHardwareNoError) { + ffCfStrGetString(name, &device->name); + } else { + ffStrbufSet(&device->name, &device->identifier); + } + + uint32_t muted; + dataSize = sizeof(muted); + if (AudioObjectGetPropertyData(deviceId, &(AudioObjectPropertyAddress) { kAudioDevicePropertyMute, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, NULL, &dataSize, &muted) != kAudioHardwareNoError) { + muted = false; // Device may not support volume control + } + + if (muted) { + device->volume = 0; + } else { + float volume; + dataSize = sizeof(volume); + if (AudioObjectGetPropertyData(deviceId, &(AudioObjectPropertyAddress) { kAudioDevicePropertyVolumeScalar, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, NULL, &dataSize, &volume) == kAudioHardwareNoError) { + device->volume = (uint8_t) (volume * 100 + 0.5); + } else { + // Try detecting volume from channels + uint32_t channels[2]; + dataSize = sizeof(channels); + if (AudioObjectGetPropertyData(deviceId, &(AudioObjectPropertyAddress) { kAudioDevicePropertyPreferredChannelsForStereo, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain }, 0, NULL, &dataSize, channels) == kAudioHardwareNoError) { + dataSize = sizeof(volume); + if (AudioObjectGetPropertyData(deviceId, &(AudioObjectPropertyAddress) { kAudioDevicePropertyVolumeScalar, kAudioObjectPropertyScopeOutput, channels[0] }, 0, NULL, &dataSize, &volume) == kAudioHardwareNoError) { + float temp; + if (AudioObjectGetPropertyData(deviceId, &(AudioObjectPropertyAddress) { kAudioDevicePropertyVolumeScalar, kAudioObjectPropertyScopeOutput, channels[1] }, 0, NULL, &dataSize, &temp) == kAudioHardwareNoError) { + device->volume = (uint8_t) ((volume + temp) / 2 * 100 + 0.5); + } + } + } + } + } + } + + return NULL; +} diff --git a/src/detection/sound/sound_bsd.c b/src/detection/sound/sound_bsd.c new file mode 100644 index 0000000..cbac868 --- /dev/null +++ b/src/detection/sound/sound_bsd.c @@ -0,0 +1,90 @@ +#include "sound.h" +#include "common/io.h" +#include "common/sysctl.h" + +#include <fcntl.h> +#include <sys/soundcard.h> +#include <unistd.h> + +const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { +#ifndef __NetBSD__ + int defaultDev = ffSysctlGetInt("hw.snd.default_unit", -1); + if (defaultDev == -1) { + return "sysctl(hw.snd.default_unit) failed"; + } +#else + int defaultDev; + { + char mixerp[12]; + ssize_t plen = readlink("/dev/mixer", mixerp, ARRAY_SIZE(mixerp)); + if (plen < 6) { + return "readlink(/dev/mixer) failed"; + } + defaultDev = mixerp[plen - 1] - '0'; + if (defaultDev < 0 || defaultDev > 9) { + return "Invalid mixer device"; + } + } +#endif + + char path[] = "/dev/mixer0"; + + struct oss_sysinfo info = { .nummixers = 9 }; + + for (int idev = 0; idev <= info.nummixers; ++idev) { + bool isMain = idev == defaultDev; + if ((options->soundType & FF_SOUND_TYPE_MAIN) && !isMain) { + continue; + } + + path[strlen("/dev/mixer")] = (char) ('0' + idev); + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + break; + } + + if (idev == 0) { + if (ioctl(fd, SNDCTL_SYSINFO, &info) != 0) { + return "ioctl(SNDCTL_SYSINFO) failed"; + } + } + + uint32_t devmask = 0; + if (ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask) < 0) { + continue; + } + if (!(devmask & SOUND_MASK_VOLUME)) { + continue; + } + +#if defined(SOUND_MIXER_MUTE) && (SOUND_MIXER_MUTE != SOUND_MIXER_NONE) + #define FF_SOUND_HAVE_MIXER_MUTE 1 + uint32_t mutemask = 0; + ioctl(fd, SOUND_MIXER_READ_MUTE, &mutemask); +#endif + + struct oss_card_info ci = { .card = idev }; + if (ioctl(fd, SNDCTL_CARDINFO, &ci) < 0) { + continue; + } + + uint32_t volume; + if (ioctl(fd, SOUND_MIXER_READ_VOLUME, &volume) < 0) { + continue; + } + + FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *devices); + ffStrbufInitS(&device->identifier, path); + ffStrbufInitF(&device->name, "%s %s", ci.longname, ci.hw_info); + ffStrbufTrimRightSpace(&device->name); + ffStrbufInitF(&device->platformApi, "%s %s", info.product, info.version); + device->volume = +#ifdef FF_SOUND_HAVE_MIXER_MUTE + mutemask & SOUND_MASK_VOLUME ? 0 : +#endif + ((uint8_t) volume /*left*/ + (uint8_t) (volume >> 8) /*right*/) / 2; + device->type = FF_SOUND_TYPE_ACTIVE | (isMain ? FF_SOUND_TYPE_MAIN : FF_SOUND_TYPE_NONE); + } + + return NULL; +} diff --git a/src/detection/sound/sound_haiku.cpp b/src/detection/sound/sound_haiku.cpp new file mode 100644 index 0000000..d69602e --- /dev/null +++ b/src/detection/sound/sound_haiku.cpp @@ -0,0 +1,88 @@ +extern "C" { +#include "sound.h" +#include "common/strutil.h" +} +#include <MediaAddOn.h> +#include <MediaNode.h> +#include <MediaRoster.h> +#include <ParameterWeb.h> + +const char* ffDetectSound(FF_A_UNUSED FFSoundOptions* options, FFlist* devices /* List of FFSoundDevice */) { + BMediaRoster* roster = BMediaRoster::Roster(); + media_node mediaNode; + live_node_info liveInfo; + dormant_node_info dormantInfo; + + if (roster->GetAudioOutput(&mediaNode) != B_OK) { + return NULL; + } + + FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *devices); + ffStrbufInit(&device->identifier); + if (roster->GetDormantNodeFor(mediaNode, &dormantInfo) == B_OK) { + ffStrbufAppendS(&device->identifier, dormantInfo.name); + } + ffStrbufInit(&device->name); + if (roster->GetLiveNodeInfo(mediaNode, &liveInfo) == B_OK) { + ffStrbufAppendS(&device->name, liveInfo.name); + ffStrbufTrimRightSpace(&device->name); + } + ffStrbufInitStatic(&device->platformApi, "MediaKit"); + // We'll check the Mixer actually + device->volume = 0; + device->type = (FFSoundType) (FF_SOUND_TYPE_ACTIVE | FF_SOUND_TYPE_MAIN); + + roster->ReleaseNode(mediaNode); + + media_node mixer; + if (roster->GetAudioMixer(&mixer) != B_OK) { + return NULL; + } + + BParameterWeb* web; + status_t status = roster->GetParameterWebFor(mixer, &web); + roster->ReleaseNode(mixer); // the web is all we need :-) + if (status != B_OK) { + return NULL; + } + + BContinuousParameter* gain = NULL; + BParameter* mute = NULL; + BParameter* parameter; + for (int32 index = 0; (parameter = web->ParameterAt(index)) != NULL; index++) { + // assume the mute preceding master gain control + if (ffStrEquals(parameter->Kind(), B_MUTE)) { + mute = parameter; + } + + if (ffStrEquals(parameter->Kind(), B_MASTER_GAIN)) { + // Can not use dynamic_cast due to fno-rtti + // gain = dynamic_cast<BContinuousParameter *>(parameter); + gain = (BContinuousParameter*) (parameter); + break; + } + } + + if (gain == NULL) { + return NULL; + } + + bigtime_t when; + size_t size; + + if (mute) { + int32 isMute = false; + size = sizeof(isMute); + if (mute->GetValue(&isMute, &size, &when) == B_OK && isMute) { + return NULL; + } + } + + float volume = 0.0; + size = sizeof(volume); + if (gain->GetValue(&volume, &size, &when) == B_OK) { + device->volume = (uint8_t) (100 * (volume - gain->MinValue()) / (gain->MaxValue() - gain->MinValue())); + } + + return NULL; +} diff --git a/src/detection/sound/sound_linux.c b/src/detection/sound/sound_linux.c new file mode 100644 index 0000000..8af7e93 --- /dev/null +++ b/src/detection/sound/sound_linux.c @@ -0,0 +1,164 @@ +#include "sound.h" + +#ifdef FF_HAVE_PULSE + #include "common/library.h" + #include <pulse/pulseaudio.h> + +struct DetectionInfoBundle { + FFstrbuf serverName; + FFstrbuf defaultDeviceId; + FFlist* result; + FFSoundOptions* options; +}; + +static void paSinkInfoCallback(FF_A_UNUSED pa_context* c, const pa_sink_info* i, int eol, void* userdata) { + if (eol > 0 || !i) { + return; + } + + struct DetectionInfoBundle* bundle = userdata; + + bool isMain = ffStrbufEqualS(&bundle->defaultDeviceId, i->name); + if ((bundle->options->soundType & FF_SOUND_TYPE_MAIN) && !isMain) { + return; + } + + bool isActive = i->active_port && i->active_port->available != PA_PORT_AVAILABLE_NO; + if ((bundle->options->soundType & FF_SOUND_TYPE_ACTIVE) && !isActive) { + return; + } + + FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *bundle->result); + ffStrbufInitS(&device->identifier, i->name); + ffStrbufInitCopy(&device->platformApi, &bundle->serverName); + ffStrbufTrimRightSpace(&device->identifier); + ffStrbufInitS(&device->name, i->description); + ffStrbufTrimRightSpace(&device->name); + ffStrbufTrimLeft(&device->name, ' '); + device->volume = i->mute ? 0 : (uint8_t) ((i->volume.values[0] * 100 + PA_VOLUME_NORM / 2 /*round*/) / PA_VOLUME_NORM); + device->type = (isMain ? FF_SOUND_TYPE_MAIN : FF_SOUND_TYPE_NONE) | (isActive ? FF_SOUND_TYPE_ACTIVE : FF_SOUND_TYPE_NONE); +} + +static void paServerInfoCallback(FF_A_UNUSED pa_context* c, const pa_server_info* i, void* userdata) { + if (!i) { + return; + } + + struct DetectionInfoBundle* bundle = userdata; + + const char* realServer = strstr(i->server_name, "(on "); + if (realServer) { + ffStrbufSetS(&bundle->serverName, realServer + strlen("(on ")); + ffStrbufTrimRight(&bundle->serverName, ')'); + } else { + ffStrbufSetF(&bundle->serverName, "%s %s", i->server_name, i->server_version); + } + + ffStrbufSetS(&bundle->defaultDeviceId, i->default_sink_name); +} + +static const char* detectSound(FFSoundOptions* options, FFlist* devices) { + FF_LIBRARY_LOAD_MESSAGE(pulse, "libpulse" FF_LIBRARY_EXTENSION, 0) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_mainloop_new) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_mainloop_get_api) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_mainloop_iterate) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_mainloop_free) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_context_new) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_context_connect) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_context_get_state) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_context_get_sink_info_list) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_context_get_server_info) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_context_unref) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_operation_cancel) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_operation_get_state) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(pulse, pa_operation_unref) + + pa_mainloop* mainloop = ffpa_mainloop_new(); + if (!mainloop) { + return "Failed to create pulseaudio mainloop"; + } + + pa_mainloop_api* mainloopApi = ffpa_mainloop_get_api(mainloop); + if (!mainloopApi) { + ffpa_mainloop_free(mainloop); + return "Failed to get pulseaudio mainloop api"; + } + + pa_context* context = ffpa_context_new(mainloopApi, "fastfetch"); + if (!context) { + ffpa_mainloop_free(mainloop); + return "Failed to create pulseaudio context"; + } + + struct DetectionInfoBundle bundle = { + .serverName = ffStrbufCreate(), + .defaultDeviceId = ffStrbufCreate(), + .result = devices, + .options = options, + }; + const char* error = NULL; + + if (ffpa_context_connect(context, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) { + error = "Failed to connect to pulseaudio context"; + goto exit; + } + + pa_context_state_t state; + while ((state = ffpa_context_get_state(context)) != PA_CONTEXT_READY) { + if (!PA_CONTEXT_IS_GOOD(state)) { + error = "Failed to get pulseaudio context state"; + goto exit; + } + + ffpa_mainloop_iterate(mainloop, 1, NULL); + } + + { + pa_operation* operation = ffpa_context_get_server_info(context, paServerInfoCallback, &bundle); + if (!operation) { + error = "Failed to get pulseaudio server info"; + goto exit; + } + while (ffpa_operation_get_state(operation) == PA_OPERATION_RUNNING) { + ffpa_mainloop_iterate(mainloop, 1, NULL); + } + + ffpa_operation_unref(operation); + } + + { + pa_operation* operation = ffpa_context_get_sink_info_list(context, paSinkInfoCallback, &bundle); + if (!operation) { + error = "Failed to get pulseaudio sink info list"; + goto exit; + } + + while (ffpa_operation_get_state(operation) == PA_OPERATION_RUNNING) { + if (options->soundType & FF_SOUND_TYPE_MAIN && devices->length > 0) { + ffpa_operation_cancel(operation); + } + + ffpa_mainloop_iterate(mainloop, 1, NULL); + } + + ffpa_operation_unref(operation); + } + +exit: + ffStrbufDestroy(&bundle.serverName); + ffStrbufDestroy(&bundle.defaultDeviceId); + ffpa_context_unref(context); + ffpa_mainloop_free(mainloop); + return error; +} + +#endif // FF_HAVE_PULSE + +const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { +#ifdef FF_HAVE_PULSE + return detectSound(options, devices); +#else + FF_UNUSED(options, devices); + return "Fastfetch was built without libpulse support"; +#endif +} diff --git a/src/detection/sound/sound_nbsd.c b/src/detection/sound/sound_nbsd.c new file mode 100644 index 0000000..efd276e --- /dev/null +++ b/src/detection/sound/sound_nbsd.c @@ -0,0 +1,58 @@ +#include "sound.h" +#include "common/io.h" + +#include <fcntl.h> +#include <stdint.h> +#include <unistd.h> +#include <sys/audioio.h> +#include <sys/ioctl.h> + +const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { + int defaultDev; + { + char audiop[12]; + ssize_t plen = readlink("/dev/audio", audiop, ARRAY_SIZE(audiop)); + if (plen < (ssize_t) strlen("audioN")) { + return "readlink(/dev/audio) failed"; + } + defaultDev = audiop[plen - 1] - '0'; + if (defaultDev < 0 || defaultDev > 9) { + return "Invalid audio device"; + } + } + + char path[] = "/dev/audio0"; + + for (int idev = 0; idev < 9; ++idev) { + bool isMain = idev == defaultDev; + if ((options->soundType & FF_SOUND_TYPE_MAIN) && !isMain) { + continue; + } + + path[strlen("/dev/audio")] = (char) ('0' + idev); + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + break; + } + + audio_device_t ad; + if (ioctl(fd, AUDIO_GETDEV, &ad) < 0) { + continue; + } + + audio_info_t ai; + if (ioctl(fd, AUDIO_GETINFO, &ai) < 0) { + continue; + } + + FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *devices); + ffStrbufInitS(&device->identifier, path); + ffStrbufInitS(&device->name, ad.name); + ffStrbufTrimRightSpace(&device->name); + ffStrbufInitF(&device->platformApi, "%s", "SunAudio"); + device->volume = (uint8_t) ((ai.play.gain * 100 + AUDIO_MAX_GAIN / 2) / AUDIO_MAX_GAIN); + device->type = FF_SOUND_TYPE_ACTIVE | (isMain ? FF_SOUND_TYPE_MAIN : FF_SOUND_TYPE_NONE); + } + + return NULL; +} diff --git a/src/detection/sound/sound_nosupport.c b/src/detection/sound/sound_nosupport.c new file mode 100644 index 0000000..b672360 --- /dev/null +++ b/src/detection/sound/sound_nosupport.c @@ -0,0 +1,5 @@ +#include "sound.h" + +const char* ffDetectSound(FF_A_UNUSED FFlist* devices /* List of FFSoundDevice */) { + return "Not supported on this platform"; +} diff --git a/src/detection/sound/sound_obsd.c b/src/detection/sound/sound_obsd.c new file mode 100644 index 0000000..a6dd6df --- /dev/null +++ b/src/detection/sound/sound_obsd.c @@ -0,0 +1,90 @@ +#include "sound.h" +#include "common/strutil.h" + +#include <fcntl.h> +#include <sndio.h> + +static void close_hdl(struct sioctl_hdl** phdl) { + assert(phdl); + if (*phdl) { + sioctl_close(*phdl); + } +} + +enum { MAX_CHANNEL_NUM = 8 }; + +typedef struct FFSoundDeviceBundle { + char name[SIOCTL_DISPLAYMAX]; + double level[MAX_CHANNEL_NUM]; + uint8_t iLevel; + bool mute[MAX_CHANNEL_NUM]; + uint8_t iMute; +} FFSoundDeviceBundle; + +static void enumerate_props(FFSoundDeviceBundle* bundle, struct sioctl_desc* desc, int val) { + if (!desc) { + return; + } + + if (desc->type == SIOCTL_SEL) { + if (desc->display[0] != '\0' && ffStrEquals(desc->node0.name, "server")) { + ffStrCopy(bundle->name, desc->display, SIOCTL_DISPLAYMAX); + } + return; + } + + if (desc->type != SIOCTL_NUM && desc->type != SIOCTL_SW) { + return; + } + + if (!ffStrEquals(desc->node0.name, "output")) { + return; + } + + if (ffStrEquals(desc->func, "level")) { + if (__builtin_expect(bundle->iLevel == MAX_CHANNEL_NUM, false)) { + return; + } + bundle->level[bundle->iLevel] = (double) val / (double) desc->maxval; + ++bundle->iLevel; + } else if (ffStrEquals(desc->func, "mute")) { + if (__builtin_expect(bundle->iMute == MAX_CHANNEL_NUM, false)) { + return; + } + bundle->mute[bundle->iMute] = !!val; + ++bundle->iMute; + } +} + +const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { + FF_A_CLEANUP(close_hdl) struct sioctl_hdl* hdl = sioctl_open(SIO_DEVANY, SIOCTL_READ, 0); + if (!hdl) { + return "sio_open() failed"; + } + + FFSoundDeviceBundle bundle = {}; + if (sioctl_ondesc(hdl, (void*) enumerate_props, &bundle) == 0) { + return "sioctl_ondesc() failed"; + } + + if (bundle.iLevel != bundle.iMute || bundle.iLevel == 0) { + return "Unexpected sioctl_ondesc() result"; + } + + FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *devices); + ffStrbufInitS(&device->name, bundle.name); + ffStrbufInitS(&device->identifier, SIO_DEVANY); + ffStrbufInitStatic(&device->platformApi, "sndio"); + device->volume = 0; + device->type = FF_SOUND_TYPE_ACTIVE | FF_SOUND_TYPE_MAIN; + + double totalLevel = 0; + for (uint8_t i = 0; i < bundle.iLevel; ++i) { + if (!bundle.mute[i]) { + totalLevel += bundle.level[i]; + } + } + device->volume = (uint8_t) ((totalLevel * 100 + bundle.iLevel / 2) / bundle.iLevel); + + return NULL; +} diff --git a/src/detection/sound/sound_sunos.c b/src/detection/sound/sound_sunos.c new file mode 100644 index 0000000..7c84e55 --- /dev/null +++ b/src/detection/sound/sound_sunos.c @@ -0,0 +1,109 @@ +#include "sound.h" +#include "common/io.h" +#include "common/strutil.h" + +#include <fcntl.h> +#include <unistd.h> +#if __has_include(<sys/soundcard.h>) + #include <sys/soundcard.h> +#else + // Strangely, they don't provide this file on default installation + #include "audio_oss_sunos.h" +#endif + +const char* ffDetectSound(FFSoundOptions* options, FFlist* devices) { + int defaultDev; + { + char mixerp[12]; + ssize_t plen = readlink("/dev/audio", mixerp, ARRAY_SIZE(mixerp)); + if (plen < 6) { + return "readlink(/dev/audio) failed"; + } + defaultDev = mixerp[plen - 1] - '0'; + if (defaultDev < 0 || defaultDev > 9) { + return "Invalid mixer device"; + } + } + + char path[] = "/dev/mixer0"; + + FF_STRBUF_AUTO_DESTROY sndstat = ffStrbufCreate(); + + struct oss_sysinfo info = { .nummixers = 9 }; + + // The implementation is very different from *BSD's. They call it OSS4 + for (int idev = 0; idev < info.nummixers; ++idev) { + bool isMain = idev == defaultDev; + if ((options->soundType & FF_SOUND_TYPE_MAIN) && !isMain) { + continue; + } + + path[strlen("/dev/mixer")] = (char) ('0' + idev); + FF_AUTO_CLOSE_FD int fd = open(path, O_RDWR | O_CLOEXEC); + if (fd < 0) { + break; + } + + if (idev == 0) { + if (ioctl(fd, SNDCTL_SYSINFO, &info) != 0) { + return "ioctl(SNDCTL_SYSINFO) failed"; + } + if (ffAppendFDBuffer(fd, &sndstat)) { + ffStrbufSubstrAfterFirstS(&sndstat, "\nMixers:"); + } + } + + struct oss_mixerinfo mi = {}; + if (ioctl(fd, SNDCTL_MIXERINFO, &mi) < 0) { + continue; + } + + if (options->soundType == FF_SOUND_TYPE_ACTIVE && !mi.enabled) { + continue; + } + + int volume = -1; + for (int iext = 0; iext < mi.nrext; ++iext) { + struct oss_mixext me = { .dev = mi.dev, .ctrl = iext }; + if (ioctl(fd, SNDCTL_MIX_EXTINFO, &me) < 0) { + continue; + } + if (me.flags & MIXF_PCMVOL) { + struct oss_mixer_value mv = { .dev = mi.dev, .ctrl = iext, .timestamp = me.timestamp }; + if (ioctl(fd, SNDCTL_MIX_READ, &mv) >= 0) { + mv.value -= me.minvalue; + me.maxvalue -= me.minvalue; + volume = (uint8_t) ((mv.value * 100 + me.maxvalue / 2) / me.maxvalue); + } + break; + } + } + if (volume == -1) { + continue; + } + + FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *devices); + ffStrbufInitS(&device->identifier, path); + char buf[16]; + int bufLen = snprintf(buf, ARRAY_SIZE(buf), "\n%d: ", mi.dev); + assert(bufLen > 3); + const char* pLine = memmem(sndstat.chars, sndstat.length, buf, (size_t) bufLen); + if (pLine) { + pLine += bufLen; + const char* pEnd = strchr(pLine, '\n'); + if (!pEnd) { + pEnd = sndstat.chars + sndstat.length; + } + ffStrbufInitNS(&device->name, (uint32_t) (pEnd - pLine), pLine); + } else { + ffStrbufInitS(&device->name, mi.name); + } + ffStrbufTrimRightSpace(&device->name); + ffStrbufInitF(&device->platformApi, "%s %s", info.product, info.version); + device->volume = (uint8_t) volume; + device->type = (mi.enabled ? FF_SOUND_TYPE_ACTIVE : FF_SOUND_TYPE_NONE) | + (isMain ? FF_SOUND_TYPE_MAIN : FF_SOUND_TYPE_NONE); + } + + return NULL; +} diff --git a/src/detection/sound/sound_windows.cpp b/src/detection/sound/sound_windows.cpp new file mode 100644 index 0000000..6d6307a --- /dev/null +++ b/src/detection/sound/sound_windows.cpp @@ -0,0 +1,122 @@ +extern "C" { +#include "sound.h" +#include "common/windows/com.h" +} +#include "common/windows/unicode.hpp" +#include "common/windows/variant.hpp" + +#include <initguid.h> +#include <mmdeviceapi.h> +#include <endpointvolume.h> +#include <functiondiscoverykeys_devpkey.h> + +static void ffCoTaskMemFreeWrapper(void* pptr) { + assert(pptr != NULL); + void* ptr = *(void**) pptr; + if (ptr) { + CoTaskMemFree(ptr); + } +} +#define FF_COTASK_AUTO_FREE FF_A_CLEANUP(ffCoTaskMemFreeWrapper) + +static const char* detectSoundDevice(FFlist* devices /* List of FFSoundDevice */, IMMDevice* immDevice, LPWSTR mainDeviceId) { + LPWSTR FF_COTASK_AUTO_FREE immDeviceId = NULL; + if (FAILED(immDevice->GetId(&immDeviceId))) { + return "immDevice->GetId() failed"; + } + + IPropertyStore* FF_AUTO_RELEASE_COM_OBJECT immPropStore = NULL; + if (FAILED(immDevice->OpenPropertyStore(STGM_READ, &immPropStore))) { + return "immDevice->OpenPropertyStore() failed"; + } + + DWORD immState; + if (FAILED(immDevice->GetState(&immState))) { + return "immDevice->GetState() failed"; + } + + FFSoundDevice* device = FF_LIST_ADD(FFSoundDevice, *devices); + device->type = (FFSoundType) ((!mainDeviceId || wcscmp(immDeviceId, mainDeviceId) == 0 ? FF_SOUND_TYPE_MAIN : FF_SOUND_TYPE_NONE) | + ((immState & DEVICE_STATE_ACTIVE) ? FF_SOUND_TYPE_ACTIVE : FF_SOUND_TYPE_NONE)); + device->volume = FF_SOUND_VOLUME_UNKNOWN; + ffStrbufInitWS(&device->identifier, immDeviceId); + ffStrbufInit(&device->name); + ffStrbufInitStatic(&device->platformApi, "Core Audio APIs"); + + { + FFPropVariant friendlyName; + if (SUCCEEDED(immPropStore->GetValue(PKEY_Device_FriendlyName, &friendlyName))) { + ffStrbufSetWSV(&device->name, friendlyName.get<std::wstring_view>()); + } else if (SUCCEEDED(immPropStore->GetValue(PKEY_Device_DeviceDesc, &friendlyName))) { + ffStrbufSetWSV(&device->name, friendlyName.get<std::wstring_view>()); + } else { + ffStrbufSetStatic(&device->name, "Unknown Device"); + } + } + + IAudioEndpointVolume* FF_AUTO_RELEASE_COM_OBJECT immEndpointVolume = NULL; + if (SUCCEEDED(immDevice->Activate(IID_IAudioEndpointVolume, CLSCTX_ALL, NULL, (void**) &immEndpointVolume))) { + BOOL muted; + if (FAILED(immEndpointVolume->GetMute(&muted)) || !muted) { + FLOAT volume; + if (SUCCEEDED(immEndpointVolume->GetMasterVolumeLevelScalar(&volume))) { + device->volume = (uint8_t) (volume * 100 + 0.5); + } + } + } + + return NULL; +} + +const char* ffDetectSound(FFSoundOptions* options, FFlist* devices /* List of FFSoundDevice */) { + const char* error = ffInitCom(); + if (error) { + return error; + } + + IMMDeviceEnumerator* FF_AUTO_RELEASE_COM_OBJECT pEnum = NULL; + + if (FAILED(CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnum)))) { + return "CoCreateInstance(CLSID_MMDeviceEnumerator) failed"; + } + + LPWSTR FF_COTASK_AUTO_FREE mainDeviceId = NULL; + + { + IMMDevice* FF_AUTO_RELEASE_COM_OBJECT pDefaultDevice = NULL; + + if (FAILED(pEnum->GetDefaultAudioEndpoint(eRender, eMultimedia, &pDefaultDevice))) { + return "GetDefaultAudioEndpoint() failed"; + } + + if (options->soundType & FF_SOUND_TYPE_MAIN) { + return detectSoundDevice(devices, pDefaultDevice, NULL); + } + + if (FAILED(pDefaultDevice->GetId(&mainDeviceId))) { + return "pDefaultDevice->GetId() failed"; + } + } + + IMMDeviceCollection* FF_AUTO_RELEASE_COM_OBJECT pDevices = NULL; + + if (FAILED(pEnum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE | (options->soundType & FF_SOUND_TYPE_ACTIVE ? 0 : DEVICE_STATE_DISABLED), &pDevices))) { + return "EnumAudioEndpoints() failed"; + } + + uint32_t deviceCount; + if (FAILED(pDevices->GetCount(&deviceCount))) { + return "pDevices->GetCount() failed"; + } + + for (uint32_t deviceIdx = 0; deviceIdx < deviceCount; ++deviceIdx) { + IMMDevice* FF_AUTO_RELEASE_COM_OBJECT immDevice = NULL; + if (FAILED(pDevices->Item(deviceIdx, &immDevice))) { + continue; + } + + detectSoundDevice(devices, immDevice, mainDeviceId); + } + + return NULL; +} diff --git a/src/detection/swap/swap.h b/src/detection/swap/swap.h new file mode 100644 index 0000000..eb32f75 --- /dev/null +++ b/src/detection/swap/swap.h @@ -0,0 +1,12 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/swap/option.h" + +typedef struct FFSwapResult { + FFstrbuf name; + uint64_t bytesUsed; + uint64_t bytesTotal; +} FFSwapResult; + +const char* ffDetectSwap(FFlist* result /* List of FFSwapResult */); diff --git a/src/detection/swap/swap_apple.c b/src/detection/swap/swap_apple.c new file mode 100644 index 0000000..40dda49 --- /dev/null +++ b/src/detection/swap/swap_apple.c @@ -0,0 +1,28 @@ +#include "swap.h" + +#include "common/sysctl.h" +#include <mach/mach.h> + +const char* ffDetectSwap(FFlist* result) { + struct xsw_usage xsw; + size_t size = sizeof(xsw); + if (sysctl((int[]) { CTL_VM, VM_SWAPUSAGE }, 2, &xsw, &size, NULL, 0) != 0) { + return "Failed to read vm.swapusage"; + } + + if (xsw.xsu_total == 0) { + if (__builtin_available(macOS 26.0, *)) { + // "vm.compressor_mode" no longer exists in macOS 26.0 + } else { + if (ffSysctlGetInt("vm.compressor_mode", 4) <= 2) { + return NULL; // Swap is disabled + } + } + } + + FFSwapResult* swap = FF_LIST_ADD(FFSwapResult, *result); + ffStrbufInitStatic(&swap->name, xsw.xsu_encrypted ? "Encrypted" : "Normal"); + swap->bytesTotal = xsw.xsu_total; + swap->bytesUsed = xsw.xsu_used; + return NULL; +} diff --git a/src/detection/swap/swap_bsd.c b/src/detection/swap/swap_bsd.c new file mode 100644 index 0000000..a54cd52 --- /dev/null +++ b/src/detection/swap/swap_bsd.c @@ -0,0 +1,78 @@ +#include "swap.h" +#include "common/sysctl.h" + +#include <vm/vm_param.h> +#include <sys/stat.h> +#include <sys/param.h> + +static void addSwapEntry(FFlist* result, struct xswdev* xsw, uint32_t pageSize) { + if (xsw->xsw_nblks == 0) { // DFBSD reports some /dev/wdog devices with nblks == 0 + return; + } + + FFSwapResult* swap = FF_LIST_ADD(FFSwapResult, *result); + if (xsw->xsw_dev == NODEV) { + ffStrbufInitStatic(&swap->name, "[NFS]"); + } else { + ffStrbufInitF(&swap->name, "/dev/%s", devname(xsw->xsw_dev, S_IFCHR)); + } + swap->bytesUsed = (uint64_t) xsw->xsw_used * pageSize; + swap->bytesTotal = (uint64_t) xsw->xsw_nblks * pageSize; +} + +#if __DragonFly__ + +const char* ffDetectSwap(FFlist* result) { + struct xswdev xsws[32]; + size_t size = sizeof(xsws); + if (sysctlbyname("vm.swap_info_array", xsws, &size, NULL, 0) < 0) { + return "sysctlbyname(\"vm.swap_info_array\") failed"; + } + + uint32_t pageSize = instance.state.platform.sysinfo.pageSize; + + size_t count = size / sizeof(struct xswdev); + if (count == 0) { + return NULL; + } + + if (xsws->xsw_version != XSWDEV_VERSION) { + return "xswdev version mismatch"; + } + + for (uint32_t i = 0; i < count; ++i) { + addSwapEntry(result, &xsws[i], pageSize); + } + + return NULL; +} + +#elif __FreeBSD__ + +const char* ffDetectSwap(FFlist* result) { + int mib[16]; + size_t mibsize = ARRAY_SIZE(mib); + if (sysctlnametomib("vm.swap_info", mib, &mibsize) < 0) { + return "sysctlnametomib(\"vm.swap_info\") failed"; + } + + uint32_t pageSize = instance.state.platform.sysinfo.pageSize; + + for (int n = 0;; ++n) { + mib[mibsize] = n; + struct xswdev xsw; + size_t size = sizeof(xsw); + if (sysctl(mib, (uint32_t) (mibsize + 1), &xsw, &size, NULL, 0) < 0) { + break; + } + if (xsw.xsw_version != XSWDEV_VERSION) { + return "xswdev version mismatch"; + } + + addSwapEntry(result, &xsw, pageSize); + } + + return NULL; +} + +#endif diff --git a/src/detection/swap/swap_haiku.c b/src/detection/swap/swap_haiku.c new file mode 100644 index 0000000..8a3b08c --- /dev/null +++ b/src/detection/swap/swap_haiku.c @@ -0,0 +1,27 @@ +#include "swap.h" + +#include <OS.h> +#include <driver_settings.h> + +const char* ffDetectSwap(FFlist* result) { + system_info info; + if (get_system_info(&info) != B_OK) { + return "Error getting system info"; + } + + uint32_t pageSize = instance.state.platform.sysinfo.pageSize; + FFSwapResult* swap = FF_LIST_ADD(FFSwapResult, *result); + ffStrbufInitStatic(&swap->name, "System"); + void* kvms = load_driver_settings("virtual_memory"); // /boot/home/config/settings/kernel/drivers/virtual_memory + if (kvms) { + const char* swapAuto = get_driver_parameter(kvms, "swap_auto", NULL, NULL); + if (swapAuto) { + ffStrbufSetStatic(&swap->name, swapAuto[0] == 'y' ? "Auto" : "Manual"); + } + unload_driver_settings(kvms); + } + swap->bytesTotal = pageSize * (uint64_t) info.max_swap_pages; + swap->bytesUsed = pageSize * (uint64_t) (info.max_swap_pages - info.free_swap_pages); + + return NULL; +} diff --git a/src/detection/swap/swap_linux.c b/src/detection/swap/swap_linux.c new file mode 100644 index 0000000..b31f268 --- /dev/null +++ b/src/detection/swap/swap_linux.c @@ -0,0 +1,82 @@ +#include "swap.h" + +#include "common/io.h" +#include "common/mallocHelper.h" + +#include <inttypes.h> + +static const char* detectByProcMeminfo(FFlist* result) { + // For Android + // Ref: #620 + char buf[PROC_FILE_BUFFSIZ]; + ssize_t nRead = ffReadFileData("/proc/meminfo", ARRAY_SIZE(buf) - 1, buf); + if (nRead < 0) { + return "ffReadFileData(\"/proc/meminfo\", ARRAY_SIZE(buf)-1, buf)"; + } + buf[nRead] = '\0'; + + uint64_t swapTotal = 0, swapFree = 0; + + char* token = NULL; + if ((token = strstr(buf, "SwapTotal:")) != NULL) { + swapTotal = strtoul(token + strlen("SwapTotal:"), NULL, 10); + } + + if ((token = strstr(buf, "SwapFree:")) != NULL) { + swapFree = strtoul(token + strlen("SwapFree:"), NULL, 10); + } + + FFSwapResult* swap = FF_LIST_ADD(FFSwapResult, *result); + ffStrbufInitStatic(&swap->name, "Total"); + swap->bytesTotal = swapTotal * 1024lu; + swap->bytesUsed = (swapTotal - swapFree) * 1024lu; + + return NULL; +} + +static const char* detectByProcSwaps(FFlist* result) { + // Ref: #620 + char buf[PROC_FILE_BUFFSIZ]; + ssize_t nRead = ffReadFileData("/proc/swaps", ARRAY_SIZE(buf) - 1, buf); + if (nRead <= 0) { + return "ffReadFileData(\"/proc/swaps\", ARRAY_SIZE(buf)-1, buf) failed"; + } + buf[nRead] = '\0'; + + // Skip header + char* line = memchr(buf, '\n', (size_t) nRead); + + while (line && *++line) { + uint64_t total, used; + char name[256]; + if (sscanf(line, "%255s %*[^\t]%" SCNu64 "%" SCNu64, name, &total, &used) != 3) { + return "Invalid /proc/swaps format found"; + } + + uint32_t nameLen = (uint32_t) strnlen(name, sizeof(name)); + FFSwapResult* swap = FF_LIST_ADD(FFSwapResult, *result); + ffStrbufInitA(&swap->name, nameLen); + for (size_t i = 0; i < nameLen; ++i) { + if (name[i] == '\\') { + char octal[4] = { name[i + 1], name[i + 2], name[i + 3], '\0' }; + ffStrbufAppendC(&swap->name, (char) strtol(octal, NULL, 8)); + i += 3; + } else { + ffStrbufAppendC(&swap->name, name[i]); + } + } + swap->bytesTotal = total * 1024u; + swap->bytesUsed = used * 1024u; + + line = memchr(line, '\n', (size_t) (nRead - (line - buf))); + } + + return NULL; +} + +const char* ffDetectSwap(FFlist* result) { + if (detectByProcSwaps(result) == NULL) { + return NULL; + } + return detectByProcMeminfo(result); +} diff --git a/src/detection/swap/swap_nosupport.c b/src/detection/swap/swap_nosupport.c new file mode 100644 index 0000000..ebc29b4 --- /dev/null +++ b/src/detection/swap/swap_nosupport.c @@ -0,0 +1,5 @@ +#include "swap.h" + +const char* ffDetectSwap(FFSwapResult* swap) { + return "Not supported on this platform"; +} diff --git a/src/detection/swap/swap_obsd.c b/src/detection/swap/swap_obsd.c new file mode 100644 index 0000000..1296aeb --- /dev/null +++ b/src/detection/swap/swap_obsd.c @@ -0,0 +1,35 @@ +#include "swap.h" +#include "common/FFlist.h" +#include "common/mallocHelper.h" + +#include <sys/types.h> +#include <sys/swap.h> +#include <sys/param.h> +#include <unistd.h> + +const char* ffDetectSwap(FFlist* result) { + int nswap = swapctl(SWAP_NSWAP, 0, 0); + if (nswap < 0) { + return "swapctl(SWAP_NSWAP) failed"; + } + if (nswap == 0) { + return NULL; + } + + FF_AUTO_FREE struct swapent* swdev = malloc((uint32_t) nswap * sizeof(*swdev)); + + if (swapctl(SWAP_STATS, swdev, nswap) < 0) { + return "swapctl(SWAP_STATS) failed"; + } + + for (int i = 0; i < nswap; i++) { + if (swdev[i].se_flags & SWF_ENABLE) { + FFSwapResult* swap = FF_LIST_ADD(FFSwapResult, *result); + ffStrbufInitS(&swap->name, swdev[i].se_path); + swap->bytesUsed = (uint64_t) swdev[i].se_inuse * DEV_BSIZE; + swap->bytesTotal = (uint64_t) swdev[i].se_nblks * DEV_BSIZE; + } + } + + return NULL; +} diff --git a/src/detection/swap/swap_sunos.c b/src/detection/swap/swap_sunos.c new file mode 100644 index 0000000..fa3260d --- /dev/null +++ b/src/detection/swap/swap_sunos.c @@ -0,0 +1,32 @@ +#include "swap.h" +#include <sys/stat.h> +#include <sys/swap.h> +#include <limits.h> +#include <stdalign.h> + +enum { FFMaxNSwap = 8 }; + +const char* ffDetectSwap(FFlist* result) { + char strings[FFMaxNSwap][PATH_MAX]; + alignas(swaptbl_t) uint8_t buffer[sizeof(swaptbl_t) + sizeof(swapent_t) * (FFMaxNSwap - 1)] = {}; + swaptbl_t* table = (swaptbl_t*) buffer; + table->swt_n = FFMaxNSwap; + for (int i = 0; i < FFMaxNSwap; ++i) { + table->swt_ent[i].ste_path = strings[i]; + } + + int size = swapctl(SC_LIST, table); + if (size < 0) { + return "swapctl() failed"; + } + + uint32_t pageSize = instance.state.platform.sysinfo.pageSize; + for (int i = 0; i < size; ++i) { + FFSwapResult* swap = FF_LIST_ADD(FFSwapResult, *result); + ffStrbufInitS(&swap->name, table->swt_ent[i].ste_path); + swap->bytesTotal = (uint64_t) table->swt_ent[i].ste_pages * pageSize; + swap->bytesUsed = swap->bytesTotal - (uint64_t) table->swt_ent[i].ste_free * pageSize; + } + + return NULL; +} diff --git a/src/detection/swap/swap_windows.c b/src/detection/swap/swap_windows.c new file mode 100644 index 0000000..e96298b --- /dev/null +++ b/src/detection/swap/swap_windows.c @@ -0,0 +1,35 @@ +#include "swap.h" +#include "common/windows/unicode.h" + +#include <winternl.h> +#include <ntstatus.h> +#include <windows.h> +#include <stdalign.h> + +const char* ffDetectSwap(FFlist* result) { + alignas(SYSTEM_PAGEFILE_INFORMATION) uint8_t buffer[4096]; + ULONG size = sizeof(buffer); + SYSTEM_PAGEFILE_INFORMATION* pstart = (SYSTEM_PAGEFILE_INFORMATION*) buffer; + if (!NT_SUCCESS(NtQuerySystemInformation(SystemPagefileInformation, pstart, size, &size))) { + return "NtQuerySystemInformation(SystemPagefileInformation, size) failed"; + } + + if (size == 0) { + return NULL; + } + + uint32_t pageSize = instance.state.platform.sysinfo.pageSize; + for (SYSTEM_PAGEFILE_INFORMATION* current = pstart;; current = (SYSTEM_PAGEFILE_INFORMATION*) ((uint8_t*) current + current->NextEntryOffset)) { + FFSwapResult* swap = FF_LIST_ADD(FFSwapResult, *result); + ffStrbufInitNWS(&swap->name, current->FileName.Length / sizeof(wchar_t), current->FileName.Buffer); + if (ffStrbufStartsWithS(&swap->name, "\\??\\")) { + ffStrbufSubstrAfter(&swap->name, strlen("\\??\\") - 1); + } + swap->bytesUsed = (uint64_t) current->TotalUsed * pageSize; + swap->bytesTotal = (uint64_t) current->CurrentSize * pageSize; + if (current->NextEntryOffset == 0) { + break; + } + } + return NULL; +} diff --git a/src/detection/terminalfont/terminalfont.c b/src/detection/terminalfont/terminalfont.c new file mode 100644 index 0000000..7ef7120 --- /dev/null +++ b/src/detection/terminalfont/terminalfont.c @@ -0,0 +1,407 @@ +#include "terminalfont.h" +#include "common/io.h" +#include "common/properties.h" +#include "common/processing.h" +#include "common/debug.h" +#include "common/strutil.h" +#include "detection/terminalshell/terminalshell.h" + +static void detectAlacritty(FFTerminalFontResult* terminalFont) { + // Maybe using a toml parser to read the config file is better? + // https://github.com/cktan/tomlc17 + + // Doc: https://alacritty.org/config-alacritty.html#s26 + FF_STRBUF_AUTO_DESTROY fontNormal = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontFamily = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontStyle = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + do { + FFpropquery fontQueryToml[] = { + { "normal =", &fontNormal }, + { "size =", &fontSize }, + }; + + // alacritty parses config files in this order + if (ffParsePropFileConfigValues("alacritty/alacritty.toml", 2, fontQueryToml)) { + break; + } + if (ffParsePropFileConfigValues("alacritty.toml", 2, fontQueryToml)) { + break; + } + if (ffParsePropFileConfigValues(".alacritty.toml", 2, fontQueryToml)) { + break; + } + } while (false); + + if (fontNormal.length > 0) { + // { family = "Fira Code", style = "Medium" } + ffStrbufTrimSpace(&fontNormal); + ffStrbufTrimRight(&fontNormal, '}'); + ffStrbufTrimLeft(&fontNormal, '{'); + ffStrbufTrimSpace(&fontNormal); + + // family = "Fira Code", style = "Medium" + ffStrbufReplaceAllC(&fontNormal, ',', '\n'); // Assume no commas in font names + ffParsePropLines(fontNormal.chars, "family =", &fontFamily); + ffParsePropLines(fontNormal.chars, "style =", &fontStyle); + } + + if (fontFamily.length == 0) { +#if __APPLE__ + ffStrbufSetStatic(&fontFamily, "Menlo"); +#elif _WIN32 + ffStrbufSetStatic(&fontFamily, "Consolas"); +#else + ffStrbufSetStatic(&fontFamily, "monospace"); +#endif + } + if (fontStyle.length == 0) { + ffStrbufSetStatic(&fontStyle, "Regular"); + } + + if (fontSize.length == 0) { + ffStrbufSetStatic(&fontSize, "11.25"); + } + + ffFontInitMoveValues(&terminalFont->font, &fontFamily, &fontSize, &fontStyle); +} + +static bool parseGhosttyConfig(FFstrbuf* path, FFstrbuf* fontName, FFstrbuf* fontNameFallback, FFstrbuf* fontSize) { + FF_DEBUG("parsing config: %s", path->chars); + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY temp = ffStrbufCreate(); + if (!ffAppendFileBuffer(path->chars, &buffer)) { + FF_DEBUG("cannot read config: %s", path->chars); + return false; + } + + char* line = NULL; + size_t len = 0; + while (ffStrbufGetline(&line, &len, &buffer)) { + if (ffParsePropLine(line, "font-family =", &temp)) { + FF_DEBUG("found font-family='%s' in %s", temp.chars, path->chars); + if (fontName->length > 0) { + ffStrbufDestroy(fontNameFallback); + ffStrbufInitMove(fontNameFallback, fontName); + } + ffStrbufDestroy(fontName); + ffStrbufInitMove(fontName, &temp); + } else if (ffParsePropLine(line, "font-size =", fontSize)) { + FF_DEBUG("found font-size='%s' in %s", temp.chars, path->chars); + // Latter overrides former + ffStrbufDestroy(fontSize); + ffStrbufInitMove(fontSize, &temp); + } + } + return true; +} + +static void detectGhostty(const FFstrbuf* exe, FFTerminalFontResult* terminalFont, const char* configPathMac, const char* configPathUnix) { + FF_DEBUG("detectGhostty: start"); + FF_STRBUF_AUTO_DESTROY configPath = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontNameFallback = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + if (configPathMac && configPathUnix) { +#if __APPLE__ + ffStrbufSet(&configPath, &instance.state.platform.homeDir); + ffStrbufAppendS(&configPath, "Library/Application Support/"); + ffStrbufAppendS(&configPath, configPathMac); // com.mitchellh.ghostty/config + parseGhosttyConfig(&configPath, &fontName, &fontNameFallback, &fontSize); +#endif + + if (instance.state.platform.configDirs.length > 0) { + ffStrbufSet(&configPath, FF_LIST_FIRST(FFstrbuf, instance.state.platform.configDirs)); + ffStrbufAppendS(&configPath, configPathUnix); // ghostty/config + parseGhosttyConfig(&configPath, &fontName, &fontNameFallback, &fontSize); + } + } else { + // Try ghostty +show-config first + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + const char* error = ffProcessAppendStdOut(&buffer, (char* const[]){ + exe->chars, + "+show-config", + NULL, + }); + if (error == NULL) { + char* line = NULL; + size_t len = 0; + while (ffStrbufGetline(&line, &len, &buffer)) { + if (ffStrStartsWith(line, "font-family = ")) { + FF_DEBUG("found %s", line); + if (fontName.length > 0) { + ffStrbufDestroy(&fontNameFallback); + ffStrbufInitMove(&fontNameFallback, &fontName); + } + ffStrbufSetNS(&fontName, (uint32_t) (len - strlen("font-family = ")), line + strlen("font-family = ")); + } else if (ffStrStartsWith(line, "font-size = ")) { + FF_DEBUG("found %s", line); + // `ghostty +show-config` reports only one font size even if the config has multiple font sizes + ffStrbufSetNS(&fontSize, (uint32_t) (len - strlen("font-size = ")), line + strlen("font-size = ")); + } + } + } else { + FF_DEBUG("`ghostty +show-config` failed: %s", error); + } + } + + if (fontName.length == 0) { + ffStrbufAppendS(&fontName, "JetBrainsMono Nerd Font"); + FF_DEBUG("using default family='%s'", fontName.chars); + } + + if (fontSize.length == 0) { + ffStrbufAppendS(&fontSize, +#if __APPLE__ + "13" +#else + "12" +#endif + ); + FF_DEBUG("using default size='%s'", fontSize.chars); + } + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); + if (fontNameFallback.length > 0) { + FF_DEBUG("applying fallback family='%s'", fontNameFallback.chars); + ffFontInitValues(&terminalFont->fallback, fontNameFallback.chars, NULL); + } + FF_DEBUG("result family='%s' size='%s'%s", fontName.chars, fontSize.chars, fontNameFallback.length ? " (with fallback)" : ""); + FF_DEBUG("detectGhostty: end"); +} + +FF_A_UNUSED static void detectTTY(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + + ffParsePropFile(FASTFETCH_TARGET_DIR_ETC "/vconsole.conf", "Font =", &fontName); + + if (fontName.length == 0) { + ffStrbufAppendS(&fontName, "VGA default kernel font "); + ffProcessAppendStdOut(&fontName, (char* const[]){ "showconsolefont", "--info", NULL }); + + ffStrbufTrimRight(&fontName, ' '); + } + + if (fontName.length > 0) { + ffFontInitCopy(&terminalFont->font, fontName.chars); + } else { + ffStrbufAppendS(&terminalFont->error, "Couldn't find Font in " FASTFETCH_TARGET_DIR_ETC "/vconsole.conf"); + } +} + +FF_A_UNUSED static bool detectKitty(const FFstrbuf* exe, FFTerminalFontResult* result) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + char fontHex[512] = "", sizeHex[512] = ""; + // https://github.com/fastfetch-cli/fastfetch/discussions/1030#discussioncomment-9845233 + if (ffGetTerminalResponse( + "\eP+q6b697474792d71756572792d666f6e745f66616d696c79;6b697474792d71756572792d666f6e745f73697a65\e\\", // kitty-query-font_family;kitty-query-font_size + 2, + "\eP1+r%*[^=]=%511[^\e]\e\\\eP1+r%*[^=]=%511[^\e]\e\\", + fontHex, + sizeHex) == NULL && + *fontHex && *sizeHex) { + // decode hex string + for (const char* p = fontHex; p[0] && p[1]; p += 2) { + unsigned value; + if (sscanf(p, "%2x", &value) == 1) { + ffStrbufAppendC(&fontName, (char) value); + } + } + for (const char* p = sizeHex; p[0] && p[1]; p += 2) { + unsigned value; + if (sscanf(p, "%2x", &value) == 1) { + ffStrbufAppendC(&fontSize, (char) value); + } + } + } else { + FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); + if (!ffProcessAppendStdOut(&buf, (char* const[]){ + exe->chars, + "+kitten", + "query-terminal", + NULL, + })) { + ffParsePropLines(buf.chars, "font_family: ", &fontName); + ffParsePropLines(buf.chars, "font_size: ", &fontSize); + } else { + FFpropquery fontQuery[] = { + { "font_family ", &fontName }, + { "font_size ", &fontSize }, + }; + + ffParsePropFileConfigValues("kitty/kitty.conf", 2, fontQuery); + + if (fontName.length == 0) { + ffStrbufSetS(&fontName, "monospace"); + } + if (fontSize.length == 0) { + ffStrbufSetS(&fontSize, "11.0"); + } + } + } + + ffFontInitValues(&result->font, fontName.chars, fontSize.chars); + + return true; +} + +static bool detectWezterm(const FFstrbuf* exe, FFTerminalFontResult* result) { + FF_STRBUF_AUTO_DESTROY cli = ffStrbufCreateCopy(exe); + ffStrbufSubstrBeforeLastC(&cli, '-'); + +#ifdef _WIN32 + ffStrbufAppendS(&cli, ".exe"); +#endif + + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + + ffStrbufSetS(&result->error, ffProcessAppendStdOut(&fontName, (char* const[]){ cli.chars, "ls-fonts", "--text", "a", NULL })); + if (result->error.length) { + return false; + } + + // LeftToRight + // 0 a \u{61} x_adv=7 cells=1 glyph=a,180 wezterm.font("JetBrains Mono", {weight="Regular", stretch="Normal", style="Normal"}) + // <built-in>, BuiltIn + ffStrbufSubstrAfterFirstC(&fontName, '"'); + ffStrbufSubstrBeforeFirstC(&fontName, '"'); + + if (!fontName.length) { + return false; + } + + ffFontInitCopy(&result->font, fontName.chars); + return true; +} + +static bool detectTabby(FFTerminalFontResult* result) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + FFpropquery fontQuery[] = { + { "font: ", &fontName }, + { "fontSize: ", &fontSize }, + }; + + if (!ffParsePropFileConfigValues("tabby/config.yaml", 2, fontQuery)) { + return false; + } + + if (fontName.length == 0) { + ffStrbufSetS(&fontName, "monospace"); + } + if (fontSize.length == 0) { + ffStrbufSetS(&fontSize, "14"); + } + + ffFontInitValues(&result->font, fontName.chars, fontSize.chars); + + return true; +} + +static bool detectContour(const FFstrbuf* exe, FFTerminalFontResult* result) { + FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); + if (ffProcessAppendStdOut(&buf, (char* const[]){ exe->chars, "font-locator", NULL })) { + ffStrbufAppendS(&result->error, "`contour font-locator` failed"); + return false; + } + + //[error] Missing key .logging.enabled. Using default: false. + //[error] ... + // Matching fonts using : Fontconfig + // Font description : (family=Sarasa Term SC Nerd weight=Regular slant=Roman spacing=Monospace, strict_spacing=yes) + // Number of fonts found : 49 + // path /usr/share/fonts/google-noto/NotoSansMono-Regular.ttf Regular Roman + // path ... + + uint32_t index = ffStrbufFirstIndexS(&buf, "Font description : (family="); + if (index >= buf.length) { + return false; + } + index += (uint32_t) strlen("Font description : (family="); + ffStrbufSubstrBefore(&buf, ffStrbufNextIndexS(&buf, index, " weight=")); + ffFontInitCopy(&result->font, buf.chars + index); + return true; +} + +static bool detectRio(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + FFpropquery fontQueryToml[] = { + { "family =", &fontName }, + { "size =", &fontSize }, + }; + + ffParsePropFileConfigValues("rio/config.toml", 2, fontQueryToml); + + if (fontName.length == 0) { + ffStrbufAppendS(&fontName, "Cascadia Code"); + } + + if (fontSize.length == 0) { + ffStrbufAppendS(&fontSize, "18"); + } + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); + + return true; +} + +bool ffDetectTerminalFontPlatform(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont); + +static bool detectTerminalFontCommon(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont) { + if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "alacritty")) { + detectAlacritty(terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "wezterm-gui")) { + detectWezterm(&terminal->exe, terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "tabby")) { + detectTabby(terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "contour")) { + detectContour(&terminal->exe, terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "ghostty")) { + detectGhostty(&terminal->exe, terminalFont, NULL, NULL); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "Muxy")) { + detectGhostty(&terminal->exe, terminalFont, "Muxy/ghostty.conf", "muxy/ghostty.conf"); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "rio")) { + detectRio(terminalFont); + } +#ifndef _WIN32 + else if (ffStrbufStartsWithIgnCaseS(&terminal->exe, "/dev/pts/")) { + ffStrbufAppendS(&terminalFont->error, "Terminal font detection is not supported on PTS"); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "kitty")) { + detectKitty(&terminal->exe, terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->exe, "/dev/tty")) { + detectTTY(terminalFont); + } +#endif + + else { + return false; + } + + return true; +} + +bool ffDetectTerminalFont(FFTerminalFontResult* result) { + const FFTerminalResult* terminal = ffDetectTerminal(); + + if (terminal->processName.length == 0) { + ffStrbufAppendS(&result->error, "Terminal font needs successful terminal detection"); + } + + else if (!detectTerminalFontCommon(terminal, result)) { + ffDetectTerminalFontPlatform(terminal, result); + } + + if (result->error.length == 0 && result->font.pretty.length == 0) { + ffStrbufAppendF(&result->error, "Unknown terminal: %s", terminal->processName.chars); + } + + return result->error.length == 0; +} diff --git a/src/detection/terminalfont/terminalfont.h b/src/detection/terminalfont/terminalfont.h new file mode 100644 index 0000000..bdc416c --- /dev/null +++ b/src/detection/terminalfont/terminalfont.h @@ -0,0 +1,13 @@ +#pragma once + +#include "fastfetch.h" +#include "common/font.h" +#include "modules/font/option.h" + +typedef struct FFTerminalFontResult { + FFstrbuf error; + FFfont font; + FFfont fallback; +} FFTerminalFontResult; + +bool ffDetectTerminalFont(FFTerminalFontResult* result); diff --git a/src/detection/terminalfont/terminalfont_android.c b/src/detection/terminalfont/terminalfont_android.c new file mode 100644 index 0000000..a721078 --- /dev/null +++ b/src/detection/terminalfont/terminalfont_android.c @@ -0,0 +1,71 @@ +#include "fastfetch.h" +#include "terminalfont.h" +#include "detection/terminalshell/terminalshell.h" +#include "common/io.h" + +#ifdef FF_HAVE_FREETYPE + #include "common/library.h" + #include <ft2build.h> + #include FT_FREETYPE_H +#endif + +#define FF_TERMUX_FONT_PATH FASTFETCH_TARGET_DIR_HOME "/.termux/font.ttf" + +const char* detectTermux(FFTerminalFontResult* terminalFont) { + if (!ffPathExists(FF_TERMUX_FONT_PATH, FF_PATHTYPE_FILE)) { + ffFontInitCopy(&terminalFont->font, "monospace"); + return NULL; + } + +#ifdef FF_HAVE_FREETYPE + + FF_LIBRARY_LOAD_MESSAGE(freetype, "libfreetype" FF_LIBRARY_EXTENSION, 2) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(freetype, FT_Init_FreeType); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(freetype, FT_New_Face); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(freetype, FT_Done_Face); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(freetype, FT_Done_FreeType); + + FT_Library library = NULL; + FT_Face face = NULL; + const char* error = NULL; + + if (ffFT_Init_FreeType(&library)) { + error = "FT_Init_FreeType() failed"; + goto exit; + } + + if (ffFT_New_Face(library, FF_TERMUX_FONT_PATH, 0, &face)) { + error = "FT_NEW_Face(" FF_TERMUX_FONT_PATH ") failed"; + goto exit; + } + + ffFontInitCopy(&terminalFont->font, face->family_name); + +exit: + if (face) { + ffFT_Done_Face(face); + } + if (library) { + ffFT_Done_FreeType(library); + } + + return error; + +#else + + FF_UNUSED(terminalFont); + return "Fastfetch was built without freetype2 support"; + +#endif +} + +bool ffDetectTerminalFontPlatform(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont) { + if (ffStrbufEqualS(&terminal->processName, "com.termux")) { + ffStrbufSetS(&terminalFont->error, detectTermux(terminalFont)); + } else { + bool ffDetectTerminalFontPlatformLinux(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont); + return ffDetectTerminalFontPlatformLinux(terminal, terminalFont); + } + + return true; +} diff --git a/src/detection/terminalfont/terminalfont_apple.m b/src/detection/terminalfont/terminalfont_apple.m new file mode 100644 index 0000000..126235f --- /dev/null +++ b/src/detection/terminalfont/terminalfont_apple.m @@ -0,0 +1,106 @@ +#include "terminalfont.h" +#include "common/font.h" +#include "detection/terminalshell/terminalshell.h" +#include "common/apple/osascript.h" + +#include <stdlib.h> +#include <string.h> +#import <Foundation/Foundation.h> + +static void detectIterm2(FFTerminalFontResult* terminalFont) +{ + const char* profile = getenv("ITERM_PROFILE"); + if (profile == NULL) + { + ffStrbufAppendS(&terminalFont->error, "environment variable ITERM_PROFILE not set"); + return; + } + + NSError* error; + NSString* fileName = [NSString stringWithFormat:@"file://%s/Library/Preferences/com.googlecode.iterm2.plist", instance.state.platform.homeDir.chars]; + NSDictionary* dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL URLWithString:fileName] + error:&error]; + if(error) + { + ffStrbufAppendS(&terminalFont->error, error.localizedDescription.UTF8String); + return; + } + + for(NSDictionary* bookmark in dict[@"New Bookmarks"]) + { + if(![bookmark[@"Name"] isEqualToString:@(profile)]) + continue; + + NSString* normalFont = bookmark[@"Normal Font"]; + if(!normalFont) + { + ffStrbufAppendF(&terminalFont->error, "`Normal Font` key in profile `%s` doesn't exist", profile); + return; + } + ffFontInitWithSpace(&terminalFont->font, normalFont.UTF8String); + + NSNumber* useNonAsciiFont = bookmark[@"Use Non-ASCII Font"]; + if(useNonAsciiFont.boolValue) + { + NSString* nonAsciiFont = bookmark[@"Non Ascii Font"]; + if (nonAsciiFont) + ffFontInitWithSpace(&terminalFont->fallback, nonAsciiFont.UTF8String); + } + return; + } + + ffStrbufAppendF(&terminalFont->error, "find profile `%s` bookmark failed", profile); +} + +static void detectAppleTerminal(FFTerminalFontResult* terminalFont) +{ + FF_STRBUF_AUTO_DESTROY font = ffStrbufCreate(); + ffOsascript("tell application \"Terminal\" to font name of window frontmost & \" \" & font size of window frontmost", &font); + + if(font.length == 0) + { + ffStrbufAppendS(&terminalFont->error, "executing osascript failed"); + return; + } + + ffFontInitWithSpace(&terminalFont->font, font.chars); +} + +static void detectWarpTerminal(FFTerminalFontResult* terminalFont) +{ + NSError* error; + NSString* fileName = [NSString stringWithFormat:@"file://%s/Library/Preferences/dev.warp.Warp-Stable.plist", instance.state.platform.homeDir.chars]; + NSDictionary* dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL URLWithString:fileName] + error:&error]; + if(error) + { + ffStrbufAppendS(&terminalFont->error, error.localizedDescription.UTF8String); + return; + } + + NSString* fontName = dict[@"FontName"]; + if(!fontName) + fontName = @"Hack"; + else + fontName = [fontName stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\""]]; + + NSString* fontSize = dict[@"FontSize"]; + if(!fontSize) + fontSize = @"13"; + + ffFontInitValues(&terminalFont->font, fontName.UTF8String, fontSize.UTF8String); +} + +bool ffDetectTerminalFontPlatform(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont) +{ + if(ffStrbufIgnCaseEqualS(&terminal->processName, "iterm.app") || + ffStrbufStartsWithIgnCaseS(&terminal->processName, "iTermServer-")) + detectIterm2(terminalFont); + else if(ffStrbufIgnCaseEqualS(&terminal->processName, "Apple_Terminal")) + detectAppleTerminal(terminalFont); + else if(ffStrbufIgnCaseEqualS(&terminal->processName, "WarpTerminal")) + detectWarpTerminal(terminalFont); + else + return false; + return true; +} diff --git a/src/detection/terminalfont/terminalfont_linux.c b/src/detection/terminalfont/terminalfont_linux.c new file mode 100644 index 0000000..bfc8959 --- /dev/null +++ b/src/detection/terminalfont/terminalfont_linux.c @@ -0,0 +1,545 @@ +#include "common/font.h" +#include "terminalfont.h" +#include "common/settings.h" +#include "common/properties.h" +#include "common/parsing.h" +#include "common/io.h" +#include "common/processing.h" +#include "common/mallocHelper.h" +#include "common/strutil.h" +#include "common/binary.h" +#include "detection/terminalshell/terminalshell.h" +#include "detection/displayserver/displayserver.h" + +static const char* getSystemMonospaceFont(void) { + const FFDisplayServerResult* wmde = ffConnectDisplayServer(); + + if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, "Cinnamon")) { + const char* systemMonospaceFont = ffSettingsGetGnome("/org/cinnamon/desktop/interface/monospace-font-name", "org.cinnamon.desktop.interface", NULL, "monospace-font-name", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(systemMonospaceFont)) { + return systemMonospaceFont; + } + } else if (ffStrbufIgnCaseEqualS(&wmde->dePrettyName, "Mate")) { + const char* systemMonospaceFont = ffSettingsGetGnome("/org/mate/interface/monospace-font-name", "org.mate.interface", NULL, "monospace-font-name", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(systemMonospaceFont)) { + return systemMonospaceFont; + } + } + + return ffSettingsGetGnome("/org/gnome/desktop/interface/monospace-font-name", "org.gnome.desktop.interface", NULL, "monospace-font-name", FF_VARIANT_TYPE_STRING).strValue; +} + +static void detectKgx(FFTerminalFontResult* terminalFont) { + // kgx (gnome console) doesn't support profiles + if (!ffSettingsGetGnome("/org/gnome/Console/use-system-font", "org.gnome.Console", NULL, "use-system-font", FF_VARIANT_TYPE_BOOL).boolValue) { + FF_AUTO_FREE const char* fontName = ffSettingsGetGnome("/org/gnome/Console/custom-font", "org.gnome.Console", NULL, "custom-font", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(fontName)) { + ffFontInitPango(&terminalFont->font, fontName); + } else { + ffStrbufAppendF(&terminalFont->error, "Couldn't get terminal font from GSettings (org.gnome.Console::custom-font)"); + } + } else { + FF_AUTO_FREE const char* fontName = getSystemMonospaceFont(); + if (ffStrSet(fontName)) { + ffFontInitPango(&terminalFont->font, fontName); + } else { + ffStrbufAppendS(&terminalFont->error, "Couldn't get system monospace font name from GSettings / DConf"); + } + } +} + +static void detectPtyxis(FFTerminalFontResult* terminalFont) { + if (!ffSettingsGetGnome("/org/gnome/Ptyxis/use-system-font", "org.gnome.Ptyxis", NULL, "use-system-font", FF_VARIANT_TYPE_BOOL).boolValue) { + FF_AUTO_FREE const char* fontName = ffSettingsGetGnome("/org/gnome/Ptyxis/font-name", "org.gnome.Ptyxis", NULL, "font-name", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(fontName)) { + ffFontInitPango(&terminalFont->font, fontName); + } else { + ffStrbufAppendF(&terminalFont->error, "Couldn't get terminal font from GSettings (org.gnome.Ptyxis::font-name)"); + } + } else { + FF_AUTO_FREE const char* fontName = getSystemMonospaceFont(); + if (ffStrSet(fontName)) { + ffFontInitPango(&terminalFont->font, fontName); + } else { + ffStrbufAppendS(&terminalFont->error, "Couldn't get system monospace font name from GSettings / DConf"); + } + } +} + +static void detectFromGSettings(const char* profilePath, const char* profileList, const char* profile, const char* defaultProfileKey, FFTerminalFontResult* terminalFont) { + FF_AUTO_FREE const char* defaultProfile = ffSettingsGetGSettings(profileList, NULL, defaultProfileKey, FF_VARIANT_TYPE_STRING).strValue; + if (!ffStrSet(defaultProfile)) { + ffStrbufAppendF(&terminalFont->error, "Could not get default profile from gsettings: %s", profileList); + return; + } + + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateA(128); + ffStrbufAppendS(&path, profilePath); + ffStrbufAppendS(&path, defaultProfile); + ffStrbufAppendC(&path, '/'); + + if (!ffSettingsGetGSettings(profile, path.chars, "use-system-font", FF_VARIANT_TYPE_BOOL).boolValue) { + FF_AUTO_FREE const char* fontName = ffSettingsGetGSettings(profile, path.chars, "font", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(fontName)) { + ffFontInitPango(&terminalFont->font, fontName); + } else { + ffStrbufAppendF(&terminalFont->error, "Couldn't get terminal font from GSettings (%s::%s::font)", profile, path.chars); + } + } else { + FF_AUTO_FREE const char* fontName = getSystemMonospaceFont(); + if (ffStrSet(fontName)) { + ffFontInitPango(&terminalFont->font, fontName); + } else { + ffStrbufAppendS(&terminalFont->error, "Couldn't get system monospace font name from GSettings / DConf"); + } + } +} + +static void detectFromConfigFile(const char* configFile, const char* start, FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + ffParsePropFileConfig(configFile, start, &fontName); + + if (fontName.length == 0) { + ffStrbufAppendF(&terminalFont->error, "Couldn't find %s in .config/%s", start, configFile); + } else { + ffFontInitPango(&terminalFont->font, fontName.chars); + } +} + +static void detectKonsole(FFTerminalFontResult* terminalFont, const char* rcFile) { + FF_STRBUF_AUTO_DESTROY profile = ffStrbufCreate(); + if (!ffParsePropFileConfig(rcFile, "DefaultProfile =", &profile)) { + ffStrbufAppendF(&terminalFont->error, "Configuration \".config/%s\" doesn't exist", rcFile); + return; + } + + if (profile.length == 0) { + ffStrbufAppendS(&terminalFont->error, "Built-in profile is used"); + return; + } + + FF_STRBUF_AUTO_DESTROY profilePath = ffStrbufCreateA(32); + ffStrbufAppendS(&profilePath, "konsole/"); + ffStrbufAppend(&profilePath, &profile); + + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + ffParsePropFileData(profilePath.chars, "Font =", &fontName); + + if (fontName.length == 0) { + ffStrbufAppendF(&terminalFont->error, "Couldn't find \"Font=%%[^\\n]\" in \"%s\"", profilePath.chars); + } else { + ffFontInitQt(&terminalFont->font, fontName.chars); + } +} + +static void detectXFCETerminal(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY useSysFont = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + + const char* path = "xfce4/xfconf/xfce-perchannel-xml/xfce4-terminal.xml"; + bool configFound = ffParsePropFileConfigValues(path, 2, (FFpropquery[]) { { "<property name=\"font-use-system\" type=\"bool\" value=\"", &useSysFont }, { "<property name=\"font-name\" type=\"string\" value=\"", &fontName } }); + + if (configFound) { + ffStrbufSubstrBeforeLastC(&useSysFont, '"'); + ffStrbufSubstrBeforeLastC(&fontName, '"'); + } else { + path = "xfce4/terminal/terminalrc"; + configFound = ffParsePropFileConfigValues(path, 2, (FFpropquery[]) { { "FontUseSystem = ", &useSysFont }, { "FontName = ", &fontName } }); + } + + if (configFound && (useSysFont.length == 0 || ffStrbufIgnCaseEqualS(&useSysFont, "false"))) { + if (fontName.length == 0) { + ffStrbufAppendF(&terminalFont->error, "Couldn't find FontName in %s", path); + } else { + ffFontInitPango(&terminalFont->font, fontName.chars); + } + } else { + const char* systemFontName = ffSettingsGetXFConf("xsettings", "/Gtk/MonospaceFontName", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(systemFontName)) { + ffFontInitPango(&terminalFont->font, systemFontName); + } else { + ffStrbufAppendS(&terminalFont->error, "Couldn't find xsettings::/Gtk/MonospaceFontName in XFConf"); + } + } +} + +static void detectDeepinTerminal(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + FF_STRBUF_AUTO_DESTROY profile = ffStrbufCreateA(64); + ffSearchUserConfigFile(&instance.state.platform.configDirs, "deepin/deepin-terminal/config.conf", &profile); + FILE* file = fopen(profile.chars, "r"); + + if (file) { + char* line = NULL; + size_t len = 0; + + for (int count = 0; getline(&line, &len, file) != -1 && count < 2;) { + if (ffStrEquals(line, "[basic.interface.font]\n")) { + if (getline(&line, &len, file) != -1) { + ffParsePropLine(line, "value=", &fontName); + } + ++count; + } else if (ffStrEquals(line, "[basic.interface.font_size]\n")) { + if (getline(&line, &len, file) != -1) { + ffParsePropLine(line, "value=", &fontSize); + } + ++count; + } + } + + free(line); + fclose(file); + } + + if (fontName.length == 0) { + ffStrbufAppendS(&fontName, "Noto Sans Mono"); + } + if (fontSize.length == 0) { + ffStrbufAppendS(&fontSize, "11"); + } + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); +} + +static void detectFootTerminal(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY font = ffStrbufCreate(); + + if (!ffParsePropFileConfig("foot/foot.ini", "font=", &font) || !ffStrSet(font.chars)) { + ffFontInitValues(&terminalFont->font, "monospace", "8"); + return; + } + + // Sarasa Term SC Nerd:size=8 + uint32_t colon = ffStrbufFirstIndexC(&font, ':'); + if (colon == font.length) { + ffFontInitValues(&terminalFont->font, font.chars, "8"); + return; + } + uint32_t equal = ffStrbufNextIndexS(&font, colon, "size="); + font.chars[colon] = '\0'; + if (equal == font.length) { + ffFontInitValues(&terminalFont->font, font.chars, "8"); + return; + } + uint32_t size = equal + (uint32_t) strlen("size="); + uint32_t comma = ffStrbufNextIndexC(&font, size, ','); + if (comma < font.length) { + font.chars[comma] = '\0'; + } + ffFontInitValues(&terminalFont->font, font.chars, &font.chars[size]); + if (comma < font.length) { + ffFontInitValues(&terminalFont->fallback, &font.chars[comma + 1], NULL); + } +} + +static void detectQTerminal(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + ffParsePropFileConfigValues("qterminal.org/qterminal.ini", 2, (FFpropquery[]) { + { "fontFamily=", &fontName }, + { "fontSize=", &fontSize }, + }); + + if (fontName.length == 0) { + ffStrbufAppendS(&fontName, "monospace"); + } + if (fontSize.length == 0) { + ffStrbufAppendS(&fontSize, "12"); + } + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); +} + +static void detectXterm(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + ffParsePropFileHomeValues(".Xresources", 2, (FFpropquery[]) { + { "xterm*faceName:", &fontName }, + { "xterm*faceSize:", &fontSize }, + }); + + if (fontName.length == 0) { + ffParsePropFileHomeValues(".Xresources", 2, (FFpropquery[]) { + { "xterm.vt100.faceName:", &fontName }, + { "xterm.vt100.faceSize:", &fontSize }, + }); + } + + if (fontName.length == 0) { + ffStrbufAppendS(&fontName, "fixed"); + } + if (fontSize.length == 0) { + ffStrbufAppendS(&fontSize, "8.0"); + } + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); +} + +static bool extractStTermFont(const char* str, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffStrContains(str, "size=")) { + return true; + } + ffStrbufSetNS((FFstrbuf*) userdata, len, str); + return false; +} + +static void detectSt(FFTerminalFontResult* terminalFont, const FFTerminalResult* terminal) { + FF_STRBUF_AUTO_DESTROY size = ffStrbufCreateF("/proc/%u/cmdline", terminal->pid); + FF_STRBUF_AUTO_DESTROY font = ffStrbufCreate(); + if (!ffAppendFileBuffer(size.chars, &font)) { + ffStrbufAppendF(&terminalFont->error, "Failed to open %s", size.chars); + return; + } + + const char* p = memmem(font.chars, font.length, "\0-f", sizeof("\0-f")); // find parameter of `-f` + if (p) { + // st was executed with `-f` parameter + ffStrbufSubstrAfter(&font, (uint32_t) (p + (sizeof("\0-f") - 1) - font.chars)); + ffStrbufRecalculateLength(&font); + } else { + ffStrbufClear(&font); + + const char* error = ffBinaryExtractStrings(terminal->exePath.chars, extractStTermFont, &font, (uint32_t) strlen("size=0")); + if (error) { + ffStrbufAppendS(&terminalFont->error, error); + return; + } + if (font.length == 0) { + ffStrbufAppendS(&terminalFont->error, "No font config found in st binary"); + return; + } + } + + // JetBrainsMono Nerd Font Mono:pixelsize=12:antialias=true:autohint=true + + uint32_t index = ffStrbufFirstIndexC(&font, ':'); + if (index != font.length) { + uint32_t sIndex = ffStrbufNextIndexS(&font, index + 1, "size="); + if (sIndex != font.length) { + sIndex += (uint32_t) strlen("size="); + uint32_t sIndexEnd = ffStrbufNextIndexC(&font, sIndex, ':'); + ffStrbufSetNS(&size, sIndexEnd - sIndex, font.chars + sIndex); + } + ffStrbufSubstrBefore(&font, index); + } else { + ffStrbufClear(&size); + } + ffFontInitValues(&terminalFont->font, font.chars, size.chars); +} + +static void detectWarp(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY baseDir = ffStrbufCreateA(64); + + FF_LIST_FOR_EACH (FFstrbuf, dirPrefix, instance.state.platform.configDirs) { + // We need to copy the dir each time, because it used by multiple threads, so we can't directly write to it. + ffStrbufSet(&baseDir, dirPrefix); + ffStrbufAppendS(&baseDir, "warp-terminal/user_preferences.json"); + + yyjson_doc* doc = yyjson_read_file(baseDir.chars, YYJSON_READ_INSITU | YYJSON_READ_ALLOW_TRAILING_COMMAS | YYJSON_READ_ALLOW_COMMENTS, NULL, NULL); + if (!doc) { + continue; + } + + yyjson_val* prefs = yyjson_obj_get(yyjson_doc_get_root(doc), "prefs"); + if (yyjson_is_obj(prefs)) { + const char* fontName = yyjson_get_str(yyjson_obj_get(prefs, "FontName")); + if (!fontName) { + fontName = "Hack"; + } + const char* fontSize = yyjson_get_str(yyjson_obj_get(prefs, "FontSize")); + if (!fontSize) { + fontSize = "13"; + } + + ffFontInitValues(&terminalFont->font, fontName, fontSize); + } + yyjson_doc_free(doc); + return; + } +} + +static void detectTerminator(FFTerminalFontResult* result) { + FF_STRBUF_AUTO_DESTROY useSystemFont = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + + if (!ffParsePropFileConfigValues("terminator/config", 2, (FFpropquery[]) { + { "use_system_font =", &useSystemFont }, + { "font =", &fontName }, + }) || + ffStrbufIgnCaseEqualS(&useSystemFont, "True")) { + FF_AUTO_FREE const char* fontName = getSystemMonospaceFont(); + if (ffStrSet(fontName)) { + ffFontInitPango(&result->font, fontName); + } else { + ffStrbufAppendS(&result->error, "Couldn't get system monospace font name from GSettings / DConf"); + } + return; + } + + if (fontName.length == 0) { + ffFontInitValues(&result->font, "Mono", "10"); + } else { + ffFontInitPango(&result->font, fontName.chars); + } +} + +static void detectWestonTerminal(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY font = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY size = ffStrbufCreate(); + ffParsePropFileConfigValues("weston.ini", 2, (FFpropquery[]) { + { "font=", &font }, + { "font-size=", &size }, + }); + if (!font.length) { + ffStrbufSetStatic(&font, "DejaVu Sans Mono"); + } + if (!size.length) { + ffStrbufSetStatic(&size, "14"); + } + ffFontInitValues(&terminalFont->font, font.chars, size.chars); +} + +static void detectUrxvt(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + if (!(ffParsePropFileHomeValues(".Xresources", 1, (FFpropquery[]) { + { "URxvt.font:", &buffer }, + }) || + ffParsePropFileHomeValues(".Xdefaults", 1, (FFpropquery[]) { + { "URxvt.font:", &buffer }, + }))) { + ffStrbufAppendS(&terminalFont->error, "Could not find URxvt.font in .Xresources or .Xdefaults"); + return; + } + + uint32_t index = 0; + + char* line = NULL; + size_t len = 0; + while (ffStrbufGetdelim(&line, &len, ',', &buffer)) { + FFfont* font = index == 0 ? &terminalFont->font : &terminalFont->fallback; + if (line[0] == '-') { + ffFontInitXlfd(font, line); + } else if (ffStrStartsWith(line, "xft:")) { + ffFontInitXft(font, line + 4); + } else { + ffStrbufAppendF(&terminalFont->error, "Unknown URxvt font format: %s", line); + continue; + } + index++; + if (index > 1) { + break; + } + } +} + +static bool detectCosmicTerm(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateCopy(&instance.state.platform.homeDir); + ffStrbufAppendS(&path, ".config/cosmic/com.system76.CosmicTerm/v1/"); + uint32_t baseLen = path.length; + + ffStrbufAppendS(&path, "font_name"); + ffReadFileBuffer(path.chars, &fontName); + ffStrbufTrim(&path, '"'); + ffStrbufSubstrBefore(&path, baseLen); + if (fontName.length == 0) { + ffStrbufSetStatic(&fontName, "Noto Sans Mono"); + } + + ffStrbufAppendS(&path, "font_size"); + if (ffReadFileBuffer(path.chars, &fontSize)) { + ffStrbufAppendS(&fontSize, "px"); + } + ffStrbufSubstrBefore(&path, baseLen); + if (fontSize.length == 0) { + ffStrbufSetStatic(&fontSize, "14px"); + } + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); + + return true; +} + +#ifdef __HAIKU__ +static void detectHaikuTerminal(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY font = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY size = ffStrbufCreate(); + ffParsePropFileConfigValues("Terminal/Default", 2, (FFpropquery[]) { + { "\"Half Font Family\" , ", &font }, + { "\"Half Font Size\" , ", &size }, + }); + if (!font.length) { + ffStrbufSetStatic(&font, "Noto Sans Mono"); + } + if (!size.length) { + ffStrbufSetStatic(&size, "12"); + } + ffFontInitValues(&terminalFont->font, font.chars, size.chars); +} +#endif + +bool +#ifdef __ANDROID__ +ffDetectTerminalFontPlatformLinux +#else +ffDetectTerminalFontPlatform +#endif + (const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont) { + if (ffStrbufIgnCaseEqualS(&terminal->processName, "konsole")) { + detectKonsole(terminalFont, "konsolerc"); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "yakuake")) { + detectKonsole(terminalFont, "yakuakerc"); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "xfce4-terminal")) { + detectXFCETerminal(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "lxterminal")) { + detectFromConfigFile("lxterminal/lxterminal.conf", "fontname =", terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "tilix")) { + detectFromGSettings("/com/gexperts/Tilix/profiles/", "com.gexperts.Tilix.ProfilesList", "com.gexperts.Tilix.Profile", "default", terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "gnome-terminal")) { + detectFromGSettings("/org/gnome/terminal/legacy/profiles:/:", "org.gnome.Terminal.ProfilesList", "org.gnome.Terminal.Legacy.Profile", "default", terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "ptyxis-agent")) { + detectPtyxis(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "kgx")) { + detectKgx(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "mate-terminal")) { + detectFromGSettings("/org/mate/terminal/profiles/", "org.mate.terminal.global", "org.mate.terminal.profile", "default-profile", terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "deepin-terminal")) { + detectDeepinTerminal(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "foot")) { + detectFootTerminal(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "qterminal")) { + detectQTerminal(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "xterm")) { + detectXterm(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "st")) { + detectSt(terminalFont, terminal); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "warp")) { + detectWarp(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "weston-terminal")) { + detectWestonTerminal(terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "terminator")) { + detectTerminator(terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "sakura")) { + detectFromConfigFile("sakura/sakura.conf", "font=", terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "cosmic-term")) { + detectCosmicTerm(terminalFont); + } +#ifdef __HAIKU__ + else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "Terminal")) { + detectHaikuTerminal(terminalFont); + } +#endif + else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "termite")) { + detectFromConfigFile("termite/config", "font =", terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "rxvt") || ffStrbufIgnCaseEqualS(&terminal->processName, "urxvt") || ffStrbufIgnCaseEqualS(&terminal->processName, "urxvtd")) { + detectUrxvt(terminalFont); + } else { + return false; + } + return true; +} diff --git a/src/detection/terminalfont/terminalfont_windows.c b/src/detection/terminalfont/terminalfont_windows.c new file mode 100644 index 0000000..87566af --- /dev/null +++ b/src/detection/terminalfont/terminalfont_windows.c @@ -0,0 +1,287 @@ +#include "common/library.h" +#include "common/io.h" +#include "common/path.h" +#include "common/processing.h" +#include "common/properties.h" +#include "common/windows/unicode.h" +#include "common/windows/registry.h" +#include "common/strutil.h" +#include "detection/terminalshell/terminalshell.h" +#include "terminalfont.h" + +#include <shlobj.h> +#include <windows.h> +#include <stdlib.h> + +static const char* detectWTProfile(yyjson_val* profile, FFstrbuf* name, double* size) { + yyjson_val* font = yyjson_obj_get(profile, "font"); + if (!font) { + return "yyjson_obj_get(profile, \"font\"); failed"; + } + + if (!yyjson_is_obj(font)) { + return "yyjson_is_obj(font) returns false"; + } + + if (name->length == 0) { + ffStrbufAppendJsonVal(name, yyjson_obj_get(font, "face")); + } + + if (*size < 0) { + yyjson_val* psize = yyjson_obj_get(font, "size"); + if (yyjson_is_num(psize)) { + *size = unsafe_yyjson_get_num(psize); + } + } + return NULL; +} + +static inline void wrapYyjsonFree(yyjson_doc** doc) { + assert(doc); + if (*doc) { + yyjson_doc_free(*doc); + } +} + +static const char* detectFromWTImpl(FFstrbuf* content, FFstrbuf* name, double* size) { + yyjson_doc* FF_A_CLEANUP(wrapYyjsonFree) doc = yyjson_read_opts(content->chars, content->length, YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS, NULL, NULL); + if (!doc) { + return "Failed to parse WT JSON config file"; + } + + yyjson_val* const root = yyjson_doc_get_root(doc); + assert(root); + + yyjson_val* profiles = yyjson_obj_get(root, "profiles"); + if (!profiles) { + return "yyjson_obj_get(root, \"profiles\") failed"; + } + + FF_STRBUF_AUTO_DESTROY wtProfileId = ffStrbufCreateS(getenv("WT_PROFILE_ID")); + ffStrbufTrim(&wtProfileId, '\''); + if (wtProfileId.length > 0) { + yyjson_val* list = yyjson_obj_get(profiles, "list"); + if (yyjson_is_arr(list)) { + yyjson_val* profile; + size_t idx, max; + yyjson_arr_foreach (list, idx, max, profile) { + yyjson_val* guid = yyjson_obj_get(profile, "guid"); + + if (ffStrbufEqualS(&wtProfileId, yyjson_get_str(guid))) { + detectWTProfile(profile, name, size); + break; + } + } + } + } + + yyjson_val* defaults = yyjson_obj_get(profiles, "defaults"); + if (defaults) { + detectWTProfile(defaults, name, size); + } + + if (name->length == 0) { + ffStrbufSetS(name, "Cascadia Mono"); + } + if (*size < 0) { + *size = 12; + } + return NULL; +} + +static void detectFromWindowsTerminal(const FFstrbuf* terminalExe, FFTerminalFontResult* terminalFont) { + // https://learn.microsoft.com/en-us/windows/terminal/install#settings-json-file + FF_STRBUF_AUTO_DESTROY json = ffStrbufCreate(); + const char* error = NULL; + + if (terminalExe && ffIsAbsolutePath(terminalExe->chars)) { + FF_STRBUF_AUTO_DESTROY jsonPath = ffStrbufCreateA(MAX_PATH); + ffStrbufAppendNS(&jsonPath, ffStrbufLastIndexC(terminalExe, '\\') + 1, terminalExe->chars); + ffStrbufAppendS(&jsonPath, ".portable"); + + if (ffPathExists(jsonPath.chars, FF_PATHTYPE_ANY)) { + ffStrbufSubstrBefore(&jsonPath, jsonPath.length - strlen(".portable")); + ffStrbufAppendS(&jsonPath, "settings\\settings.json"); + if (!ffAppendFileBuffer(jsonPath.chars, &json)) { + error = "Error reading Windows Terminal portable settings JSON file"; + } + } else { + PWSTR localAppDataW = NULL; + if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_LocalAppData, KF_FLAG_DEFAULT, NULL, &localAppDataW))) { + ffStrbufSetWS(&jsonPath, localAppDataW); + CoTaskMemFree(localAppDataW); + + if (ffStrbufContainIgnCaseS(terminalExe, "_8wekyb3d8bbwe\\")) { + // Microsoft Store version + if (ffStrbufContainIgnCaseS(terminalExe, ".WindowsTerminalPreview_")) { + // Preview version + ffStrbufAppendS(&jsonPath, "\\Packages\\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\\LocalState\\settings.json"); + if (!ffAppendFileBuffer(jsonPath.chars, &json)) { + error = "Error reading Windows Terminal Preview settings JSON file"; + } + } else { + // Stable version + ffStrbufAppendS(&jsonPath, "\\Packages\\Microsoft.WindowsTerminal_8wekyb3d8bbwe\\LocalState\\settings.json"); + if (!ffAppendFileBuffer(jsonPath.chars, &json)) { + error = "Error reading Windows Terminal settings JSON file"; + } + } + } else { + ffStrbufAppendS(&jsonPath, "\\Microsoft\\Windows Terminal\\settings.json"); + if (!ffAppendFileBuffer(jsonPath.chars, &json)) { + error = "Error reading Windows Terminal settings JSON file"; + } + } + } + } + } + + if (!error && json.length == 0) { + error = ffProcessAppendStdOut(&json, (char* const[]) { "cmd.exe", "/c", + // print the file content directly, so we don't need to handle the difference of Windows and POSIX path + "if exist %LOCALAPPDATA%\\Packages\\Microsoft.WindowsTerminal_8wekyb3d8bbwe\\LocalState\\settings.json " + "( type %LOCALAPPDATA%\\Packages\\Microsoft.WindowsTerminal_8wekyb3d8bbwe\\LocalState\\settings.json ) " + "else if exist %LOCALAPPDATA%\\Packages\\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\\LocalState\\settings.json " + "( type %LOCALAPPDATA%\\Packages\\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\\LocalState\\settings.json ) " + "else if exist \"%LOCALAPPDATA%\\Microsoft\\Windows Terminal\\settings.json\" " + "( type %LOCALAPPDATA%\\Microsoft\\Windows Terminal\\settings.json ) " + "else ( call )", + NULL }); + } + + if (error) { + ffStrbufAppendS(&terminalFont->error, error); + return; + } + ffStrbufTrimRight(&json, '\n'); + if (json.length == 0) { + ffStrbufAppendS(&terminalFont->error, "Cannot find file \"settings.json\""); + return; + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + double size = -1; + error = detectFromWTImpl(&json, &name, &size); + + if (error) { + ffStrbufAppendS(&terminalFont->error, error); + } else { + char sizeStr[16]; + snprintf(sizeStr, ARRAY_SIZE(sizeStr), "%g", size); + ffFontInitValues(&terminalFont->font, name.chars, sizeStr); + } +} + +static void detectMintty(FFTerminalFontResult* terminalFont) { + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + if (!ffParsePropFileConfigValues("mintty/config", 2, (FFpropquery[]) { { "Font=", &fontName }, { "FontHeight=", &fontSize } })) { + ffParsePropFileConfigValues(".minttyrc", 2, (FFpropquery[]) { { "Font=", &fontName }, { "FontHeight=", &fontSize } }); + } + if (fontName.length == 0) { + ffStrbufAppendS(&fontName, "Lucida Console"); + } + if (fontSize.length == 0) { + ffStrbufAppendC(&fontSize, '9'); + } + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); +} + +static void detectConhost(FFTerminalFontResult* terminalFont) { + CONSOLE_FONT_INFOEX cfi = { .cbSize = sizeof(cfi) }; + if (!GetCurrentConsoleFontEx(GetStdHandle(STD_OUTPUT_HANDLE), FALSE, &cfi)) { + ffStrbufAppendS(&terminalFont->error, "GetCurrentConsoleFontEx() failed"); + return; + } + + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreateWS(cfi.FaceName); + + char fontSize[16]; + _ultoa((unsigned long) (cfi.dwFontSize.Y), fontSize, 10); + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize); +} + +static void detectConEmu(FFTerminalFontResult* terminalFont) { + // https://conemu.github.io/en/ConEmuXml.html#search-sequence + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + + const char* paths[] = { "ConEmuDir", "ConEmuBaseDir", "APPDATA" }; + for (uint32_t i = 0; i < ARRAY_SIZE(paths); ++i) { + ffStrbufSetS(&path, getenv(paths[i])); + if (path.length > 0) { + ffStrbufAppendS(&path, "/ConEmu.xml"); + if (ffParsePropFileValues(path.chars, 2, (FFpropquery[]) { { "<value name=\"FontName\" type=\"string\" data=\"", &fontName }, { "<value name=\"FontSize\" type=\"ulong\" data=\"", &fontSize } })) { + break; + } + } + } + + if (fontName.length == 0 && fontSize.length == 0) { + ffStrbufAppendS(&terminalFont->error, "Failed to parse ConEmu.xml"); + return; + } + + if (fontName.length > 0) { + ffStrbufSubstrBeforeLastC(&fontName, '"'); + } else { + ffStrbufAppendS(&fontName, "Consola"); + } + + if (fontSize.length > 0) { + ffStrbufSubstrBeforeLastC(&fontSize, '"'); + } else { + ffStrbufAppendS(&fontSize, "14"); + } + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); +} + +static void detectWarp(FFTerminalFontResult* terminalFont) { + FF_AUTO_CLOSE_FD HANDLE key = NULL; + if (!ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Software\\Warp.dev\\Warp", &key, &terminalFont->error)) { + return; + } + + FF_STRBUF_AUTO_DESTROY fontName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY fontSize = ffStrbufCreate(); + if (ffRegReadValues(key, 2, (FFRegValueArg[]) { FF_ARG(fontName, L"FontName"), FF_ARG(fontSize, L"FontSize") }, &terminalFont->error)) { + ffStrbufTrim(&fontName, '"'); + ffStrbufAppendS(&fontSize, "px"); + } else { + ffStrbufSetS(&fontName, "Hack"); + ffStrbufSetS(&fontSize, "13.0px"); + } + + ffFontInitValues(&terminalFont->font, fontName.chars, fontSize.chars); + + FFstrbuf* fontWeight = FF_LIST_ADD(FFstrbuf, terminalFont->font.styles); + ffStrbufInit(fontWeight); + if (ffRegReadStrbuf(key, L"FontWeight", fontWeight, NULL)) { + ffStrbufTrim(fontWeight, '"'); + } else { + ffStrbufSetStatic(fontWeight, "Normal"); + } +} + +bool ffDetectTerminalFontPlatform(const FFTerminalResult* terminal, FFTerminalFontResult* terminalFont) { + if (ffStrbufIgnCaseEqualS(&terminal->processName, "Windows Terminal") || + ffStrbufIgnCaseEqualS(&terminal->processName, "WindowsTerminal.exe")) { + detectFromWindowsTerminal(&terminal->exe, terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "mintty")) { + detectMintty(terminalFont); + } else if (ffStrbufIgnCaseEqualS(&terminal->processName, "conhost.exe")) { + detectConhost(terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "ConEmu")) { + detectConEmu(terminalFont); + } else if (ffStrbufStartsWithIgnCaseS(&terminal->processName, "warp")) { + detectWarp(terminalFont); + } else { + return false; + } + return true; +} diff --git a/src/detection/terminalshell/terminalshell.c b/src/detection/terminalshell/terminalshell.c new file mode 100644 index 0000000..2222216 --- /dev/null +++ b/src/detection/terminalshell/terminalshell.c @@ -0,0 +1,949 @@ +#include "fastfetch.h" +#include "common/io.h" +#include "common/processing.h" +#include "common/properties.h" +#include "common/path.h" +#include "common/strutil.h" +#include "common/binary.h" + +#include <ctype.h> +#include <stdint.h> +#ifdef __FreeBSD__ + #include <paths.h> + #ifndef _PATH_LOCALBASE + #define _PATH_LOCALBASE "/usr/local" + #endif +#elif __OpenBSD__ + #define _PATH_LOCALBASE "/usr/local" +#elif __NetBSD__ + #define _PATH_LOCALBASE "/usr/pkg" +#elif _WIN32 + + #include "common/windows/version.h" + #include <windows.h> + +static bool getFileVersion(const FFstrbuf* exePath, const wchar_t* stringName, FFstrbuf* version) { + wchar_t exePathW[PATH_MAX + 1]; + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(exePathW, (ULONG) sizeof(exePathW), NULL, exePath->chars, (ULONG) exePath->length + 1))) { + return false; + } + return ffGetFileVersion(exePathW, stringName, version); +} + +#elif __HAIKU__ + #include "common/haiku/version.h" +#elif __APPLE__ + #include "common/apple/version.h" +#endif + +static bool getExeVersionRaw(FFstrbuf* exe, FFstrbuf* version) { + return ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", NULL }) == NULL; +} + +static bool getExeVersionGeneral(FFstrbuf* exe, FFstrbuf* version) { + if (!getExeVersionRaw(exe, version)) { + return false; + } + + ffStrbufSubstrAfterFirstC(version, ' '); + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; +} + +static bool extractBashVersion(const char* line, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffStrStartsWith(line, "@(#)Bash version ")) { + return true; + } + const char* start = line + strlen("@(#)Bash version "); + const char* end = strchr(start, '('); + if (!end) { + return true; + } + ffStrbufSetNS((FFstrbuf*) userdata, (uint32_t) (end - start), start); + return false; +} + +static bool getShellVersionBash(FFstrbuf* exe, FFstrbuf* version) { + ffBinaryExtractStrings(exe->chars, extractBashVersion, version, (uint32_t) strlen("@(#)Bash version 0.0.0(0) release GNU")); + if (version->length > 0) { + return true; + } + + if (!getExeVersionRaw(exe, version)) { + return false; + } + + // GNU bash, version 5.1.16(1)-release (x86_64-pc-msys)\nCopyright... + ffStrbufSubstrBeforeFirstC(version, '('); // GNU bash, version 5.1.16 + ffStrbufSubstrAfterLastC(version, ' '); // 5.1.16 + return true; +} + +static bool getShellVersionFish(FFstrbuf* exe, FFstrbuf* version) { + if (!getExeVersionRaw(exe, version)) { + return false; + } + + // fish, version 4.0.2-1 (Built by MSYS2 project) // version can be localized if LC_ALL is set + if (version->length < strlen("fish, v") || !ffStrbufStartsWithS(version, "fish")) { + return false; + } + uint32_t index = ffStrbufNextIndexC(version, strlen("fish, "), ' '); + ffStrbufSubstrAfter(version, index); + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; +} + +static bool getShellVersionPwsh(FFstrbuf* exe, FFstrbuf* version) { + // Requires manually setting $POWERSHELL_VERSION + // $env:POWERSHELL_VERSION = $PSVersionTable.PSVersion.ToString(); fastfetch.exe + const char* env = getenv("POWERSHELL_VERSION"); + if (env) { + ffStrbufSetS(version, env); + return true; + } + +#ifdef _WIN32 + if (getFileVersion(exe, NULL, version)) { + ffStrbufSubstrBeforeLastC(version, '.'); + return true; + } +#endif + + if (!getExeVersionRaw(exe, version)) { + return false; + } + + ffStrbufSubstrAfterLastC(version, ' '); + return true; +} + +static bool getShellVersionKsh(FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "--version", NULL }) == NULL && ffStrbufSubstrAfterFirstS(version, " (AT&T Research) ")) { + // version sh (AT&T Research) 93u+ 2012-08-01 + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; + } + + ffStrbufClear(version); + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-c", "echo $KSH_VERSION", NULL }) == NULL && ffStrbufSubstrAfterFirstS(version, " KSH ")) { + // OKSH: @(#)PD KSH v5.2.14 99/07/13.2 + // MKSH: @(#)MIRBSD KSH R59 2025/04/26 +Debian + // $OKSH_VERSION doesn't exist on OpenBSD + ffStrbufSubstrBeforeFirstC(version, ' '); + ffStrbufTrimLeft(version, 'v'); + return true; + } + + return false; +} + +static bool getShellVersionOksh(FFstrbuf* exe, FFstrbuf* version) { + // Homebrew version + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-c", "echo $OKSH_VERSION", NULL }) != NULL) { + return false; + } + + // oksh 7.3 + ffStrbufSubstrAfterFirstC(version, ' '); + return true; +} + +static bool getShellVersionOils(FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", NULL }) != NULL) { + return false; + } + + // Oils 0.18.0 https://www.oilshell.org/... + ffStrbufSubstrAfterFirstC(version, ' '); + ffStrbufSubstrBeforeFirstC(version, '\t'); + return true; +} + +static bool getShellVersionNushell(FFstrbuf* exe, FFstrbuf* version) { + ffStrbufSetS(version, getenv("NU_VERSION")); + if (version->length) { + return true; + } + return getExeVersionRaw(exe, version); // 0.73.0 +} + +static bool extractBusyboxVersion(const char* line, uint32_t len, void* userdata) { + if (!ffStrStartsWith(line, "BusyBox v")) { + return true; + } + + line += strlen("BusyBox v"); + len -= (uint32_t) strlen("BusyBox v"); + const char* space = memchr(line, ' ', len); + if (space) { + len = (uint32_t) (space - line); + } + + ffStrbufSetNS((FFstrbuf*) userdata, len, line); + return false; +} + +static bool getShellVersionAsh(FFstrbuf* exe, FFstrbuf* version) { + ffBinaryExtractStrings(exe->chars, extractBusyboxVersion, version, (uint32_t) strlen("BusyBox v0.0.0")); + + const char* error = ffStrbufEndsWithS(exe, "busybox") + ? ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "ash", "--help", NULL }) + : ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "--help", NULL }); + if (error != NULL) { + return false; + } + + // BusyBox v1.36.1 (2023-11-07 18:53:09 UTC) multi-call binary... + ffStrbufSubstrAfterFirstC(version, ' '); + ffStrbufSubstrBeforeFirstC(version, ' '); + ffStrbufTrimLeft(version, 'v'); + return true; +} + +static bool getShellVersionXonsh(FF_A_UNUSED FFstrbuf* exe, FFstrbuf* version) { + ffStrbufSetS(version, getenv("XONSH_VERSION")); + if (version->length) { + return true; + } + + // exe is python here + if (ffProcessAppendStdErr(version, (char* const[]) { "xonsh", "--version", NULL }) != NULL) { + return false; + } + + // xonsh/0.14.1 + ffStrbufSubstrAfterFirstC(version, '/'); + return true; +} + +static bool extractZshVersion(const char* line, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffStrStartsWith(line, "zsh-")) { + return true; + } + const char* start = line + strlen("zsh-"); + const char* end = strchr(start, '-'); + if (!end) { + return true; + } + + ffStrbufSetNS((FFstrbuf*) userdata, (uint32_t) (end - start), start); + return false; +} + +static bool getShellVersionZsh(FFstrbuf* exe, FFstrbuf* version) { + ffBinaryExtractStrings(exe->chars, extractZshVersion, version, (uint32_t) strlen("zsh-0.0-0")); + if (version->length) { + return true; + } + + return getExeVersionGeneral(exe, version); // zsh 5.9 (arm-apple-darwin21.3.0) +} + +#ifdef _WIN32 +static bool getShellVersionWinPowerShell(FFstrbuf* exe, FFstrbuf* version) { + const char* env = getenv("POWERSHELL_VERSION"); + if (env) { + ffStrbufSetS(version, env); + return true; + } + + return ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-NoLogo", "-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()", NULL }) == NULL; +} +#endif + +bool fftsGetShellVersion(FFstrbuf* exe, const char* exeName, FFstrbuf* version) { + if (ffStrEqualsIgnCase(exeName, "sh")) { // #849 + return false; + } + + if (ffStrEqualsIgnCase(exeName, "bash")) { + return getShellVersionBash(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "zsh")) { + return getShellVersionZsh(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "fish")) { + return getShellVersionFish(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "pwsh")) { + return getShellVersionPwsh(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "csh") || ffStrEqualsIgnCase(exeName, "tcsh")) { + return getExeVersionGeneral(exe, version); // tcsh 6.24.07 (Astron) 2022-12-21 (aarch64-apple-darwin) options wide,nls,dl,al,kan,sm,rh,color,filec + } + if (ffStrEqualsIgnCase(exeName, "nu")) { + return getShellVersionNushell(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "ksh") || ffStrEqualsIgnCase(exeName, "mksh")) { + return getShellVersionKsh(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "oksh")) { + return getShellVersionOksh(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "oil.ovm")) { + return getShellVersionOils(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "elvish")) { + return getExeVersionRaw(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "ash") || ffStrEqualsIgnCase(exeName, "busybox")) { + return getShellVersionAsh(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "xonsh")) { + return getShellVersionXonsh(exe, version); + } + if (ffStrEqualsIgnCase(exeName, "brush")) { + return getExeVersionGeneral(exe, version); // brush 0.2.23 (git:2835487) + } + +#ifdef _WIN32 + if (ffStrEqualsIgnCase(exeName, "powershell") || ffStrEqualsIgnCase(exeName, "powershell_ise")) { + return getShellVersionWinPowerShell(exe, version); + } + + return getFileVersion(exe, NULL, version); +#endif + + return false; +} + +FF_A_UNUSED static bool getTerminalVersionTermux(FFstrbuf* version) { + ffStrbufSetS(version, getenv("TERMUX_VERSION")); + return version->length > 0; +} + +static bool extractGeneralVersion(const char* str, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffCharIsDigit(str[0])) { + return true; + } + int count = 0; + sscanf(str, "%*d.%*d.%*d%n", &count); + if (count == 0) { + return true; + } + ffStrbufSetS((FFstrbuf*) userdata, str); + return false; +} + +FF_A_UNUSED static bool getTerminalVersionGnome(FFstrbuf* exe, FFstrbuf* version) { + if (ffIsAbsolutePath(exe->chars)) { + ffBinaryExtractStrings(exe->chars, extractGeneralVersion, version, (uint32_t) strlen("0.0.0")); + if (version->length) { + return true; + } + } + + if (ffProcessAppendStdOut(version, (char* const[]) { "gnome-terminal", "--version", NULL })) { + return false; + } + + // # GNOME Terminal 3.46.7 using VTE 0.70.2 +BIDI +GNUTLS +ICU +SYSTEMD + ffStrbufSubstrAfterFirstS(version, "Terminal "); + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; +} + +FF_A_UNUSED static bool getTerminalVersionXfce4Terminal(FFstrbuf* exe, FFstrbuf* version) { + if (ffIsAbsolutePath(exe->chars)) { + ffBinaryExtractStrings(exe->chars, extractGeneralVersion, version, (uint32_t) strlen("0.0.0")); + if (version->length) { + return true; + } + } + + return getExeVersionGeneral(exe, version); // xfce4-terminal 1.0.4 (Xfce 4.18)... +} + +FF_A_UNUSED static bool getTerminalVersionKgx(FFstrbuf* version) { + if (ffProcessAppendStdOut(version, (char* const[]) { "kgx", "--version", NULL })) { + return false; + } + + // # KGX 45.0 using VTE 0.74.0 +BIDI +GNUTLS +ICU +SYSTEMD + ffStrbufSubstrAfterFirstS(version, "KGX "); + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; +} + +FF_A_UNUSED static bool getTerminalVersionKonsole(FFstrbuf* exe, FFstrbuf* version) { + const char* konsoleVersion = getenv("KONSOLE_VERSION"); + if (konsoleVersion) { + // 221201 + long major = strtol(konsoleVersion, NULL, 10); + if (major >= 0) { + long patch = major % 100; + major /= 100; + long minor = major % 100; + major /= 100; + ffStrbufSetF(version, "%ld.%ld.%ld", major, minor, patch); + return true; + } + } + + // Likely TDE konsole. See #2319 + if (!getExeVersionRaw(exe, version)) { + return false; + } + return ffStrbufSubstrAfterLastC(version, ' '); +} + +FF_A_UNUSED static bool getTerminalVersionFoot(FFstrbuf* exe, FFstrbuf* version) { + uint32_t major = 0, minor = 0, patch = 0; + if (ffGetTerminalResponse("\e[>c", 3, "\e[>1;%2u%2u%2u;0c", &major, &minor, &patch) == NULL) { + ffStrbufSetF(version, "%u.%u.%u", major, minor, patch); + return true; + } + + if (!getExeVersionRaw(exe, version)) { + return false; + } + + // foot version: 1.13.1 -pgo +ime -graphemes -assertions + ffStrbufSubstrAfterFirstS(version, "version: "); + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; +} + +FF_A_UNUSED static bool getTerminalVersionMateTerminal(FFstrbuf* exe, FFstrbuf* version) { + ffBinaryExtractStrings(exe->chars, extractGeneralVersion, version, (uint32_t) strlen("0.0.0")); + if (version->length > 0) { + return true; + } + + if (!getExeVersionRaw(exe, version)) { + return false; + } + + // MATE Terminal 1.26.1 + ffStrbufSubstrAfterLastC(version, ' '); + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionCockpit(FFstrbuf* exe, FFstrbuf* version) { + if (!getExeVersionRaw(exe, version)) { + return false; + } + + // Version: 295\n... + ffStrbufSubstrBeforeFirstC(version, '\n'); + ffStrbufSubstrAfterFirstC(version, ' '); + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionXterm(FFstrbuf* exe, FFstrbuf* version) { + ffStrbufSetS(version, getenv("XTERM_VERSION")); + if (!version->length) { + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-v", NULL })) { + return false; + } + } + + // xterm(273) + ffStrbufTrimRight(version, ')'); + ffStrbufSubstrAfterFirstC(version, '('); + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionBlackbox(FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", NULL })) { + return false; + } + + // BlackBox version 0.14.0 (flatpak) + ffStrbufSubstrAfterFirstS(version, "version "); + ffStrbufSubstrBeforeFirstC(version, ' '); + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionUrxvt(FF_A_UNUSED FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdErr(version, (char* const[]) { "urxvt", // Don't use exe because of urxvtd + "-invalid", + NULL })) { + return false; + } + + // urxvt: "invalid": unknown or malformed option. + // rxvt-unicode (urxvt) v9.31 - released: 2023-01-02 + ffStrbufSubstrAfterFirstS(version, "(urxvt) v"); + ffStrbufSubstrBeforeFirstC(version, ' '); + + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionSt(FF_A_UNUSED FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "-v", NULL })) { + return false; + } + + // st 0.9 + ffStrbufSubstrAfterFirstC(version, ' '); + + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionLxterminal(FFstrbuf* exe, FFstrbuf* version) { + if (!getExeVersionRaw(exe, version)) { + return false; + } + // lxterminal 0.3.2 + ffStrbufSubstrAfterFirstC(version, ' '); + return version->length > 0; +} + +FF_A_UNUSED static bool getTerminalVersionWeston(FF_A_UNUSED FFstrbuf* exe, FFstrbuf* version) { + // weston-terminal doesn't report a version, use weston version instead + if (ffProcessAppendStdOut(version, (char* const[]) { "weston", "--version", NULL })) { + return false; + } + + // weston 8.0.0 + ffStrbufSubstrAfterFirstC(version, ' '); + + return version->length > 0; +} + +static bool getTerminalVersionContour(FFstrbuf* exe, FFstrbuf* version) { + const char* env = getenv("TERMINAL_VERSION_STRING"); + if (env) { + ffStrbufAppendS(version, env); + return true; + } + if (!getExeVersionRaw(exe, version)) { + return false; + } + // Contour Terminal Emulator 0.3.12.262 + ffStrbufSubstrAfterLastC(version, ' '); + return version->length > 0; +} + +static bool getTerminalVersionScreen(FFstrbuf* exe, FFstrbuf* version) { + if (!getExeVersionRaw(exe, version)) { + return false; + } + // Screen version 4.09.01 (GNU) 20-Aug-23 + ffStrbufSubstrAfter(version, (uint32_t) strlen("Screen version ") - 1); + ffStrbufSubstrBeforeFirstC(version, ' '); + return version->length > 0; +} + +static bool getTerminalVersionTmux(FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "-V", NULL }) != NULL) { + return false; + } + + // tmux 3.4 + ffStrbufSubstrAfterFirstC(version, ' '); + return version->length > 0; +} + +static bool getTerminalVersionZellij(FFstrbuf* exe, FFstrbuf* version) { + if (!getExeVersionRaw(exe, version)) { + return false; + } + + // zellij 0.39.2 + ffStrbufSubstrAfterFirstC(version, ' '); + return version->length > 0; +} + +static bool getTerminalVersionZed(FFstrbuf* exe, FFstrbuf* version) { + FF_STRBUF_AUTO_DESTROY cli = ffStrbufCreateCopy(exe); + ffStrbufSubstrBeforeLastC(&cli, '/'); + ffStrbufAppendS(&cli, "/cli" +#ifdef _WIN32 + ".exe" +#endif + ); + + if (ffProcessAppendStdOut(version, (char* const[]) { cli.chars, "--version", NULL }) != NULL) { + return false; + } + + // Zed 0.142.6 – /Applications/Zed.app + ffStrbufSubstrAfterFirstC(version, ' '); + ffStrbufSubstrBeforeFirstC(version, ' '); + return true; +} + +static bool extractSshdVersion(const char* str, FF_A_UNUSED uint32_t len, void* userdata) { + if (!ffStrStartsWith(str, "OpenSSH_") || !ffCharIsDigit(str[strlen("OpenSSH_")])) { + return true; + } + str += strlen("OpenSSH_"); + int count = 0; + sscanf(str, "%*d.%*dp%*d%n", &count); + if (count == 0) { + return true; + } + ffStrbufSetS((FFstrbuf*) userdata, str); + return false; +} + +static bool getTerminalVersionSshd(FFstrbuf* exe, FFstrbuf* version) { + FF_STRBUF_AUTO_DESTROY exePath = ffStrbufCreate(); + if (ffIsAbsolutePath(exe->chars)) { + ffStrbufSet(&exePath, exe); + } else if (ffFindExecutableInPath("sshd", &exePath) != NULL) { + return false; + } + + ffBinaryExtractStrings(exePath.chars, extractSshdVersion, version, (uint32_t) strlen("OpenSSH0.0")); + if (version->length) { + return true; + } + + if (ffProcessAppendStdOut(version, (char* const[]) { exePath.chars, "-V", NULL }) != NULL) { + return false; + } + + if (ffStrbufStartsWithS(version, "unknown ")) { // `unknown option -- V` (ancient OpenSSH version) + ffStrbufSubstrAfterFirstC(version, '\n'); + } + + // OpenSSH_10.0p2 Ubuntu-5ubuntu5, OpenSSL 3.5.3 16 Sep 2025 + ffStrbufSubstrAfterFirstC(version, '_'); + ffStrbufSubstrBeforeFirstC(version, ','); + return true; +} + +#ifndef _WIN32 +static bool getTerminalVersionKitty(FFstrbuf* exe, FFstrbuf* version) { + #if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__GNU__) + char buffer[1024] = {}; + if ( + #if __linux__ || __GNU__ + ffReadFileData(FASTFETCH_TARGET_DIR_USR "/lib64/kitty/kitty/constants.py", ARRAY_SIZE(buffer) - 1, buffer) || + ffReadFileData(FASTFETCH_TARGET_DIR_USR "/lib/kitty/kitty/constants.py", ARRAY_SIZE(buffer) - 1, buffer) + #else + ffReadFileData(_PATH_LOCALBASE "/share/kitty/kitty/constants.py", ARRAY_SIZE(buffer) - 1, buffer) + #endif + ) { + // Starts from version 0.17.0 + // https://github.com/kovidgoyal/kitty/blob/master/kitty/constants.py#L25 + const char* p = memmem(buffer, ARRAY_SIZE(buffer) - 1, "version: Version = Version(", strlen("version: Version = Version(")); + if (p) { + p += strlen("version: Version = Version("); + int major, minor, patch; + if (sscanf(p, "%d,%d,%d", &major, &minor, &patch) == 3) { + ffStrbufSetF(version, "%d.%d.%d", major, minor, patch); + return true; + } + } + } + #elif __APPLE__ + if (ffGetAppNameAndVersion(exe->chars, NULL, version)) { + return true; + } + #endif + + char versionHex[64]; + // https://github.com/fastfetch-cli/fastfetch/discussions/1030#discussioncomment-9845233 + if (ffGetTerminalResponse( + "\eP+q6b697474792d71756572792d76657273696f6e\e\\", // kitty-query-version + 1, + "\eP1+r%*[^=]=%63[^\e]\e\\\\", + versionHex) == NULL) { + // decode hex string + for (const char* p = versionHex; p[0] && p[1]; p += 2) { + unsigned value; + if (sscanf(p, "%2x", &value) == 1) { + ffStrbufAppendC(version, (char) value); + } + } + return true; + } + + // kitty 0.21.2 created by Kovid Goyal + return getExeVersionGeneral(exe, version); +} + +FF_A_UNUSED static bool getTerminalVersionPtyxis(FF_A_UNUSED FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdOut(version, (char* const[]) { "ptyxis", "--version", NULL }) != NULL) { + return false; + } + + ffStrbufSubstrBeforeFirstC(version, '\n'); + ffStrbufSubstrAfterFirstC(version, ' '); + return true; +} + +FF_A_UNUSED static bool getTerminalVersionTilix(FFstrbuf* exe, FFstrbuf* version) { + if (ffIsAbsolutePath(exe->chars)) { + ffBinaryExtractStrings(exe->chars, extractGeneralVersion, version, (uint32_t) strlen("0.0.0")); + if (version->length) { + return true; + } + } + + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", NULL }) != NULL) { + return false; + } + + uint32_t index = ffStrbufFirstIndexS(version, "Tilix version: "); + if (index == version->length) { + return false; + } + + index += (uint32_t) strlen("Tilix version:"); + uint32_t end = ffStrbufNextIndexC(version, index, '\n'); + + ffStrbufSubstrBefore(version, end); + ffStrbufSubstrAfter(version, index); + return true; +} + +FF_A_UNUSED static bool getTerminalVersionSakura(FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdErr(version, (char* const[]) { exe->chars, "--version", NULL }) != NULL) { // sakura version is 3.8.8 + return false; + } + + ffStrbufSubstrAfterLastC(version, ' '); + return true; +} + +FF_A_UNUSED static bool getTerminalVersionTermite(FFstrbuf* exe, FFstrbuf* version) { + if (ffProcessAppendStdOut(version, (char* const[]) { exe->chars, "--version", NULL }) != NULL) { // termite v16.9\nvte 0.78.1 +BIDI +GNUTLS +ICU +SYSTEMD + return false; + } + + ffStrbufSubstrBeforeFirstC(version, '\n'); + ffStrbufSubstrAfterLastC(version, 'v'); + return true; +} +#endif + +#ifdef _WIN32 + +static bool getTerminalVersionWindowsTerminal(FFstrbuf* exe, FFstrbuf* version) { + FF_STRBUF_AUTO_DESTROY buildInfoPath; + ffStrbufInitNS(&buildInfoPath, ffStrbufLastIndexC(exe, '\\') + 1, exe->chars); + ffStrbufAppendS(&buildInfoPath, "BuildInfo.xml"); + + if (ffParsePropFile(buildInfoPath.chars, "StoreVersion=\"", version)) { + ffStrbufTrimRight(version, '"'); + return true; + } + + return getFileVersion(exe, NULL, version); +} + +static bool getTerminalVersionConEmu(FFstrbuf* exe, FFstrbuf* version) { + ffStrbufSetS(version, getenv("ConEmuBuild")); + + if (version->length) { + return true; + } + + return getFileVersion(exe, NULL, version); +} + +#endif + +bool fftsGetTerminalVersion(FFstrbuf* processName, FF_A_UNUSED FFstrbuf* exe, FFstrbuf* version) { +#ifdef __ANDROID__ + + if (ffStrbufEqualS(processName, "com.termux")) { + return getTerminalVersionTermux(version); + } + +#endif + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__sun) || defined(__NetBSD__) || defined(__HAIKU__) || defined(__GNU__) + + if (ffStrbufStartsWithIgnCaseS(processName, "gnome-terminal")) { + return getTerminalVersionGnome(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "konsole")) { + return getTerminalVersionKonsole(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "yakuake")) { + return getTerminalVersionKonsole(exe, version); // yakuake shares code with konsole + } + + if (ffStrbufIgnCaseEqualS(processName, "xfce4-terminal")) { + return getTerminalVersionXfce4Terminal(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "terminator")) { + return getExeVersionGeneral(exe, version); // terminator 2.1.3 + } + + if (ffStrbufIgnCaseEqualS(processName, "deepin-terminal")) { + return getExeVersionGeneral(exe, version); // deepin-terminal 5.4.36 + } + + if (ffStrbufIgnCaseEqualS(processName, "foot")) { + return getTerminalVersionFoot(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "qterminal")) { + return getExeVersionRaw(exe, version); // 1.2.0 + } + + if (ffStrbufIgnCaseEqualS(processName, "mate-terminal")) { + return getTerminalVersionMateTerminal(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "cockpit-bridge")) { + return getTerminalVersionCockpit(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "xterm")) { + return getTerminalVersionXterm(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "blackbox")) { + return getTerminalVersionBlackbox(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "st")) { + return getTerminalVersionSt(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "lxterminal")) { + return getTerminalVersionLxterminal(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "weston-terminal")) { + return getTerminalVersionWeston(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "urxvt") || + ffStrbufIgnCaseEqualS(processName, "urxvtd") || + ffStrbufIgnCaseEqualS(processName, "rxvt") || + ffStrbufIgnCaseEqualS(processName, "rxvt-unicode")) { + return getTerminalVersionUrxvt(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "ptyxis-agent")) { + return getTerminalVersionPtyxis(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "tilix")) { + return getTerminalVersionTilix(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "sakura")) { + return getTerminalVersionSakura(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "termite")) { + return getTerminalVersionTermite(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "cosmic-term")) { + return getTerminalVersionTmux(exe, version); + } + +#endif + +#ifdef _WIN32 + + if (ffStrbufIgnCaseEqualS(processName, "WindowsTerminal.exe")) { + return getTerminalVersionWindowsTerminal(exe, version); + } + + if (ffStrbufStartsWithIgnCaseS(processName, "ConEmu")) { + return getTerminalVersionConEmu(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "warp.exe")) { + return getFileVersion(exe, L"ProductVersion", version); + } + +#endif + +#ifndef _WIN32 + + if (ffStrbufIgnCaseEqualS(processName, "kitty")) { + return getTerminalVersionKitty(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "Tabby") && getExeVersionRaw(exe, version)) { + return true; + } + +#endif + + if (ffStrbufStartsWithIgnCaseS(processName, "alacritty")) { + return getExeVersionGeneral(exe, version); + } + + if (ffStrbufStartsWithIgnCaseS(processName, "contour")) { + return getTerminalVersionContour(exe, version); + } + + if (ffStrbufStartsWithIgnCaseS(processName, "screen")) { + return getTerminalVersionScreen(exe, version); + } + + if (ffStrbufStartsWithIgnCaseS(processName, "zellij")) { + return getTerminalVersionZellij(exe, version); + } + + if (ffStrbufStartsWithIgnCaseS(processName, "zed")) { + return getTerminalVersionZed(exe, version); + } + +#if __HAIKU__ + if (ffStrbufEqualS(processName, "Terminal")) { + return ffGetFileVersion(exe->chars, version); + } +#endif + + const char* termProgramVersion = getenv("TERM_PROGRAM_VERSION"); + if (termProgramVersion) { + const char* termProgram = getenv("TERM_PROGRAM"); + if (termProgram) { + if (ffStrbufStartsWithIgnCaseS(processName, termProgram) || // processName ends with `.exe` on Windows + (ffStrEquals(termProgram, "vscode") && ffStrbufStartsWithIgnCaseS(processName, "code")) || +#ifdef __APPLE__ + (ffStrEquals(termProgram, "iTerm.app") && ffStrbufStartsWithIgnCaseS(processName, "iTermServer-")) || +#elif defined(__linux__) + (ffStrEquals(termProgram, "WarpTerminal") && ffStrbufEqualS(processName, "warp")) || +#endif + false) { + ffStrbufSetS(version, termProgramVersion); + return true; + } + } + } + + termProgramVersion = getenv("LC_TERMINAL_VERSION"); + if (termProgramVersion) { + const char* termProgram = getenv("LC_TERMINAL"); + if (termProgram) { + if (ffStrbufStartsWithIgnCaseS(processName, termProgram) || // processName ends with `.exe` on Windows + (ffStrEquals(termProgram, "vscode") && ffStrbufStartsWithIgnCaseS(processName, "code")) || + (ffStrStartsWith(termProgram, "iTerm") && ffStrbufStartsWithIgnCaseS(processName, "iTermServer-"))) { + ffStrbufSetS(version, termProgramVersion); + return true; + } + } + } + + if (ffStrbufStartsWithIgnCaseS(processName, "tmux")) { + return getTerminalVersionTmux(exe, version); + } + + if (ffStrbufIgnCaseEqualS(processName, "sshd") || ffStrbufStartsWithIgnCaseS(processName, "sshd-")) { + return getTerminalVersionSshd(exe, version); + } + +#ifdef _WIN32 + + return getFileVersion(exe, NULL, version); + +#elif __APPLE__ + + return ffGetAppNameAndVersion(exe->chars, NULL, version); + +#else + + return false; + +#endif +} diff --git a/src/detection/terminalshell/terminalshell.h b/src/detection/terminalshell/terminalshell.h new file mode 100644 index 0000000..008e30c --- /dev/null +++ b/src/detection/terminalshell/terminalshell.h @@ -0,0 +1,35 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/terminal/option.h" +#include "modules/shell/option.h" + +typedef struct FFShellResult { + FFstrbuf processName; + FFstrbuf exe; // Actually arg0 in *nix + const char* exeName; // pointer to a char in exe + FFstrbuf exePath; // Full real path to executable file + FFstrbuf prettyName; + FFstrbuf version; + uint32_t pid; + uint32_t ppid; + int32_t tty; +} FFShellResult; + +typedef struct FFTerminalResult { + FFstrbuf processName; + FFstrbuf exe; + FFstrbuf prettyName; + const char* exeName; // pointer to a char in exe + FFstrbuf exePath; // Full real path to executable file + FFstrbuf version; + FFstrbuf tty; + uint32_t pid; + uint32_t ppid; +} FFTerminalResult; + +const FFShellResult* ffDetectShell(); +const FFTerminalResult* ffDetectTerminal(); + +bool fftsGetShellVersion(FFstrbuf* exe, const char* exeName, FFstrbuf* version); +bool fftsGetTerminalVersion(FFstrbuf* processName, FFstrbuf* exe, FFstrbuf* version); diff --git a/src/detection/terminalshell/terminalshell_linux.c b/src/detection/terminalshell/terminalshell_linux.c new file mode 100644 index 0000000..1eccf26 --- /dev/null +++ b/src/detection/terminalshell/terminalshell_linux.c @@ -0,0 +1,449 @@ +#include "terminalshell.h" +#include "common/io.h" +#include "common/parsing.h" +#include "common/processing.h" +#include "common/thread.h" +#include "common/strutil.h" + +#include <string.h> +#include <stdlib.h> +#include <unistd.h> + +static void setExeName(FFstrbuf* exe, const char** exeName) { + assert(exe->length > 0); + uint32_t lastSlashIndex = ffStrbufLastIndexC(exe, '/'); + if (lastSlashIndex < exe->length) { + *exeName = exe->chars + lastSlashIndex + 1; + } +} + +static pid_t getShellInfo(FFShellResult* result, pid_t pid) { + pid_t ppid = 0; + int32_t tty = -1; + + const char* userShellName = NULL; + { + uint32_t index = ffStrbufLastIndexC(&instance.state.platform.userShell, '/'); + if (index == instance.state.platform.userShell.length) { + userShellName = instance.state.platform.userShell.chars; + } else { + userShellName = instance.state.platform.userShell.chars + index + 1; + } + } + + while (pid > 1 && ffProcessGetBasicInfoLinux(pid, &result->processName, &ppid, &tty) == NULL) { + if (!ffStrbufEqualS(&result->processName, userShellName)) { + // Common programs that are between terminal and own process, but are not the shell + if ( + // tty < 0 || //A shell should connect to a tty + pid == 1 || // init/systemd + ffStrbufEqualS(&result->processName, "sh") || // This prevents us from detecting things like pipes and redirects, i hope nobody uses plain `sh` as shell + ffStrbufEqualS(&result->processName, "sudo") || + ffStrbufEqualS(&result->processName, "su") || + ffStrbufEqualS(&result->processName, "strace") || + ffStrbufEqualS(&result->processName, "gdb") || + ffStrbufEqualS(&result->processName, "lldb") || + ffStrbufEqualS(&result->processName, "lldb-mi") || + ffStrbufEqualS(&result->processName, "login") || + ffStrbufEqualS(&result->processName, "ltrace") || + ffStrbufEqualS(&result->processName, "perf") || + ffStrbufEqualS(&result->processName, "guake-wrapped") || + ffStrbufEqualS(&result->processName, "time") || + ffStrbufEqualS(&result->processName, "clifm") || // https://github.com/leo-arch/clifm/issues/289 + ffStrbufEqualS(&result->processName, "valgrind") || + ffStrbufEqualS(&result->processName, "fastfetch") || // #994 + ffStrbufEqualS(&result->processName, "flashfetch") || + ffStrbufEqualS(&result->processName, "proot") || + ffStrbufEqualS(&result->processName, "script") || +#ifdef __linux__ + ffStrbufEqualS(&result->processName, "run-parts") || +#endif + ffStrbufContainS(&result->processName, "debug") || + ffStrbufContainS(&result->processName, "command-not-") || + ffStrbufEndsWithS(&result->processName, ".sh")) { + pid = ppid; + ffStrbufClear(&result->processName); + continue; + } + } + + result->pid = (uint32_t) pid; + result->ppid = (uint32_t) ppid; + result->tty = tty; + ffProcessGetInfoLinux(pid, &result->processName, &result->exe, &result->exeName, &result->exePath); + break; + } + return pid > 1 ? ppid : 0; +} + +static pid_t getTerminalInfo(FFTerminalResult* result, pid_t pid) { + pid_t ppid = 0; + + while (pid > 1 && ffProcessGetBasicInfoLinux(pid, &result->processName, &ppid, NULL) == NULL) { + // Known shells + if ( + pid == 1 || // init/systemd + ffStrbufEqualS(&result->processName, "sudo") || + ffStrbufEqualS(&result->processName, "su") || + ffStrbufEqualS(&result->processName, "sh") || + ffStrbufEqualS(&result->processName, "ash") || + ffStrbufEqualS(&result->processName, "bash") || + ffStrbufEqualS(&result->processName, "zsh") || + ffStrbufEqualS(&result->processName, "ksh") || + ffStrbufEqualS(&result->processName, "mksh") || + ffStrbufEqualS(&result->processName, "oksh") || + ffStrbufEqualS(&result->processName, "csh") || + ffStrbufEqualS(&result->processName, "tcsh") || + ffStrbufEqualS(&result->processName, "fish") || + ffStrbufEqualS(&result->processName, "dash") || + ffStrbufEqualS(&result->processName, "pwsh") || + ffStrbufEqualS(&result->processName, "nu") || + ffStrbufEqualS(&result->processName, "git-shell") || + ffStrbufEqualS(&result->processName, "elvish") || + ffStrbufEqualS(&result->processName, "oil.ovm") || + ffStrbufEqualS(&result->processName, "xonsh") || // works in Linux but not in macOS because kernel returns `Python` in this case + ffStrbufEqualS(&result->processName, "login") || + ffStrbufEqualS(&result->processName, "clifm") || // https://github.com/leo-arch/clifm/issues/289 + ffStrbufEqualS(&result->processName, "chezmoi") || // #762 + ffStrbufEqualS(&result->processName, "proot") || + ffStrbufEqualS(&result->processName, "script") || +#ifdef __linux__ + ffStrbufStartsWithS(&result->processName, "Relay(") || // Unknown process in WSL2 + ffStrbufStartsWithS(&result->processName, "flatpak-") || // #707 + ffStrbufEqualS(&result->processName, "run-parts") || // #2048 +#endif + ffStrbufEndsWithS(&result->processName, ".sh")) { + pid = ppid; + ffStrbufClear(&result->processName); + continue; + } + +#ifdef __APPLE__ + // https://github.com/fastfetch-cli/fastfetch/discussions/501 + const char* pLeft = strstr(result->processName.chars, " ("); + if (pLeft) { + pLeft += 2; + const char* pRight = strstr(pLeft, "term)"); + if (pRight && pRight[5] == '\0') { + for (; pLeft < pRight; ++pLeft) { + if (*pLeft < 'a' || *pLeft > 'z') { + break; + } + } + if (pLeft == pRight && ffProcessGetBasicInfoLinux(ppid, &result->processName, &ppid, NULL) != NULL) { + return 0; + } + } + } +#endif + + result->pid = (uint32_t) pid; + result->ppid = (uint32_t) ppid; + ffProcessGetInfoLinux(pid, &result->processName, &result->exe, &result->exeName, &result->exePath); + break; + } + return pid > 1 ? ppid : 0; +} + +static bool getTerminalInfoByPidEnv(FFTerminalResult* result, const char* pidEnv) { + const char* envStr = getenv(pidEnv); + if (envStr == NULL) { + return false; + } + + pid_t pid = (pid_t) strtol(envStr, NULL, 10); + result->pid = (uint32_t) pid; + if (ffProcessGetBasicInfoLinux(pid, &result->processName, (pid_t*) &result->ppid, NULL) == NULL) { + ffProcessGetInfoLinux(pid, &result->processName, &result->exe, &result->exeName, &result->exePath); + return true; + } + + return false; +} + +static void getTerminalFromEnv(FFTerminalResult* result) { + if (result->processName.length > 0) { + if (!ffStrbufStartsWithS(&result->processName, "login") && + !ffStrbufEqualS(&result->processName, "(login)") && + +#ifdef __APPLE__ + !ffStrbufEqualS(&result->processName, "launchd") && +#else + !ffStrbufEqualS(&result->processName, "systemd") && + !ffStrbufEqualS(&result->processName, "init") && + !ffStrbufEqualS(&result->processName, "(init)") && + !ffStrbufEqualS(&result->processName, "SessionLeader") && // #750 +#endif + + !ffStrbufEqualS(&result->processName, "0")) + return; + + ffStrbufClear(&result->processName); + ffStrbufClear(&result->exe); + result->exeName = result->exe.chars; + ffStrbufClear(&result->exePath); + result->pid = result->ppid = 0; + } + + const char* term = NULL; + + // SSH + if ( + getenv("SSH_TTY") != NULL) { + term = getenv("SSH_TTY"); + } else if ( + getenv("KITTY_PID") != NULL || + getenv("KITTY_INSTALLATION_DIR") != NULL) { + if (getTerminalInfoByPidEnv(result, "KITTY_PID")) { + return; + } + term = "kitty"; + } + +#ifdef __linux__ // WSL + // Windows Terminal + else if ( + getenv("WT_SESSION") != NULL || + getenv("WT_PROFILE_ID") != NULL) + term = "Windows Terminal"; + + // ConEmu + else if ( + getenv("ConEmuPID") != NULL) { + term = "ConEmu"; + } +#endif + + // Alacritty + else if ( + getenv("ALACRITTY_SOCKET") != NULL || + getenv("ALACRITTY_LOG") != NULL || + getenv("ALACRITTY_WINDOW_ID") != NULL) { + term = "Alacritty"; + } +#ifdef __ANDROID__ + // Termux + else if ( + getenv("TERMUX_VERSION") != NULL || + getenv("TERMUX_MAIN_PACKAGE_FORMAT") != NULL) { + if (getTerminalInfoByPidEnv(result, "TERMUX_APP__PID")) { + return; + } + term = "com.termux"; + } +#endif + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__GNU__) + // Konsole + else if ( + getenv("KONSOLE_VERSION") != NULL) + term = "konsole"; + + else if ( + getenv("GNOME_TERMINAL_SCREEN") != NULL || + getenv("GNOME_TERMINAL_SERVICE") != NULL) { + term = "gnome-terminal"; + } +#endif + + // MacOS, mintty + else if (getenv("TERM_PROGRAM") != NULL) { + term = getenv("TERM_PROGRAM"); + } + + else if (getenv("LC_TERMINAL") != NULL) { + term = getenv("LC_TERMINAL"); + } + + // Normal Terminal + else { + term = getenv("TERM"); + // TTY + if (!ffStrSet(term) || ffStrEquals(term, "linux")) { + term = ttyname(STDIN_FILENO); + } + } + + if (ffStrSet(term)) { + ffStrbufSetS(&result->processName, term); + ffStrbufSetS(&result->exe, term); + setExeName(&result->exe, &result->exeName); + } +} + +static void getUserShellFromEnv(FFShellResult* result) { + // If shell detection via processes failed + if (result->processName.length == 0 && instance.state.platform.userShell.length > 0) { + ffStrbufSet(&result->exe, &instance.state.platform.userShell); + setExeName(&result->exe, &result->exeName); + ffStrbufAppendS(&result->processName, result->exeName); + } +} + +static void setShellInfoDetails(FFShellResult* result) { + if (ffStrbufEqualS(&result->processName, "pwsh")) { + ffStrbufInitStatic(&result->prettyName, "PowerShell"); + } else if (ffStrbufEqualS(&result->processName, "nu")) { + ffStrbufInitStatic(&result->prettyName, "nushell"); + } else if (ffStrbufEqualS(&result->processName, "oil.ovm")) { + ffStrbufInitStatic(&result->prettyName, "Oils"); + } else if (ffStrbufEqualS(&result->processName, "busybox")) { + ffStrbufInitStatic(&result->prettyName, "ash"); + } else { + // https://github.com/fastfetch-cli/fastfetch/discussions/280#discussioncomment-3831734 + ffStrbufInitS(&result->prettyName, result->exeName); + } +} + +static void setTerminalInfoDetails(FFTerminalResult* result) { + // For Nixpkgs. Ref: #510 and https://github.com/NixOS/nixpkgs/pull/249428 + // We use processName when detecting version and font, overriding it for simplification + if (ffStrbufStartsWithC(&result->processName, '.') && ffStrbufStartsWithS(&result->exePath,"/nix/store")) { + ffStrbufSubstrAfter(&result->processName, 0); + if (strlen(result->exeName) < 15) { + ffStrbufSubstrBeforeLastC(&result->processName, '-'); + } + } + + if (ffStrbufEqualS(&result->processName, "wezterm-gui")) { + ffStrbufInitStatic(&result->prettyName, "WezTerm"); + } else if (ffStrbufStartsWithS(&result->processName, "tmux:")) { + ffStrbufInitStatic(&result->prettyName, "tmux"); + } else if (ffStrbufStartsWithS(&result->processName, "screen-")) { + ffStrbufInitStatic(&result->prettyName, "screen"); + } else if (ffStrbufEqualS(&result->processName, "sshd") || ffStrbufStartsWithS(&result->processName, "sshd-")) { + if (result->tty.length) { + ffStrbufInitCopy(&result->prettyName, &result->tty); + } else { + ffStrbufSetStatic(&result->prettyName, "sshd"); + } + } + +#if defined(__ANDROID__) + + else if (ffStrbufEqualS(&result->processName, "com.termux")) + ffStrbufInitStatic(&result->prettyName, "Termux"); + +#elif defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__GNU__) + + else if (ffStrbufStartsWithS(&result->processName, "gnome-terminal")) + ffStrbufInitStatic(&result->prettyName, "GNOME Terminal"); + else if (ffStrbufStartsWithS(&result->processName, "kgx")) { + ffStrbufInitStatic(&result->prettyName, "GNOME Console"); + } else if (ffStrbufEqualS(&result->processName, "urxvt") || + ffStrbufEqualS(&result->processName, "urxvtd") || + ffStrbufEqualS(&result->processName, "rxvt")) { + ffStrbufInitStatic(&result->prettyName, "rxvt-unicode"); + } else if (ffStrbufStartsWithS(&result->processName, "ptyxis-agent")) { + ffStrbufInitStatic(&result->prettyName, "Ptyxis"); + } + +#elif defined(__APPLE__) + + else if (ffStrbufEqualS(&result->processName, "iTerm.app") || ffStrbufStartsWithS(&result->processName, "iTermServer-")) + ffStrbufInitStatic(&result->prettyName, "iTerm"); + else if (ffStrbufEndsWithS(&result->exePath, "Terminal.app/Contents/MacOS/Terminal")) { + ffStrbufSetStatic(&result->processName, "Apple_Terminal"); // $TERM_PROGRAM, for terminal font detection + ffStrbufInitStatic(&result->prettyName, "Apple Terminal"); + } else if (ffStrbufEqualS(&result->processName, "Apple_Terminal")) { + ffStrbufInitStatic(&result->prettyName, "Apple Terminal"); + } else if (ffStrbufEndsWithS(&result->exePath, "Warp.app/Contents/MacOS/stable")) { + ffStrbufSetStatic(&result->processName, "WarpTerminal"); // $TERM_PROGRAM, for terminal font detection + ffStrbufInitStatic(&result->prettyName, "Warp"); + } else if (ffStrbufEqualS(&result->processName, "WarpTerminal")) { + ffStrbufInitStatic(&result->prettyName, "Warp"); + } + +#elif defined(__HAIKU__) + + else if (ffStrbufEqualS(&result->processName, "Terminal")) + ffStrbufInitStatic(&result->prettyName, "Haiku Terminal"); + +#endif + + else if (strncmp(result->exeName, result->processName.chars, result->processName.length) == 0 || (ffStrbufStartsWithS(&result->exePath,"/nix/store") && strlen(result->exeName) > 15)) { // if exeName starts with processName, print it. Otherwise print processName. For nixpkgs, use exeName if processName can't be unwrapped + ffStrbufInitS(&result->prettyName, result->exeName); + } else { + ffStrbufInitCopy(&result->prettyName, &result->processName); + } +} + +#if defined(MAXPATH) + #define FF_EXE_PATH_LEN MAXPATH +#elif defined(PATH_MAX) + #define FF_EXE_PATH_LEN PATH_MAX +#else + #define FF_EXE_PATH_LEN 260 +#endif + +const FFShellResult* ffDetectShell() { + static FFShellResult result; + static bool init = false; + if (init) { + return &result; + } + init = true; + + ffStrbufInit(&result.processName); + ffStrbufInitA(&result.exe, FF_EXE_PATH_LEN); + result.exeName = result.exe.chars; + ffStrbufInit(&result.exePath); + ffStrbufInit(&result.version); + result.pid = 0; + result.ppid = 0; + result.tty = -1; + + pid_t ppid = getppid(); + + const char* ignoreParent = getenv("FFTS_IGNORE_PARENT"); + if (ignoreParent && ffStrEquals(ignoreParent, "1")) { + FF_STRBUF_AUTO_DESTROY _ = ffStrbufCreate(); + ffProcessGetBasicInfoLinux(ppid, &_, &ppid, NULL); + } + + ppid = getShellInfo(&result, ppid); + getUserShellFromEnv(&result); + + if (result.processName.length > 0) { + setShellInfoDetails(&result); + if (instance.config.general.detectVersion) { + fftsGetShellVersion(result.exePath.length > 0 ? &result.exePath : &result.exe, result.exeName, &result.version); + } + } + + return &result; +} + +const FFTerminalResult* ffDetectTerminal() { + static FFTerminalResult result; + static bool init = false; + if (init) { + return &result; + } + init = true; + + ffStrbufInit(&result.processName); + ffStrbufInitA(&result.exe, FF_EXE_PATH_LEN); + result.exeName = result.exe.chars; + ffStrbufInit(&result.exePath); + ffStrbufInit(&result.version); + ffStrbufInitS(&result.tty, ttyname(STDOUT_FILENO)); + result.pid = 0; + result.ppid = 0; + + pid_t ppid = (pid_t) ffDetectShell()->ppid; + + if (ppid) { + ppid = getTerminalInfo(&result, ppid); + } + getTerminalFromEnv(&result); + + if (result.processName.length > 0) { + setTerminalInfoDetails(&result); + if (instance.config.general.detectVersion) { + fftsGetTerminalVersion(&result.processName, result.exePath.length > 0 ? &result.exePath : &result.exe, &result.version); + } + } + + return &result; +} diff --git a/src/detection/terminalshell/terminalshell_windows.c b/src/detection/terminalshell/terminalshell_windows.c new file mode 100644 index 0000000..d037663 --- /dev/null +++ b/src/detection/terminalshell/terminalshell_windows.c @@ -0,0 +1,379 @@ +#include "terminalshell.h" +#include "common/io.h" +#include "common/processing.h" +#include "common/thread.h" +#include "common/mallocHelper.h" +#include "common/windows/registry.h" +#include "common/windows/unicode.h" +#include "common/windows/version.h" +#include "common/windows/nt.h" +#include "common/strutil.h" + +#include <stdalign.h> +#include <windows.h> +#include <wchar.h> +#include <tlhelp32.h> +#include <ntstatus.h> +#include <winternl.h> +#include <shlobj.h> + +static uint32_t getShellInfo(FFShellResult* result, uint32_t pid) { + uint32_t ppid = 0; + bool gui = false; + + while (pid != 0 && ffProcessGetInfoWindows(pid, &ppid, &result->processName, &result->exe, &result->exeName, &result->exePath, &gui)) { + ffStrbufSet(&result->prettyName, &result->processName); + if (ffStrbufEndsWithIgnCaseS(&result->prettyName, ".exe")) { + ffStrbufSubstrBefore(&result->prettyName, result->prettyName.length - 4); + } + + // Common programs that are between terminal and own process, but are not the shell + if ( + !gui && (ffStrbufIgnCaseEqualS(&result->prettyName, "sudo") || ffStrbufIgnCaseEqualS(&result->prettyName, "su") || ffStrbufIgnCaseEqualS(&result->prettyName, "gdb") || ffStrbufIgnCaseEqualS(&result->prettyName, "lldb") || ffStrbufIgnCaseEqualS(&result->prettyName, "lldb-dap") || ffStrbufIgnCaseEqualS(&result->prettyName, "python") || // python on windows generates shim executables + ffStrbufIgnCaseEqualS(&result->prettyName, "fastfetch") || // scoop warps the real binaries with a "shim" exe + ffStrbufIgnCaseEqualS(&result->prettyName, "flashfetch") || ffStrbufContainIgnCaseS(&result->prettyName, "debug") || ffStrbufContainIgnCaseS(&result->prettyName, "time") || ffStrbufStartsWithIgnCaseS(&result->prettyName, "ConEmuC") // https://github.com/fastfetch-cli/fastfetch/issues/488#issuecomment-1619982014 + )) { + ffStrbufClear(&result->processName); + ffStrbufClear(&result->prettyName); + ffStrbufClear(&result->exe); + result->exeName = NULL; + pid = ppid; + continue; + } + + result->pid = pid; + + if (gui) { + // Started without shell + // In this case, terminal process will be created by fastfetch itself. + ppid = 0; + if (ffStrbufIgnCaseEqualS(&result->prettyName, "explorer")) { + ffStrbufSetS(&result->prettyName, "Windows Explorer"); + } + } else { + result->ppid = ppid; + } + + break; + } + return ppid; +} + +static void setShellInfoDetails(FFShellResult* result) { + if (ffStrbufIgnCaseEqualS(&result->prettyName, "pwsh")) { + ffStrbufSetS(&result->prettyName, "PowerShell"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "powershell")) { + ffStrbufSetS(&result->prettyName, "Windows PowerShell"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "powershell_ise")) { + ffStrbufSetS(&result->prettyName, "Windows PowerShell ISE"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "cmd")) { + ffStrbufSetS(&result->prettyName, "CMD"); + + if (instance.config.general.detectVersion) { + FF_AUTO_CLOSE_FD HANDLE snapshot = NULL; + while (!(snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, result->pid)) && GetLastError() == ERROR_BAD_LENGTH) {} + + if (snapshot) { + MODULEENTRY32W module; + module.dwSize = sizeof(module); + for (BOOL success = Module32FirstW(snapshot, &module); success; success = Module32NextW(snapshot, &module)) { + if (wcsncmp(module.szModule, L"clink_dll_", strlen("clink_dll_")) == 0) { + FF_STRBUF_AUTO_DESTROY clinkVersion = ffStrbufCreate(); + if (ffGetFileVersion(module.szExePath, NULL, &clinkVersion)) { + ffStrbufAppendF(&result->prettyName, " (with Clink %s)", clinkVersion.chars); + } else { + ffStrbufAppendS(&result->prettyName, " (with Clink)"); + } + break; + } + } + } + } + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "nu")) { + ffStrbufSetS(&result->prettyName, "nushell"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "explorer")) { + ffStrbufSetS(&result->prettyName, "Windows Explorer"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "busybox")) { + ffStrbufInitStatic(&result->prettyName, "ash"); + } +} + +static bool getTerminalFromEnv(FFTerminalResult* result) { + if ( + result->processName.length > 0 && + ffStrbufIgnCaseCompS(&result->processName, "explorer") != 0) { + return false; + } + + const char* term = getenv("ConEmuPID"); + + if (term) { + // ConEmu + uint32_t pid = (uint32_t) strtoul(term, NULL, 10); + result->pid = pid; + if (ffProcessGetInfoWindows(pid, NULL, &result->processName, &result->exe, &result->exeName, &result->exePath, NULL)) { + ffStrbufSet(&result->prettyName, &result->processName); + if (ffStrbufEndsWithIgnCaseS(&result->prettyName, ".exe")) { + ffStrbufSubstrBefore(&result->prettyName, result->prettyName.length - 4); + } + return true; + } else { + term = "ConEmu"; + } + } + + // SSH + if (getenv("SSH_TTY") != NULL) { + term = getenv("SSH_TTY"); + } + + // Windows Terminal + if (!term && (getenv("WT_SESSION") != NULL || getenv("WT_PROFILE_ID") != NULL)) { + term = "WindowsTerminal"; + } + + // Alacritty + if (!term && (getenv("ALACRITTY_SOCKET") != NULL || getenv("ALACRITTY_LOG") != NULL || getenv("ALACRITTY_WINDOW_ID") != NULL)) { + term = "Alacritty"; + } + + if (!term) { + term = getenv("TERM_PROGRAM"); + } + + // Normal Terminal + if (!term) { + term = getenv("TERM"); + } + + if (term) { + ffStrbufSetS(&result->processName, term); + ffStrbufSetS(&result->prettyName, term); + ffStrbufSetS(&result->exe, term); + result->exeName = ""; + return true; + } + + return false; +} + +static bool detectDefaultTerminal(FFTerminalResult* result) { + wchar_t regPath[128] = L"SOFTWARE\\Classes\\PackagedCom\\ClassIndex\\"; + wchar_t* uuid = regPath + strlen("SOFTWARE\\Classes\\PackagedCom\\ClassIndex\\"); + FF_AUTO_CLOSE_FD HANDLE hkcu = NULL; + if (ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Console\\%%Startup", &hkcu, NULL) && + ffRegReadData(hkcu, L"DelegationTerminal", &(FFArgBuffer) { + .data = uuid, + .length = (uint32_t) (sizeof(regPath) - (size_t) (uuid - regPath) * sizeof(wchar_t)), + }, + NULL)) { + if (wcscmp(uuid, L"{00000000-0000-0000-0000-000000000000}") == 0 || // Let Windows decide + wcscmp(uuid, L"{B23D10C0-E52E-411E-9D5B-C09FDF709C7D}") == 0) // Conhost + { + goto conhost; + } + + FF_AUTO_CLOSE_FD HANDLE hklm = NULL; + if (ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, regPath, &hklm, NULL)) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + if (ffRegGetSubKey(hklm, 0, &path, NULL)) { + if (ffStrbufStartsWithS(&path, "Microsoft.WindowsTerminal")) { + ffStrbufSetS(&result->processName, "WindowsTerminal.exe"); + ffStrbufSetS(&result->prettyName, "WindowsTerminal"); + + PWSTR programFiles = NULL; + if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_ProgramFiles, KF_FLAG_DEFAULT, NULL, &programFiles))) { + ffStrbufSetWS(&result->exe, programFiles); + CoTaskMemFree(programFiles); + programFiles = NULL; + + ffStrbufAppendS(&result->exe, "\\WindowsApps\\"); + ffStrbufAppend(&result->exe, &path); + ffStrbufAppendS(&result->exe, "\\WindowsTerminal.exe"); + + if (ffPathExists(result->exe.chars, FF_PATHTYPE_FILE)) { + result->exeName = result->exe.chars + ffStrbufLastIndexC(&result->exe, '\\') + 1; + ffStrbufSet(&result->exePath, &result->exe); + } else { + ffStrbufDestroy(&result->exe); + ffStrbufInitMove(&result->exe, &path); + result->exeName = ""; + } + } + return true; + } + } + } + } + +conhost:; + ULONG_PTR conhostPid = 0; + ULONG size; + if (NT_SUCCESS(NtQueryInformationProcess(NtCurrentProcess(), ProcessConsoleHostProcess, &conhostPid, sizeof(conhostPid), &size)) && conhostPid != 0) { + // For Windows Terminal, it reports the PID of OpenConsole + if (ffProcessGetInfoWindows((uint32_t) conhostPid, NULL, &result->processName, &result->exe, &result->exeName, &result->exePath, NULL)) { + ffStrbufSet(&result->prettyName, &result->processName); + if (ffStrbufEndsWithIgnCaseS(&result->prettyName, ".exe")) { + ffStrbufSubstrBefore(&result->prettyName, result->prettyName.length - 4); + } + return true; + } + } + + ffStrbufClear(&result->exe); + return false; +} + +static uint32_t getTerminalInfo(FFTerminalResult* result, uint32_t pid) { + if (getenv("MSYSTEM")) { + // Don't try to detect terminals in MSYS shell + // It won't work because MSYS doesn't follow process tree of native Windows programs + return 0; + } + + uint32_t ppid = 0; + bool gui; + + while (pid != 0 && ffProcessGetInfoWindows(pid, &ppid, &result->processName, &result->exe, &result->exeName, &result->exePath, &gui)) { + if (!gui) { + // We are in nested shell + ffStrbufClear(&result->processName); + ffStrbufClear(&result->prettyName); + ffStrbufClear(&result->exe); + ffStrbufClear(&result->exePath); + result->exeName = ""; + pid = ppid; + continue; + } + + ffStrbufSet(&result->prettyName, &result->processName); + if (ffStrbufEndsWithIgnCaseS(&result->prettyName, ".exe")) { + ffStrbufSubstrBefore(&result->prettyName, result->prettyName.length - 4); + } + + if (ffStrbufIgnCaseEqualS(&result->prettyName, "sihost") || + ffStrbufIgnCaseEqualS(&result->prettyName, "explorer") || + ffStrbufIgnCaseEqualS(&result->prettyName, "wininit")) { + // A CUI program created by Windows Explorer will spawn a conhost as its child. + // However the conhost process is just a placeholder; + // The true terminal can be Windows Terminal or others. + ffStrbufClear(&result->processName); + ffStrbufClear(&result->prettyName); + ffStrbufClear(&result->exe); + ffStrbufClear(&result->exePath); + result->exeName = ""; + return 0; + } else { + result->pid = pid; + result->ppid = ppid; + } + + break; + } + return ppid; +} + +static void setTerminalInfoDetails(FFTerminalResult* result) { + if (ffStrbufIgnCaseEqualS(&result->prettyName, "WindowsTerminal")) { + ffStrbufSetStatic(&result->prettyName, ffStrbufContainIgnCaseS(&result->exe, ".WindowsTerminalPreview_") ? "Windows Terminal Preview" : "Windows Terminal"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "conhost")) { + ffStrbufSetStatic(&result->prettyName, "Windows Console"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "Code")) { + ffStrbufSetStatic(&result->prettyName, "Visual Studio Code"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "explorer")) { + ffStrbufSetStatic(&result->prettyName, "Windows Explorer"); + } else if (ffStrbufEqualS(&result->prettyName, "wezterm-gui")) { + ffStrbufSetStatic(&result->prettyName, "WezTerm"); + } else if (ffStrbufIgnCaseEqualS(&result->prettyName, "sshd") || ffStrbufStartsWithIgnCaseS(&result->prettyName, "sshd-")) { + const char* tty = getenv("SSH_TTY"); + if (tty) { + ffStrbufSetS(&result->prettyName, tty); + } + } +} + +const FFShellResult* ffDetectShell(void) { + static FFShellResult result; + static bool init = false; + if (init) { + return &result; + } + init = true; + + ffStrbufInit(&result.processName); + ffStrbufInitA(&result.exe, MAX_PATH); + result.exeName = ""; + ffStrbufInit(&result.exePath); + ffStrbufInit(&result.prettyName); + ffStrbufInit(&result.version); + result.pid = 0; + result.ppid = 0; + result.tty = -1; + + uint32_t ppid; + if (!ffProcessGetInfoWindows(0, &ppid, NULL, NULL, NULL, NULL, NULL)) { + return &result; + } + + const char* ignoreParent = getenv("FFTS_IGNORE_PARENT"); + if (ignoreParent && ffStrEquals(ignoreParent, "1")) { + ffProcessGetInfoWindows(ppid, &ppid, NULL, NULL, NULL, NULL, NULL); + } + + ppid = getShellInfo(&result, ppid); + + if (result.processName.length > 0) { + setShellInfoDetails(&result); + char tmp[MAX_PATH]; + strcpy(tmp, result.exeName); + char* ext = strrchr(tmp, '.'); + if (ext) { + *ext = '\0'; + } + if (instance.config.general.detectVersion) { + fftsGetShellVersion(result.exePath.length > 0 ? &result.exePath : &result.exe, tmp, &result.version); + } + } + + return &result; +} + +const FFTerminalResult* ffDetectTerminal(void) { + static FFTerminalResult result; + static bool init = false; + if (init) { + return &result; + } + init = true; + + ffStrbufInit(&result.processName); + ffStrbufInitA(&result.exe, MAX_PATH); + result.exeName = ""; + ffStrbufInit(&result.exePath); + ffStrbufInit(&result.prettyName); + ffStrbufInit(&result.version); + ffStrbufInit(&result.tty); + result.pid = 0; + result.ppid = 0; + + uint32_t ppid = ffDetectShell()->ppid; + if (ppid) { + getTerminalInfo(&result, ppid); + } + + if (result.processName.length == 0) { + getTerminalFromEnv(&result); + } + if (result.processName.length == 0) { + detectDefaultTerminal(&result); + } + + if (result.processName.length > 0) { + setTerminalInfoDetails(&result); + if (instance.config.general.detectVersion) { + fftsGetTerminalVersion(&result.processName, result.exePath.length > 0 ? &result.exePath : &result.exe, &result.version); + } + } + + return &result; +} diff --git a/src/detection/terminalsize/terminalsize.h b/src/detection/terminalsize/terminalsize.h new file mode 100644 index 0000000..1ad97e1 --- /dev/null +++ b/src/detection/terminalsize/terminalsize.h @@ -0,0 +1,13 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/terminalsize/option.h" + +typedef struct FFTerminalSizeResult { + uint16_t rows; + uint16_t columns; + uint16_t width; + uint16_t height; +} FFTerminalSizeResult; + +bool ffDetectTerminalSize(FFTerminalSizeResult* result); diff --git a/src/detection/terminalsize/terminalsize_linux.c b/src/detection/terminalsize/terminalsize_linux.c new file mode 100644 index 0000000..e06f958 --- /dev/null +++ b/src/detection/terminalsize/terminalsize_linux.c @@ -0,0 +1,38 @@ +#include "terminalsize.h" +#include "common/io.h" + +#include <sys/ioctl.h> +#include <fcntl.h> +#include <unistd.h> + +#ifdef __sun + #include <sys/termios.h> +#endif + +bool ffDetectTerminalSize(FFTerminalSizeResult* result) { + struct winsize winsize = {}; + static int ttyfd = STDOUT_FILENO; + if (!isatty(ttyfd)) { + ttyfd = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC); + } + + ioctl(ttyfd, TIOCGWINSZ, &winsize); + + if (winsize.ws_row == 0 || winsize.ws_col == 0) { + ffGetTerminalResponse("\e[18t", 2, "\e[8;%hu;%hut", &winsize.ws_row, &winsize.ws_col); + } + + if (winsize.ws_ypixel == 0 || winsize.ws_xpixel == 0) { + ffGetTerminalResponse("\e[14t", 2, "\e[4;%hu;%hut", &winsize.ws_ypixel, &winsize.ws_xpixel); + } + + if (winsize.ws_row == 0 && winsize.ws_col == 0) { + return false; + } + + result->rows = winsize.ws_row; + result->columns = winsize.ws_col; + result->width = winsize.ws_xpixel; + result->height = winsize.ws_ypixel; + return true; +} diff --git a/src/detection/terminalsize/terminalsize_windows.c b/src/detection/terminalsize/terminalsize_windows.c new file mode 100644 index 0000000..05888e7 --- /dev/null +++ b/src/detection/terminalsize/terminalsize_windows.c @@ -0,0 +1,45 @@ +#include "terminalsize.h" +#include "common/io.h" + +#include <windows.h> + +bool ffDetectTerminalSize(FFTerminalSizeResult* result) { + HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); + FF_AUTO_CLOSE_FD HANDLE hConout = INVALID_HANDLE_VALUE; + { + DWORD outputMode; + if (!GetConsoleMode(hOutput, &outputMode)) { + hConout = CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, NULL); + hOutput = hConout; + } + } + { + CONSOLE_SCREEN_BUFFER_INFO csbi; + if (GetConsoleScreenBufferInfo(hOutput, &csbi)) { + result->columns = (uint16_t) (csbi.srWindow.Right - csbi.srWindow.Left + 1); + result->rows = (uint16_t) (csbi.srWindow.Bottom - csbi.srWindow.Top + 1); + } else { + // Windows Terminal doesn't report `\e` for some reason + ffGetTerminalResponse("\e[18t", 2, "%*[^;];%hu;%hut", &result->rows, &result->columns); + } + } + + if (result->columns == 0 && result->rows == 0) { + return false; + } + + { + CONSOLE_FONT_INFOEX cfi = { .cbSize = sizeof(cfi) }; + if (GetCurrentConsoleFontEx(hOutput, FALSE, &cfi)) // Only works for ConHost + { + result->width = result->columns * (uint16_t) cfi.dwFontSize.X; + result->height = result->rows * (uint16_t) cfi.dwFontSize.Y; + } + if (result->width == 0 || result->height == 0) { + // Windows Terminal doesn't report `\e` for some reason + ffGetTerminalResponse("\e[14t", 2, "%*[^;];%hu;%hut", &result->height, &result->width); + } + } + + return result->columns > 0 && result->rows > 0; +} diff --git a/src/detection/terminaltheme/terminaltheme.c b/src/detection/terminaltheme/terminaltheme.c new file mode 100644 index 0000000..e609754 --- /dev/null +++ b/src/detection/terminaltheme/terminaltheme.c @@ -0,0 +1,105 @@ +#include "terminaltheme.h" +#include "common/io.h" +#include "common/strutil.h" + +#include <inttypes.h> + +static bool detectByEscapeCode(FFTerminalThemeResult* result) { + // Windows Terminal removes all `\e`s in its output + if (ffGetTerminalResponse("\e]10;?\e\\" /*fg*/ "\e]11;?\e\\" /*bg*/, + 6, + "%*[^0-9]10;rgb:%" SCNx16 "/%" SCNx16 "/%" SCNx16 /*"\e\\"*/ "%*[^0-9]11;rgb:%" SCNx16 "/%" SCNx16 "/%" SCNx16 /*"\e\\"*/, + &result->fg.r, + &result->fg.g, + &result->fg.b, + &result->bg.r, + &result->bg.g, + &result->bg.b) == NULL) { + if (result->fg.r > 0x0100 || result->fg.g > 0x0100 || result->fg.b > 0x0100) { + result->fg.r /= 0x0100, result->fg.g /= 0x0100, result->fg.b /= 0x0100; + } + if (result->bg.r > 0x0100 || result->bg.g > 0x0100 || result->bg.b > 0x0100) { + result->bg.r /= 0x0100, result->bg.g /= 0x0100, result->bg.b /= 0x0100; + } + } else { + return false; + } + + return true; +} + +static FFTerminalThemeColor fgbgToColor(int num) { + // https://github.com/dalance/termbg/blob/13c478a433fa182e65c401d26a1e7792a7f7f453/src/lib.rs#L251 + switch (num) { + case 0: + return (FFTerminalThemeColor) { 0, 0, 0, false }; // black + case 1: + return (FFTerminalThemeColor) { 205, 0, 0, false }; // red + case 2: + return (FFTerminalThemeColor) { 0, 205, 0, false }; // green + case 3: + return (FFTerminalThemeColor) { 205, 205, 0, false }; // yellow + case 4: + return (FFTerminalThemeColor) { 0, 0, 238, false }; // blue + case 5: + return (FFTerminalThemeColor) { 205, 0, 205, false }; // magenta + case 6: + return (FFTerminalThemeColor) { 0, 205, 205, false }; // cyan + case 7: + return (FFTerminalThemeColor) { 229, 229, 229, false }; // white + + case 8: + return (FFTerminalThemeColor) { 127, 127, 127, false }; // bright black + case 9: + return (FFTerminalThemeColor) { 255, 0, 0, false }; // bright red + case 10: + return (FFTerminalThemeColor) { 0, 255, 0, false }; // bright green + case 11: + return (FFTerminalThemeColor) { 255, 255, 0, false }; // bright yellow + case 12: + return (FFTerminalThemeColor) { 92, 92, 255, false }; // bright blue + case 13: + return (FFTerminalThemeColor) { 255, 0, 255, false }; // bright magenta + case 14: + return (FFTerminalThemeColor) { 0, 255, 255, false }; // bright cyan + case 15: + return (FFTerminalThemeColor) { 255, 255, 255, false }; // bright white + + default: + return (FFTerminalThemeColor) { 0, 0, 0, false }; // invalid + } +} + +static bool detectByEnv(FFTerminalThemeResult* result) { + const char* color = getenv("COLORFGBG"); // 7;0 + + if (!ffStrSet(color)) { + return false; + } + + int f, g; + if (sscanf(color, "%d;%d", &f, &g) != 2) { + return false; + } + + result->fg = fgbgToColor(f); + result->bg = fgbgToColor(g); + return true; +} + +static inline bool detectColor(FFTerminalThemeResult* result, bool forceEnv) { + if (!forceEnv && detectByEscapeCode(result)) { + return true; + } + + return detectByEnv(result); +} + +bool ffDetectTerminalTheme(FFTerminalThemeResult* result, bool forceEnv) { + if (!detectColor(result, forceEnv)) { + return false; + } + result->fg.dark = result->fg.r * 299 + result->fg.g * 587 + result->fg.b * 114 < 128000; + result->bg.dark = result->bg.r * 299 + result->bg.g * 587 + result->bg.b * 114 < 128000; + return true; +} diff --git a/src/detection/terminaltheme/terminaltheme.h b/src/detection/terminaltheme/terminaltheme.h new file mode 100644 index 0000000..a2808dc --- /dev/null +++ b/src/detection/terminaltheme/terminaltheme.h @@ -0,0 +1,18 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/terminaltheme/option.h" + +typedef struct FFTerminalThemeColor { + uint16_t r; + uint16_t g; + uint16_t b; + bool dark; +} FFTerminalThemeColor; + +typedef struct FFTerminalThemeResult { + FFTerminalThemeColor fg; + FFTerminalThemeColor bg; +} FFTerminalThemeResult; + +bool ffDetectTerminalTheme(FFTerminalThemeResult* result, bool forceEnv); diff --git a/src/detection/theme/theme.h b/src/detection/theme/theme.h new file mode 100644 index 0000000..3498b1d --- /dev/null +++ b/src/detection/theme/theme.h @@ -0,0 +1,11 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/theme/option.h" + +typedef struct FFThemeResult { + FFstrbuf theme1; + FFstrbuf theme2; +} FFThemeResult; + +const char* ffDetectTheme(FFThemeResult* result); diff --git a/src/detection/theme/theme_apple.c b/src/detection/theme/theme_apple.c new file mode 100644 index 0000000..a60d39c --- /dev/null +++ b/src/detection/theme/theme_apple.c @@ -0,0 +1,22 @@ +#include "theme.h" + +#include "detection/os/os.h" + +const char* ffDetectTheme(FFThemeResult* result) { + const FFOSResult* os = ffDetectOS(); + + char* str_end; + const char* version = os->version.chars; + unsigned long osNum = strtoul(version, &str_end, 10); + if (str_end != version) { + if (osNum > 15) { // Tahoe + ffStrbufSetStatic(&result->theme1, "Liquid Glass"); + } else if (osNum < 10) { + ffStrbufSetStatic(&result->theme1, "Platinum"); + } else { + ffStrbufSetStatic(&result->theme1, "Aqua"); + } + } + + return NULL; +} diff --git a/src/detection/theme/theme_linux.c b/src/detection/theme/theme_linux.c new file mode 100644 index 0000000..1de7db8 --- /dev/null +++ b/src/detection/theme/theme_linux.c @@ -0,0 +1,60 @@ +#include "theme.h" +#include "common/parsing.h" +#include "detection/gtk_qt/gtk_qt.h" +#include "detection/displayserver/displayserver.h" + +const char* ffDetectTheme(FFThemeResult* result) { + const FFDisplayServerResult* wmde = ffConnectDisplayServer(); + + if (ffStrbufIgnCaseEqualS(&wmde->wmProtocolName, FF_WM_PROTOCOL_TTY)) { + return "Theme isn't supported in TTY"; + } + + const FFQtResult* plasma = ffDetectQt(); + const FFstrbuf* gtk2 = &ffDetectGTK2()->theme; + const FFstrbuf* gtk3 = &ffDetectGTK3()->theme; + const FFstrbuf* gtk4 = &ffDetectGTK4()->theme; + + if (plasma->widgetStyle.length == 0 && plasma->colorScheme.length == 0 && gtk2->length == 0 && gtk3->length == 0 && gtk4->length == 0) { + return "No themes found"; + } + + ffParseGTK(&result->theme2, gtk2, gtk3, gtk4); + + FF_STRBUF_AUTO_DESTROY plasmaColorPretty = ffStrbufCreate(); + if (ffStrbufStartsWithIgnCase(&plasma->colorScheme, &plasma->widgetStyle)) { + ffStrbufAppendNS(&plasmaColorPretty, plasma->colorScheme.length - plasma->widgetStyle.length, &plasma->colorScheme.chars[plasma->widgetStyle.length]); + } else { + ffStrbufAppend(&plasmaColorPretty, &plasma->colorScheme); + } + + ffStrbufTrim(&plasmaColorPretty, ' '); + + if (plasma->widgetStyle.length > 0) { + ffStrbufAppend(&result->theme1, &plasma->widgetStyle); + + if (plasma->colorScheme.length > 0) { + ffStrbufAppendS(&result->theme1, " ("); + + if (plasmaColorPretty.length > 0) { + ffStrbufAppend(&result->theme1, &plasmaColorPretty); + } else { + ffStrbufAppend(&result->theme1, &plasma->colorScheme); + } + + ffStrbufAppendC(&result->theme1, ')'); + } + } else if (plasma->colorScheme.length > 0) { + if (plasmaColorPretty.length > 0) { + ffStrbufAppend(&result->theme1, &plasmaColorPretty); + } else { + ffStrbufAppend(&result->theme1, &plasma->colorScheme); + } + } + + if (plasma->widgetStyle.length > 0 || plasma->colorScheme.length > 0) { + ffStrbufAppendS(&result->theme1, " [Qt]"); + } + + return NULL; +} diff --git a/src/detection/theme/theme_nosupport.c b/src/detection/theme/theme_nosupport.c new file mode 100644 index 0000000..6f42b96 --- /dev/null +++ b/src/detection/theme/theme_nosupport.c @@ -0,0 +1,5 @@ +#include "theme.h" + +const char* ffDetectTheme(FF_A_UNUSED FFThemeResult* result) { + return "Not supported on this platform"; +} diff --git a/src/detection/theme/theme_windows.c b/src/detection/theme/theme_windows.c new file mode 100644 index 0000000..339b79b --- /dev/null +++ b/src/detection/theme/theme_windows.c @@ -0,0 +1,27 @@ +#include "theme.h" + +#include "detection/os/os.h" + +const char* ffDetectTheme(FFThemeResult* result) { + const FFOSResult* os = ffDetectOS(); + uint32_t ver = (uint32_t) ffStrbufToUInt(&os->version, 0); + if (ver > 1000) { + // Windows Server + if (ver >= 2016) { + ffStrbufSetStatic(&result->theme1, "Fluent"); + } else if (ver >= 2012) { + ffStrbufSetStatic(&result->theme1, "Metro"); + } else { + ffStrbufSetStatic(&result->theme1, "Aero"); + } + } else { + if (ver >= 10) { + ffStrbufSetStatic(&result->theme1, "Fluent"); + } else if (ver >= 8) { + ffStrbufSetStatic(&result->theme1, "Metro"); + } else { + ffStrbufSetStatic(&result->theme1, "Aero"); + } + } + return NULL; +} diff --git a/src/detection/tpm/tpm.h b/src/detection/tpm/tpm.h new file mode 100644 index 0000000..da842ec --- /dev/null +++ b/src/detection/tpm/tpm.h @@ -0,0 +1,11 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/tpm/option.h" + +typedef struct FFTPMResult { + FFstrbuf version; + FFstrbuf description; +} FFTPMResult; + +const char* ffDetectTPM(FFTPMResult* result); diff --git a/src/detection/tpm/tpm_apple.c b/src/detection/tpm/tpm_apple.c new file mode 100644 index 0000000..250dd01 --- /dev/null +++ b/src/detection/tpm/tpm_apple.c @@ -0,0 +1,30 @@ +#include "tpm.h" + +#ifndef __aarch64__ + #include "common/apple/cf_helpers.h" + #include <IOKit/IOKitLib.h> +#endif + +const char* ffDetectTPM(FFTPMResult* result) { +#ifdef __aarch64__ + + ffStrbufSetStatic(&result->version, "2.0"); + ffStrbufSetStatic(&result->description, "Apple Silicon Security"); + return NULL; + +#else + + FF_IOOBJECT_AUTO_RELEASE io_service_t t2Service = IOServiceGetMatchingService( + MACH_PORT_NULL, + IOServiceMatching("AppleT2")); + + if (t2Service) { + ffStrbufSetStatic(&result->version, "2.0"); + ffStrbufSetStatic(&result->description, "Apple T2 Security Chip"); + return NULL; + } + +#endif + + return "No Apple Security hardware detected"; +} diff --git a/src/detection/tpm/tpm_bsd.c b/src/detection/tpm/tpm_bsd.c new file mode 100644 index 0000000..4b73413 --- /dev/null +++ b/src/detection/tpm/tpm_bsd.c @@ -0,0 +1,22 @@ +#include "tpm.h" +#include "common/sysctl.h" +#include "common/kmod.h" + +const char* ffDetectTPM(FFTPMResult* result) { + if (ffSysctlGetString("dev.tpmcrb.0.%desc", &result->description) != NULL) { + if (!ffKmodLoaded("tpm")) { + return "`tpm` kernel module is not loaded"; + } + return "TPM device is not found"; + } + + if (ffStrbufContainS(&result->description, "2.0")) { + ffStrbufSetStatic(&result->version, "2.0"); + } else if (ffStrbufContainS(&result->description, "1.2")) { + ffStrbufSetStatic(&result->version, "1.2"); + } else { + ffStrbufSetStatic(&result->version, "unknown"); + } + + return NULL; +} diff --git a/src/detection/tpm/tpm_linux.c b/src/detection/tpm/tpm_linux.c new file mode 100644 index 0000000..040da38 --- /dev/null +++ b/src/detection/tpm/tpm_linux.c @@ -0,0 +1,24 @@ +#include "tpm.h" +#include "common/io.h" + +const char* ffDetectTPM(FFTPMResult* result) { + if (!ffPathExists("/sys/class/tpm/tpm0/", FF_PATHTYPE_DIRECTORY)) { + if (!ffPathExists("/sys/class/tpm/", FF_PATHTYPE_DIRECTORY)) { + return "TPM is not supported by kernel"; + } + return "TPM device is not found"; + } + + if (ffReadFileBuffer("/sys/class/tpm/tpm0/tpm_version_major", &result->version)) { + ffStrbufTrimRightSpace(&result->version); + if (ffStrbufEqualS(&result->version, "2")) { + ffStrbufSetStatic(&result->version, "2.0"); + } + } + + if (ffReadFileBuffer("/sys/class/tpm/tpm0/device/description", &result->description)) { + ffStrbufTrimRightSpace(&result->description); + } + + return NULL; +} diff --git a/src/detection/tpm/tpm_nosupport.c b/src/detection/tpm/tpm_nosupport.c new file mode 100644 index 0000000..492f37f --- /dev/null +++ b/src/detection/tpm/tpm_nosupport.c @@ -0,0 +1,5 @@ +#include "tpm.h" + +const char* ffDetectTPM(FF_A_UNUSED FFTPMResult* result) { + return "Not supported on this platform"; +} diff --git a/src/detection/tpm/tpm_windows.c b/src/detection/tpm/tpm_windows.c new file mode 100644 index 0000000..14f6e8a --- /dev/null +++ b/src/detection/tpm/tpm_windows.c @@ -0,0 +1,51 @@ +#include "tpm.h" +#include "common/library.h" + +#include <windef.h> +#include <tbs.h> +#include <winerror.h> + +const char* ffDetectTPM(FFTPMResult* result) { + FF_LIBRARY_LOAD_MESSAGE(tbs, "TBS" FF_LIBRARY_EXTENSION, -1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(tbs, Tbsi_GetDeviceInfo) + + TPM_DEVICE_INFO deviceInfo = {}; + TBS_RESULT code = ffTbsi_GetDeviceInfo(sizeof(deviceInfo), &deviceInfo); + if (code != TBS_SUCCESS) { + return code == (TBS_RESULT) TBS_E_TPM_NOT_FOUND ? "TPM device is not found" : "Tbsi_GetDeviceInfo() failed"; + } + + switch (deviceInfo.tpmVersion) { + case TPM_VERSION_12: + ffStrbufSetStatic(&result->version, "1.2"); + break; + case TPM_VERSION_20: + ffStrbufSetStatic(&result->version, "2.0"); + break; + default: + ffStrbufSetStatic(&result->version, "unknown"); + break; + } + + switch (deviceInfo.tpmInterfaceType) { + case TPM_IFTYPE_1: + ffStrbufSetF(&result->description, "I/O-port or MMIO TPM %s", result->version.chars); + break; + case TPM_IFTYPE_TRUSTZONE: + ffStrbufSetF(&result->description, "Trustzone TPM %s", result->version.chars); + break; + case TPM_IFTYPE_HW: + ffStrbufSetF(&result->description, "HW TPM %s", result->version.chars); + break; + case TPM_IFTYPE_EMULATOR: + ffStrbufSetF(&result->description, "SW-emulator TPM %s", result->version.chars); + break; + case TPM_IFTYPE_SPB: + ffStrbufSetF(&result->description, "SPB attached TPM %s", result->version.chars); + break; + default: + break; + } + + return NULL; +} diff --git a/src/detection/uptime/uptime.h b/src/detection/uptime/uptime.h new file mode 100644 index 0000000..3f8631c --- /dev/null +++ b/src/detection/uptime/uptime.h @@ -0,0 +1,11 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/uptime/option.h" + +typedef struct FFUptimeResult { + uint64_t bootTime; + uint64_t uptime; +} FFUptimeResult; + +const char* ffDetectUptime(FFUptimeResult* result); diff --git a/src/detection/uptime/uptime_bsd.c b/src/detection/uptime/uptime_bsd.c new file mode 100644 index 0000000..46943b0 --- /dev/null +++ b/src/detection/uptime/uptime_bsd.c @@ -0,0 +1,27 @@ +#include "uptime.h" +#include "common/time.h" + +#include <sys/sysctl.h> +#include <sys/time.h> + +const char* ffDetectUptime(FFUptimeResult* result) { +#if __NetBSD__ + struct timespec bootTime; +#else + struct timeval bootTime; +#endif + size_t bootTimeSize = sizeof(bootTime); + if (sysctl( + (int[]) { CTL_KERN, KERN_BOOTTIME }, 2, &bootTime, &bootTimeSize, NULL, 0) != 0) { + return "sysctl({CTL_KERN, KERN_BOOTTIME}) failed"; + } + +#if __NetBSD__ + result->bootTime = (uint64_t) bootTime.tv_sec * 1000 + (uint64_t) bootTime.tv_nsec / 1000000; +#else + result->bootTime = (uint64_t) bootTime.tv_sec * 1000 + (uint64_t) bootTime.tv_usec / 1000; +#endif + result->uptime = ffTimeGetNow() - result->bootTime; + + return NULL; +} diff --git a/src/detection/uptime/uptime_haiku.c b/src/detection/uptime/uptime_haiku.c new file mode 100644 index 0000000..bf5dd25 --- /dev/null +++ b/src/detection/uptime/uptime_haiku.c @@ -0,0 +1,8 @@ +#include "uptime.h" +#include "common/time.h" + +const char* ffDetectUptime(FFUptimeResult* result) { + result->uptime = (uint64_t) system_time() / 1000; + result->bootTime = ffTimeGetNow() - result->uptime; + return NULL; +} diff --git a/src/detection/uptime/uptime_linux.c b/src/detection/uptime/uptime_linux.c new file mode 100644 index 0000000..75c7768 --- /dev/null +++ b/src/detection/uptime/uptime_linux.c @@ -0,0 +1,38 @@ +#include "uptime.h" +#include "common/time.h" +#include "common/io.h" + +#include <inttypes.h> + +const char* ffDetectUptime(FFUptimeResult* result) { +#ifndef __ANDROID__ // cat: /proc/uptime: Permission denied + + // #620 + char buf[64]; + ssize_t nRead = ffReadFileData("/proc/uptime", ARRAY_SIZE(buf) - 1, buf); + if (nRead > 0) { + buf[nRead] = '\0'; + + char* err = NULL; + double sec = strtod(buf, &err); + if (err != buf) { + result->uptime = (uint64_t) (sec * 1000); + result->bootTime = ffTimeGetNow() - result->uptime; + return NULL; + } + } + +#endif +#ifndef __GNU__ + struct timespec uptime; + if (clock_gettime(CLOCK_BOOTTIME, &uptime) != 0) { + return "clock_gettime(CLOCK_BOOTTIME) failed"; + } + + result->uptime = (uint64_t) uptime.tv_sec * 1000 + (uint64_t) uptime.tv_nsec / 1000000; + result->bootTime = ffTimeGetNow() - result->uptime; + return NULL; +#else + return "read(/proc/uptime) failed"; +#endif +} diff --git a/src/detection/uptime/uptime_sunos.c b/src/detection/uptime/uptime_sunos.c new file mode 100644 index 0000000..2a37299 --- /dev/null +++ b/src/detection/uptime/uptime_sunos.c @@ -0,0 +1,19 @@ +#include "uptime.h" +#include "common/time.h" + +#include <utmpx.h> + +const char* ffDetectUptime(FFUptimeResult* result) { + struct utmpx* ut; + + setutxent(); + while (NULL != (ut = getutxent())) { + if (ut->ut_type == BOOT_TIME) { + result->bootTime = (uint64_t) ut->ut_tv.tv_sec * 1000 + (uint64_t) ut->ut_tv.tv_usec / 1000000; + result->uptime = ffTimeGetNow() - result->bootTime; + break; + } + } + endutxent(); + return NULL; +} diff --git a/src/detection/uptime/uptime_windows.c b/src/detection/uptime/uptime_windows.c new file mode 100644 index 0000000..695edad --- /dev/null +++ b/src/detection/uptime/uptime_windows.c @@ -0,0 +1,17 @@ +#include "uptime.h" +#include "common/time.h" +#include "common/windows/nt.h" + +const char* ffDetectUptime(FFUptimeResult* result) { + // QueryInterruptTime with Win7 support + uint64_t interruptTime = ffKSystemTimeToUInt64(&SharedUserData->InterruptTime); + + result->uptime = interruptTime / 10000; // Convert from 100-nanosecond intervals to milliseconds + result->bootTime = ffTimeGetNow() - result->uptime; + + // Alternatively, `NtQuerySystemInformation(SystemTimeOfDayInformation)` reports the boot time directly, + // whose result exactly equals what WMI `Win32_OperatingSystem` reports + // with much lower accuracy (0.5 seconds) + + return NULL; +} diff --git a/src/detection/users/users.h b/src/detection/users/users.h new file mode 100644 index 0000000..8f24cc0 --- /dev/null +++ b/src/detection/users/users.h @@ -0,0 +1,14 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/users/option.h" + +typedef struct FFUserResult { + FFstrbuf name; + FFstrbuf hostName; + FFstrbuf clientIp; + FFstrbuf sessionName; + uint64_t loginTime; // ms +} FFUserResult; + +const char* ffDetectUsers(FFUsersOptions* options, FFlist* users); diff --git a/src/detection/users/users_linux.c b/src/detection/users/users_linux.c new file mode 100644 index 0000000..bc84eed --- /dev/null +++ b/src/detection/users/users_linux.c @@ -0,0 +1,204 @@ +#include "common/io.h" +#include "common/properties.h" +#include "fastfetch.h" +#include "users.h" + +#include <unistd.h> + +#if FF_HAVE_UTMPX + #include <utmpx.h> +#else + // for Android compatibility + #include <utmp.h> + #define utmpx utmp + #define setutxent setutent + #define getutxent getutent +#endif +#if __linux__ || __GNU__ + #include <netinet/in.h> + #include <arpa/inet.h> +#endif + +#if __linux__ +bool detectUserBySystemd(const FFstrbuf* pathUsers, FFlist* users) { + FF_STRBUF_AUTO_DESTROY state = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY userName = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY loginTime = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY sessions = ffStrbufCreate(); + + // WARNING: This is private data. Do not parse + if (!ffParsePropFileValues(pathUsers->chars, 4, (FFpropquery[]) { + { "NAME=", &userName }, + { "STATE=", &state }, + { "REALTIME=", &loginTime }, + { "ONLINE_SESSIONS=", &sessions }, + }) || + !ffStrbufEqualS(&state, "active")) { + return false; + } + + FFUserResult* user = FF_LIST_ADD(FFUserResult, *users); + ffStrbufInitMove(&user->name, &userName); + ffStrbufInit(&user->hostName); + ffStrbufInit(&user->sessionName); + ffStrbufInit(&user->clientIp); + ffStrbufSubstrBefore(&loginTime, loginTime.length - 3); // converts us to ms + user->loginTime = ffStrbufToUInt(&loginTime, 0); + + FF_STRBUF_AUTO_DESTROY pathSessions = ffStrbufCreateS("/run/systemd/sessions/"); + const uint32_t pathSessionsBaseLen = pathSessions.length; + + FF_STRBUF_AUTO_DESTROY tty = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY remoteHost = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY service = ffStrbufCreate(); + + char* token = NULL; + size_t n = 0; + while (ffStrbufGetdelim(&token, &n, ' ', &sessions)) { + ffStrbufSubstrBefore(&pathSessions, pathSessionsBaseLen); + ffStrbufAppendS(&pathSessions, token); + + ffStrbufClear(&remoteHost); + ffStrbufClear(&service); + ffStrbufClear(&tty); + ffStrbufClear(&loginTime); + + // WARNING: This is private data. Do not parse + if (ffParsePropFileValues(pathSessions.chars, 4, (FFpropquery[]) { + { "REMOTE_HOST=", &remoteHost }, + { "TTY=", &tty }, + { "SERVICE=", &service }, + { "REALTIME=", &loginTime }, + }) && + !ffStrbufEqualS(&service, "systemd-user")) { + if (remoteHost.length) { + ffStrbufTrimRight(&remoteHost, ']'); + ffStrbufTrimLeft(&remoteHost, '['); + ffStrbufInitMove(&user->hostName, &remoteHost); + } else { + ffStrbufSetStatic(&user->hostName, "localhost"); + } + ffStrbufInitMove(&user->sessionName, tty.length ? &tty : &service); + if (loginTime.length) { + ffStrbufSubstrBefore(&loginTime, loginTime.length - 3); // converts us to ms + user->loginTime = ffStrbufToUInt(&loginTime, 0); + } + break; + } + } + + return true; +} + +const char* detectBySystemd(FFUsersOptions* options, FFlist* users) { + // For some reason, debian/ubuntu no longer updates `/var/run/utmp` (#2064) + // Query systemd instead + FF_STRBUF_AUTO_DESTROY pathUsers = ffStrbufCreateS("/run/systemd/users/"); + + if (options->myselfOnly) { + ffStrbufAppendUInt(&pathUsers, instance.state.platform.uid); + detectUserBySystemd(&pathUsers, users); + } else { + const uint32_t pathUsersBaseLen = pathUsers.length; + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(pathUsers.chars); + if (!dirp) { + return "opendir(\"/run/systemd/users/\") failed"; + } + + struct dirent* entry; + while ((entry = readdir(dirp))) { + if (entry->d_type != DT_REG) { + continue; + } + + ffStrbufAppendS(&pathUsers, entry->d_name); + detectUserBySystemd(&pathUsers, users); + ffStrbufSubstrBefore(&pathUsers, pathUsersBaseLen); + } + } + return NULL; +} +#endif + +#if __linux__ || __GNU__ +static void fillUtmpIpAddr(FFUserResult* user, struct utmpx* n) { + bool isIpv6 = false; + for (int i = 1; i < 4; ++i) { + if (n->ut_addr_v6[i] != 0) { + isIpv6 = true; + break; + } + } + + if (isIpv6) { + char ipv6_str[INET6_ADDRSTRLEN]; + if (inet_ntop(AF_INET6, n->ut_addr_v6, ipv6_str, INET6_ADDRSTRLEN) != NULL) { + ffStrbufSetS(&user->clientIp, ipv6_str); + } + } else if (n->ut_addr_v6[0] != 0) { + char ipv4_str[INET_ADDRSTRLEN]; + if (inet_ntop(AF_INET, n->ut_addr_v6, ipv4_str, INET_ADDRSTRLEN) != NULL) { + ffStrbufSetS(&user->clientIp, ipv4_str); + } + } +} +#else +static void fillUtmpIpAddr(FF_A_UNUSED FFUserResult* user, FF_A_UNUSED struct utmpx* n) { +} +#endif + +const char* detectByUtmp(FFUsersOptions* options, FFlist* users) { + struct utmpx* n = NULL; + setutxent(); + +next: + while ((n = getutxent())) { + if (n->ut_type != USER_PROCESS) { + continue; + } + + if (options->myselfOnly && !ffStrbufEqualS(&instance.state.platform.userName, n->ut_user)) { + continue; + } + + FF_LIST_FOR_EACH (FFUserResult, user, *users) { + if (ffStrbufEqualS(&user->name, n->ut_user)) { + uint64_t newLoginTime = (uint64_t) n->ut_tv.tv_sec * 1000 + (uint64_t) n->ut_tv.tv_usec / 1000; + if (newLoginTime > user->loginTime) { + ffStrbufSetS(&user->hostName, n->ut_host); + ffStrbufSetS(&user->sessionName, n->ut_line); + fillUtmpIpAddr(user, n); + user->loginTime = newLoginTime; + } + goto next; + } + } + + FFUserResult* user = FF_LIST_ADD(FFUserResult, *users); + ffStrbufInitS(&user->name, n->ut_user); + ffStrbufInitS(&user->hostName, n->ut_host); + ffStrbufInitS(&user->sessionName, n->ut_line); + ffStrbufInit(&user->clientIp); + fillUtmpIpAddr(user, n); + user->loginTime = (uint64_t) n->ut_tv.tv_sec * 1000 + (uint64_t) n->ut_tv.tv_usec / 1000; + } + + endutxent(); + + return NULL; +} + +const char* ffDetectUsers(FFUsersOptions* options, FFlist* users) { + const char* err = detectByUtmp(options, users); + if (err) { + return err; + } + +#if __linux__ + if (users->length == 0) { + detectBySystemd(options, users); + } +#endif + + return NULL; +} diff --git a/src/detection/users/users_nosupport.c b/src/detection/users/users_nosupport.c new file mode 100644 index 0000000..0a7e30a --- /dev/null +++ b/src/detection/users/users_nosupport.c @@ -0,0 +1,6 @@ +#include "fastfetch.h" +#include "users.h" + +const char* ffDetectUsers(FFUsersOptions* options, FFlist* users) { + return "Not supported on this platform"; +} diff --git a/src/detection/users/users_obsd.c b/src/detection/users/users_obsd.c new file mode 100644 index 0000000..97a22f0 --- /dev/null +++ b/src/detection/users/users_obsd.c @@ -0,0 +1,39 @@ +#include "fastfetch.h" +#include "users.h" +#include "common/io.h" + +#include <utmp.h> + +const char* ffDetectUsers(FF_A_UNUSED FFUsersOptions* options, FFlist* users) { + FF_AUTO_CLOSE_FILE FILE* fp = fopen(_PATH_UTMP, "r"); + if (!fp) { + return "fopen(_PATH_UTMP, r) failed"; + } + + struct utmp n; +next: + while (fread(&n, sizeof(n), 1, fp) == 1) { + if (!n.ut_name[0]) { + continue; + } + + if (options->myselfOnly && !ffStrbufEqualS(&instance.state.platform.userName, n.ut_name)) { + continue; + } + + FF_LIST_FOR_EACH (FFUserResult, user, *users) { + if (ffStrbufEqualS(&user->name, n.ut_name)) { + goto next; + } + } + + FFUserResult* user = FF_LIST_ADD(FFUserResult, *users); + ffStrbufInitS(&user->name, n.ut_name); + ffStrbufInitS(&user->hostName, n.ut_host); + ffStrbufInitS(&user->sessionName, n.ut_line); + ffStrbufInit(&user->clientIp); + user->loginTime = (uint64_t) n.ut_time * 1000; + } + + return NULL; +} diff --git a/src/detection/users/users_windows.c b/src/detection/users/users_windows.c new file mode 100644 index 0000000..4cde44e --- /dev/null +++ b/src/detection/users/users_windows.c @@ -0,0 +1,61 @@ +#include "users.h" +#include "common/windows/unicode.h" +#include "common/time.h" + +#include <windows.h> +#include <wtsapi32.h> +#include <ws2tcpip.h> + +const char* ffDetectUsers(FFUsersOptions* options, FFlist* users) { + WTS_SESSION_INFO_1W* sessionInfo; + DWORD sessionCount; + DWORD level = 1; + + if (!WTSEnumerateSessionsExW(WTS_CURRENT_SERVER_HANDLE, &level, 0, &sessionInfo, &sessionCount)) { + return "WTSEnumerateSessionsW(WTS_CURRENT_SERVER_HANDLE) failed"; + } + + for (DWORD i = 0; i < sessionCount; i++) { + WTS_SESSION_INFO_1W* session = &sessionInfo[i]; + if (session->State != WTSActive) { + continue; + } + + FF_STRBUF_AUTO_DESTROY userName = ffStrbufCreateWS(session->pUserName); + + if (options->myselfOnly && !ffStrbufEqual(&instance.state.platform.userName, &userName)) { + continue; + } + + FFUserResult* user = FF_LIST_ADD(FFUserResult, *users); + ffStrbufInitMove(&user->name, &userName); + ffStrbufInitWS(&user->hostName, session->pHostName); + ffStrbufInitWS(&user->sessionName, session->pSessionName); + ffStrbufInit(&user->clientIp); + user->loginTime = 0; + + DWORD bytes = 0; + PWTS_CLIENT_ADDRESS address = NULL; + if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, session->SessionId, WTSClientAddress, (LPWSTR*) &address, &bytes)) { + if (address->AddressFamily == AF_INET) { + ffStrbufSetF(&user->clientIp, "%u.%u.%u.%u", address->Address[2], address->Address[3], address->Address[4], address->Address[5]); + } else if (address->AddressFamily == AF_INET6) { + char ipStr[INET6_ADDRSTRLEN]; + const char* end = RtlIpv6AddressToStringA((const IN6_ADDR*) address->Address, ipStr); + ffStrbufSetNS(&user->clientIp, (uint32_t) (end - ipStr), ipStr); + } + WTSFreeMemory(address); + } + + bytes = 0; + PWTSINFOW wtsInfo = NULL; + if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, session->SessionId, WTSSessionInfo, (LPWSTR*) &wtsInfo, &bytes)) { + user->loginTime = ffFileTimeToUnixMs((uint64_t) wtsInfo->LogonTime.QuadPart); + WTSFreeMemory(wtsInfo); + } + } + + WTSFreeMemoryExW(WTSTypeSessionInfoLevel1, sessionInfo, 1); + + return NULL; +} diff --git a/src/detection/version/version.c b/src/detection/version/version.c new file mode 100644 index 0000000..8bd066e --- /dev/null +++ b/src/detection/version/version.c @@ -0,0 +1,110 @@ +#include "version.h" + +#if defined(__x86_64__) + #define FF_ARCHITECTURE "x86_64" +#elif defined(__i386__) + #define FF_ARCHITECTURE "i386" +#elif defined(__ia64__) + #define FF_ARCHITECTURE "ia64" +#elif defined(__aarch64__) + #define FF_ARCHITECTURE "aarch64" +#elif defined(__arm__) + #define FF_ARCHITECTURE "arm" +#elif defined(__mips__) + #define FF_ARCHITECTURE "mips" +#elif defined(__powerpc__) || defined(__powerpc) + #define FF_ARCHITECTURE "powerpc" +#elif defined(__riscv__) || defined(__riscv) + #define FF_ARCHITECTURE "riscv" +#elif defined(__s390x__) + #define FF_ARCHITECTURE "s390x" +#elif defined(__loongarch__) + #define FF_ARCHITECTURE "loongarch" +#elif defined(__sparc__) + #define FF_ARCHITECTURE "sparc" +#elif defined(__alpha__) + #define FF_ARCHITECTURE "alpha" +#elif defined(__hppa__) + #define FF_ARCHITECTURE "hppa" +#elif defined(__sh__) + #define FF_ARCHITECTURE "sh" +#elif defined(__m68k__) + #define FF_ARCHITECTURE "m68k" +#else + #define FF_ARCHITECTURE "Unknown" +#endif + +#if defined(__ANDROID__) + #define FF_SYSNAME "Android" +#elif defined(__linux__) + #define FF_SYSNAME "Linux" +#elif defined(__DragonFly__) // We define `__FreeBSD__` on DragonFly BSD for simplification + #define FF_SYSNAME "DragonFly" +#elif defined(__MidnightBSD__) + #define FF_SYSNAME "MidnightBSD" +#elif defined(__FreeBSD__) + #define FF_SYSNAME "FreeBSD" +#elif defined(__APPLE__) + #define FF_SYSNAME "macOS" +#elif defined(_WIN32) + #define FF_SYSNAME "Windows" +#elif defined(__sun) + #define FF_SYSNAME "SunOS" +#elif defined(__OpenBSD__) + #define FF_SYSNAME "OpenBSD" +#elif defined(__NetBSD__) + #define FF_SYSNAME "NetBSD" +#elif defined(__HAIKU__) + #define FF_SYSNAME "Haiku" +#elif defined(__GNU__) + #define FF_SYSNAME "GNU" +#else + #define FF_SYSNAME "Unknown" +#endif + +#define FF_STR_INDIR(x) #x +#define FF_STR(x) FF_STR_INDIR(x) + +FFVersionResult ffVersionResult = { + .projectName = FASTFETCH_PROJECT_NAME, + .sysName = FF_SYSNAME, + .architecture = FF_ARCHITECTURE, + .version = FASTFETCH_PROJECT_VERSION, + .versionTweak = FASTFETCH_PROJECT_VERSION_TWEAK, + .versionGit = FASTFETCH_PROJECT_VERSION_GIT, + .cmakeBuiltType = FASTFETCH_PROJECT_CMAKE_BUILD_TYPE, + .compileTime = __DATE__ ", " __TIME__, + .compiler = + +#ifdef __clang__ + #ifdef _MSC_VER + "clang-cl "; + #elif defined(__APPLE__) && defined(__apple_build_version__) + "Apple clang " + #else + "clang " + #endif + +FF_STR(__clang_major__) +"." FF_STR(__clang_minor__) "." FF_STR(__clang_patchlevel__) + + #if defined(__APPLE__) && defined(__apple_build_version__) + " (" FF_STR(__apple_build_version__) ")" + #endif + , +#elif defined(__GNUC__) + "gcc " FF_STR(__GNUC__) "." FF_STR(__GNUC_MINOR__) "." FF_STR(__GNUC_PATCHLEVEL__), +#elif defined(_MSC_VER) + "msvc " FF_STR(_MSC_VER), +#else + "unknown", +#endif + + .debugMode = +#ifndef NDEBUG + true, +#else + false, +#endif +} +; diff --git a/src/detection/version/version.h b/src/detection/version/version.h new file mode 100644 index 0000000..1330535 --- /dev/null +++ b/src/detection/version/version.h @@ -0,0 +1,19 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/version/option.h" + +typedef struct FFVersionResult { + const char* projectName; + const char* sysName; + const char* architecture; + const char* version; + const char* versionTweak; + const char* versionGit; + const char* cmakeBuiltType; + const char* compileTime; + const char* compiler; + bool debugMode; +} FFVersionResult; + +extern FFVersionResult ffVersionResult; diff --git a/src/detection/vulkan/vulkan.c b/src/detection/vulkan/vulkan.c new file mode 100644 index 0000000..013c457 --- /dev/null +++ b/src/detection/vulkan/vulkan.c @@ -0,0 +1,332 @@ +#include "fastfetch.h" +#include "common/debug.h" +#include "detection/gpu/gpu.h" +#include "detection/vulkan/vulkan.h" + +#ifdef FF_HAVE_VULKAN + #include "common/library.h" + #include "common/io.h" + #include "common/parsing.h" + #include "common/strutil.h" + + #include <stdlib.h> + #include <vulkan/vulkan.h> + +static inline void applyVulkanVersion(uint32_t vulkanVersion, FFVersion* ffVersion) { + ffVersion->major = VK_VERSION_MAJOR(vulkanVersion); + ffVersion->minor = VK_VERSION_MINOR(vulkanVersion); + ffVersion->patch = VK_VERSION_PATCH(vulkanVersion); +} + +static void applyDriverName(VkPhysicalDeviceDriverPropertiesKHR* properties, FFstrbuf* result) { + if (!ffStrSet(properties->driverName)) { + return; + } + + ffStrbufAppendS(result, properties->driverName); + + /* + * Some drivers (android for example) expose a multiline string as driver info. + * It contains too much info anyways, so we just don't append it. + */ + if (!ffStrSet(properties->driverInfo) || strchr(properties->driverInfo, '\n') != NULL) { + return; + } + + ffStrbufAppendS(result, " ["); + ffStrbufAppendS(result, properties->driverInfo); + ffStrbufAppendC(result, ']'); +} + +static const char* detectVulkan(FFVulkanResult* result) { + FF_DEBUG("Starting Vulkan detection"); + + FF_LIBRARY_LOAD_MESSAGE(vulkan, + #if __APPLE__ + "libMoltenVK" FF_LIBRARY_EXTENSION, + -1 + #elif _WIN32 + "vulkan-1" FF_LIBRARY_EXTENSION, + -1 + #else + "libvulkan" FF_LIBRARY_EXTENSION, + 2 + #endif + ) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(vulkan, vkGetInstanceProcAddr) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(vulkan, vkCreateInstance) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(vulkan, vkDestroyInstance) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(vulkan, vkEnumeratePhysicalDevices) + + // Some drivers (nvdc) print messages to stdout + // and that is the best way I found to disable that + FF_SUPPRESS_IO(); + FF_DEBUG("Suppressed stdout/stderr during Vulkan probing to avoid noisy drivers"); + + FFVersion instanceVersion = FF_VERSION_INIT; + + // We need to get the function pointer this way, because it is only provided by vulkan 1.1 and higher. + // a dlsym would fail on 1.0 implementations + PFN_vkEnumerateInstanceVersion ffvkEnumerateInstanceVersion = (PFN_vkEnumerateInstanceVersion) ffvkGetInstanceProcAddr(NULL, "vkEnumerateInstanceVersion"); + if (ffvkEnumerateInstanceVersion != NULL) { + uint32_t version; + if (ffvkEnumerateInstanceVersion(&version) == VK_SUCCESS) { + applyVulkanVersion(version, &instanceVersion); + FF_DEBUG("Detected Vulkan instance version: %u.%u.%u", instanceVersion.major, instanceVersion.minor, instanceVersion.patch); + } else { + FF_DEBUG("vkEnumerateInstanceVersion() is available but returned a non-success status"); + } + } else { + FF_DEBUG("vkEnumerateInstanceVersion() is unavailable (likely Vulkan 1.0 runtime)"); + } + + const uint32_t projectVersion = VK_MAKE_VERSION( + FASTFETCH_PROJECT_VERSION_MAJOR, + FASTFETCH_PROJECT_VERSION_MINOR, + FASTFETCH_PROJECT_VERSION_PATCH); + + VkInstance vkInstance; + FF_DEBUG("Creating Vulkan instance with requested API version %s", instanceVersion.minor >= 1 ? "1.1" : "1.0"); + VkResult res = ffvkCreateInstance(&(VkInstanceCreateInfo) { + .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .pNext = NULL, + .pApplicationInfo = &(VkApplicationInfo) { + .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, + .pNext = NULL, + .pApplicationName = FASTFETCH_PROJECT_NAME, + .applicationVersion = projectVersion, + .pEngineName = "vulkanPrintGPUs", + .engineVersion = projectVersion, + + // We need to request 1.1 to get physicalDeviceDriverProperties + .apiVersion = instanceVersion.minor >= 1 ? VK_API_VERSION_1_1 : VK_API_VERSION_1_0 }, + .enabledLayerCount = 0, + .ppEnabledLayerNames = NULL, + .enabledExtensionCount = 0, + .ppEnabledExtensionNames = NULL, + .flags = 0 }, + NULL, + &vkInstance); + if (res != VK_SUCCESS) { + FF_DEBUG("ffvkCreateInstance() failed with VkResult=%d", res); + switch (res) { + case VK_ERROR_OUT_OF_HOST_MEMORY: + return "ffvkCreateInstance() failed: VK_ERROR_OUT_OF_HOST_MEMORY"; + case VK_ERROR_OUT_OF_DEVICE_MEMORY: + return "ffvkCreateInstance() failed: VK_ERROR_OUT_OF_DEVICE_MEMORY"; + case VK_ERROR_INITIALIZATION_FAILED: + return "ffvkCreateInstance() failed: VK_ERROR_INITIALIZATION_FAILED"; + case VK_ERROR_LAYER_NOT_PRESENT: + return "ffvkCreateInstance() failed: VK_ERROR_LAYER_NOT_PRESENT"; + case VK_ERROR_EXTENSION_NOT_PRESENT: + return "ffvkCreateInstance() failed: VK_ERROR_EXTENSION_NOT_PRESENT"; + case VK_ERROR_INCOMPATIBLE_DRIVER: + return "ffvkCreateInstance() failed: VK_ERROR_INCOMPATIBLE_DRIVER"; + default: + return "ffvkCreateInstance() failed: unknown error"; + } + } + FF_DEBUG("Vulkan instance created successfully"); + + // if instance creation succeeded, but vkEnumerateInstanceVersion didn't, this means we are running against a vulkan 1.0 implementation + // explicitly set this version, if no device is found, so we still have at least this info + if (instanceVersion.major == 0 && instanceVersion.minor == 0 && instanceVersion.patch == 0) { + instanceVersion.major = 1; + FF_DEBUG("Falling back to Vulkan instance version 1.0 due to unavailable enumerate call"); + } + + VkPhysicalDevice physicalDevices[128]; + uint32_t physicalDeviceCount = (uint32_t) ARRAY_SIZE(physicalDevices); + res = ffvkEnumeratePhysicalDevices(vkInstance, &physicalDeviceCount, physicalDevices); + if (res != VK_SUCCESS) { + FF_DEBUG("ffvkEnumeratePhysicalDevices() failed with VkResult=%d", res); + ffvkDestroyInstance(vkInstance, NULL); + switch (res) { + case VK_ERROR_OUT_OF_HOST_MEMORY: + return "ffvkEnumeratePhysicalDevices() failed: VK_ERROR_OUT_OF_HOST_MEMORY"; + case VK_ERROR_OUT_OF_DEVICE_MEMORY: + return "ffvkEnumeratePhysicalDevices() failed: VK_ERROR_OUT_OF_DEVICE_MEMORY"; + case VK_ERROR_INITIALIZATION_FAILED: + return "ffvkEnumeratePhysicalDevices() failed: VK_ERROR_INITIALIZATION_FAILED"; + case VK_INCOMPLETE: + return "ffvkEnumeratePhysicalDevices() failed: VK_INCOMPLETE"; + default: + return "ffvkEnumeratePhysicalDevices() failed"; + } + } + FF_DEBUG("Enumerated %u Vulkan physical device(s)", physicalDeviceCount); + + PFN_vkGetPhysicalDeviceProperties ffvkGetPhysicalDeviceProperties = NULL; + PFN_vkGetPhysicalDeviceProperties2 ffvkGetPhysicalDeviceProperties2 = (PFN_vkGetPhysicalDeviceProperties2) ffvkGetInstanceProcAddr(vkInstance, "vkGetPhysicalDeviceProperties2"); // 1.1 + if (!ffvkGetPhysicalDeviceProperties2) { + ffvkGetPhysicalDeviceProperties = (PFN_vkGetPhysicalDeviceProperties) ffvkGetInstanceProcAddr(vkInstance, "vkGetPhysicalDeviceProperties"); + } + + FF_DEBUG("Using %s for querying physical device properties", ffvkGetPhysicalDeviceProperties2 ? "vkGetPhysicalDeviceProperties2" : "vkGetPhysicalDeviceProperties"); + + PFN_vkGetPhysicalDeviceMemoryProperties ffvkGetPhysicalDeviceMemoryProperties = (PFN_vkGetPhysicalDeviceMemoryProperties) ffvkGetInstanceProcAddr(vkInstance, "vkGetPhysicalDeviceMemoryProperties"); + if (!ffvkGetPhysicalDeviceMemoryProperties) { + FF_DEBUG("vkGetPhysicalDeviceMemoryProperties is unavailable"); + ffvkDestroyInstance(vkInstance, NULL); + return "vkGetPhysicalDeviceMemoryProperties is not available"; + } + + FFVersion maxDeviceApiVersion = FF_VERSION_INIT; + FFVersion maxDeviceConformanceVersion = FF_VERSION_INIT; + + for (uint32_t i = 0; i < physicalDeviceCount; i++) { + // Get device properties. + // On VK 1.1 and up, we use vkGetPhysicalDeviceProperties2, so we can put VkPhysicalDeviceDriverProperties in the pNext chain. + // This is required to get the driver name and conformance version. + + VkPhysicalDeviceDriverPropertiesKHR driverProperties = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES_KHR, + }; + VkPhysicalDeviceProperties2 physicalDeviceProperties = { + .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, + .pNext = &driverProperties, + }; + + if (ffvkGetPhysicalDeviceProperties2 != NULL) { + ffvkGetPhysicalDeviceProperties2(physicalDevices[i], &physicalDeviceProperties); + } else { + ffvkGetPhysicalDeviceProperties(physicalDevices[i], &physicalDeviceProperties.properties); + } + + FF_DEBUG("Processing Vulkan device #%u: name='%s', vendorId=0x%04X, deviceId=0x%04X, type=%u", i, physicalDeviceProperties.properties.deviceName, physicalDeviceProperties.properties.vendorID, physicalDeviceProperties.properties.deviceID, physicalDeviceProperties.properties.deviceType); + + // We don't want software rasterizers to show up as physical gpu + if (physicalDeviceProperties.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU) { + FF_DEBUG("Skipping CPU Vulkan device '%s'", physicalDeviceProperties.properties.deviceName); + continue; + } + + // If the device api version is higher than the current highest device api version, overwrite it + // In this case, also use the current device driver name as the shown driver name + + FFVersion deviceAPIVersion = FF_VERSION_INIT; + applyVulkanVersion(physicalDeviceProperties.properties.apiVersion, &deviceAPIVersion); + if (ffVersionCompare(&deviceAPIVersion, &maxDeviceApiVersion) > 0) { + maxDeviceApiVersion = deviceAPIVersion; + applyDriverName(&driverProperties, &result->driver); + FF_DEBUG("Updated max Vulkan device API version to %u.%u.%u (driver='%s')", + maxDeviceApiVersion.major, + maxDeviceApiVersion.minor, + maxDeviceApiVersion.patch, + result->driver.chars); + } + + // If the device conformance version is higher than the current highest device conformance version, overwrite it + if (ffvkGetPhysicalDeviceProperties2) { + FFVersion deviceConformanceVersion = { + .major = driverProperties.conformanceVersion.major, + .minor = driverProperties.conformanceVersion.minor, + .patch = driverProperties.conformanceVersion.patch, + }; + + if (ffVersionCompare(&deviceConformanceVersion, &maxDeviceConformanceVersion) > 0) { + maxDeviceConformanceVersion = deviceConformanceVersion; + FF_DEBUG("Updated max Vulkan conformance version to %u.%u.%u", + maxDeviceConformanceVersion.major, + maxDeviceConformanceVersion.minor, + maxDeviceConformanceVersion.patch); + } + } + + // Add the device to the list of devices shown by the GPU module + + // #456 + FF_LIST_FOR_EACH (FFGPUResult, gpu, result->gpus) { + if (gpu->deviceId == physicalDeviceProperties.properties.deviceID) { + FF_DEBUG("Skipping duplicate Vulkan GPU entry for deviceId=0x%04X", physicalDeviceProperties.properties.deviceID); + goto next; + } + } + + FFGPUResult* gpu = FF_LIST_ADD(FFGPUResult, result->gpus); + + ffStrbufInitF(&gpu->platformApi, "Vulkan %u.%u.%u", deviceAPIVersion.major, deviceAPIVersion.minor, deviceAPIVersion.patch); + gpu->deviceId = physicalDeviceProperties.properties.deviceID; + + ffStrbufInitS(&gpu->name, physicalDeviceProperties.properties.deviceName); + + gpu->type = physicalDeviceProperties.properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU ? FF_GPU_TYPE_DISCRETE : FF_GPU_TYPE_INTEGRATED; + ffStrbufInitS(&gpu->vendor, ffGPUGetVendorString(physicalDeviceProperties.properties.vendorID)); + ffStrbufInitS(&gpu->driver, driverProperties.driverInfo); + ffStrbufInit(&gpu->memoryType); + FF_DEBUG("Added Vulkan GPU '%s' (vendor='%s', type=%s)", + gpu->name.chars, + gpu->vendor.chars, + gpu->type == FF_GPU_TYPE_DISCRETE ? "discrete" : "integrated"); + + VkPhysicalDeviceMemoryProperties memoryProperties = {}; + ffvkGetPhysicalDeviceMemoryProperties(physicalDevices[i], &memoryProperties); + + gpu->dedicated.total = gpu->shared.total = 0; + gpu->dedicated.used = gpu->shared.used = FF_GPU_VMEM_SIZE_UNSET; + for (uint32_t index = 0; index < memoryProperties.memoryHeapCount; ++index) { + const VkMemoryHeap* heap = &memoryProperties.memoryHeaps[index]; + FFGPUMemory* vmem = gpu->type == FF_GPU_TYPE_DISCRETE && (heap->flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) ? &gpu->dedicated : &gpu->shared; + vmem->total += heap->size; + } + FF_DEBUG("Computed memory for '%s': dedicatedTotal=%llu, sharedTotal=%llu", + gpu->name.chars, + (unsigned long long) gpu->dedicated.total, + (unsigned long long) gpu->shared.total); + + // No way to detect those using vulkan + gpu->index = FF_GPU_INDEX_UNSET; + gpu->coreCount = FF_GPU_CORE_COUNT_UNSET; + gpu->temperature = FF_GPU_TEMP_UNSET; + gpu->frequency = FF_GPU_FREQUENCY_UNSET; + gpu->coreUsage = FF_GPU_CORE_USAGE_UNSET; + + next: + continue; + } + + ffVersionToPretty(&instanceVersion, &result->instanceVersion); + ffVersionToPretty(&maxDeviceApiVersion, &result->apiVersion); + ffVersionToPretty(&maxDeviceConformanceVersion, &result->conformanceVersion); + + FF_DEBUG("Vulkan detection finished: instanceVersion=%s, apiVersion=%s, conformanceVersion=%s, gpuCount=%u", + result->instanceVersion.chars, + result->apiVersion.chars, + result->conformanceVersion.chars, + result->gpus.length); + + ffvkDestroyInstance(vkInstance, NULL); + FF_DEBUG("Destroyed Vulkan instance"); + return NULL; +} + +#endif + +FFVulkanResult* ffDetectVulkan(void) { + static FFVulkanResult result; + static bool initialized; + + if (!initialized) { + FF_DEBUG("Initializing Vulkan detection cache"); + initialized = true; + ffStrbufInit(&result.driver); + ffStrbufInit(&result.apiVersion); + ffStrbufInit(&result.conformanceVersion); + ffStrbufInit(&result.instanceVersion); + ffListInit(&result.gpus); + +#ifdef FF_HAVE_VULKAN + result.error = detectVulkan(&result); + if (result.error) { + FF_DEBUG("Vulkan detection returned error: %s", result.error); + } +#else + result.error = "fastfetch was compiled without vulkan support"; + FF_DEBUG("Vulkan support is disabled at compile time"); +#endif + } else { + FF_DEBUG("Reusing cached Vulkan detection result"); + } + + return &result; +} diff --git a/src/detection/vulkan/vulkan.h b/src/detection/vulkan/vulkan.h new file mode 100644 index 0000000..6a49722 --- /dev/null +++ b/src/detection/vulkan/vulkan.h @@ -0,0 +1,15 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/vulkan/option.h" + +typedef struct FFVulkanResult { + FFstrbuf driver; + FFstrbuf apiVersion; + FFstrbuf conformanceVersion; + FFstrbuf instanceVersion; + FFlist gpus; // List of FFGPUResult, see detection/gpu/gpu.h + const char* error; +} FFVulkanResult; + +FFVulkanResult* ffDetectVulkan(); diff --git a/src/detection/wallpaper/wallpaper.h b/src/detection/wallpaper/wallpaper.h new file mode 100644 index 0000000..96fead9 --- /dev/null +++ b/src/detection/wallpaper/wallpaper.h @@ -0,0 +1,5 @@ +#pragma once + +#include "fastfetch.h" + +const char* ffDetectWallpaper(FFstrbuf* result); diff --git a/src/detection/wallpaper/wallpaper_apple.m b/src/detection/wallpaper/wallpaper_apple.m new file mode 100644 index 0000000..800aee9 --- /dev/null +++ b/src/detection/wallpaper/wallpaper_apple.m @@ -0,0 +1,72 @@ +#include "wallpaper.h" +#include "common/settings.h" +#include "common/apple/osascript.h" + +#import <Foundation/Foundation.h> + +const char* ffDetectWallpaper(FFstrbuf* result) +{ + { + // For Sonoma + // https://github.com/JohnCoates/Aerial/issues/1332 + NSError* error; + NSString* fileName = [NSString stringWithFormat:@"file://%s/Library/Application Support/com.apple.wallpaper/Store/Index.plist", instance.state.platform.homeDir.chars]; + NSDictionary* dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL URLWithString:fileName] + error:&error]; + if (!error) + { + NSArray* choices = [dict valueForKeyPath:@"SystemDefault.Desktop.Content.Choices"]; + if (choices.count > 0) + { + NSDictionary* choice = choices[0]; + NSArray* files = choice[@"Files"]; + if (files.count > 0) + { + NSString* file = files[0][@"relative"]; + ffStrbufAppendS(result, [NSURL URLWithString:file].path.UTF8String); + } + else + { + NSString* provider = choice[@"Provider"]; + NSString* builtinPrefix = @"com.apple.wallpaper.choice."; + if ([provider hasPrefix:builtinPrefix]) + provider = [provider substringFromIndex:builtinPrefix.length]; + if ([provider isEqualToString:@"sonoma"]) + ffStrbufSetStatic(result, "macOS Sonoma"); + else if ([provider isEqualToString:@"aerials"]) // Most builtin aerial wallpapers are private + ffStrbufSetStatic(result, "Built-in aerial photography"); + else + ffStrbufAppendF(result, "Built-in %s wallpaper", provider.UTF8String); + } + } + if (result->length > 0) + return NULL; + } + } + + #ifdef FF_HAVE_SQLITE3 + + { + // For Ventura + // https://stackoverflow.com/questions/301215/getting-desktop-background-on-mac + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateCopy(&instance.state.platform.homeDir); + ffStrbufAppendS(&path, "Library/Application Support/Dock/desktoppicture.db"); + if (ffSettingsGetSQLite3String(path.chars, + "SELECT value\n" + "FROM preferences\n" + "JOIN data ON preferences.data_id=data.ROWID\n" + "JOIN pictures ON preferences.picture_id=pictures.ROWID\n" + "JOIN displays ON pictures.display_id=displays.ROWID\n" + "JOIN spaces ON pictures.space_id=spaces.ROWID\n" + "WHERE display_id=1 AND space_id=1 AND key=1", result) + ) + return NULL; + } + + #endif + + if (ffOsascript("tell application \"Finder\" to get POSIX path of (get desktop picture as alias)", result)) + return NULL; + + return "All detection methods failed"; +} diff --git a/src/detection/wallpaper/wallpaper_haiku.cpp b/src/detection/wallpaper/wallpaper_haiku.cpp new file mode 100644 index 0000000..0bfad62 --- /dev/null +++ b/src/detection/wallpaper/wallpaper_haiku.cpp @@ -0,0 +1,54 @@ +extern "C" { +#include "wallpaper.h" +#include "common/mallocHelper.h" +} + +#include <Application.h> +#include <FindDirectory.h> +#include <InterfaceDefs.h> +#include <Node.h> +#include <Path.h> +#include <Screen.h> +#include <fs_attr.h> +#include <be_apps/Tracker/Background.h> + +const char* ffDetectWallpaper(FFstrbuf* result) { + BMessage backgrounds; + BPath pDesktop; + BString path; + + if (find_directory(B_DESKTOP_DIRECTORY, &pDesktop) < B_OK) { + return "find_directory(B_DESKTOP_DIRECTORY) failed"; + } + + // We need a valid be_app to query the app_server here. + BApplication app("application/x-vnd.fastfetch-cli-fastfetch"); + + BNode nDesktop(pDesktop.Path()); + if (nDesktop.InitCheck() == B_OK) { + struct attr_info ai; + if (nDesktop.GetAttrInfo(B_BACKGROUND_INFO, &ai) == B_OK && ai.size > 0) { + FF_AUTO_FREE char* pAttr = (char*) malloc(ai.size); + if (nDesktop.ReadAttr(B_BACKGROUND_INFO, ai.type, 0LL, pAttr, (size_t) ai.size) >= B_OK) { + if (backgrounds.Unflatten(pAttr) == B_OK) { + for (int i = 0; backgrounds.FindString(B_BACKGROUND_IMAGE, i, &path) == B_OK; i++) { + int32 ws; + if (backgrounds.FindInt32(B_BACKGROUND_WORKSPACES, i, &ws) == B_OK) { + if (ws & (1 << current_workspace())) { + // We try to match the one for the current workspace + break; + } + } + } + } + } + } + } + + if (path.Length() < 1) { + return "Failed to detect the current wallpaper path"; + } + + ffStrbufAppendS(result, path.String()); + return NULL; +} diff --git a/src/detection/wallpaper/wallpaper_linux.c b/src/detection/wallpaper/wallpaper_linux.c new file mode 100644 index 0000000..f5ad4ec --- /dev/null +++ b/src/detection/wallpaper/wallpaper_linux.c @@ -0,0 +1,102 @@ +#include "wallpaper.h" +#include "common/io.h" +#include "common/settings.h" +#include "detection/displayserver/displayserver.h" +#include "detection/gtk_qt/gtk_qt.h" + +static const char* detectCosmicComp(FFstrbuf* result) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateCopy(FF_LIST_FIRST(FFstrbuf, instance.state.platform.configDirs)); + ffStrbufAppendS(&path, "cosmic/com.system76.CosmicBackground/v1/"); + uint32_t basePathLength = path.length; + + FF_STRBUF_AUTO_DESTROY backgrounds = ffStrbufCreate(); + ffStrbufAppendS(&path, "backgrounds"); + bool sharedWallpaper = true; + FF_STRBUF_AUTO_DESTROY monitorName = ffStrbufCreate(); + if (ffReadFileBuffer(path.chars, &backgrounds)) { + FF_STRBUF_AUTO_DESTROY cleaned = ffStrbufCreateCopy(&backgrounds); + ffStrbufRemoveStrings(&cleaned, 4, (const char*[]){ " ", "\n", "\r", "\t" }); + + if (!ffStrbufEqualS(&cleaned, "[]")) { + const char* firstQuote = strchr(backgrounds.chars, '"'); + if (firstQuote != NULL) { + const char* secondQuote = strchr(firstQuote + 1, '"'); + if (secondQuote != NULL && secondQuote > firstQuote + 1) { + ffStrbufAppendNS(&monitorName, (uint32_t) (secondQuote - firstQuote - 1), firstQuote + 1); + sharedWallpaper = false; + } + } + } + } + ffStrbufSubstrBefore(&path, basePathLength); + + if (sharedWallpaper) { + ffStrbufAppendS(&path, "all"); + } else { + ffStrbufAppendS(&path, "output."); + ffStrbufAppend(&path, &monitorName); + } + + FF_STRBUF_AUTO_DESTROY output = ffStrbufCreate(); + if (!ffReadFileBuffer(path.chars, &output)) { + return "Failed to read COSMIC wallpaper config"; + } + + const char* sourceStart = strstr(output.chars, "source:"); + if (sourceStart == NULL) { + return "COSMIC wallpaper config doesn't contain source"; + } + + const char* pathStart = strstr(sourceStart, "Path("); + if (pathStart == NULL) { + return "COSMIC wallpaper source is not a Path value"; + } + + pathStart += strlen("Path("); + while (*pathStart == ' ' || *pathStart == '\t') { + ++pathStart; + } + + if (*pathStart != '\'' && *pathStart != '"') { + return "COSMIC wallpaper Path format is invalid"; + } + + char quote = *pathStart; + ++pathStart; + const char* pathEnd = strchr(pathStart, quote); + if (pathEnd == NULL || pathEnd == pathStart) { + return "COSMIC wallpaper path is empty"; + } + + ffStrbufAppendNS(result, (uint32_t) (pathEnd - pathStart), pathStart); + return NULL; +} + +const char* ffDetectWallpaper(FFstrbuf* result) { + const FFDisplayServerResult* wm = ffConnectDisplayServer(); + if (ffStrbufIgnCaseEqualS(&wm->wmPrettyName, FF_WM_PRETTY_COSMIC_COMP)) { + return detectCosmicComp(result); + } + + const FFstrbuf* wallpaper = NULL; + const FFGTKResult* gtk = ffDetectGTK4(); + if (gtk->wallpaper.length) { + wallpaper = >k->wallpaper; + } else { + const FFQtResult* qt = ffDetectQt(); + if (qt->wallpaper.length) { + wallpaper = &qt->wallpaper; + } + } + + if (!wallpaper) { + return "Failed to detect the current wallpaper path"; + } + + if (ffStrbufStartsWithS(wallpaper, "file:///")) { + ffStrbufAppendS(result, wallpaper->chars + strlen("file://")); + } else { + ffStrbufAppend(result, wallpaper); + } + return NULL; +} diff --git a/src/detection/wallpaper/wallpaper_nosupport.c b/src/detection/wallpaper/wallpaper_nosupport.c new file mode 100644 index 0000000..9be8e91 --- /dev/null +++ b/src/detection/wallpaper/wallpaper_nosupport.c @@ -0,0 +1,5 @@ +#include "wallpaper.h" + +const char* ffDetectWallpaper(FF_A_UNUSED FFstrbuf* result) { + return "Not supported on this platform"; +} diff --git a/src/detection/wallpaper/wallpaper_windows.c b/src/detection/wallpaper/wallpaper_windows.c new file mode 100644 index 0000000..2adf083 --- /dev/null +++ b/src/detection/wallpaper/wallpaper_windows.c @@ -0,0 +1,15 @@ +#include "wallpaper.h" +#include "common/windows/registry.h" + +const char* ffDetectWallpaper(FFstrbuf* result) { + FF_AUTO_CLOSE_FD HANDLE hKey = NULL; + if (!ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Control Panel\\Desktop", &hKey, NULL)) { + return "ffRegOpenKeyForRead(Control Panel\\Desktop) failed"; + } + + if (!ffRegReadStrbuf(hKey, L"WallPaper", result, NULL)) { + return "ffRegReadStrbuf(WallPaper) failed"; + } + + return NULL; +} diff --git a/src/detection/weather/weather.c b/src/detection/weather/weather.c new file mode 100644 index 0000000..69df642 --- /dev/null +++ b/src/detection/weather/weather.c @@ -0,0 +1,62 @@ +#include "weather.h" +#include "common/networking.h" + +#define FF_UNITIALIZED ((const char*) (uintptr_t) -1) +static FFNetworkingState state; +static const char* status = FF_UNITIALIZED; + +void ffPrepareWeather(FFWeatherOptions* options) { + if (status != FF_UNITIALIZED) { + fputs("Error: Weather module can only be used once due to internal limitations\n", stderr); + exit(1); + } + + state.timeout = options->timeout; + + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateS("/"); + if (options->location.length) { + ffStrbufAppend(&path, &options->location); + } + ffStrbufAppendS(&path, "?format="); + ffStrbufAppend(&path, &options->outputFormat); + switch (instance.config.display.tempUnit) { + case FF_TEMPERATURE_UNIT_CELSIUS: + ffStrbufAppendS(&path, "&m"); + break; + case FF_TEMPERATURE_UNIT_FAHRENHEIT: + ffStrbufAppendS(&path, "&u"); + break; + default: + break; + } + status = ffNetworkingSendHttpRequest(&state, "wttr.in", path.chars, "User-Agent: curl/0.0.0\r\n"); +} + +const char* ffDetectWeather(FFWeatherOptions* options, FFstrbuf* result) { + if (status == FF_UNITIALIZED) { + ffPrepareWeather(options); + } + + if (status != NULL) { + return status; + } + + ffStrbufEnsureFree(result, 4095); + const char* error = ffNetworkingRecvHttpResponse(&state, result); + + state = (FFNetworkingState) {}; + status = FF_UNITIALIZED; + + if (error == NULL) { + ffStrbufSubstrAfterFirstS(result, "\r\n\r\n"); + ffStrbufTrimRightSpace(result); + } else { + return error; + } + + if (result->length == 0) { + return "Empty server response received"; + } + + return NULL; +} diff --git a/src/detection/weather/weather.h b/src/detection/weather/weather.h new file mode 100644 index 0000000..bd3d1bc --- /dev/null +++ b/src/detection/weather/weather.h @@ -0,0 +1,7 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/weather/option.h" + +void ffPrepareWeather(FFWeatherOptions* options); +const char* ffDetectWeather(FFWeatherOptions* options, FFstrbuf* result); diff --git a/src/detection/wifi/wifi.h b/src/detection/wifi/wifi.h new file mode 100644 index 0000000..e457295 --- /dev/null +++ b/src/detection/wifi/wifi.h @@ -0,0 +1,62 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/wifi/option.h" + +struct FFWifiInterface { + FFstrbuf description; + FFstrbuf status; +}; + +struct FFWifiConnection { + FFstrbuf status; + FFstrbuf ssid; + FFstrbuf bssid; + FFstrbuf protocol; + FFstrbuf security; + double signalQuality; // Percentage + double rxRate; + double txRate; + uint16_t channel; + uint16_t frequency; // MHz +}; + +typedef struct FFWifiResult { + struct FFWifiInterface inf; + struct FFWifiConnection conn; +} FFWifiResult; + +const char* ffDetectWifi(FFlist* result /*list of FFWifiItem*/); + +static inline uint16_t ffWifiFreqToChannel(uint16_t frequency) { + // Return 0 for unknown / non-standard frequencies. + if (frequency == 2484) { + return 14; + } + + // 2.4 GHz channels 1-13 + if (frequency >= 2412 && frequency <= 2472 && ((frequency - 2407) % 5) == 0) { + return (uint16_t) ((frequency - 2407) / 5); + } + + // 4.9 GHz public safety band (e.g. channels 182-196) + if (frequency >= 4910 && frequency <= 4980 && ((frequency - 4000) % 5) == 0) { + return (uint16_t) ((frequency - 4000) / 5); + } + + // 5 GHz channels + if (frequency >= 5000 && frequency <= 5895 && ((frequency - 5000) % 5) == 0) { + return (uint16_t) ((frequency - 5000) / 5); + } + + // 6 GHz channels (Wi-Fi 6E/7) + // 5935 MHz is a special case mapped to channel 2. + if (frequency == 5935) { + return 2; + } + if (frequency >= 5955 && frequency <= 7115 && ((frequency - 5950) % 5) == 0) { + return (uint16_t) ((frequency - 5950) / 5); + } + + return 0; +} diff --git a/src/detection/wifi/wifi_android.c b/src/detection/wifi/wifi_android.c new file mode 100644 index 0000000..c4537da --- /dev/null +++ b/src/detection/wifi/wifi_android.c @@ -0,0 +1,69 @@ +#include "wifi.h" + +#include "common/processing.h" +#include "common/properties.h" + +#define FF_TERMUX_API_PATH FASTFETCH_TARGET_DIR_ROOT "/libexec/termux-api" +#define FF_TERMUX_API_PARAM "WifiConnectionInfo" + +static inline void wrapYyjsonFree(yyjson_doc** doc) { + assert(doc); + if (*doc) { + yyjson_doc_free(*doc); + } +} + +const char* ffDetectWifi(FFlist* result) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + if (ffProcessAppendStdOut(&buffer, (char* const[]) { FF_TERMUX_API_PATH, FF_TERMUX_API_PARAM, NULL })) { + return "Starting `" FF_TERMUX_API_PATH " " FF_TERMUX_API_PARAM "` failed"; + } + + yyjson_doc* FF_A_CLEANUP(wrapYyjsonFree) doc = yyjson_read_opts(buffer.chars, buffer.length, 0, NULL, NULL); + if (!doc) { + return "Failed to parse wifi connection info"; + } + + yyjson_val* root = yyjson_doc_get_root(doc); + if (!yyjson_is_obj(root)) { + return "Wifi info result is not a JSON object"; + } + + FFWifiResult* item = FF_LIST_ADD(FFWifiResult, *result); + ffStrbufInit(&item->inf.description); + ffStrbufInit(&item->inf.status); + ffStrbufInit(&item->conn.status); + ffStrbufInit(&item->conn.ssid); + ffStrbufInit(&item->conn.bssid); + ffStrbufInit(&item->conn.protocol); + ffStrbufInit(&item->conn.security); + item->conn.signalQuality = -DBL_MAX; + item->conn.rxRate = -DBL_MAX; + item->conn.txRate = -DBL_MAX; + item->conn.channel = 0; + item->conn.frequency = 0; + + ffStrbufAppendJsonVal(&item->inf.status, yyjson_obj_get(root, "supplicant_state")); + if (!item->inf.status.length) { + ffStrbufAppendS(&item->inf.status, "Unknown"); + return NULL; + } + + if (!ffStrbufEqualS(&item->inf.status, "COMPLETED")) { + return NULL; + } + + double rssi = yyjson_get_num(yyjson_obj_get(root, "rssi")); + item->conn.signalQuality = rssi >= -50 ? 100 : rssi <= -100 ? 0 + : (rssi + 100) * 2; + + ffStrbufAppendJsonVal(&item->inf.description, yyjson_obj_get(root, "ip")); + ffStrbufAppendJsonVal(&item->conn.bssid, yyjson_obj_get(root, "bssid")); + ffStrbufAppendJsonVal(&item->conn.ssid, yyjson_obj_get(root, "ssid")); + item->conn.frequency = (uint16_t) yyjson_get_int(yyjson_obj_get(root, "frequency_mhz")); + item->conn.txRate = yyjson_get_num(yyjson_obj_get(root, "link_speed_mbps")); + item->conn.channel = ffWifiFreqToChannel(item->conn.frequency); + + return NULL; +} diff --git a/src/detection/wifi/wifi_apple.m b/src/detection/wifi/wifi_apple.m new file mode 100644 index 0000000..21808e0 --- /dev/null +++ b/src/detection/wifi/wifi_apple.m @@ -0,0 +1,165 @@ +#include "wifi.h" +#include "common/processing.h" +#include "common/strutil.h" + +#import <CoreWLAN/CoreWLAN.h> + +static inline double rssiToSignalQuality(int rssi) +{ + return (double) (rssi >= -50 ? 100 : rssi <= -100 ? 0 : (rssi + 100) * 2); +} + +@interface CWNetworkProfile() +@property(readonly, retain, nullable) NSArray<NSDictionary *> *bssidList; +@end + +const char* ffDetectWifi(FFlist* result) +{ + NSArray<CWInterface*>* interfaces = CWWiFiClient.sharedWiFiClient.interfaces; + if (!interfaces) + return "CWWiFiClient.sharedWiFiClient.interfaces is nil"; + + for (CWInterface* inf in interfaces) + { + FFWifiResult* item = FF_LIST_ADD(FFWifiResult, *result); + ffStrbufInit(&item->inf.description); + ffStrbufInit(&item->inf.status); + ffStrbufInit(&item->conn.status); + ffStrbufInit(&item->conn.ssid); + ffStrbufInit(&item->conn.bssid); + ffStrbufInit(&item->conn.protocol); + ffStrbufInit(&item->conn.security); + item->conn.signalQuality = -DBL_MAX; + item->conn.rxRate = -DBL_MAX; + item->conn.txRate = -DBL_MAX; + item->conn.channel = 0; + item->conn.frequency = 0; + + ffStrbufAppendS(&item->inf.description, inf.interfaceName.UTF8String); + ffStrbufSetStatic(&item->inf.status, inf.powerOn ? "Power On" : "Power Off"); + if(!inf.powerOn) + continue; + + ffStrbufSetStatic(&item->conn.status, inf.interfaceMode != kCWInterfaceModeNone ? "Active" : "Inactive"); + if(inf.interfaceMode == kCWInterfaceModeNone) + continue; + + FF_STRBUF_AUTO_DESTROY ipconfig = ffStrbufCreate(); + + CWNetworkProfile* networkProfile = inf.configuration.networkProfiles.firstObject; + + if (inf.ssid) // https://developer.apple.com/forums/thread/732431 + ffStrbufAppendS(&item->conn.ssid, inf.ssid.UTF8String); + else if (networkProfile.ssid) + ffStrbufSetStatic(&item->conn.ssid, inf.configuration.networkProfiles.firstObject.ssid.UTF8String); + else + ffStrbufSetStatic(&item->conn.ssid, "<redacted>"); // https://developer.apple.com/forums/thread/732431 + + if (inf.bssid) + ffStrbufAppendS(&item->conn.bssid, inf.bssid.UTF8String); + else if (networkProfile.bssidList) + ffStrbufSetStatic(&item->conn.bssid, [networkProfile.bssidList.firstObject[@"BSSID"] UTF8String]); + else + ffStrbufSetStatic(&item->conn.bssid, "<redacted>"); + + switch(inf.activePHYMode) + { + case kCWPHYModeNone: + ffStrbufSetStatic(&item->conn.protocol, "none"); + break; + case kCWPHYMode11a: + ffStrbufSetStatic(&item->conn.protocol, "802.11a"); + break; + case kCWPHYMode11b: + ffStrbufSetStatic(&item->conn.protocol, "802.11b"); + break; + case kCWPHYMode11g: + ffStrbufSetStatic(&item->conn.protocol, "802.11g"); + break; + case kCWPHYMode11n: + ffStrbufSetStatic(&item->conn.protocol, "802.11n (Wi-Fi 4)"); + break; + case kCWPHYMode11ac: + ffStrbufSetStatic(&item->conn.protocol, "802.11ac (Wi-Fi 5)"); + break; + case 6 /*kCWPHYMode11ax*/: + ffStrbufSetStatic(&item->conn.protocol, "802.11ax (Wi-Fi 6)"); + break; + case 7 /*kCWPHYMode11be?*/: + ffStrbufSetStatic(&item->conn.protocol, "802.11be (Wi-Fi 7)"); + break; + default: + if (inf.activePHYMode < 8) + ffStrbufAppendF(&item->conn.protocol, "Unknown (%ld)", inf.activePHYMode); + break; + } + item->conn.signalQuality = rssiToSignalQuality((int) inf.rssiValue); + item->conn.txRate = inf.transmitRate; + + switch(inf.security) + { + case kCWSecurityNone: + ffStrbufSetStatic(&item->conn.security, "Insecure"); + break; + case kCWSecurityWEP: + ffStrbufSetStatic(&item->conn.security, "WEP"); + break; + case kCWSecurityWPAPersonal: + ffStrbufSetStatic(&item->conn.security, "WPA Personal"); + break; + case kCWSecurityWPAPersonalMixed: + ffStrbufSetStatic(&item->conn.security, "WPA Personal Mixed"); + break; + case kCWSecurityWPA2Personal: + ffStrbufSetStatic(&item->conn.security, "WPA2 Personal"); + break; + case kCWSecurityPersonal: + ffStrbufSetStatic(&item->conn.security, "Personal"); + break; + case kCWSecurityDynamicWEP: + ffStrbufSetStatic(&item->conn.security, "Dynamic WEP"); + break; + case kCWSecurityWPAEnterprise: + ffStrbufSetStatic(&item->conn.security, "WPA Enterprise"); + break; + case kCWSecurityWPAEnterpriseMixed: + ffStrbufSetStatic(&item->conn.security, "WPA Enterprise Mixed"); + break; + case kCWSecurityWPA2Enterprise: + ffStrbufSetStatic(&item->conn.security, "WPA2 Enterprise"); + break; + case kCWSecurityEnterprise: + ffStrbufSetStatic(&item->conn.security, "Enterprise"); + break; + case 11 /*kCWSecurityWPA3Personal*/: + ffStrbufSetStatic(&item->conn.security, "WPA3 Personal"); + break; + case 12 /*kCWSecurityWPA3Enterprise*/: + ffStrbufSetStatic(&item->conn.security, "WPA3 Enterprise"); + break; + case 13 /*kCWSecurityWPA3Transition*/: + ffStrbufSetStatic(&item->conn.security, "WPA3 Transition"); + break; + case 14 /*kCWSecurityOWE*/: + ffStrbufSetStatic(&item->conn.security, "OWE"); + break; + case 15 /*kCWSecurityOWETransition*/: + ffStrbufSetStatic(&item->conn.security, "OWE Transition"); + break; + default: + ffStrbufAppendF(&item->conn.security, "Unknown (%ld)", inf.security); + break; + } + + item->conn.channel = (uint16_t) inf.wlanChannel.channelNumber; + switch (inf.wlanChannel.channelBand) + { + case kCWChannelBand2GHz: item->conn.frequency = 2400; break; + case kCWChannelBand5GHz: item->conn.frequency = 5400; break; + case 3 /*kCWChannelBand6GHz*/: item->conn.frequency = 6400; break; + default: item->conn.frequency = 0; break; + } + } + + return NULL; +} diff --git a/src/detection/wifi/wifi_bsd.c b/src/detection/wifi/wifi_bsd.c new file mode 100644 index 0000000..fab1293 --- /dev/null +++ b/src/detection/wifi/wifi_bsd.c @@ -0,0 +1,153 @@ +#include "wifi.h" +#include "common/io.h" +#include "common/strutil.h" + +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <net/if.h> +#include <net/if_media.h> +#include <net80211/ieee80211_ioctl.h> + +const char* ffDetectWifi(FFlist* result) { + struct if_nameindex* infs = if_nameindex(); + if (!infs) { + return "if_nameindex() failed"; + } + + FF_AUTO_CLOSE_FD int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) { + if_freenameindex(infs); + return "socket() failed"; + } + + for (struct if_nameindex* i = infs; !(i->if_index == 0 && i->if_name == NULL); ++i) { + if (!ffStrStartsWith(i->if_name, "wlan")) { + continue; + } + + FFWifiResult* item = FF_LIST_ADD(FFWifiResult, *result); + ffStrbufInitS(&item->inf.description, i->if_name); + ffStrbufInit(&item->inf.status); + ffStrbufInit(&item->conn.status); + ffStrbufInit(&item->conn.ssid); + ffStrbufInit(&item->conn.bssid); + ffStrbufInit(&item->conn.protocol); + ffStrbufInit(&item->conn.security); + item->conn.signalQuality = -DBL_MAX; + item->conn.rxRate = -DBL_MAX; + item->conn.txRate = -DBL_MAX; + item->conn.channel = 0; + item->conn.frequency = 0; + + char ssid[IEEE80211_NWID_LEN + 1] = {}; + struct ieee80211req ireq = {}; + strlcpy(ireq.i_name, i->if_name, sizeof(ireq.i_name)); + ireq.i_type = IEEE80211_IOC_SSID; + ireq.i_data = ssid; + ireq.i_len = sizeof(ssid) - 1; + + if (ioctl(sock, SIOCG80211, &ireq) < 0 || ireq.i_len == 0) { + struct ifreq ifr; + strlcpy(ifr.ifr_name, i->if_name, sizeof(ifr.ifr_name)); + if (ioctl(sock, SIOCGIFFLAGS, &ifr) < 0) { + ffStrbufSetStatic(&item->inf.status, "Unknown"); + } else { + ffStrbufSetStatic(&item->inf.status, ifr.ifr_flags & IFF_UP ? "Up" : "Down"); + } + ffStrbufAppendS(&item->conn.status, "Not associated"); + continue; + } + + ffStrbufSetStatic(&item->inf.status, "Up"); + ffStrbufSetStatic(&item->conn.status, "Associated"); + ffStrbufAppendNS(&item->conn.ssid, ireq.i_len, ssid); + + uint8_t bssid[IEEE80211_ADDR_LEN] = {}; + ireq.i_type = IEEE80211_IOC_BSSID; + ireq.i_data = bssid; + ireq.i_len = sizeof(bssid); + + if (ioctl(sock, SIOCG80211, &ireq) >= 0) { + ffStrbufSetF(&item->conn.bssid, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); + } + + struct ieee80211_channel curchan = {}; + ireq.i_type = IEEE80211_IOC_CURCHAN; + ireq.i_data = &curchan; + ireq.i_len = sizeof(curchan); + + if (ioctl(sock, SIOCG80211, &ireq) >= 0) { + item->conn.channel = curchan.ic_ieee; + item->conn.frequency = curchan.ic_freq; + +#ifdef IEEE80211_IS_CHAN_HE // for future use + if (IEEE80211_IS_CHAN_HE(&curchan)) { + ffStrbufSetStatic(&item->conn.protocol, "802.11ax (Wi-Fi 6)"); + } else +#endif + if (IEEE80211_IS_CHAN_VHT(&curchan)) + ffStrbufSetStatic(&item->conn.protocol, "802.11ac (Wi-Fi 5)"); + else if (IEEE80211_IS_CHAN_HT(&curchan)) { + ffStrbufSetStatic(&item->conn.protocol, "802.11n (Wi-Fi 4)"); + } else if (IEEE80211_IS_CHAN_ANYG(&curchan)) { + ffStrbufSetStatic(&item->conn.protocol, "802.11g"); + } else if (IEEE80211_IS_CHAN_B(&curchan)) { + ffStrbufSetStatic(&item->conn.protocol, "802.11b"); + } else if (IEEE80211_IS_CHAN_A(&curchan)) { + ffStrbufSetStatic(&item->conn.protocol, "802.11a"); + } else if (IEEE80211_IS_CHAN_FHSS(&curchan)) { + ffStrbufSetStatic(&item->conn.protocol, "802.11 (FHSS)"); + } + } + + union { + struct ieee80211req_sta_req req; + uint8_t buf[1024]; + } stareq = {}; + memcpy(stareq.req.is_u.macaddr, bssid, sizeof(bssid)); + ireq.i_type = IEEE80211_IOC_STA_INFO; + ireq.i_data = &stareq; + ireq.i_len = sizeof(stareq); + + if (ioctl(sock, SIOCG80211, &ireq) >= 0) { + struct ieee80211req_sta_info* sta = stareq.req.info; + if (sta->isi_len != 0) { + item->conn.signalQuality = (sta->isi_rssi >= -50 ? 100 : sta->isi_rssi <= -100 ? 0 + : (sta->isi_rssi + 100) * 2); + item->conn.rxRate = sta->isi_txmbps * 0.5; + } + } + + ireq.i_type = IEEE80211_IOC_AUTHMODE; + ireq.i_data = NULL; + ireq.i_len = 0; + if (ioctl(sock, SIOCG80211, &ireq) >= 0) { + switch (ireq.i_val) { + case IEEE80211_AUTH_NONE: + ffStrbufSetStatic(&item->conn.security, "Insecure"); + break; + case IEEE80211_AUTH_OPEN: + ffStrbufSetStatic(&item->conn.security, "Open"); + break; + case IEEE80211_AUTH_SHARED: + ffStrbufSetStatic(&item->conn.security, "Shared"); + break; + case IEEE80211_AUTH_8021X: + ffStrbufSetStatic(&item->conn.security, "8021X"); + break; + case IEEE80211_AUTH_AUTO: + ffStrbufSetStatic(&item->conn.security, "Auto"); + break; + case IEEE80211_AUTH_WPA: + ffStrbufSetStatic(&item->conn.security, "WPA"); + break; + default: + ffStrbufSetF(&item->conn.security, "Unknown (%d)", ireq.i_val); + break; + } + } + } + + if_freenameindex(infs); + return NULL; +} diff --git a/src/detection/wifi/wifi_linux.c b/src/detection/wifi/wifi_linux.c new file mode 100644 index 0000000..6f76159 --- /dev/null +++ b/src/detection/wifi/wifi_linux.c @@ -0,0 +1,976 @@ +#include "wifi.h" +#include "common/io.h" +#include "common/debug.h" +#include "common/strutil.h" + +#include <sys/time.h> +#include <sys/socket.h> +#include <sys/ioctl.h> +#include <sys/types.h> +#include <net/if.h> +#include <linux/wireless.h> +#include <unistd.h> + +#if !__BIG_ENDIAN__ + #include <linux/genetlink.h> + #include <linux/nl80211.h> + + // Silence warning of `NLA_HDRLEN` and `NLA_ALIGN` + #pragma GCC diagnostic ignored "-Wsign-conversion" + +typedef struct FFWifiNlContext { + int sockFd; + uint16_t nl80211FamilyId; + uint32_t portId; + uint32_t seq; +} FFWifiNlContext; + +typedef struct FFWifiSecurityFlags { + bool privacy : 1; + bool wep : 1; + bool wpa : 1; + bool wpa2 : 1; + bool wpa3 : 1; + bool owe : 1; + bool eap : 1; +} FFWifiSecurityFlags; + +static inline double rssiToSignalQuality(int rssi) { + return (double) (rssi >= -50 ? 100 : rssi <= -100 ? 0 + : (rssi + 100) * 2); +} + +static inline uint32_t ffWifiGetNetlinkPortId(int sockFd) { + struct sockaddr_nl addr = {}; + socklen_t addrLen = sizeof(addr); + if (getsockname(sockFd, (struct sockaddr*) &addr, &addrLen) < 0) { + FF_DEBUG("Failed to query netlink socket address (use PID instead): %s", strerror(errno)); + return instance.state.platform.pid; + } + + return addr.nl_pid; +} + +static inline bool ffWifiNlAttrOk(const struct nlattr* attr, size_t remaining) { + return remaining >= sizeof(*attr) && + attr->nla_len >= sizeof(*attr) && + attr->nla_len <= remaining; +} + +static const struct nlattr* ffWifiNlAttrNext(const struct nlattr* attr, size_t* remaining) { + size_t alignedLen = NLA_ALIGN(attr->nla_len); + if (alignedLen > *remaining) { + *remaining = 0; + return NULL; + } + + *remaining -= alignedLen; + return (const struct nlattr*) ((const char*) attr + alignedLen); +} + +static inline size_t ffWifiNlAttrPayload(const struct nlattr* attr) { + return attr->nla_len > NLA_HDRLEN ? attr->nla_len - NLA_HDRLEN : 0; +} + +static inline const void* ffWifiNlAttrData(const struct nlattr* attr) { + // Big endian? + return (const uint8_t*) attr + NLA_HDRLEN; +} + +static bool ffWifiNlAppendAttr(struct nlmsghdr* nlh, size_t maxLen, uint16_t type, const void* data, uint16_t dataLen) { + size_t offset = NLMSG_ALIGN(nlh->nlmsg_len); + size_t attrLen = NLA_HDRLEN + dataLen; + size_t alignedLen = NLA_ALIGN(attrLen); + size_t newLen = offset + alignedLen; + if (newLen > maxLen || attrLen > UINT16_MAX || newLen > UINT32_MAX) { + return false; + } + + struct nlattr* attr = (struct nlattr*) ((char*) nlh + offset); + attr->nla_type = type; + attr->nla_len = (uint16_t) attrLen; + memcpy((char*) attr + NLA_HDRLEN, data, dataLen); + memset((char*) attr + attrLen, 0, alignedLen - attrLen); + nlh->nlmsg_len = (uint32_t) newLen; + return true; +} + +static bool ffWifiNlGetFamilyId(FFWifiNlContext* ctx) { + struct { + struct nlmsghdr nlh; + struct genlmsghdr genl; + char attrs[64]; + } req = { + .nlh = { + .nlmsg_len = NLMSG_LENGTH(sizeof(struct genlmsghdr)), + .nlmsg_type = GENL_ID_CTRL, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK, + .nlmsg_seq = ++ctx->seq, + .nlmsg_pid = ctx->portId, + }, + .genl = { + .cmd = CTRL_CMD_GETFAMILY, + .version = 1, // generic netlink control protocol version + }, + }; + + if (!ffWifiNlAppendAttr(&req.nlh, sizeof(req), CTRL_ATTR_FAMILY_NAME, "nl80211", sizeof("nl80211"))) { + FF_DEBUG("Failed to append CTRL_ATTR_FAMILY_NAME attribute"); + return false; + } + + struct sockaddr_nl addr = { + .nl_family = AF_NETLINK, + }; + + ssize_t sent = sendto(ctx->sockFd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr*) &addr, sizeof(addr)); + if (sent != (ssize_t) req.nlh.nlmsg_len) { + FF_DEBUG("Failed to send nl80211 family request: sent=%zd expected=%u", sent, req.nlh.nlmsg_len); + return false; + } + + uint8_t buffer[8192]; + while (true) { + ssize_t received = recvfrom(ctx->sockFd, buffer, sizeof(buffer), 0, NULL, NULL); + if (received < 0) { + FF_DEBUG("Failed to receive nl80211 family reply: %s", strerror(errno)); + return false; + } + + for (const struct nlmsghdr* nlh = (const struct nlmsghdr*) buffer; + NLMSG_OK(nlh, received); + nlh = NLMSG_NEXT(nlh, received)) { + if (nlh->nlmsg_seq != req.nlh.nlmsg_seq) { + continue; + } + + if (nlh->nlmsg_type == NLMSG_ERROR) { + const struct nlmsgerr* err = (const struct nlmsgerr*) NLMSG_DATA(nlh); + if (err->error != 0) { + FF_DEBUG("nl80211 family query failed: %s", strerror(-err->error)); + return false; + } + continue; + } + + if (nlh->nlmsg_type != GENL_ID_CTRL) { + continue; + } + + const struct genlmsghdr* genl = (const struct genlmsghdr*) NLMSG_DATA(nlh); + if (genl->cmd != CTRL_CMD_NEWFAMILY) { + continue; + } + + size_t attrRemaining = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN; + for (const struct nlattr* attr = (const struct nlattr*) ((const char*) genl + GENL_HDRLEN); + ffWifiNlAttrOk(attr, attrRemaining); + attr = ffWifiNlAttrNext(attr, &attrRemaining)) { + if ((attr->nla_type & NLA_TYPE_MASK) != CTRL_ATTR_FAMILY_ID || ffWifiNlAttrPayload(attr) < sizeof(uint16_t)) { + continue; + } + + ctx->nl80211FamilyId = *(const uint16_t*) ffWifiNlAttrData(attr); + return true; + } + } + } +} + +static bool ffWifiNlInit(FFWifiNlContext* ctx) { + FF_AUTO_CLOSE_FD int _ = ctx->sockFd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_GENERIC); + if (ctx->sockFd < 0) { + FF_DEBUG("Failed to create generic netlink socket: %s", strerror(errno)); + return false; + } + + struct sockaddr_nl addr = { + .nl_family = AF_NETLINK, + }; + + if (bind(ctx->sockFd, (struct sockaddr*) &addr, sizeof(addr)) < 0) { + FF_DEBUG("Failed to bind generic netlink socket: %s", strerror(errno)); + return false; + } + + if (setsockopt( + ctx->sockFd, + SOL_SOCKET, + SO_RCVTIMEO, + &(struct timeval){ .tv_sec = 0, .tv_usec = 250000 }, // 250 ms recv timeout + sizeof(struct timeval)) < 0) { + FF_DEBUG("Failed to set netlink receive timeout: %s", strerror(errno)); + return false; + } + + ctx->portId = ffWifiGetNetlinkPortId(ctx->sockFd); + if (!ffWifiNlGetFamilyId(ctx)) { + return false; + } + + _ = -1; // We are ok now + return true; +} + +static double ffWifiParseBitrateFromRateInfo(const struct nlattr* rateAttr, FFstrbuf* protocol) { + double rate = -DBL_MAX; + size_t remaining = ffWifiNlAttrPayload(rateAttr); + + for (const struct nlattr* info = (const struct nlattr*) ffWifiNlAttrData(rateAttr); + ffWifiNlAttrOk(info, remaining); + info = ffWifiNlAttrNext(info, &remaining)) { + uint16_t type = (uint16_t) (info->nla_type & NLA_TYPE_MASK); + size_t payload = ffWifiNlAttrPayload(info); + + switch (type) { + case 30 /* NL80211_RATE_INFO_UHR_MCS */: + ffStrbufSetStatic(protocol, "802.11bn (Wi-Fi 8)"); + break; + case 23 /* NL80211_RATE_INFO_S1G_MCS */: + ffStrbufSetStatic(protocol, "802.11ah (Wi-Fi HaLow)"); + break; + case 19 /* NL80211_RATE_INFO_EHT_MCS */: + ffStrbufSetStatic(protocol, "802.11be (Wi-Fi 7)"); + break; + case 13 /* NL80211_RATE_INFO_HE_MCS */: + ffStrbufSetStatic(protocol, "802.11ax (Wi-Fi 6)"); + break; + case NL80211_RATE_INFO_VHT_MCS: + ffStrbufSetStatic(protocol, "802.11ac (Wi-Fi 5)"); + break; + case NL80211_RATE_INFO_MCS: + ffStrbufSetStatic(protocol, "802.11n (Wi-Fi 4)"); + break; + case NL80211_RATE_INFO_BITRATE32: + if (payload >= sizeof(uint32_t)) { + rate = *(uint32_t*) ffWifiNlAttrData(info) / 10.0; // nl80211 bitrate unit: 100 kbps => Mbps + } + break; + case NL80211_RATE_INFO_BITRATE: + if (payload >= sizeof(uint16_t) && rate == -DBL_MAX) { + rate = *(uint16_t*) ffWifiNlAttrData(info) / 10.0; // nl80211 bitrate unit: 100 kbps => Mbps + } + break; + } + } + + return rate; +} + +static void ffWifiApplySecurityFlags(FFWifiResult* item, const FFWifiSecurityFlags* sec) { + ffStrbufClear(&item->conn.security); + + if (sec->wep) { + ffStrbufAppendS(&item->conn.security, "WEP/"); + } + if (sec->wpa) { + ffStrbufAppendS(&item->conn.security, "WPA/"); + } + if (sec->wpa2) { + ffStrbufAppendS(&item->conn.security, "WPA2/"); + } + if (sec->wpa3) { + ffStrbufAppendS(&item->conn.security, "WPA3/"); + } + if (sec->owe) { + ffStrbufAppendS(&item->conn.security, "OWE/"); + } + if (sec->eap) { + ffStrbufAppendS(&item->conn.security, "802.1X/"); + } + + if (!item->conn.security.length) { + if (sec->privacy) { + ffStrbufSetStatic(&item->conn.security, "WEP"); + } else { + ffStrbufSetStatic(&item->conn.security, "Insecure"); + } + } else { + ffStrbufTrimRight(&item->conn.security, '/'); + } +} + +static void ffWifiParseRsnIe(const uint8_t* ie, size_t len, FFWifiSecurityFlags* sec) { + if (len < 8) { // version(2) + group cipher suite(4) + pairwise count(2) + return; + } + + sec->wpa2 = true; + size_t pos = 0; + + pos += 2; // RSN version field length + if (pos + 4 > len) { // group cipher suite selector length + return; + } + pos += 4; // skip group cipher suite selector + + if (pos + 2 > len) { // pairwise cipher suite count field length + return; + } + uint16_t pairwiseCount = *(uint16_t*) (ie + pos); + pos += 2; // skip pairwise cipher suite count field + + size_t pairwiseLen = (size_t) pairwiseCount * 4; // each suite selector is 4 bytes + if (pos + pairwiseLen > len) { + return; + } + pos += pairwiseLen; + + if (pos + 2 > len) { // AKM suite count field length + return; + } + uint16_t akmCount = *(uint16_t*) (ie + pos); + pos += 2; // skip AKM suite count field + + for (uint16_t i = 0; i < akmCount && pos + 4 <= len; ++i, pos += 4) { // each AKM suite selector is 4 bytes + const uint8_t* akm = ie + pos; + if (akm[0] != 0x00 || akm[1] != 0x0f || akm[2] != 0xac) { // RSN OUI 00:0f:ac + continue; + } + + switch (akm[3]) { + case 1: // 802.1X + case 5: // FT/802.1X + case 11: // 802.1X-SHA256 + case 12: // FT/802.1X-SHA384 (suite selector value) + sec->eap = true; + break; + case 8: // SAE (WPA3-Personal) + sec->wpa3 = true; + break; + case 18: // OWE + sec->owe = true; + break; + default: + break; + } + } + + if (sec->owe) { + sec->wpa2 = false; + } +} + +static void ffWifiParseWpaVendorIe(const uint8_t* ie, size_t len, FFWifiSecurityFlags* sec) { + if (len < 8) { // OUI+type(4) + version(2) + multicast cipher suite(4) starts here + return; + } + + if (!(ie[0] == 0x00 && ie[1] == 0x50 && ie[2] == 0xf2 && ie[3] == 0x01)) { // Microsoft WPA OUI/type + return; + } + + sec->wpa = true; + + size_t pos = 4; // WPA vendor OUI/type selector length + if (pos + 2 > len) { // WPA version field length + return; + } + pos += 2; // skip WPA version + + if (pos + 4 > len) { // multicast cipher suite selector length + return; + } + pos += 4; // skip multicast cipher suite selector + + if (pos + 2 > len) { // unicast cipher suite count field length + return; + } + uint16_t pairwiseCount = *(uint16_t*) (ie + pos); + pos += 2 + (size_t) pairwiseCount * 4; // count field(2) + N unicast suite selectors(4 each) + + if (pos + 2 > len) { // AKM suite count field length + return; + } + uint16_t akmCount = *(uint16_t*) (ie + pos); + pos += 2; // skip AKM suite count field + + for (uint16_t i = 0; i < akmCount && pos + 4 <= len; ++i, pos += 4) { // each AKM suite selector is 4 bytes + const uint8_t* akm = ie + pos; + if (!(akm[0] == 0x00 && akm[1] == 0x50 && akm[2] == 0xf2)) { // WPA vendor OUI 00:50:f2 + continue; + } + if (akm[3] == 1) { // WPA Enterprise (802.1X) + sec->eap = true; + } + } +} + +static void ffWifiParseInformationElements(const uint8_t* ies, size_t length, FFWifiResult* item, FFWifiSecurityFlags* sec) { + size_t pos = 0; + while (pos + 2 <= length) { + uint8_t id = ies[pos]; + uint8_t len = ies[pos + 1]; + pos += 2; + if (pos + len > length) { + break; + } + + const uint8_t* ie = ies + pos; + if (id == 0) { // SSID element ID + ffStrbufSetNS(&item->conn.ssid, len, (const char*) ie); + } else if (id == 48) { // RSN element ID + ffWifiParseRsnIe(ie, len, sec); + } else if (id == 221) { // vendor-specific element ID (WPA IE lives here) + ffWifiParseWpaVendorIe(ie, len, sec); + } + + pos += len; + } +} + +static bool ffWifiIsBssAssociated(const struct nlattr* bssAttr) { + size_t remaining = ffWifiNlAttrPayload(bssAttr); + for (const struct nlattr* attr = (const struct nlattr*) ffWifiNlAttrData(bssAttr); + ffWifiNlAttrOk(attr, remaining); + attr = ffWifiNlAttrNext(attr, &remaining)) { + uint16_t type = (uint16_t) (attr->nla_type & NLA_TYPE_MASK); + size_t payload = ffWifiNlAttrPayload(attr); + + if (type == NL80211_BSS_STATUS && payload >= sizeof(uint32_t)) { + return *(uint32_t*) ffWifiNlAttrData(attr) == NL80211_BSS_STATUS_ASSOCIATED; + } + } + + return false; +} + +static void ffWifiParseBssAttr(const struct nlattr* bssAttr, FFWifiResult* item) { + FFWifiSecurityFlags sec = {}; + size_t remaining = ffWifiNlAttrPayload(bssAttr); + + for (const struct nlattr* attr = (const struct nlattr*) ffWifiNlAttrData(bssAttr); + ffWifiNlAttrOk(attr, remaining); + attr = ffWifiNlAttrNext(attr, &remaining)) { + uint16_t type = (uint16_t) (attr->nla_type & NLA_TYPE_MASK); + size_t payload = ffWifiNlAttrPayload(attr); + + if (type == NL80211_BSS_BSSID && payload >= 6) { + const uint8_t* mac = (const uint8_t*) ffWifiNlAttrData(attr); + ffStrbufSetF(&item->conn.bssid, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + } else if (type == NL80211_BSS_FREQUENCY && payload >= sizeof(uint32_t)) { + item->conn.frequency = (uint16_t) *(uint32_t*) ffWifiNlAttrData(attr); + item->conn.channel = ffWifiFreqToChannel(item->conn.frequency); + } else if (type == NL80211_BSS_SIGNAL_MBM && payload >= sizeof(int32_t)) { + int rssi = *(int32_t*) ffWifiNlAttrData(attr) / 100; // mBm (100 * dBm) => dBm + item->conn.signalQuality = rssiToSignalQuality(rssi); + } else if (type == NL80211_BSS_CAPABILITY && payload >= sizeof(uint16_t)) { + uint16_t capability = *(uint16_t*) ffWifiNlAttrData(attr); + sec.privacy = (capability & (1u << 4u)) != 0; // IEEE 802.11 capability bit 4: privacy + } else if (type == NL80211_BSS_INFORMATION_ELEMENTS || type == NL80211_BSS_BEACON_IES) { + ffWifiParseInformationElements((const uint8_t*) ffWifiNlAttrData(attr), payload, item, &sec); + } + } + + ffWifiApplySecurityFlags(item, &sec); + return; +} + +static bool ffWifiFetchScanInfo(FFWifiNlContext* ctx, FFWifiResult* item, uint32_t ifIndex) { + struct { + struct nlmsghdr nlh; + struct genlmsghdr genl; + char attrs[32]; + } req = { + .nlh = { + .nlmsg_len = NLMSG_LENGTH(sizeof(struct genlmsghdr)), + .nlmsg_type = ctx->nl80211FamilyId, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP | NLM_F_ACK, + .nlmsg_seq = ++ctx->seq, + .nlmsg_pid = ctx->portId, + }, + .genl = { + .cmd = NL80211_CMD_GET_SCAN, + .version = 0, // nl80211 command version + }, + }; + + if (!ffWifiNlAppendAttr(&req.nlh, sizeof(req), NL80211_ATTR_IFINDEX, &ifIndex, sizeof(ifIndex))) { + FF_DEBUG("Failed to build nl80211 scan request"); + return false; + } + + struct sockaddr_nl addr = { + .nl_family = AF_NETLINK, + }; + + ssize_t sent = sendto(ctx->sockFd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr*) &addr, sizeof(addr)); + if (sent != (ssize_t) req.nlh.nlmsg_len) { + FF_DEBUG("Failed to send nl80211 scan request"); + return false; + } + + uint8_t buffer[1024 * 16]; + while (true) { + ssize_t received = recvfrom(ctx->sockFd, buffer, sizeof(buffer), 0, NULL, NULL); + if (received < 0) { + FF_DEBUG("Failed to receive nl80211 scan reply: %s", strerror(errno)); + return false; + } + + for (const struct nlmsghdr* nlh = (const struct nlmsghdr*) buffer; + NLMSG_OK(nlh, received); + nlh = NLMSG_NEXT(nlh, received)) { + if (nlh->nlmsg_seq != req.nlh.nlmsg_seq) { + continue; + } + + if (nlh->nlmsg_type == NLMSG_DONE) { + return false; + } + + if (nlh->nlmsg_type == NLMSG_ERROR) { + const struct nlmsgerr* err = (const struct nlmsgerr*) NLMSG_DATA(nlh); + if (err->error == 0) { + continue; + } + FF_DEBUG("nl80211 scan request failed: %s", strerror(-err->error)); + return false; + } + + if (nlh->nlmsg_type != ctx->nl80211FamilyId) { + continue; + } + + const struct genlmsghdr* genl = (const struct genlmsghdr*) NLMSG_DATA(nlh); + size_t attrRemaining = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN; + for (const struct nlattr* attr = (const struct nlattr*) ((const char*) genl + GENL_HDRLEN); + ffWifiNlAttrOk(attr, attrRemaining); + attr = ffWifiNlAttrNext(attr, &attrRemaining)) { + if ((attr->nla_type & NLA_TYPE_MASK) != NL80211_ATTR_BSS) { + continue; + } + + if (!ffWifiIsBssAssociated(attr)) { + continue; + } + + ffWifiParseBssAttr(attr, item); + ffStrbufSetStatic(&item->conn.status, "connected"); + return true; + } + } + } + + return false; +} + +static void ffWifiParseStationInfo(const struct nlattr* staInfoAttr, FFWifiResult* item) { + size_t remaining = ffWifiNlAttrPayload(staInfoAttr); + for (const struct nlattr* attr = (const struct nlattr*) ffWifiNlAttrData(staInfoAttr); + ffWifiNlAttrOk(attr, remaining); + attr = ffWifiNlAttrNext(attr, &remaining)) { + uint16_t type = (uint16_t) (attr->nla_type & NLA_TYPE_MASK); + size_t payload = ffWifiNlAttrPayload(attr); + + if (type == NL80211_STA_INFO_SIGNAL && payload >= sizeof(uint8_t) && item->conn.signalQuality == -DBL_MAX) { + int rssi = (int8_t) *(const uint8_t*) ffWifiNlAttrData(attr); + item->conn.signalQuality = rssiToSignalQuality(rssi); + } else if (type == NL80211_STA_INFO_TX_BITRATE && item->conn.txRate == -DBL_MAX) { + double tx = ffWifiParseBitrateFromRateInfo(attr, &item->conn.protocol); + if (tx != -DBL_MAX) { + item->conn.txRate = tx; + } + } else if (type == NL80211_STA_INFO_RX_BITRATE && item->conn.rxRate == -DBL_MAX) { + double rx = ffWifiParseBitrateFromRateInfo(attr, &item->conn.protocol); + if (rx != -DBL_MAX) { + item->conn.rxRate = rx; + } + } + } +} + +static bool ffWifiFetchStationInfo(FFWifiNlContext* ctx, FFWifiResult* item, uint32_t ifIndex) { + struct { + struct nlmsghdr nlh; + struct genlmsghdr genl; + char attrs[32]; + } req = { + .nlh = { + .nlmsg_len = NLMSG_LENGTH(sizeof(struct genlmsghdr)), + .nlmsg_type = ctx->nl80211FamilyId, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP | NLM_F_ACK, + .nlmsg_seq = ++ctx->seq, + .nlmsg_pid = ctx->portId, + }, + .genl = { + .cmd = NL80211_CMD_GET_STATION, + .version = 0, // nl80211 command version + }, + }; + + if (!ffWifiNlAppendAttr(&req.nlh, sizeof(req), NL80211_ATTR_IFINDEX, &ifIndex, sizeof(ifIndex))) { + FF_DEBUG("Failed to build nl80211 station request"); + return false; + } + + struct sockaddr_nl addr = { + .nl_family = AF_NETLINK, + }; + + ssize_t sent = sendto(ctx->sockFd, &req, req.nlh.nlmsg_len, 0, (struct sockaddr*) &addr, sizeof(addr)); + if (sent != (ssize_t) req.nlh.nlmsg_len) { + FF_DEBUG("Failed to send nl80211 station request"); + return false; + } + + uint8_t buffer[8192]; + bool gotStation = false; + while (true) { + ssize_t received = recvfrom(ctx->sockFd, buffer, sizeof(buffer), 0, NULL, NULL); + if (received < 0) { + FF_DEBUG("Failed to receive nl80211 station reply: %s", strerror(errno)); + return gotStation; + } + + for (const struct nlmsghdr* nlh = (const struct nlmsghdr*) buffer; + NLMSG_OK(nlh, received); + nlh = NLMSG_NEXT(nlh, received)) { + if (nlh->nlmsg_seq != req.nlh.nlmsg_seq) { + continue; + } + + if (nlh->nlmsg_type == NLMSG_DONE) { + return gotStation; + } + + if (nlh->nlmsg_type == NLMSG_ERROR) { + const struct nlmsgerr* err = (const struct nlmsgerr*) NLMSG_DATA(nlh); + if (err->error != 0) { + FF_DEBUG("nl80211 station request failed: %s", strerror(-err->error)); + } + return gotStation; + } + + if (nlh->nlmsg_type != ctx->nl80211FamilyId) { + continue; + } + + const struct genlmsghdr* genl = (const struct genlmsghdr*) NLMSG_DATA(nlh); + size_t attrRemaining = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN; + for (const struct nlattr* attr = (const struct nlattr*) ((const char*) genl + GENL_HDRLEN); + ffWifiNlAttrOk(attr, attrRemaining); + attr = ffWifiNlAttrNext(attr, &attrRemaining)) { + if ((attr->nla_type & NLA_TYPE_MASK) != NL80211_ATTR_STA_INFO) { + continue; + } + + ffWifiParseStationInfo(attr, item); + gotStation = true; + } + } + } +} + +static const char* detectWithNetlink(FFWifiNlContext* ctx, FFWifiResult* item, uint32_t ifIndex) { + if (ctx->sockFd < 0) { + if (ctx->sockFd == -1) { + if (!ffWifiNlInit(ctx)) { + FF_DEBUG("Failed to initialize netlink context, skipping"); + ctx->sockFd = -2; // sentinel: permanent netlink failure, don't retry + return "Netlink initialization failed"; + } + } else { + FF_DEBUG("Netlink socket is not available, skipping"); + return "Netlink socket unavailable"; + } + } + + FF_DEBUG("Starting netlink wifi detection for interface %s", item->inf.description.chars); + if (ffWifiFetchScanInfo(ctx, item, ifIndex)) { + FF_DEBUG("found associated BSS: %s", item->conn.ssid.chars); + ffStrbufSetStatic(&item->conn.status, "connected"); + ffWifiFetchStationInfo(ctx, item, ifIndex); + if (!item->conn.protocol.length && item->conn.txRate != -DBL_MAX) { + FF_DEBUG("nl80211 station info did not include MCS family fields"); + } + } else { + FF_DEBUG("No associated BSS found"); + ffStrbufSetStatic(&item->conn.status, "disconnected"); + } + + FF_DEBUG("Netlink wifi detection completed"); + return NULL; +} +#endif + +typedef struct FFWifiIcContext { + int sockFd; +} FFWifiIcContext; + +static const char* detectWithIoctl(FFWifiIcContext* ctx, FFWifiResult* item, char ifName[static IFNAMSIZ]) { + int sock = -1; + if (ctx->sockFd < 0) { + if (ctx->sockFd == -1) { + sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (sock < 0) { + FF_DEBUG("Failed to initialize ioctl context, skipping: %s", strerror(errno)); + ctx->sockFd = -2; // sentinel: permanent ioctl failure, don't retry + return "socket() failed"; + } + ctx->sockFd = sock; + } else { + FF_DEBUG("Ioctl socket is not available, skipping"); + return "ioctl socket unavailable"; + } + } else { + sock = ctx->sockFd; + } + + FF_DEBUG("Starting ioctl wifi detection for interface %s", ifName); + struct iwreq iwr = {}; + strcpy(iwr.ifr_name, ifName); + + if (!item->conn.ssid.length) { + FF_DEBUG("Getting SSID via ioctl"); + ffStrbufEnsureFree(&item->conn.ssid, IW_ESSID_MAX_SIZE); + iwr.u.essid.pointer = (caddr_t) item->conn.ssid.chars; + iwr.u.essid.length = IW_ESSID_MAX_SIZE + 1; + iwr.u.essid.flags = 0; + if (ioctl(sock, SIOCGIWESSID, &iwr) >= 0) { + ffStrbufSetStatic(&item->conn.status, "connected"); + ffStrbufRecalculateLength(&item->conn.ssid); + FF_DEBUG("SSID: %s", item->conn.ssid.chars); + } else { + FF_DEBUG("Failed to get SSID via ioctl: %s", strerror(errno)); + } + } + + if (!item->conn.protocol.length) { + FF_DEBUG("Getting protocol name via ioctl"); + if (ioctl(sock, SIOCGIWNAME, &iwr) >= 0) { + char* token = iwr.u.name; + if (ffStrStartsWithIgnCase(iwr.u.name, "IEEE ")) { + token += strlen("IEEE "); + } + if (ffStrStartsWith(token, "802.11")) { + token += strlen("802.11"); + if (*token) { + if (*token == ' ') { + token++; + } + for (char* c = token; *c; ++c) { + if (*c >= 'A' && *c <= 'Z') { + *c += 'a' - 'A'; + } + } + if (ffStrEquals(token, "n")) { + ffStrbufSetStatic(&item->conn.protocol, "802.11n (Wi-Fi 4)"); + } else if (ffStrEquals(token, "ac")) { + ffStrbufSetStatic(&item->conn.protocol, "802.11ac (Wi-Fi 5)"); + } else if (ffStrEquals(token, "ax")) { + ffStrbufSetStatic(&item->conn.protocol, "802.11ax (Wi-Fi 6)"); + } else if (ffStrEquals(token, "be")) { + ffStrbufSetStatic(&item->conn.protocol, "802.11be (Wi-Fi 7)"); + } else if (ffStrEquals(token, "bn")) { + ffStrbufSetStatic(&item->conn.protocol, "802.11bn (Wi-Fi 8)"); + } else { + ffStrbufSetStatic(&item->conn.protocol, "802.11"); + ffStrbufAppendS(&item->conn.protocol, token); + } + } + } + FF_DEBUG("Protocol: %s", item->conn.protocol.length ? item->conn.protocol.chars : "(unknown)"); + } else { + FF_DEBUG("Failed to get protocol name via ioctl: %s", strerror(errno)); + } + } + + if (!item->conn.bssid.length) { + FF_DEBUG("Getting BSSID via ioctl"); + if (ioctl(sock, SIOCGIWAP, &iwr) >= 0) { + for (int i = 0; i < 6; ++i) { + ffStrbufAppendF(&item->conn.bssid, "%.2X:", (uint8_t) iwr.u.ap_addr.sa_data[i]); + } + ffStrbufTrimRight(&item->conn.bssid, ':'); + FF_DEBUG("BSSID: %s", item->conn.bssid.chars); + } else { + FF_DEBUG("Failed to get BSSID via ioctl: %s", strerror(errno)); + } + } + + if (item->conn.txRate == -DBL_MAX) { + FF_DEBUG("Getting bitrate via ioctl"); + if (ioctl(sock, SIOCGIWRATE, &iwr) >= 0) { + if (iwr.u.bitrate.value > 0) { + item->conn.txRate = iwr.u.bitrate.value / 1000000.; // bps => Mbps + FF_DEBUG("TX bitrate: %.2f Mbps", item->conn.txRate); + } else { + FF_DEBUG("Bitrate value is zero or negative, ignoring"); + } + } else { + FF_DEBUG("Failed to get bitrate via ioctl: %s", strerror(errno)); + } + } + + if (item->conn.frequency == 0 && item->conn.channel == 0) { + FF_DEBUG("Getting frequency via ioctl"); + if (ioctl(sock, SIOCGIWFREQ, &iwr) >= 0) { + if (iwr.u.freq.e == 0 && iwr.u.freq.m <= 1000) { // kernel may return direct channel number + item->conn.channel = (uint16_t) iwr.u.freq.m; + FF_DEBUG("Direct channel value: %u", item->conn.channel); + } else { + // convert it to MHz + while (iwr.u.freq.e < 6) { // normalize exponent to 10^6 (MHz) + iwr.u.freq.m /= 10; + iwr.u.freq.e++; + } + while (iwr.u.freq.e > 6) { // normalize exponent to 10^6 (MHz) + iwr.u.freq.m *= 10; + iwr.u.freq.e--; + } + item->conn.frequency = (uint16_t) iwr.u.freq.m; + item->conn.channel = ffWifiFreqToChannel(item->conn.frequency); + FF_DEBUG("Frequency: %u MHz, Channel: %u", item->conn.frequency, item->conn.channel); + } + } else { + FF_DEBUG("Failed to get frequency via ioctl: %s", strerror(errno)); + } + } + + if (item->conn.signalQuality == -DBL_MAX) { + FF_DEBUG("Getting signal stats via ioctl"); + struct iw_statistics stats; + iwr.u.data.pointer = &stats; + iwr.u.data.length = sizeof(stats); + iwr.u.data.flags = 0; + + if (ioctl(sock, SIOCGIWSTATS, &iwr) >= 0) { + int8_t level = (int8_t) stats.qual.level; + item->conn.signalQuality = level >= -50 ? 100 : level <= -100 ? 0 + : (level + 100) * 2; + FF_DEBUG("Signal level: %d dBm, quality: %.0f%%", level, item->conn.signalQuality); + } else { + FF_DEBUG("Failed to get signal stats via ioctl: %s", strerror(errno)); + } + } + + if (!item->conn.security.length) { + FF_DEBUG("Getting security info via ioctl"); + struct iw_encode_ext iwe; + iwr.u.data.pointer = &iwe; + iwr.u.data.length = sizeof(iwe); + iwr.u.data.flags = 0; + if (ioctl(sock, SIOCGIWENCODEEXT, &iwr) >= 0) { + switch (iwe.alg) { + case IW_ENCODE_ALG_WEP: + ffStrbufAppendS(&item->conn.security, "WEP"); + FF_DEBUG("Security: WEP"); + break; + case IW_ENCODE_ALG_TKIP: + ffStrbufAppendS(&item->conn.security, "TKIP"); + FF_DEBUG("Security: TKIP"); + break; + case IW_ENCODE_ALG_CCMP: + ffStrbufAppendS(&item->conn.security, "CCMP"); + FF_DEBUG("Security: CCMP"); + break; + case IW_ENCODE_ALG_PMK: + ffStrbufAppendS(&item->conn.security, "PMK"); + FF_DEBUG("Security: PMK"); + break; + case IW_ENCODE_ALG_AES_CMAC: + ffStrbufAppendS(&item->conn.security, "CMAC"); + FF_DEBUG("Security: CMAC"); + break; + default: + ffStrbufAppendF(&item->conn.security, "Unknown (%d)", (int) iwe.alg); + FF_DEBUG("Security: Unknown (%d)", (int) iwe.alg); + break; + } + } else { + FF_DEBUG("Failed to get security info via ioctl: %s", strerror(errno)); + } + } + + FF_DEBUG("Ioctl wifi detection completed"); + return NULL; +} + +const char* ffDetectWifi(FFlist* result) { + FF_DEBUG("Starting wifi detection"); + + struct if_nameindex* infs = if_nameindex(); + if (!infs) { + FF_DEBUG("if_nameindex failed: %s", strerror(errno)); + return "if_nameindex() failed"; + } + +#if !__BIG_ENDIAN__ + FFWifiNlContext nl = { .sockFd = -1 }; +#endif + FFWifiIcContext ic = { .sockFd = -1 }; + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + for (struct if_nameindex* i = infs; !(i->if_index == 0 && i->if_name == NULL); ++i) { + FF_DEBUG("Checking interface: %s (index: %u)", i->if_name, i->if_index); + ffStrbufSetF(&buffer, "/sys/class/net/%s/phy80211/", i->if_name); + if (!ffPathExists(buffer.chars, FF_PATHTYPE_DIRECTORY)) { + FF_DEBUG("Not a wifi interface (no phy80211 directory)"); + continue; + } + + FFWifiResult* item = FF_LIST_ADD(FFWifiResult, *result); + ffStrbufInitS(&item->inf.description, i->if_name); + ffStrbufInit(&item->inf.status); + ffStrbufInit(&item->conn.status); + ffStrbufInit(&item->conn.ssid); + ffStrbufInit(&item->conn.bssid); + ffStrbufInit(&item->conn.protocol); + ffStrbufInit(&item->conn.security); + item->conn.signalQuality = -DBL_MAX; + item->conn.rxRate = -DBL_MAX; + item->conn.txRate = -DBL_MAX; + item->conn.channel = 0; + item->conn.frequency = 0; + + char operstate; + ffStrbufSetF(&buffer, "/sys/class/net/%s/operstate", i->if_name); + if (!ffReadFileData(buffer.chars, 1, &operstate)) { + ffStrbufSetStatic(&item->inf.status, "unknown"); + ffStrbufSetStatic(&item->conn.status, "disconnected"); + continue; + } + + if (operstate == 'u') { + ffStrbufSetStatic(&item->inf.status, "up"); + +#if !__BIG_ENDIAN__ + detectWithNetlink(&nl, item, i->if_index); +#endif + detectWithIoctl(&ic, item, i->if_name); + } else { + ffStrbufSetStatic(&item->conn.status, "disconnected"); + + ffStrbufSetF(&buffer, "/sys/class/net/%s/flags", i->if_name); + char flags[16]; + ssize_t len = ffReadFileData(buffer.chars, sizeof(flags) - 1, flags); + if (len <= 0) { + ffStrbufSetStatic(&item->inf.status, "unknown"); + continue; + } + flags[len] = '\0'; + + unsigned flagsVal = (unsigned) strtoul(flags, NULL, 16); // parse /sys flags as hexadecimal + if (flagsVal & IFF_UP) { + ffStrbufSetStatic(&item->inf.status, "up"); + } else { + ffStrbufSetStatic(&item->inf.status, "down"); + } + } + } + + if_freenameindex(infs); +#if !__BIG_ENDIAN__ + if (nl.sockFd >= 0) { + close(nl.sockFd); + } +#endif + if (ic.sockFd >= 0) { + close(ic.sockFd); + } + + FF_DEBUG("Wifi detection completed, found %u wifi interfaces", result->length); + return NULL; +} diff --git a/src/detection/wifi/wifi_nbsd.c b/src/detection/wifi/wifi_nbsd.c new file mode 100644 index 0000000..52897c4 --- /dev/null +++ b/src/detection/wifi/wifi_nbsd.c @@ -0,0 +1,168 @@ +#include "wifi.h" +#include "common/io.h" +#include "common/strutil.h" + +#define COMPAT_FREEBSD_NET80211 1 +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <net/if.h> +#include <net/if_media.h> +#include <net80211/ieee80211.h> +#include <net80211/ieee80211_ioctl.h> + +// ieee80211 header of NetBSD is full of mess. Add compatibility macros from FreeBSD +#undef IEEE80211_IS_CHAN_ANYG +#define IEEE80211_IS_CHAN_ANYG(x) (IEEE80211_IS_CHAN_PUREG(x) || IEEE80211_IS_CHAN_G(x)) +#undef IEEE80211_IS_CHAN_HT +#define IEEE80211_IS_CHAN_HT(x) (((x)->ic_flags & IEEE80211_CHAN_HT) != 0) +#undef IEEE80211_IS_CHAN_VHT +#define IEEE80211_IS_CHAN_VHT(x) (((x)->ic_flags & IEEE80211_CHAN_VHT) != 0) + +const char* ffDetectWifi(FFlist* result) { + struct if_nameindex* infs = if_nameindex(); + if (!infs) { + return "if_nameindex() failed"; + } + + FF_AUTO_CLOSE_FD int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) { + if_freenameindex(infs); + return "socket() failed"; + } + + for (struct if_nameindex* i = infs; !(i->if_index == 0 && i->if_name == NULL); ++i) { + if (!ffStrStartsWith(i->if_name, "iwm")) { + continue; + } + + FFWifiResult* item = FF_LIST_ADD(FFWifiResult, *result); + ffStrbufInitS(&item->inf.description, i->if_name); + ffStrbufInit(&item->inf.status); + ffStrbufInit(&item->conn.status); + ffStrbufInit(&item->conn.ssid); + ffStrbufInit(&item->conn.bssid); + ffStrbufInit(&item->conn.protocol); + ffStrbufInit(&item->conn.security); + item->conn.signalQuality = -DBL_MAX; + item->conn.rxRate = -DBL_MAX; + item->conn.txRate = -DBL_MAX; + item->conn.channel = 0; + item->conn.frequency = 0; + + char ssid[IEEE80211_NWID_LEN + 1] = {}; + struct ieee80211req ireq = {}; + strlcpy(ireq.i_name, i->if_name, sizeof(ireq.i_name)); + ireq.i_type = IEEE80211_IOC_SSID; + ireq.i_data = ssid; + ireq.i_len = sizeof(ssid) - 1; + + if (ioctl(sock, SIOCG80211, &ireq) < 0 || ireq.i_len == 0) { + struct ifreq ifr; + strlcpy(ifr.ifr_name, i->if_name, sizeof(ifr.ifr_name)); + if (ioctl(sock, SIOCGIFFLAGS, &ifr) < 0) { + ffStrbufSetStatic(&item->inf.status, "Unknown"); + } else { + ffStrbufSetStatic(&item->inf.status, ifr.ifr_flags & IFF_UP ? "Up" : "Down"); + } + ffStrbufAppendS(&item->conn.status, "Not associated"); + continue; + } + + ffStrbufSetStatic(&item->inf.status, "Up"); + ffStrbufSetStatic(&item->conn.status, "Associated"); + ffStrbufAppendNS(&item->conn.ssid, ireq.i_len, ssid); + + uint8_t bssid[IEEE80211_ADDR_LEN] = {}; + ireq.i_type = IEEE80211_IOC_BSSID; + ireq.i_data = bssid; + ireq.i_len = sizeof(bssid); + + if (ioctl(sock, SIOCG80211, &ireq) >= 0) { + ffStrbufSetF(&item->conn.bssid, "%02X:%02X:%02X:%02X:%02X:%02X", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); + } + + struct ieee80211_channel curchan = {}; + ireq.i_type = IEEE80211_IOC_CHANNEL; + ireq.i_data = &curchan; + ireq.i_len = sizeof(curchan); + + if (ioctl(sock, SIOCG80211, &ireq) >= 0) { + // item->conn.channel = curchan.ic_ieee; // No ic_ieee in NetBSD + item->conn.channel = ffWifiFreqToChannel(curchan.ic_freq); + item->conn.frequency = curchan.ic_freq; + +#ifdef IEEE80211_IS_CHAN_HE // for future use + if (IEEE80211_IS_CHAN_HE(&curchan)) { + ffStrbufSetStatic(&item->conn.protocol, "802.11ax (Wi-Fi 6)"); + } else +#endif + if (IEEE80211_IS_CHAN_VHT(&curchan)) + ffStrbufSetStatic(&item->conn.protocol, "802.11ac (Wi-Fi 5)"); + else if (IEEE80211_IS_CHAN_HT(&curchan)) { + ffStrbufSetStatic(&item->conn.protocol, "802.11n (Wi-Fi 4)"); + } else if (IEEE80211_IS_CHAN_ANYG(&curchan)) { + ffStrbufSetStatic(&item->conn.protocol, "802.11g"); + } else if (IEEE80211_IS_CHAN_B(&curchan)) { + ffStrbufSetStatic(&item->conn.protocol, "802.11b"); + } else if (IEEE80211_IS_CHAN_A(&curchan)) { + ffStrbufSetStatic(&item->conn.protocol, "802.11a"); + } else if (IEEE80211_IS_CHAN_FHSS(&curchan)) { + ffStrbufSetStatic(&item->conn.protocol, "802.11 (FHSS)"); + } + } + + union { + struct ieee80211req_sta_req req; + uint8_t buf[1024]; + } stareq = {}; + memcpy(stareq.req.is_u.macaddr, bssid, sizeof(bssid)); + ireq.i_type = IEEE80211_IOC_STA_INFO; + ireq.i_data = &stareq; + ireq.i_len = sizeof(stareq); + + if (ioctl(sock, SIOCG80211, &ireq) >= 0) { + struct ieee80211req_sta_info* sta = stareq.req.info; + if (sta->isi_len != 0) { + int8_t rssi = (int8_t) sta->isi_rssi; // Strangely, `sta->isi_rssi` is unsigned + item->conn.signalQuality = (rssi >= -50 ? 100 : rssi <= -100 ? 0 + : (rssi + 100) * 2); + + if (sta->isi_txrate) { + item->conn.txRate = (double) sta->isi_txrate / 2.0; + } + } + } + + ireq.i_type = IEEE80211_IOC_AUTHMODE; + ireq.i_data = NULL; + ireq.i_len = 0; + if (ioctl(sock, SIOCG80211, &ireq) >= 0) { + switch (ireq.i_val) { + case IEEE80211_AUTH_NONE: + ffStrbufSetStatic(&item->conn.security, "Insecure"); + break; + case IEEE80211_AUTH_OPEN: + ffStrbufSetStatic(&item->conn.security, "Open"); + break; + case IEEE80211_AUTH_SHARED: + ffStrbufSetStatic(&item->conn.security, "Shared"); + break; + case IEEE80211_AUTH_8021X: + ffStrbufSetStatic(&item->conn.security, "8021X"); + break; + case IEEE80211_AUTH_AUTO: + ffStrbufSetStatic(&item->conn.security, "Auto"); + break; + case IEEE80211_AUTH_WPA: + ffStrbufSetStatic(&item->conn.security, "WPA"); + break; + default: + ffStrbufSetF(&item->conn.security, "Unknown (%d)", ireq.i_val); + break; + } + } + } + + if_freenameindex(infs); + return NULL; +} diff --git a/src/detection/wifi/wifi_nosupport.c b/src/detection/wifi/wifi_nosupport.c new file mode 100644 index 0000000..bb4a806 --- /dev/null +++ b/src/detection/wifi/wifi_nosupport.c @@ -0,0 +1,5 @@ +#include "wifi.h" + +const char* ffDetectWifi(FF_A_UNUSED FFlist* result) { + return "Not support on this platform"; +} diff --git a/src/detection/wifi/wifi_obsd.c b/src/detection/wifi/wifi_obsd.c new file mode 100644 index 0000000..f686231 --- /dev/null +++ b/src/detection/wifi/wifi_obsd.c @@ -0,0 +1,109 @@ +#include "wifi.h" +#include "common/io.h" +#include "common/strutil.h" + +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <net/if.h> +#include <net80211/ieee80211.h> +#include <net80211/ieee80211_ioctl.h> +#include <unistd.h> + +const char* ffDetectWifi(FFlist* result) { + struct if_nameindex* infs = if_nameindex(); + if (!infs) { + return "if_nameindex() failed"; + } + + FF_AUTO_CLOSE_FD int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) { + return "socket() failed"; + } + + for (struct if_nameindex* i = infs; !(i->if_index == 0 && i->if_name == NULL); ++i) { + if (!ffStrStartsWith(i->if_name, "iwm")) { + continue; + } + + FFWifiResult* item = FF_LIST_ADD(FFWifiResult, *result); + ffStrbufInitS(&item->inf.description, i->if_name); + ffStrbufInit(&item->inf.status); + ffStrbufInit(&item->conn.status); + ffStrbufInit(&item->conn.ssid); + ffStrbufInit(&item->conn.bssid); + ffStrbufInit(&item->conn.protocol); + ffStrbufInit(&item->conn.security); + item->conn.signalQuality = -DBL_MAX; + item->conn.rxRate = -DBL_MAX; + item->conn.txRate = -DBL_MAX; + item->conn.channel = 0; + item->conn.frequency = 0; + + struct ieee80211_nodereq nr = {}; + strlcpy(nr.nr_ifname, i->if_name, sizeof(nr.nr_ifname)); + + struct ifreq ifr = {}; + strlcpy(ifr.ifr_name, i->if_name, sizeof(ifr.ifr_name)); + if (ioctl(sock, SIOCGIFFLAGS, &ifr) < 0) { + ffStrbufSetStatic(&item->inf.status, "Unknown"); + } else { + ffStrbufSetStatic(&item->inf.status, ifr.ifr_flags & IFF_UP ? "Up" : "Down"); + } + + if (ioctl(sock, SIOCG80211NODE, &nr) < 0) { + ffStrbufSetStatic(&item->conn.status, "Not associated"); + continue; + } + + if (nr.nr_nwid_len > 0) { + ffStrbufSetStatic(&item->conn.status, "Associated"); + ffStrbufAppendNS(&item->conn.ssid, nr.nr_nwid_len, (char*) nr.nr_nwid); + } else { + ffStrbufSetStatic(&item->conn.status, "Not associated"); + continue; + } + + ffStrbufSetF(&item->conn.bssid, "%02X:%02X:%02X:%02X:%02X:%02X", nr.nr_bssid[0], nr.nr_bssid[1], nr.nr_bssid[2], nr.nr_bssid[3], nr.nr_bssid[4], nr.nr_bssid[5]); + + item->conn.channel = nr.nr_channel; + + if (nr.nr_max_rssi) { + item->conn.signalQuality = ((float) nr.nr_rssi / nr.nr_max_rssi) * 100.0; + } + + if (nr.nr_flags & IEEE80211_NODEREQ_HT) { + ffStrbufSetStatic(&item->conn.protocol, "802.11n (Wi-Fi 4)"); + } else if (nr.nr_flags & IEEE80211_NODEREQ_VHT) { + ffStrbufSetStatic(&item->conn.protocol, "802.11ac (Wi-Fi 5)"); + } else if (nr.nr_chan_flags & IEEE80211_CHANINFO_5GHZ) { + ffStrbufSetStatic(&item->conn.protocol, "802.11a"); + } else if (nr.nr_chan_flags & IEEE80211_CHANINFO_2GHZ) { + ffStrbufSetStatic(&item->conn.protocol, "802.11g"); + } + + struct ieee80211_wpaparams wpa = {}; + strlcpy(wpa.i_name, i->if_name, sizeof(wpa.i_name)); + + if (ioctl(sock, SIOCG80211WPAPARMS, &wpa) >= 0 && wpa.i_enabled) { + if (wpa.i_protos & IEEE80211_WPA_PROTO_WPA2) { + ffStrbufSetStatic(&item->conn.security, "WPA2"); + } else if (wpa.i_protos & IEEE80211_WPA_PROTO_WPA1) { + ffStrbufSetStatic(&item->conn.security, "WPA"); + } + } else { + struct ieee80211_nwkey nwkey = {}; + strlcpy(nwkey.i_name, i->if_name, sizeof(nwkey.i_name)); + + if (ioctl(sock, SIOCG80211NWKEY, &nwkey) >= 0) { + if (nwkey.i_wepon) { + ffStrbufSetStatic(&item->conn.security, "WEP"); + } else { + ffStrbufSetStatic(&item->conn.security, "Open"); + } + } + } + } + + if_freenameindex(infs); + return NULL; +} diff --git a/src/detection/wifi/wifi_windows.c b/src/detection/wifi/wifi_windows.c new file mode 100644 index 0000000..125c314 --- /dev/null +++ b/src/detection/wifi/wifi_windows.c @@ -0,0 +1,242 @@ +#include "wifi.h" +#include "common/library.h" +#include "common/windows/unicode.h" + +#include <windows.h> +#include <wlanapi.h> + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch" + +static void convertIfStateToString(WLAN_INTERFACE_STATE state, FFstrbuf* result) { + switch (state) { + case wlan_interface_state_not_ready: + ffStrbufAppendS(result, "Not ready"); + break; + case wlan_interface_state_connected: + ffStrbufAppendS(result, "Connected"); + break; + case wlan_interface_state_ad_hoc_network_formed: + ffStrbufAppendS(result, "Ad hoc network formed"); + break; + case wlan_interface_state_disconnecting: + ffStrbufAppendS(result, "Disconnecting"); + break; + case wlan_interface_state_disconnected: + ffStrbufAppendS(result, "Disconnected"); + break; + case wlan_interface_state_associating: + ffStrbufAppendS(result, "Associating"); + break; + case wlan_interface_state_discovering: + ffStrbufAppendS(result, "Discovering"); + break; + case wlan_interface_state_authenticating: + ffStrbufAppendS(result, "Authenticating"); + break; + default: + ffStrbufAppendS(result, "Unknown"); + break; + } +} + +const char* ffDetectWifi(FFlist* result) { + FF_LIBRARY_LOAD_MESSAGE(wlanapi, "wlanapi" FF_LIBRARY_EXTENSION, 1) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wlanapi, WlanOpenHandle) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wlanapi, WlanEnumInterfaces) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wlanapi, WlanQueryInterface) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wlanapi, WlanFreeMemory) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wlanapi, WlanCloseHandle) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(wlanapi, WlanGetNetworkBssList) + + DWORD curVersion; + HANDLE hClient = NULL; + WLAN_INTERFACE_INFO_LIST* ifList = NULL; + const char* error = NULL; + + if (ffWlanOpenHandle(2, NULL, &curVersion, &hClient) != ERROR_SUCCESS) { + error = "WlanOpenHandle() failed"; + goto exit; + } + + if (ffWlanEnumInterfaces(hClient, NULL, &ifList) != ERROR_SUCCESS) { + error = "WlanEnumInterfaces() failed"; + goto exit; + } + + for (uint32_t index = 0; index < ifList->dwNumberOfItems; ++index) { + WLAN_INTERFACE_INFO* ifInfo = (WLAN_INTERFACE_INFO*) &ifList->InterfaceInfo[index]; + + FFWifiResult* item = FF_LIST_ADD(FFWifiResult, *result); + ffStrbufInitWS(&item->inf.description, ifInfo->strInterfaceDescription); + ffStrbufInit(&item->inf.status); + ffStrbufInit(&item->conn.status); + ffStrbufInit(&item->conn.ssid); + ffStrbufInit(&item->conn.bssid); + ffStrbufInit(&item->conn.protocol); + ffStrbufInit(&item->conn.security); + item->conn.signalQuality = -DBL_MAX; + item->conn.rxRate = -DBL_MAX; + item->conn.txRate = -DBL_MAX; + item->conn.channel = 0; + item->conn.frequency = 0; + + convertIfStateToString(ifInfo->isState, &item->inf.status); + + if (ifInfo->isState != wlan_interface_state_connected) { + continue; + } + + WLAN_CONNECTION_ATTRIBUTES* connInfo = NULL; + DWORD bufSize = sizeof(*connInfo); + WLAN_OPCODE_VALUE_TYPE opCode = wlan_opcode_value_type_query_only; + + if (ffWlanQueryInterface(hClient, + &ifInfo->InterfaceGuid, + wlan_intf_opcode_current_connection, + NULL, + &bufSize, + (PVOID*) &connInfo, + &opCode) != ERROR_SUCCESS) { + continue; + } + + convertIfStateToString(connInfo->isState, &item->conn.status); + ffStrbufAppendNS(&item->conn.ssid, + connInfo->wlanAssociationAttributes.dot11Ssid.uSSIDLength, + (const char*) connInfo->wlanAssociationAttributes.dot11Ssid.ucSSID); + + for (size_t i = 0; i < sizeof(connInfo->wlanAssociationAttributes.dot11Bssid); i++) { + ffStrbufAppendF(&item->conn.bssid, "%.2X:", connInfo->wlanAssociationAttributes.dot11Bssid[i]); + } + ffStrbufTrimRight(&item->conn.bssid, ':'); + + switch (connInfo->wlanAssociationAttributes.dot11PhyType) { + case dot11_phy_type_fhss: + ffStrbufAppendS(&item->conn.protocol, "802.11 (FHSS)"); + break; + case dot11_phy_type_dsss: + ffStrbufAppendS(&item->conn.protocol, "802.11 (DSSS)"); + break; + case dot11_phy_type_irbaseband: + ffStrbufAppendS(&item->conn.protocol, "802.11 (IR)"); + break; + case dot11_phy_type_ofdm: + ffStrbufAppendS(&item->conn.protocol, "802.11a"); + break; + case dot11_phy_type_hrdsss: + ffStrbufAppendS(&item->conn.protocol, "802.11b"); + break; + case dot11_phy_type_erp: + ffStrbufAppendS(&item->conn.protocol, "802.11g"); + break; + case dot11_phy_type_ht: + ffStrbufAppendS(&item->conn.protocol, "802.11n (Wi-Fi 4)"); + break; + case dot11_phy_type_vht: + ffStrbufAppendS(&item->conn.protocol, "802.11ac (Wi-Fi 5)"); + break; + case dot11_phy_type_dmg: + ffStrbufAppendS(&item->conn.protocol, "802.11ad (WiGig)"); + break; + case dot11_phy_type_he: + ffStrbufAppendS(&item->conn.protocol, "802.11ax (Wi-Fi 6)"); + break; + case dot11_phy_type_eht: + ffStrbufAppendS(&item->conn.protocol, "802.11be (Wi-Fi 7)"); + break; + default: + ffStrbufAppendF(&item->conn.protocol, "Unknown (%u)", (unsigned) connInfo->wlanAssociationAttributes.dot11PhyType); + break; + } + + item->conn.signalQuality = connInfo->wlanAssociationAttributes.wlanSignalQuality; + item->conn.rxRate = connInfo->wlanAssociationAttributes.ulRxRate / 1000.; + item->conn.txRate = connInfo->wlanAssociationAttributes.ulTxRate / 1000.; + + if (connInfo->wlanSecurityAttributes.bSecurityEnabled) { + switch (connInfo->wlanSecurityAttributes.dot11AuthAlgorithm) { + case DOT11_AUTH_ALGO_80211_OPEN: + ffStrbufAppendS(&item->conn.security, "802.11 Open"); + break; + case DOT11_AUTH_ALGO_80211_SHARED_KEY: + ffStrbufAppendS(&item->conn.security, "802.11 Shared"); + break; + case DOT11_AUTH_ALGO_WPA: + ffStrbufAppendS(&item->conn.security, "WPA"); + break; + case DOT11_AUTH_ALGO_WPA_PSK: + ffStrbufAppendS(&item->conn.security, "WPA-PSK"); + break; + case DOT11_AUTH_ALGO_WPA_NONE: + ffStrbufAppendS(&item->conn.security, "WPA-None"); + break; + case DOT11_AUTH_ALGO_RSNA: + ffStrbufAppendS(&item->conn.security, "WPA2"); + break; + case DOT11_AUTH_ALGO_RSNA_PSK: + ffStrbufAppendS(&item->conn.security, "WPA2-PSK"); + break; + case DOT11_AUTH_ALGO_WPA3: + ffStrbufAppendS(&item->conn.security, "WPA3"); + break; + case DOT11_AUTH_ALGO_WPA3_SAE: + ffStrbufAppendS(&item->conn.security, "WPA3-SAE"); + break; + case 10 /* DOT11_AUTH_ALGO_OWE */: + ffStrbufAppendS(&item->conn.security, "OWE"); + break; + case 11 /* DOT11_AUTH_ALGO_WPA3_ENT */: + ffStrbufAppendS(&item->conn.security, "WPA3-ENT"); + break; + default: + ffStrbufAppendF(&item->conn.security, "Unknown (%u)", (unsigned) connInfo->wlanSecurityAttributes.dot11AuthAlgorithm); + break; + } + if (connInfo->wlanSecurityAttributes.bOneXEnabled) { + ffStrbufAppendS(&item->conn.security, " 802.11X"); + } + } else { + ffStrbufAppendS(&item->conn.security, "Insecure"); + } + + WLAN_BSS_LIST* bssList = NULL; + if (ffWlanGetNetworkBssList(hClient, + &ifInfo->InterfaceGuid, + &connInfo->wlanAssociationAttributes.dot11Ssid, + connInfo->wlanAssociationAttributes.dot11BssType, + connInfo->wlanSecurityAttributes.bSecurityEnabled, + NULL, + &bssList) == ERROR_SUCCESS && + bssList->dwNumberOfItems > 0) { + item->conn.frequency = (uint16_t) (bssList->wlanBssEntries[0].ulChCenterFrequency / 1000); + ffWlanFreeMemory(bssList); + } + + ffWlanFreeMemory(connInfo); + + ULONG* channelNumber = 0; + bufSize = sizeof(*channelNumber); + if (ffWlanQueryInterface(hClient, + &ifInfo->InterfaceGuid, + wlan_intf_opcode_channel_number, + NULL, + &bufSize, + (PVOID*) &channelNumber, + &opCode) == ERROR_SUCCESS) { + item->conn.channel = (uint16_t) *channelNumber; + ffWlanFreeMemory(channelNumber); + } + } + +exit: + if (ifList) { + ffWlanFreeMemory(ifList); + } + if (hClient) { + ffWlanCloseHandle(hClient, NULL); + } + return error; +} + +#pragma GCC diagnostic pop diff --git a/src/detection/wm/wm.h b/src/detection/wm/wm.h new file mode 100644 index 0000000..115a8cd --- /dev/null +++ b/src/detection/wm/wm.h @@ -0,0 +1,7 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/wm/wm.h" + +const char* ffDetectWMPlugin(FFstrbuf* pluginName); +const char* ffDetectWMVersion(const FFstrbuf* wmName, FFstrbuf* result, FFWMOptions* options); diff --git a/src/detection/wm/wm_apple.m b/src/detection/wm/wm_apple.m new file mode 100644 index 0000000..89273e9 --- /dev/null +++ b/src/detection/wm/wm_apple.m @@ -0,0 +1,94 @@ +#include "wm.h" + +#include "common/sysctl.h" +#include "common/mallocHelper.h" +#include "common/strutil.h" +#include "common/apple/version.h" + +#include <ctype.h> +#include <libproc.h> +#import <Foundation/Foundation.h> + +const char* ffDetectWMPlugin(FFstrbuf* pluginName) { + int request[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL }; + u_int requestLength = ARRAY_SIZE(request); + + size_t length = 0; + FF_AUTO_FREE struct kinfo_proc* processes = ffSysctlGetData(request, requestLength, &length); + if (processes == NULL) { + return "sysctl(CTL_KERN, KERN_PROC, KERN_PROC_ALL) failed"; + } + assert(length % sizeof(struct kinfo_proc) == 0); + + for (size_t i = 0; i < length / sizeof(struct kinfo_proc); i++) { + const struct kinfo_proc* proc = &processes[i]; + if (proc->kp_eproc.e_ppid != 1) { + continue; + } + + const char* comm = proc->kp_proc.p_comm; + + if ( + !ffStrEqualsIgnCase(comm, "rectangle") && // 28.6k + !ffStrEqualsIgnCase(comm, "yabai") && // 28.4k + !ffStrEqualsIgnCase(comm, "aerospace") && // 19.6k + !ffStrEqualsIgnCase(comm, "amethyst") && // 16k + !ffStrEqualsIgnCase(comm, "glazewm") && // 11.6k + +#if 0 + // Unmaintained + !ffStrEqualsIgnCase(comm, "spectacle") && // 13.6k + !ffStrEqualsIgnCase(comm, "chunkwm") && // repo deleted; was https://github.com/koekeishiya/chunkwm + !ffStrEqualsIgnCase(comm, "kwm") && // repo deleted; was https://github.com/koekeishiya/kwm +#endif + true) + continue; + + if (instance.config.general.detectVersion) { + char buf[PROC_PIDPATHINFO_MAXSIZE]; + int length = proc_pidpath(proc->kp_proc.p_pid, buf, ARRAY_SIZE(buf) - strlen("Info.plist")); + if (length > 0) { + buf[length] = '\0'; + FF_STRBUF_AUTO_DESTROY pluginVersion = ffStrbufCreate(); + if (ffGetAppNameAndVersion(buf, pluginName, &pluginVersion)) { + if (pluginName->length == 0) { + ffStrbufSetS(pluginName, comm); + } + if (pluginVersion.length > 0) { + ffStrbufAppendC(pluginName, ' '); + ffStrbufAppend(pluginName, &pluginVersion); + } + break; + } + } + } + + ffStrbufAppendS(pluginName, comm); + pluginName->chars[0] = (char) toupper(pluginName->chars[0]); + break; + } + + return NULL; +} + +const char* ffDetectWMVersion(const FFstrbuf* wmName, FFstrbuf* result, FF_A_UNUSED FFWMOptions* options) { + if (!wmName) { + return "No WM detected"; + } + + if (ffStrbufEqualS(wmName, "WindowServer")) { + NSError* error; + NSDictionary* dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:@"/System/Library/PrivateFrameworks/SkyLight.framework/Resources/version.plist" isDirectory:NO] + error:&error]; + if (!dict) { + dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:@"/System/Library/Frameworks/ApplicationServices.framework/Frameworks/CoreGraphics.framework/Resources/version.plist" isDirectory:NO] + error:&error]; + } + + if (dict) { + ffStrbufSetS(result, ((NSString*) dict[@"CFBundleShortVersionString"]).UTF8String); + } + } + + return NULL; +} diff --git a/src/detection/wm/wm_linux.c b/src/detection/wm/wm_linux.c new file mode 100644 index 0000000..a62a4af --- /dev/null +++ b/src/detection/wm/wm_linux.c @@ -0,0 +1,335 @@ +#include "wm.h" + +#include "common/processing.h" +#include "common/io.h" +#include "common/binary.h" +#include "common/path.h" +#include "common/strutil.h" +#include "common/debug.h" + +const char* ffDetectWMPlugin(FF_A_UNUSED FFstrbuf* pluginName) { + return "Not supported on this platform"; +} + +static bool extractCommonWmVersion(const char* line, FF_A_UNUSED uint32_t len, void* userdata) { + int count = 0; + sscanf(line, "%*d.%*d.%*d%n", &count); + if (count == 0) { + return true; + } + + ffStrbufSetNS((FFstrbuf*) userdata, len, line); + return false; +} + +#if !__ANDROID__ +static bool extractHyprlandVersion(const char* line, uint32_t len, void* userdata) { + if (line[0] != 'v') { + return true; + } + ++line; + --len; + int count = 0; + sscanf(line, "%*d.%*d.%*d%n", &count); + if (count == 0) { + return true; + } + + ffStrbufSetNS((FFstrbuf*) userdata, len, line); + return false; +} + +static const char* getHyprland(FFstrbuf* result) { + FF_DEBUG("Detecting Hyprland version"); + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + + FF_DEBUG("Checking for " FASTFETCH_TARGET_DIR_USR "/include/hyprland/src/version.h" + " file"); + if (ffReadFileBuffer(FASTFETCH_TARGET_DIR_USR "/include/hyprland/src/version.h", result)) { + FF_DEBUG("Found version.h file, extracting version"); + if (ffStrbufSubstrAfterFirstS(result, "\n#define GIT_TAG ")) { + ffStrbufSubstrAfterFirstC(result, '"'); + ffStrbufSubstrBeforeFirstC(result, '"'); + ffStrbufTrimLeft(result, 'v'); + FF_DEBUG("Extracted version from version.h: %s", result->chars); + return NULL; + } + FF_DEBUG("Failed to extract version from version.h"); + ffStrbufClear(result); + } else { + FF_DEBUG("version.h file not found, trying Hyprland executable"); + } + + const char* error = ffFindExecutableInPath("Hyprland", &buffer); + if (error) { + FF_DEBUG("Error finding Hyprland executable: %s", error); + return "Failed to find Hyprland executable path"; + } + FF_DEBUG("Found Hyprland executable at: %s", buffer.chars); + + ffBinaryExtractStrings(buffer.chars, extractHyprlandVersion, result, (uint32_t) strlen("v0.0.0")); + if (result->length > 0) { + FF_DEBUG("Extracted version from binary strings: %s", result->chars); + return NULL; + } + FF_DEBUG("Failed to extract version from binary strings, trying --version option"); + + if (ffProcessAppendStdOut(result, (char* const[]) { buffer.chars, "--version", NULL }) == NULL) { + // Hyprland 0.48.1 built from branch at commit 29e2e59... + // Date: ... + // Tag: v0.48.1, commits: 5937 + // ... + + FF_DEBUG("Raw version output: %s", result->chars); + // Use tag if available + if (ffStrbufSubstrAfterFirstS(result, "\nTag: v")) { + ffStrbufSubstrBeforeFirstC(result, ','); + FF_DEBUG("Extracted version from Tag: %s", result->chars); + } else { + ffStrbufSubstrAfterFirstC(result, ' '); + ffStrbufSubstrBeforeFirstC(result, ' '); + FF_DEBUG("Extracted version from output: %s", result->chars); + } + return NULL; + } + FF_DEBUG("Failed to run Hyprland --version command"); + + return "Failed to run command `Hyprland --version`"; +} + +static bool extractSwayVersion(const char* line, FF_A_UNUSED uint32_t len, void* userdata) { + FFstrbuf* result = (FFstrbuf*) userdata; + if (!ffStrStartsWith(line, "sway")) { + return true; + } + if (ffStrStartsWith(line + 4, " version ")) { + ffStrbufSetNS(result, len - (uint32_t) strlen("sway version "), line + strlen("sway version ")); + ffStrbufTrimRightSpace(result); + return false; + } else { + char swayfxVer[32], swayVer[32]; + if (sscanf(line + 4, "fx version %31[^ ] (based on sway %31[^)])", swayfxVer, swayVer) == 2) { + ffStrbufSetF(result, "%s [swayfx %s]", swayVer, swayfxVer); + return false; + } + } + + return true; +} + +static const char* getSway(FFstrbuf* result) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + const char* error = ffFindExecutableInPath("sway", &path); + if (error) { + return "Failed to find sway executable path"; + } + + ffBinaryExtractStrings(path.chars, extractSwayVersion, result, (uint32_t) strlen("sway version 0.0.0")); + if (result->length > 0) { + return NULL; + } + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + if (ffProcessAppendStdOut(&buffer, (char* const[]) { path.chars, "--version", NULL }) == NULL) { // sway version 1.10 + return extractSwayVersion(buffer.chars, buffer.length, result) ? "Failed to parse sway version output" : NULL; + } + + return "Failed to run command `sway --version`"; +} + +static const char* getLabwc(FFstrbuf* result) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + const char* error = ffFindExecutableInPath("labwc", &path); + if (error) { + return "Failed to find labwc executable path"; + } + + ffBinaryExtractStrings(path.chars, extractCommonWmVersion, result, (uint32_t) strlen("0.0.0")); + if (result->length > 0) { + return NULL; + } + + if (ffProcessAppendStdOut(result, (char* const[]) { path.chars, "--version", NULL }) == NULL) { // labwc 0.9.0 (+xwayland +nls +rsvg +libsfdo) + ffStrbufSubstrAfterFirstC(result, ' '); + ffStrbufSubstrBeforeFirstC(result, ' '); + return NULL; + } + + return "Failed to run command `labwc --version`"; +} + +static const char* getNiri(FFstrbuf* result) { + if (ffProcessAppendStdOut(result, (char* const[]) { "niri", "--version", NULL }) == NULL) { // niri 25.11 (commit b35bcae) + ffStrbufSubstrAfterFirstC(result, ' '); + ffStrbufSubstrBeforeLastC(result, '('); + ffStrbufTrimRightSpace(result); + return NULL; + } + + return "Failed to run command `niri --version`"; +} + + #ifdef __linux__ +static const char* getWslg(FFstrbuf* result) { + if (!ffAppendFileBuffer("/mnt/wslg/versions.txt", result)) { + return "Failed to read /mnt/wslg/versions.txt"; + } + + if (!ffStrbufStartsWithS(result, "WSLg ")) { + return "Failed to find WSLg version"; + } + + ffStrbufSubstrBeforeFirstC(result, '\n'); + ffStrbufSubstrBeforeFirstC(result, '+'); + ffStrbufSubstrAfterFirstC(result, ':'); + ffStrbufTrimLeft(result, ' '); + return NULL; +} + #endif + +#endif // !__ANDROID__ + +static bool extractI3Version(const char* line, FF_A_UNUSED uint32_t len, void* userdata) { + int count = 0; + sscanf(line, "%*d.%*d%n", &count); + if (count == 0) { + return true; + } + + ffStrbufSetNS((FFstrbuf*) userdata, len, line); + return false; +} + +static const char* getI3(FFstrbuf* result) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + const char* error = ffFindExecutableInPath("i3", &path); + if (error) { + return "Failed to find i3 executable path"; + } + + ffBinaryExtractStrings(path.chars, extractI3Version, result, (uint32_t) strlen("0.0")); + if (result->length > 0) { + return NULL; + } + + if (ffProcessAppendStdOut(result, (char* const[]) { path.chars, "--version", NULL }) == NULL) { // i3 version 1.10 C 2009... + ffStrbufSubstrAfterFirstS(result, "version "); + ffStrbufSubstrBeforeFirstC(result, ' '); + return NULL; + } + + return "Failed to run command `i3 --version`"; +} + +static const char* getCtwm(FFstrbuf* result) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + const char* error = ffFindExecutableInPath("ctwm", &path); + if (error) { + return "Failed to find ctwm executable path"; + } + + ffBinaryExtractStrings(path.chars, extractCommonWmVersion, result, (uint32_t) strlen("0.0.0")); + if (result->length > 0) { + return NULL; + } + + if (ffProcessAppendStdOut(result, (char* const[]) { path.chars, "--version", NULL }) == NULL) { // ctwm version 4.0.1\n... + ffStrbufSubstrBeforeFirstC(result, '\n'); + ffStrbufSubstrAfterLastC(result, ' '); + return NULL; + } + + return "Failed to run command `ctwm --version`"; +} + +static const char* getFvwm(FFstrbuf* result) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + const char* error = ffFindExecutableInPath("fvwm", &path); + if (error) { + return "Failed to find fvwm executable path"; + } + + ffBinaryExtractStrings(path.chars, extractCommonWmVersion, result, (uint32_t) strlen("0.0.0")); + if (result->length > 0) { + return NULL; + } + + if (ffProcessAppendStdOut(result, (char* const[]) { path.chars, "-version", NULL }) == NULL) { // [FVWM][main]: fvwm Version 2.2.5\n... + ffStrbufSubstrBeforeFirstC(result, '\n'); + ffStrbufSubstrAfterLastC(result, ' '); + return NULL; + } + + return "Failed to run command `fvwm -version`"; +} + +static const char* getOpenbox(FFstrbuf* result) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + const char* error = ffFindExecutableInPath("openbox", &path); + if (error) { + return "Failed to find openbox executable path"; + } + + ffBinaryExtractStrings(path.chars, extractCommonWmVersion, result, (uint32_t) strlen("0.0.0")); + if (result->length > 0) { + return NULL; + } + + if (ffProcessAppendStdOut(result, (char* const[]) { path.chars, "--version", NULL }) == NULL) { // Openbox 3.6.1\n... + ffStrbufSubstrBeforeFirstC(result, '\n'); + ffStrbufSubstrAfterLastC(result, ' '); + return NULL; + } + + return "Failed to run command `openbox --version`"; +} + +const char* ffDetectWMVersion(const FFstrbuf* wmName, FFstrbuf* result, FF_A_UNUSED FFWMOptions* options) { + if (!wmName) { + return "No WM detected"; + } + +#if !__ANDROID__ + // Wayland compositors + if (ffStrbufIgnCaseEqualS(wmName, "Hyprland")) { + return getHyprland(result); + } + + if (ffStrbufEqualS(wmName, "sway")) { + return getSway(result); + } + + if (ffStrbufEqualS(wmName, "labwc")) { + return getLabwc(result); + } + + if (ffStrbufEqualS(wmName, "niri")) { + return getNiri(result); + } + + #if __linux__ + if (ffStrbufEqualS(wmName, "WSLg")) { + return getWslg(result); + } + #endif +#endif + + // X11 WMs + if (ffStrbufEqualS(wmName, "i3")) { + return getI3(result); + } + + if (ffStrbufEqualS(wmName, "ctwm")) { + return getCtwm(result); + } + + if (ffStrbufEqualS(wmName, "fvwm")) { + return getFvwm(result); + } + + if (ffStrbufEqualS(wmName, "Openbox")) { + return getOpenbox(result); + } + + return "Unsupported WM"; +} diff --git a/src/detection/wm/wm_nosupport.c b/src/detection/wm/wm_nosupport.c new file mode 100644 index 0000000..65c5348 --- /dev/null +++ b/src/detection/wm/wm_nosupport.c @@ -0,0 +1,9 @@ +#include "wm.h" + +const char* ffDetectWMPlugin(FF_A_UNUSED FFstrbuf* pluginName) { + return "Not supported on this platform"; +} + +const char* ffDetectWMVersion(FF_A_UNUSED const FFstrbuf* wmName, FF_A_UNUSED FFstrbuf* result, FF_A_UNUSED FFWMOptions* options) { + return "Not supported on this platform"; +} diff --git a/src/detection/wm/wm_windows.c b/src/detection/wm/wm_windows.c new file mode 100644 index 0000000..ee36399 --- /dev/null +++ b/src/detection/wm/wm_windows.c @@ -0,0 +1,204 @@ +#include "wm.h" +#include "common/mallocHelper.h" +#include "common/io.h" +#include "common/library.h" +#include "common/processing.h" +#include "common/windows/nt.h" +#include "common/windows/unicode.h" +#include "common/windows/version.h" + +#include <stdalign.h> +#include <windows.h> +#include <ntstatus.h> +#include <shlobj.h> +#include <softpub.h> + +typedef enum { + FF_PROCESS_TYPE_NONE, + FF_PROCESS_TYPE_SIGNED = 1 << 0, + FF_PROCESS_TYPE_WINDOWS_STORE = 1 << 1, + FF_PROCESS_TYPE_GUI = 1 << 2, + FF_PROCESS_TYPE_CUI = 1 << 3, +} FFProcessType; + +static bool verifySignature(const wchar_t* filePath) { + FF_LIBRARY_LOAD(wintrust, true, "wintrust" FF_LIBRARY_EXTENSION, -1) + FF_LIBRARY_LOAD_SYMBOL(wintrust, WinVerifyTrustEx, true) + + WINTRUST_FILE_INFO fileInfo = { + .cbStruct = sizeof(fileInfo), + .pcwszFilePath = filePath, + }; + + GUID actionID = WINTRUST_ACTION_GENERIC_VERIFY_V2; + + WINTRUST_DATA trustData = { + .cbStruct = sizeof(trustData), + .dwUIChoice = WTD_UI_NONE, + .fdwRevocationChecks = WTD_REVOKE_NONE, + .dwUnionChoice = WTD_CHOICE_FILE, + .pFile = &fileInfo, + .dwStateAction = WTD_STATEACTION_VERIFY, + .dwProvFlags = WTD_SAFER_FLAG, + }; + + LONG status = ffWinVerifyTrustEx(NULL, &actionID, &trustData); + trustData.dwStateAction = WTD_STATEACTION_CLOSE; + ffWinVerifyTrustEx(NULL, &actionID, &trustData); + + return status == ERROR_SUCCESS; +} + +static bool isProcessTrusted(DWORD processId, FFProcessType processType, UNICODE_STRING* buffer, size_t bufSize) { + FF_AUTO_CLOSE_FD HANDLE hProcess = NULL; + if (!NT_SUCCESS(NtOpenProcess(&hProcess, PROCESS_QUERY_LIMITED_INFORMATION, &(OBJECT_ATTRIBUTES) { + .Length = sizeof(OBJECT_ATTRIBUTES), + }, + &(CLIENT_ID) { .UniqueProcess = (HANDLE) (uintptr_t) processId }))) { + return false; + } + + ULONG size; + if (!NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessImageFileNameWin32, buffer, (ULONG) bufSize, &size)) || + buffer->Length == 0) { + return false; + } + assert(buffer->MaximumLength >= buffer->Length + 2); // NULL terminated + + if (processType & FF_PROCESS_TYPE_WINDOWS_STORE) { + static wchar_t windowsAppsPath[MAX_PATH]; + static uint32_t windowsAppsPathLen; + if (windowsAppsPathLen == 0) { + PWSTR pPath = NULL; + if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_ProgramFiles, KF_FLAG_DEFAULT, NULL, &pPath))) { + windowsAppsPathLen = (uint32_t) wcslen(pPath); + memcpy(windowsAppsPath, pPath, windowsAppsPathLen * sizeof(wchar_t)); + memcpy(windowsAppsPath + windowsAppsPathLen, L"\\WindowsApps\\", sizeof(L"\\WindowsApps\\")); + windowsAppsPathLen += strlen("\\WindowsApps\\"); + } else { + windowsAppsPathLen = -1u; + } + CoTaskMemFree(pPath); + } + if (windowsAppsPathLen != -1u && + (buffer->Length <= windowsAppsPathLen * sizeof(wchar_t) || // Path is too short to be in WindowsApps + _wcsnicmp(buffer->Buffer, windowsAppsPath, windowsAppsPathLen) != 0) // Path does not start with WindowsApps + ) { + return false; + } + } + + if (processType & FF_PROCESS_TYPE_SIGNED) { + if (!verifySignature(buffer->Buffer)) { + return false; + } + } + + if (processType & (FF_PROCESS_TYPE_GUI | FF_PROCESS_TYPE_CUI)) { + SECTION_IMAGE_INFORMATION info = {}; + if (!NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessImageInformation, &info, sizeof(info), &size)) || + size != sizeof(info)) { + return false; + } + + if ((processType & FF_PROCESS_TYPE_GUI) && info.SubSystemType != IMAGE_SUBSYSTEM_WINDOWS_GUI) { + return false; + } + if ((processType & FF_PROCESS_TYPE_CUI) && info.SubSystemType != IMAGE_SUBSYSTEM_WINDOWS_CUI) { + return false; + } + } + + return true; +} + +#define ffStrEqualNWS(str, compareTo) (_wcsnicmp(str, L##compareTo, sizeof(compareTo) - 1) == 0) + +const char* ffDetectWMPlugin(FFstrbuf* pluginName) { + alignas(UNICODE_STRING) uint8_t buffer[4096]; + UNICODE_STRING* filePath = (UNICODE_STRING*) buffer; + SYSTEM_PROCESS_INFORMATION* FF_AUTO_FREE pstart = NULL; + + // Multiple attempts in case processes change while + // we are in the middle of querying them. + ULONG size = 0; + for (int attempts = 0;; ++attempts) { + if (size) { + pstart = (SYSTEM_PROCESS_INFORMATION*) realloc(pstart, size); + assert(pstart); + } + NTSTATUS status = NtQuerySystemInformation(SystemProcessInformation, pstart, size, &size); + if (NT_SUCCESS(status)) { + break; + } else if (status == STATUS_INFO_LENGTH_MISMATCH && attempts < 4) { + size += sizeof(SYSTEM_PROCESS_INFORMATION) * 5; + } else { + return "NtQuerySystemInformation(SystemProcessInformation) failed"; + } + } + + for (SYSTEM_PROCESS_INFORMATION* ptr = pstart;; ptr = (SYSTEM_PROCESS_INFORMATION*) ((uint8_t*) ptr + ptr->NextEntryOffset)) { + assert(ptr->ImageName.Length == 0 || ptr->ImageName.MaximumLength >= ptr->ImageName.Length + 2); // NULL terminated + if (ptr->ImageName.Length == strlen("FancyWM-GUI.exe") * sizeof(wchar_t) && + ffStrEqualNWS(ptr->ImageName.Buffer, "FancyWM-GUI.exe") && + isProcessTrusted((DWORD) (uintptr_t) ptr->UniqueProcessId, FF_PROCESS_TYPE_WINDOWS_STORE | FF_PROCESS_TYPE_GUI, filePath, sizeof(buffer))) { + if (instance.config.general.detectVersion && ffGetFileVersion(filePath->Buffer, NULL, pluginName)) { + ffStrbufPrependS(pluginName, "FancyWM "); + } else { + ffStrbufSetStatic(pluginName, "FancyWM"); + } + break; + } else if (ptr->ImageName.Length == strlen("glazewm-watcher.exe") * sizeof(wchar_t) && + ffStrEqualNWS(ptr->ImageName.Buffer, "glazewm-watcher.exe") && + isProcessTrusted((DWORD) (uintptr_t) ptr->UniqueProcessId, FF_PROCESS_TYPE_SIGNED | FF_PROCESS_TYPE_GUI, filePath, sizeof(buffer))) { + if (instance.config.general.detectVersion && ffGetFileVersion(filePath->Buffer, NULL, pluginName)) { + ffStrbufPrependS(pluginName, "GlazeWM "); + } else { + ffStrbufSetStatic(pluginName, "GlazeWM"); + } + break; + } else if (ptr->ImageName.Length == strlen("komorebi.exe") * sizeof(wchar_t) && + ffStrEqualNWS(ptr->ImageName.Buffer, "komorebi.exe") && + isProcessTrusted((DWORD) (uintptr_t) ptr->UniqueProcessId, FF_PROCESS_TYPE_CUI, filePath, sizeof(buffer))) { + if (instance.config.general.detectVersion) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateNWS(filePath->Length / sizeof(wchar_t), filePath->Buffer); + if (ffProcessAppendStdOut(pluginName, (char* const[]) { + path.chars, + "--version", + NULL, + }) == NULL) { + ffStrbufSubstrBeforeFirstC(pluginName, '\n'); + } + } + if (pluginName->length == 0) { + ffStrbufSetStatic(pluginName, "Komorebi"); + } + break; + } + + if (ptr->NextEntryOffset == 0) { + break; + } + } + + return NULL; +} + +const char* ffDetectWMVersion(const FFstrbuf* wmName, FFstrbuf* result, FF_A_UNUSED FFWMOptions* options) { + if (!wmName) { + return "No WM detected"; + } + + if (ffStrbufEqualS(wmName, "dwm.exe")) { + PWSTR pPath = NULL; + if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_System, KF_FLAG_DEFAULT, NULL, &pPath))) { + wchar_t fullPath[MAX_PATH]; + wcscpy(fullPath, pPath); + wcscat(fullPath, L"\\dwm.exe"); + ffGetFileVersion(fullPath, NULL, result); + } + CoTaskMemFree(pPath); + return NULL; + } + return "Not supported on this platform"; +} diff --git a/src/detection/wmtheme/wmtheme.h b/src/detection/wmtheme/wmtheme.h new file mode 100644 index 0000000..a737a7a --- /dev/null +++ b/src/detection/wmtheme/wmtheme.h @@ -0,0 +1,5 @@ +#pragma once + +#include "fastfetch.h" + +bool ffDetectWmTheme(FFstrbuf* themeOrError); diff --git a/src/detection/wmtheme/wmtheme_apple.m b/src/detection/wmtheme/wmtheme_apple.m new file mode 100644 index 0000000..8c43877 --- /dev/null +++ b/src/detection/wmtheme/wmtheme_apple.m @@ -0,0 +1,40 @@ +#include "fastfetch.h" +#include "wmtheme.h" + +#import <Foundation/Foundation.h> + +bool ffDetectWmTheme(FFstrbuf* themeOrError) +{ + NSError* error; + NSString* fileName = [NSString stringWithFormat:@"file://%s/Library/Preferences/.GlobalPreferences.plist", instance.state.platform.homeDir.chars]; + NSDictionary* dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL URLWithString:fileName] + error:&error]; + if(error) + { + ffStrbufAppendS(themeOrError, error.localizedDescription.UTF8String); + return false; + } + + NSNumber* wmThemeColor = dict[@"AppleAccentColor"]; + if(!wmThemeColor) + ffStrbufAppendS(themeOrError, "Multicolor"); + else + { + switch(wmThemeColor.intValue) + { + case -1: ffStrbufAppendS(themeOrError, "Graphite"); break; + case 0: ffStrbufAppendS(themeOrError, "Red"); break; + case 1: ffStrbufAppendS(themeOrError, "Orange"); break; + case 2: ffStrbufAppendS(themeOrError, "Yellow"); break; + case 3: ffStrbufAppendS(themeOrError, "Green"); break; + case 4: ffStrbufAppendS(themeOrError, "Blue"); break; + case 5: ffStrbufAppendS(themeOrError, "Purple"); break; + case 6: ffStrbufAppendS(themeOrError, "Pink"); break; + default: ffStrbufAppendS(themeOrError, "Unknown"); break; + } + } + + NSString* wmTheme = dict[@"AppleInterfaceStyle"]; + ffStrbufAppendF(themeOrError, " (%s)", wmTheme ? wmTheme.UTF8String : "Light"); + return true; +} diff --git a/src/detection/wmtheme/wmtheme_haiku.cpp b/src/detection/wmtheme/wmtheme_haiku.cpp new file mode 100644 index 0000000..243eaa4 --- /dev/null +++ b/src/detection/wmtheme/wmtheme_haiku.cpp @@ -0,0 +1,30 @@ +extern "C" { +#include "fastfetch.h" +#include "wmtheme.h" +} + +#include <Application.h> +#include <private/interface/DecorInfo.h> + +bool ffDetectWmTheme(FFstrbuf* themeOrError) +{ + // We need a valid be_app to query the app_server here. + BApplication app("application/x-vnd.fastfetch-cli-fastfetch"); + + DecorInfoUtility *util = new DecorInfoUtility(); + DecorInfo* decor = NULL; + + if (util) { + decor = util->CurrentDecorator(); + if (decor) { + ffStrbufAppendS(themeOrError, decor->Name().String()); + } + delete util; + if (decor) { + return true; + } + } + + ffStrbufAppendS(themeOrError, "Failed to get DecorInfo"); + return false; +} diff --git a/src/detection/wmtheme/wmtheme_linux.c b/src/detection/wmtheme/wmtheme_linux.c new file mode 100644 index 0000000..e912302 --- /dev/null +++ b/src/detection/wmtheme/wmtheme_linux.c @@ -0,0 +1,340 @@ +#include "wmtheme.h" +#include "common/io.h" +#include "common/properties.h" +#include "common/parsing.h" +#include "common/settings.h" +#include "common/strutil.h" +#include "common/mallocHelper.h" +#include "detection/gtk_qt/gtk_qt.h" +#include "detection/displayserver/displayserver.h" + +static bool detectWMThemeFromConfigFile(const char* configFile, const char* themeRegex, const char* defaultValue, FFstrbuf* themeOrError) { + if (!ffParsePropFileConfig(configFile, themeRegex, themeOrError)) { + ffStrbufAppendF(themeOrError, "Config file %s doesn't exist", configFile); + return false; + } + + if (themeOrError->length == 0) { + if (defaultValue == NULL) { + ffStrbufAppendF(themeOrError, "Couldn't find WM theme in %s", configFile); + return false; + } + + ffStrbufAppendS(themeOrError, defaultValue); + return true; + } + + // Remove Plasma-generated prefixes + uint32_t idx = 0; + + idx = ffStrbufFirstIndexS(themeOrError, "qml_"); + if (idx != themeOrError->length) { + ffStrbufSubstrAfter(themeOrError, idx + 3); + } + + idx = ffStrbufFirstIndexS(themeOrError, "svg__"); + if (idx != themeOrError->length) { + ffStrbufSubstrAfter(themeOrError, idx + 4); + } + + return true; +} + +static bool detectWMThemeFromSettings(const char* dconfKey, const char* gsettingsSchemaName, const char* gsettingsPath, const char* gsettingsKey, FFstrbuf* themeOrError) { + const char* theme = ffSettingsGetGnome(dconfKey, gsettingsSchemaName, gsettingsPath, gsettingsKey, FF_VARIANT_TYPE_STRING).strValue; + + if (!ffStrSet(theme)) { + ffStrbufAppendS(themeOrError, "Couldn't find WM theme in DConf or GSettings"); + return false; + } + + ffStrbufAppendS(themeOrError, theme); + return true; +} + +static bool detectGTKThemeAsWMTheme(FFstrbuf* themeOrError) { + const FFGTKResult* gtk = ffDetectGTK4(); + if (gtk->theme.length > 0) { + goto ok; + } + + gtk = ffDetectGTK3(); + if (gtk->theme.length > 0) { + goto ok; + } + + gtk = ffDetectGTK2(); + if (gtk->theme.length > 0) { + goto ok; + } + + ffStrbufAppendS(themeOrError, "Couldn't detect GTK4/3/2 theme"); + return false; + +ok: + ffStrbufAppend(themeOrError, >k->theme); + return true; +} + +static bool detectMutter(FFstrbuf* themeOrError) { + const char* theme = ffSettingsGetGnome("/org/gnome/shell/extensions/user-theme/name", "org.gnome.shell.extensions.user-theme", NULL, "name", FF_VARIANT_TYPE_STRING).strValue; + if (ffStrSet(theme)) { + ffStrbufAppendS(themeOrError, theme); + return true; + } + + return detectGTKThemeAsWMTheme(themeOrError); +} + +static bool detectMuffin(FFstrbuf* themeOrError) { + FF_AUTO_FREE const char* name = ffSettingsGetGnome("/org/cinnamon/theme/name", "org.cinnamon.theme", NULL, "name", FF_VARIANT_TYPE_STRING).strValue; + FF_AUTO_FREE const char* theme = ffSettingsGetGnome("/org/cinnamon/desktop/wm/preferences/theme", "org.cinnamon.desktop.wm.preferences", NULL, "theme", FF_VARIANT_TYPE_STRING).strValue; + + if (name == NULL && theme == NULL) { + ffStrbufAppendS(themeOrError, "Couldn't find muffin theme in GSettings / DConf"); + return false; + } + + if (name == NULL) { + ffStrbufAppendS(themeOrError, theme); + return true; + } + + if (theme == NULL) { + ffStrbufAppendS(themeOrError, name); + return true; + } + + ffStrbufAppendF(themeOrError, "%s (%s)", name, theme); + return true; +} + +static bool detectXFWM4(FFstrbuf* themeOrError) { + const char* theme = ffSettingsGetXFConf("xfwm4", "/general/theme", FF_VARIANT_TYPE_STRING).strValue; + + if (theme == NULL) { + ffStrbufAppendS(themeOrError, "Couldn't find xfwm4::/general/theme in XFConf"); + return false; + } + + ffStrbufAppendS(themeOrError, theme); + return true; +} + +static bool detectOpenbox(const FFstrbuf* dePrettyName, FFstrbuf* themeOrError) { + FF_STRBUF_AUTO_DESTROY absolutePath = ffStrbufCreateA(64); + const char* configFileSubpath = "openbox/rc.xml"; + if (ffStrbufIgnCaseEqualS(dePrettyName, "LXQt")) { + configFileSubpath = "openbox/lxqt-rc.xml"; + } else if (ffStrbufIgnCaseEqualS(dePrettyName, "LXDE")) { + configFileSubpath = "openbox/lxde-rc.xml"; + } + + if (!ffSearchUserConfigFile(&instance.state.platform.configDirs, configFileSubpath, &absolutePath)) { + ffStrbufAppendF(themeOrError, "Couldn't find config file \"%s\"", configFileSubpath); + return false; + } + + FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); + if (!ffReadFileBuffer(absolutePath.chars, &content)) { + ffStrbufAppendF(themeOrError, "Couldn't read \"%s\"", absolutePath.chars); + return false; + } + + const char* themeStart = strstr(content.chars, "<theme>"); + if (themeStart == NULL) { + goto theme_not_found; + } + + const char* themeEnd = strstr(themeStart, "</theme>"); + if (__builtin_expect(themeEnd == NULL, false)) { // very rare case + goto theme_not_found; + } + + const char* nameStart = strstr(themeStart, "<name>"); + if (nameStart == NULL) { + goto name_not_found; + } + + const char* nameEnd = strstr(nameStart, "</name>"); + if (nameEnd == NULL || nameEnd > themeEnd) { // (nameEnd > themeEnd) means name is not a theme's child + goto name_not_found; + } + + nameStart += strlen("<name>"); + ffStrbufAppendNS(themeOrError, (uint32_t) (nameEnd - nameStart), nameStart); + ffStrbufTrim(themeOrError, ' '); + + if (themeOrError->length == 0) { + goto name_not_found; + } + + return true; + +theme_not_found: + ffStrbufAppendF(themeOrError, "Couldn't find theme node in \"%s\"", absolutePath.chars); + return false; + +name_not_found: + ffStrbufAppendF(themeOrError, "Couldn't find theme name in \"%s\"", absolutePath.chars); + return false; +} + +static bool detectCosmicComp(FFstrbuf* themeOrError) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateCopy(FF_LIST_FIRST(FFstrbuf, instance.state.platform.configDirs)); + ffStrbufAppendS(&path, "cosmic/"); + uint32_t basePathLength = path.length; + + const char* variant; + { + char isDarkC; + ffStrbufAppendS(&path, "com.system76.CosmicTheme.Mode/v1/is_dark"); + if (ffReadFileData(path.chars, 1, &isDarkC) == 1) { + variant = isDarkC == 't' || isDarkC == '1' ? "Dark" : "Light"; + } else { + ffStrbufAppendF(themeOrError, "Couldn't read cosmic theme mode from %s", path.chars); + return false; + } + ffStrbufSubstrBefore(&path, basePathLength); + } + + FF_STRBUF_AUTO_DESTROY name = ffStrbufCreate(); + ffStrbufAppendF(&path, "com.system76.CosmicTheme.%s/v1/name", variant); + if (ffReadFileBuffer(path.chars, &name)) { + ffStrbufTrimSpace(&name); + ffStrbufTrim(&name, '"'); + } + ffStrbufSubstrBefore(&path, basePathLength); + + if (name.length > 0) { + ffStrbufAppend(themeOrError, &name); + } else { + ffStrbufAppendS(themeOrError, variant); + } + + FF_STRBUF_AUTO_DESTROY accent = ffStrbufCreate(); + ffStrbufAppendF(&path, "com.system76.CosmicTheme.%s/v1/accent", variant); + if (ffReadFileBuffer(path.chars, &accent)) { + const char* baseStart = strstr(accent.chars, "base:"); + if (baseStart != NULL) { + const char* contentStart = strchr(baseStart, '('); + if (contentStart != NULL) { + int depth = 1; + const char* contentEnd = contentStart + 1; + while (*contentEnd != '\0' && depth > 0) { + if (*contentEnd == '(') { + ++depth; + } else if (*contentEnd == ')') { + --depth; + } + ++contentEnd; + } + + if (depth == 0) { + double red = 0.0, green = 0.0, blue = 0.0, alpha = 1.0; + bool ok = true; + for (uint32_t i = 0; i < 4; ++i) { + const char* key = + i == 0 ? "red:" : (i == 1 ? "green:" : (i == 2 ? "blue:" : "alpha:")); + const char* componentStart = strstr(contentStart, key); + if (componentStart == NULL || componentStart >= contentEnd) { + ok = false; + break; + } + + componentStart += strlen(key); + char* componentEnd = NULL; + double value = strtod(componentStart, &componentEnd); + if (componentEnd == componentStart) { + ok = false; + break; + } + + if (i == 0) { + red = value; + } else if (i == 1) { + green = value; + } else if (i == 2) { + blue = value; + } else { + alpha = value; + } + } + + if (ok) { + uint32_t r = red <= 0.0 ? 0 : (red >= 1.0 ? 255 : (uint32_t) (red * 255.0 + 0.5)); + uint32_t g = green <= 0.0 ? 0 : (green >= 1.0 ? 255 : (uint32_t) (green * 255.0 + 0.5)); + uint32_t b = blue <= 0.0 ? 0 : (blue >= 1.0 ? 255 : (uint32_t) (blue * 255.0 + 0.5)); + uint32_t a = alpha <= 0.0 ? 0 : (alpha >= 1.0 ? 255 : (uint32_t) (alpha * 255.0 + 0.5)); + uint32_t rgb = (r << 16) | (g << 8) | b; + if (a == 255) { + ffStrbufAppendF(themeOrError, " - #%06X", rgb); + } else { + ffStrbufAppendF(themeOrError, " - #%06X%02X", rgb, a); + } + } + } + } + } + } + ffStrbufSubstrBefore(&path, basePathLength); + + if (name.length > 0) { + ffStrbufAppendF(themeOrError, " (%s)", variant); + } + + return true; +} + +bool ffDetectWmTheme(FFstrbuf* themeOrError) { + const FFDisplayServerResult* wm = ffConnectDisplayServer(); + + if (wm->wmPrettyName.length == 0) { + ffStrbufAppendS(themeOrError, "WM Theme needs successful WM detection"); + return false; + } + + if (ffStrbufIgnCaseEqualS(&wm->wmPrettyName, FF_WM_PRETTY_KWIN)) { + return detectWMThemeFromConfigFile("kwinrc", "theme =", "Breeze", themeOrError); + } + + if (ffStrbufIgnCaseEqualS(&wm->wmPrettyName, FF_WM_PRETTY_COSMIC_COMP)) { + return detectCosmicComp(themeOrError); + } + + if ( + ffStrbufIgnCaseEqualS(&wm->wmPrettyName, FF_WM_PRETTY_XFWM4) || + (ffStrbufIgnCaseEqualS(&wm->wmPrettyName, "labwc") && ffStrbufIgnCaseEqualS(&wm->dePrettyName, FF_DE_PRETTY_XFCE4))) { + return detectXFWM4(themeOrError); + } + + if (ffStrbufIgnCaseEqualS(&wm->wmPrettyName, FF_WM_PRETTY_MUTTER)) { + if ( + ffStrbufIgnCaseEqualS(&wm->dePrettyName, FF_DE_PRETTY_GNOME) || + ffStrbufIgnCaseEqualS(&wm->dePrettyName, FF_DE_PRETTY_GNOME_CLASSIC)) { + return detectMutter(themeOrError); + } else { + return detectGTKThemeAsWMTheme(themeOrError); + } + } + + if (ffStrbufIgnCaseEqualS(&wm->wmPrettyName, FF_WM_PRETTY_MUFFIN)) { + return detectMuffin(themeOrError); + } + + if (ffStrbufIgnCaseEqualS(&wm->wmPrettyName, FF_WM_PRETTY_MARCO)) { + return detectWMThemeFromSettings("/org/mate/Marco/general/theme", "org.mate.Marco.general", NULL, "theme", themeOrError); + } + + if (ffStrbufIgnCaseEqualS(&wm->wmPrettyName, FF_WM_PRETTY_OPENBOX)) { + return detectOpenbox(&wm->dePrettyName, themeOrError); + } + + if (ffStrbufIgnCaseEqualS(&wm->wmPrettyName, FF_WM_PRETTY_ENLIGHTENMENT)) { + return detectGTKThemeAsWMTheme(themeOrError); + } + + ffStrbufAppendS(themeOrError, "Unknown WM: "); + ffStrbufAppend(themeOrError, &wm->wmPrettyName); + return false; +} diff --git a/src/detection/wmtheme/wmtheme_nosupport.c b/src/detection/wmtheme/wmtheme_nosupport.c new file mode 100644 index 0000000..8cba51a --- /dev/null +++ b/src/detection/wmtheme/wmtheme_nosupport.c @@ -0,0 +1,7 @@ +#include "fastfetch.h" +#include "wmtheme.h" + +bool ffDetectWmTheme(FFstrbuf* themeOrError) { + ffStrbufAppendS(themeOrError, "Not supported on this platform"); + return false; +} diff --git a/src/detection/wmtheme/wmtheme_windows.c b/src/detection/wmtheme/wmtheme_windows.c new file mode 100644 index 0000000..5d16a84 --- /dev/null +++ b/src/detection/wmtheme/wmtheme_windows.c @@ -0,0 +1,182 @@ +#include "fastfetch.h" +#include "wmtheme.h" +#include "common/windows/registry.h" + +const char* colorHexToString(DWORD hex) { + switch (hex) { + case 0x696cc3: + return "Yellow gold"; + case 0xff8c00: + return "Gold"; + case 0xf7630c: + return "Orange bright"; + case 0xca5010: + return "Orange dark"; + case 0xda3b01: + return "Rust"; + case 0xef6950: + return "Pale rust"; + case 0xd13438: + return "Brick red"; + case 0xff4343: + return "Mod red"; + case 0xe74856: + return "Pale red"; + case 0xe81123: + return "Red"; + case 0xea005e: + return "Rose bright"; + case 0xc30052: + return "Rose"; + case 0xe3008c: + return "Plum light"; + case 0xbf0077: + return "Plum"; + case 0xc239b3: + return "Orchid light"; + case 0x9a0089: + return "Orchid"; + case 0x0078d4: + return "Blue"; + case 0x0063b1: + return "Navy blue"; + case 0x8d8bd7: + return "Purple shadow"; + case 0x6b69d6: + return "Purple shadow dark"; + case 0x8764b8: + return "Iris pastel"; + case 0x744da9: + return "Iris Spring"; + case 0xb146c2: + return "Violet red light"; + case 0x881798: + return "Violet red"; + case 0x0099bc: + return "Cool blue bright"; + case 0x2d7d9a: + return "Cool blue"; + case 0x00b7c3: + return "Seafoam"; + case 0x038387: + return "Seafoam teal"; + case 0x00b294: + return "Mint light"; + case 0x018574: + return "Mint dark"; + case 0x00cc6a: + return "Turf green"; + case 0x10893e: + return "Sport green"; + case 0x7a7574: + return "Gray"; + case 0x5d5a58: + return "Gray brown"; + case 0x68768a: + return "Steel blue"; + case 0x515c6b: + return "Metal blue"; + case 0x567c73: + return "Pale moss"; + case 0x486860: + return "Moss"; + case 0x498205: + return "Meadow green"; + case 0x107c10: + return "Green"; + case 0x767676: + return "Overcast"; + case 0x4c4a48: + return "Storm"; + case 0x69797e: + return "Blue gray"; + case 0x4a5459: + return "Gray dark"; + case 0x647c64: + return "Liddy green"; + case 0x4c574e: + return "Sage"; + case 0x807143: + return "Camouflage desert"; + case 0x766c59: + return "Camouflage"; + case 0x000000: + return "Black"; + case 0xFFFFFF: + return "White"; + default: + return NULL; + } +} + +bool ffDetectWmTheme(FFstrbuf* themeOrError) { + { + FF_AUTO_CLOSE_FD HANDLE hKey = NULL; + if (ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes", &hKey, NULL)) { + FF_STRBUF_AUTO_DESTROY theme = ffStrbufCreate(); + if (ffRegReadStrbuf(hKey, L"CurrentTheme", &theme, NULL)) { + ffStrbufSubstrBeforeLastC(&theme, '.'); + ffStrbufSubstrAfterLastC(&theme, '\\'); + if (isalpha(theme.chars[0])) { + theme.chars[0] = (char) toupper(theme.chars[0]); + } + + ffStrbufAppend(themeOrError, &theme); + } + } + } + + do { + uint32_t rgbColor; + uint32_t bgrColor; + FF_AUTO_CLOSE_FD HANDLE hKey = NULL; + if (ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\DWM", &hKey, NULL)) { + if (ffRegReadUint(hKey, L"AccentColor", &bgrColor, NULL)) { + rgbColor = ((bgrColor & 0xFF) << 16) | (bgrColor & 0xFF00) | ((bgrColor >> 16) & 0xFF); + } else if (ffRegReadUint(hKey, L"ColorizationColor", &rgbColor, NULL)) { + rgbColor &= 0xFFFFFF; + } else { + break; + } + } else { + break; + } + + if (themeOrError->length > 0) { + ffStrbufAppendS(themeOrError, " - "); + } + const char* text = colorHexToString(rgbColor); + if (text) { + ffStrbufAppendS(themeOrError, text); + } else { + ffStrbufAppendF(themeOrError, "#%06lX", (long) rgbColor); + } + } while (false); + + { + FF_AUTO_CLOSE_FD HANDLE hKey = NULL; + if (ffRegOpenKeyForRead(HKEY_CURRENT_USER, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", &hKey, NULL)) { + uint32_t system = 1, apps = 1; + if (ffRegReadValues(hKey, 2, (FFRegValueArg[]) { + FF_ARG(system, L"SystemUsesLightTheme"), + FF_ARG(apps, L"AppsUseLightTheme"), + }, + NULL)) { + bool paren = themeOrError->length > 0; + if (paren) { + ffStrbufAppendS(themeOrError, " ("); + } + ffStrbufAppendF(themeOrError, "System: %s, Apps: %s", system ? "Light" : "Dark", apps ? "Light" : "Dark"); + if (paren) { + ffStrbufAppendC(themeOrError, ')'); + } + } + } + } + + if (themeOrError->length == 0) { + ffStrbufSetStatic(themeOrError, "Failed to find current theme"); + return false; + } + return true; +} diff --git a/src/detection/zpool/libzfs_simplified.h b/src/detection/zpool/libzfs_simplified.h new file mode 100644 index 0000000..5faf4ec --- /dev/null +++ b/src/detection/zpool/libzfs_simplified.h @@ -0,0 +1,48 @@ +#pragma once + +#include "fastfetch.h" + +// From https://github.com/openzfs/zfs/blob/master/include/libzfs.h + +/* + * CDDL HEADER START + * + * The contents of this file are subject to the terms of the + * Common Development and Distribution License (the "License"). + * You may not use this file except in compliance with the License. + * + * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE + * or https://opensource.org/licenses/CDDL-1.0. + * See the License for the specific language governing permissions + * and limitations under the License. + * + * When distributing Covered Code, include this CDDL HEADER in each + * file and include the License file at usr/src/OPENSOLARIS.LICENSE. + * If applicable, add the following below this CDDL HEADER, with the + * fields enclosed by brackets "[]" replaced with your own identifying + * information: Portions Copyright [yyyy] [name of copyright owner] + * + * CDDL HEADER END + */ + +// zpool_prop_t and zprop_source_t were previously enums in upstream OpenZFS. +// However, the enum values for these types vary greatly between different platforms, +// making it unsafe to use the enum values directly. To ensure portability, +// we define them as simple int typedefs and use zpool_name_to_prop to look up +// the correct value for a property at runtime. +typedef int zpool_prop_t; +typedef int zprop_source_t; +typedef int boolean_t; + +typedef struct libzfs_handle libzfs_handle_t; +typedef struct zpool_handle zpool_handle_t; +typedef int (*zpool_iter_f)(zpool_handle_t*, void*); + +extern libzfs_handle_t* libzfs_init(void); +extern void libzfs_fini(libzfs_handle_t*); +extern int zpool_iter(libzfs_handle_t*, zpool_iter_f, void*); +extern zpool_prop_t zpool_name_to_prop(const char*); +// https://github.com/openzfs/zfs/blob/06c73cffabc30b61a695988ec8e290f43cb3768d/lib/libzfs/libzfs_pool.c#L300 +extern uint64_t zpool_get_prop_int(zpool_handle_t* zhp, zpool_prop_t prop, zprop_source_t* srctype); +extern int zpool_get_prop(zpool_handle_t* zhp, zpool_prop_t prop, char* buf, size_t len, zprop_source_t* srctype, boolean_t literal); +extern void zpool_close(zpool_handle_t*); diff --git a/src/detection/zpool/zpool.c b/src/detection/zpool/zpool.c new file mode 100644 index 0000000..ce7abb3 --- /dev/null +++ b/src/detection/zpool/zpool.c @@ -0,0 +1,130 @@ +#include "zpool.h" + +#if FF_HAVE_LIBZFS + + #include "common/kmod.h" + + #ifdef __sun + #include <libzfs.h> + #ifndef __illumos__ + // On Solaris 11, zpool_get_prop has only 5 arguments. #2173 + #define ffzpool_get_prop(zhp, prop, buf, len, srctype, literal) \ + ffzpool_get_prop(zhp, prop, buf, len, srctype) + #endif + #else + #include "libzfs_simplified.h" + #endif + + #include "common/library.h" + +typedef struct FFZfsData { + FF_LIBRARY_SYMBOL(libzfs_fini) + FF_LIBRARY_SYMBOL(zpool_get_prop_int) + FF_LIBRARY_SYMBOL(zpool_get_prop) + FF_LIBRARY_SYMBOL(zpool_close) + + // The fields in this struct store property IDs returned by `zpool_name_to_prop`, + // not the property values themselves. + struct { + int name; + int health; + int guid; + int size; + int free; + int allocated; + int fragmentation; + int readonly; + } props; + + libzfs_handle_t* handle; + FFlist* result; +} FFZfsData; + +static inline void cleanLibzfs(FFZfsData* data) { + if (data->fflibzfs_fini && data->handle) { + data->fflibzfs_fini(data->handle); + data->handle = NULL; + } +} + +static int enumZpoolCallback(zpool_handle_t* zpool, void* param) { + FFZfsData* data = (FFZfsData*) param; + zprop_source_t source; + FFZpoolResult* item = FF_LIST_ADD(FFZpoolResult, *data->result); + char buf[1024]; + if (data->ffzpool_get_prop(zpool, data->props.name, buf, ARRAY_SIZE(buf), &source, false) == 0) { + ffStrbufInitS(&item->name, buf); + } else { + ffStrbufInitStatic(&item->name, "unknown"); + } + if (data->ffzpool_get_prop(zpool, data->props.health, buf, ARRAY_SIZE(buf), &source, false) == 0) { + ffStrbufInitS(&item->state, buf); + } else { + ffStrbufInitStatic(&item->state, "unknown"); + } + item->guid = data->ffzpool_get_prop_int(zpool, data->props.guid, &source); + item->total = data->ffzpool_get_prop_int(zpool, data->props.size, &source); + item->used = item->total - data->ffzpool_get_prop_int(zpool, data->props.free, &source); + item->allocated = data->ffzpool_get_prop_int(zpool, data->props.allocated, &source); + uint64_t fragmentation = data->ffzpool_get_prop_int(zpool, data->props.fragmentation, &source); + item->fragmentation = fragmentation == UINT64_MAX ? -DBL_MAX : (double) fragmentation; + item->readOnly = (bool) data->ffzpool_get_prop_int(zpool, data->props.readonly, &source); + data->ffzpool_close(zpool); + return 0; +} + +const char* ffDetectZpool(FFlist* result /* list of FFZpoolResult */) { + FF_LIBRARY_LOAD_MESSAGE(libzfs, "libzfs" FF_LIBRARY_EXTENSION, 6); + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libzfs, libzfs_init); + + libzfs_handle_t* handle = fflibzfs_init(); + if (!handle) { + if (!ffKmodLoaded("zfs")) { + return "`zfs` kernel module is not loaded"; + } + return "libzfs_init() failed"; + } + + FF_A_CLEANUP(cleanLibzfs) FFZfsData data = { + .handle = handle, + .result = result, + }; + + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libzfs, zpool_name_to_prop); + + #define FF_QUERY_ZPOOL_PROP_FROM_NAME(prop_name) \ + do { \ + data.props.prop_name = ffzpool_name_to_prop(#prop_name); \ + if (data.props.prop_name < 0) \ + return "Failed to query prop: " #prop_name; \ + } while (false) + FF_QUERY_ZPOOL_PROP_FROM_NAME(name); + FF_QUERY_ZPOOL_PROP_FROM_NAME(health); + FF_QUERY_ZPOOL_PROP_FROM_NAME(guid); + FF_QUERY_ZPOOL_PROP_FROM_NAME(size); + FF_QUERY_ZPOOL_PROP_FROM_NAME(free); + FF_QUERY_ZPOOL_PROP_FROM_NAME(allocated); + FF_QUERY_ZPOOL_PROP_FROM_NAME(fragmentation); + FF_QUERY_ZPOOL_PROP_FROM_NAME(readonly); + #undef FF_QUERY_ZPOOL_PROP_FROM_NAME + + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(libzfs, zpool_iter); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libzfs, data, libzfs_fini); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libzfs, data, zpool_get_prop_int); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libzfs, data, zpool_get_prop); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libzfs, data, zpool_close); + + if (ffzpool_iter(handle, enumZpoolCallback, &data) < 0) { + return "zpool_iter() failed"; + } + + return NULL; +} + +#else + +const char* ffDetectZpool(FF_A_UNUSED FFlist* result) { + return "fastfetch was compiled without libzfs support"; +} + +#endif diff --git a/src/detection/zpool/zpool.h b/src/detection/zpool/zpool.h new file mode 100644 index 0000000..2e6523f --- /dev/null +++ b/src/detection/zpool/zpool.h @@ -0,0 +1,17 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/zpool/option.h" + +typedef struct FFZpoolResult { + FFstrbuf name; + FFstrbuf state; + uint64_t guid; + uint64_t used; + uint64_t total; + uint64_t allocated; + double fragmentation; + bool readOnly; +} FFZpoolResult; + +const char* ffDetectZpool(FFlist* result /* list of FFZpoolResult */); |