fix(app): improve macos version and arch detection (#36)
This commit is contained in:
parent
7f219a9e1e
commit
ea0a2d8aae
@ -28,6 +28,8 @@ export interface DesktopInfo {
|
|||||||
version: string;
|
version: string;
|
||||||
channel: 'stable' | 'canary';
|
channel: 'stable' | 'canary';
|
||||||
arch: string;
|
arch: string;
|
||||||
|
hardwareArch: string;
|
||||||
|
runningUnderRosetta: boolean;
|
||||||
os: NodeJS.Platform;
|
os: NodeJS.Platform;
|
||||||
osVersion: string;
|
osVersion: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import child_process from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import https from 'node:https';
|
import https from 'node:https';
|
||||||
@ -193,6 +194,39 @@ const ensureRpId = (value: string | undefined, context: string): string => {
|
|||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runSysctl = (query: string): string | null => {
|
||||||
|
try {
|
||||||
|
return child_process
|
||||||
|
.execSync(query, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
|
})
|
||||||
|
.trim();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const detectRosettaMode = (): boolean => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translated = runSysctl('sysctl -n sysctl.proc_translated');
|
||||||
|
return translated === '1';
|
||||||
|
};
|
||||||
|
|
||||||
|
const detectHardwareArch = (): string => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
return os.arch();
|
||||||
|
}
|
||||||
|
const optionalArm64 = runSysctl('sysctl -n hw.optional.arm64');
|
||||||
|
if (optionalArm64 === '1') {
|
||||||
|
return 'arm64';
|
||||||
|
}
|
||||||
|
return os.arch();
|
||||||
|
};
|
||||||
|
|
||||||
const convertMacCreationOptions = (options: PublicKeyCredentialCreationOptionsJSON): CreateCredentialOptions => ({
|
const convertMacCreationOptions = (options: PublicKeyCredentialCreationOptionsJSON): CreateCredentialOptions => ({
|
||||||
rpId: ensureRpId(options.rp.id, 'registration'),
|
rpId: ensureRpId(options.rp.id, 'registration'),
|
||||||
userId: base64UrlToBuffer(options.user.id).toString('base64'),
|
userId: base64UrlToBuffer(options.user.id).toString('base64'),
|
||||||
@ -374,6 +408,8 @@ export function registerIpcHandlers(): void {
|
|||||||
version: app.getVersion(),
|
version: app.getVersion(),
|
||||||
channel: BUILD_CHANNEL,
|
channel: BUILD_CHANNEL,
|
||||||
arch: process.arch,
|
arch: process.arch,
|
||||||
|
hardwareArch: detectHardwareArch(),
|
||||||
|
runningUnderRosetta: detectRosettaMode(),
|
||||||
os: process.platform,
|
os: process.platform,
|
||||||
osVersion: os.release(),
|
osVersion: os.release(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -19,19 +19,32 @@
|
|||||||
|
|
||||||
import {Trans, useLingui} from '@lingui/react/macro';
|
import {Trans, useLingui} from '@lingui/react/macro';
|
||||||
import {observer} from 'mobx-react-lite';
|
import {observer} from 'mobx-react-lite';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
import * as TextCopyActionCreators from '~/actions/TextCopyActionCreators';
|
import * as TextCopyActionCreators from '~/actions/TextCopyActionCreators';
|
||||||
import * as ToastActionCreators from '~/actions/ToastActionCreators';
|
import * as ToastActionCreators from '~/actions/ToastActionCreators';
|
||||||
import Config from '~/Config';
|
import Config from '~/Config';
|
||||||
import FocusRing from '~/components/uikit/FocusRing/FocusRing';
|
import FocusRing from '~/components/uikit/FocusRing/FocusRing';
|
||||||
import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
|
import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
|
||||||
import DeveloperModeStore from '~/stores/DeveloperModeStore';
|
import DeveloperModeStore from '~/stores/DeveloperModeStore';
|
||||||
import {getClientInfoSync} from '~/utils/ClientInfoUtils';
|
import {getClientInfo, getClientInfoSync} from '~/utils/ClientInfoUtils';
|
||||||
import * as DateUtils from '~/utils/DateUtils';
|
import * as DateUtils from '~/utils/DateUtils';
|
||||||
|
import {isDesktop} from '~/utils/NativeUtils';
|
||||||
import styles from './ClientInfo.module.css';
|
import styles from './ClientInfo.module.css';
|
||||||
|
|
||||||
export const ClientInfo = observer(() => {
|
export const ClientInfo = observer(() => {
|
||||||
const {t, i18n} = useLingui();
|
const {t, i18n} = useLingui();
|
||||||
const clientInfo = getClientInfoSync();
|
const [clientInfo, setClientInfo] = useState(getClientInfoSync());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let mounted = true;
|
||||||
|
void getClientInfo().then((info) => {
|
||||||
|
if (!mounted) return;
|
||||||
|
setClientInfo(info);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const buildShaShort = (Config.PUBLIC_BUILD_SHA ?? '').slice(0, 7);
|
const buildShaShort = (Config.PUBLIC_BUILD_SHA ?? '').slice(0, 7);
|
||||||
const buildNumber = Config.PUBLIC_BUILD_NUMBER;
|
const buildNumber = Config.PUBLIC_BUILD_NUMBER;
|
||||||
@ -41,10 +54,21 @@ export const ClientInfo = observer(() => {
|
|||||||
const browserName = clientInfo.browserName || 'Unknown';
|
const browserName = clientInfo.browserName || 'Unknown';
|
||||||
const browserVersion = clientInfo.browserVersion || '';
|
const browserVersion = clientInfo.browserVersion || '';
|
||||||
const osName = clientInfo.osName || 'Unknown';
|
const osName = clientInfo.osName || 'Unknown';
|
||||||
const osVersion =
|
const rawOsVersion = clientInfo.osVersion ?? '';
|
||||||
clientInfo.osVersion && clientInfo.arch
|
const isDesktopApp = isDesktop();
|
||||||
? `${clientInfo.osVersion} (${clientInfo.arch})`
|
const osArchitecture = clientInfo.desktopArch ?? clientInfo.arch;
|
||||||
: clientInfo.osVersion || clientInfo.arch || '';
|
const shouldShowOsVersion = Boolean(rawOsVersion) && (isDesktopApp || osName !== 'macOS');
|
||||||
|
const osVersionForDisplay = shouldShowOsVersion ? rawOsVersion : undefined;
|
||||||
|
|
||||||
|
const buildOsDescription = () => {
|
||||||
|
const parts = [osName];
|
||||||
|
if (osVersionForDisplay) {
|
||||||
|
parts.push(osVersionForDisplay);
|
||||||
|
}
|
||||||
|
const archSuffix = osArchitecture ? ` (${osArchitecture})` : '';
|
||||||
|
return `${parts.join(' ')}${archSuffix}`.trim();
|
||||||
|
};
|
||||||
|
const osDescription = buildOsDescription();
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
let timestamp = '';
|
let timestamp = '';
|
||||||
@ -68,7 +92,7 @@ export const ClientInfo = observer(() => {
|
|||||||
|
|
||||||
TextCopyActionCreators.copy(
|
TextCopyActionCreators.copy(
|
||||||
i18n,
|
i18n,
|
||||||
`${Config.PUBLIC_PROJECT_ENV} ${buildInfo}${timestamp}, ${browserName} ${browserVersion}, ${osName} ${osVersion}${desktopInfo}`,
|
`${Config.PUBLIC_PROJECT_ENV} ${buildInfo}${timestamp}, ${browserName} ${browserVersion}, ${osDescription}${desktopInfo}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -92,9 +116,7 @@ export const ClientInfo = observer(() => {
|
|||||||
<span>
|
<span>
|
||||||
{browserName} {browserVersion}
|
{browserName} {browserVersion}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>{osDescription}</span>
|
||||||
{osName} {osVersion}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</FocusRing>
|
</FocusRing>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -31,10 +31,125 @@ interface ClientInfo {
|
|||||||
desktopChannel?: string;
|
desktopChannel?: string;
|
||||||
desktopArch?: string;
|
desktopArch?: string;
|
||||||
desktopOS?: string;
|
desktopOS?: string;
|
||||||
|
desktopRunningUnderRosetta?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NavigatorHighEntropyHints = {
|
||||||
|
architecture?: string;
|
||||||
|
bitness?: string;
|
||||||
|
platform?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NavigatorUADataLike = NavigatorHighEntropyHints & {
|
||||||
|
getHighEntropyValues?: (hints: ReadonlyArray<keyof NavigatorHighEntropyHints>) => Promise<NavigatorHighEntropyHints>;
|
||||||
|
};
|
||||||
|
|
||||||
const normalize = <T>(value: T | null | undefined): T | undefined => value ?? undefined;
|
const normalize = <T>(value: T | null | undefined): T | undefined => value ?? undefined;
|
||||||
|
|
||||||
|
const ARCHITECTURE_PATTERNS: ReadonlyArray<{pattern: RegExp; label: string}> = [
|
||||||
|
{pattern: /\barm64\b|\baarch64\b|\barmv8\b|\barm64e\b/i, label: 'arm64'},
|
||||||
|
{pattern: /\barm\b|\barmv7\b|\barmv6\b/i, label: 'arm'},
|
||||||
|
{pattern: /MacIntel/i, label: 'x64'},
|
||||||
|
{pattern: /\bx86_64\b|\bx64\b|\bamd64\b|\bwin64\b|\bwow64\b/i, label: 'x64'},
|
||||||
|
{pattern: /\bx86\b|\bi[3-6]86\b/i, label: 'x86'},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const normalizeArchitectureValue = (value: string | null | undefined): string | undefined => {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
for (const entry of ARCHITECTURE_PATTERNS) {
|
||||||
|
if (entry.pattern.test(trimmed)) {
|
||||||
|
return entry.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmed || undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNavigatorObject = (): Navigator | undefined => {
|
||||||
|
if (typeof navigator === 'undefined') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return navigator;
|
||||||
|
};
|
||||||
|
|
||||||
|
const detectAppleSiliconViaWebGL = (): string | undefined => {
|
||||||
|
if (typeof document === 'undefined') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const gl =
|
||||||
|
(canvas.getContext('webgl') as WebGLRenderingContext | null) ??
|
||||||
|
(canvas.getContext('experimental-webgl') as WebGLRenderingContext | null);
|
||||||
|
if (!gl) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
||||||
|
if (!ext) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
|
||||||
|
if (typeof renderer !== 'string') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (/apple m|apple gpu/i.test(renderer)) {
|
||||||
|
return 'arm64';
|
||||||
|
}
|
||||||
|
if (/intel/i.test(renderer)) {
|
||||||
|
return 'x64';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isNavigatorPlatformMac = (nav: Navigator): boolean => {
|
||||||
|
const platform = nav.platform ?? '';
|
||||||
|
return /^(mac|darwin)/i.test(platform) || /Macintosh|Mac OS X/i.test(nav.userAgent ?? '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const detectArchitectureFromNavigator = (): string | undefined => {
|
||||||
|
const nav = getNavigatorObject();
|
||||||
|
if (!nav) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAgentData = (nav as Navigator & {userAgentData?: NavigatorUADataLike}).userAgentData;
|
||||||
|
if (userAgentData?.architecture) {
|
||||||
|
return normalizeArchitectureValue(userAgentData.architecture);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAgent = nav.userAgent ?? '';
|
||||||
|
const platform = nav.platform ?? '';
|
||||||
|
const isMac = isNavigatorPlatformMac(nav);
|
||||||
|
|
||||||
|
if (isMac) {
|
||||||
|
const detected = detectAppleSiliconViaWebGL();
|
||||||
|
if (detected) {
|
||||||
|
return detected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of ARCHITECTURE_PATTERNS) {
|
||||||
|
if (entry.pattern.test(userAgent)) {
|
||||||
|
if (isMac && entry.label === 'x64') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return entry.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of ARCHITECTURE_PATTERNS) {
|
||||||
|
if (entry.pattern.test(platform)) {
|
||||||
|
if (isMac && entry.label === 'x64') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return entry.label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
let cachedClientInfo: ClientInfo | null = null;
|
let cachedClientInfo: ClientInfo | null = null;
|
||||||
let preloadPromise: Promise<ClientInfo> | null = null;
|
let preloadPromise: Promise<ClientInfo> | null = null;
|
||||||
|
|
||||||
@ -43,12 +158,15 @@ const parseUserAgent = (): ClientInfo => {
|
|||||||
const userAgent = hasNavigator ? navigator.userAgent : '';
|
const userAgent = hasNavigator ? navigator.userAgent : '';
|
||||||
const parser = Bowser.getParser(userAgent);
|
const parser = Bowser.getParser(userAgent);
|
||||||
const result = parser.getResult();
|
const result = parser.getResult();
|
||||||
|
const isMac = hasNavigator && isNavigatorPlatformMac(navigator);
|
||||||
|
const fallbackArch = hasNavigator && !isMac ? normalizeArchitectureValue(navigator.platform) : undefined;
|
||||||
|
const arch = detectArchitectureFromNavigator() ?? fallbackArch;
|
||||||
return {
|
return {
|
||||||
browserName: normalize(result.browser.name),
|
browserName: normalize(result.browser.name),
|
||||||
browserVersion: normalize(result.browser.version),
|
browserVersion: normalize(result.browser.version),
|
||||||
osName: normalize(result.os.name),
|
osName: normalize(result.os.name),
|
||||||
osVersion: normalize(result.os.version),
|
osVersion: normalize(result.os.version),
|
||||||
arch: normalize(hasNavigator ? navigator.platform : undefined),
|
arch: arch,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -85,8 +203,9 @@ async function getDesktopContext(): Promise<Partial<ClientInfo>> {
|
|||||||
return {
|
return {
|
||||||
desktopVersion: normalize(desktopInfo.version),
|
desktopVersion: normalize(desktopInfo.version),
|
||||||
desktopChannel: normalize(desktopInfo.channel),
|
desktopChannel: normalize(desktopInfo.channel),
|
||||||
desktopArch: normalize(desktopInfo.arch),
|
desktopArch: normalizeArchitectureValue(desktopInfo.hardwareArch ?? desktopInfo.arch),
|
||||||
desktopOS: normalize(desktopInfo.os),
|
desktopOS: normalize(desktopInfo.os),
|
||||||
|
desktopRunningUnderRosetta: desktopInfo.runningUnderRosetta,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[ClientInfo] Failed to load desktop context', error);
|
console.warn('[ClientInfo] Failed to load desktop context', error);
|
||||||
@ -112,13 +231,51 @@ function getWindowsVersionName(osVersion: string): string {
|
|||||||
return 'Windows';
|
return 'Windows';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const detectArchitectureFromClientHints = async (): Promise<string | undefined> => {
|
||||||
|
const nav = getNavigatorObject();
|
||||||
|
if (!nav) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const userAgentData = (nav as Navigator & {userAgentData?: NavigatorUADataLike}).userAgentData;
|
||||||
|
if (!userAgentData?.getHighEntropyValues) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const hints = await userAgentData.getHighEntropyValues(['architecture', 'bitness']);
|
||||||
|
const archHint = hints.architecture?.toLowerCase() ?? '';
|
||||||
|
const bitness = hints.bitness?.toLowerCase() ?? '';
|
||||||
|
const platform = (userAgentData.platform ?? '').toLowerCase();
|
||||||
|
|
||||||
|
if (platform === 'windows') {
|
||||||
|
if (archHint === 'arm') {
|
||||||
|
return 'arm64';
|
||||||
|
}
|
||||||
|
if (archHint === 'x86' && bitness === '64') {
|
||||||
|
return 'x64';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (archHint.includes('arm')) {
|
||||||
|
return 'arm64';
|
||||||
|
}
|
||||||
|
if (archHint.includes('intel') || archHint.includes('x64')) {
|
||||||
|
return 'x64';
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeArchitectureValue(archHint);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[ClientInfo] Failed to load architecture hints', error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function getOsContext(): Promise<Partial<ClientInfo>> {
|
async function getOsContext(): Promise<Partial<ClientInfo>> {
|
||||||
const electronApi = getElectronAPI();
|
const electronApi = getElectronAPI();
|
||||||
if (electronApi) {
|
if (electronApi) {
|
||||||
try {
|
try {
|
||||||
const desktopInfo = await electronApi.getDesktopInfo();
|
const desktopInfo = await electronApi.getDesktopInfo();
|
||||||
let osName: string | undefined;
|
let osName: string | undefined;
|
||||||
let osVersion: string | undefined;
|
|
||||||
|
|
||||||
switch (desktopInfo.os) {
|
switch (desktopInfo.os) {
|
||||||
case 'darwin':
|
case 'darwin':
|
||||||
@ -126,7 +283,6 @@ async function getOsContext(): Promise<Partial<ClientInfo>> {
|
|||||||
break;
|
break;
|
||||||
case 'win32':
|
case 'win32':
|
||||||
osName = getWindowsVersionName(desktopInfo.osVersion);
|
osName = getWindowsVersionName(desktopInfo.osVersion);
|
||||||
osVersion = desktopInfo.osVersion;
|
|
||||||
break;
|
break;
|
||||||
case 'linux':
|
case 'linux':
|
||||||
osName = 'Linux';
|
osName = 'Linux';
|
||||||
@ -136,8 +292,8 @@ async function getOsContext(): Promise<Partial<ClientInfo>> {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
osName,
|
osName,
|
||||||
osVersion,
|
osVersion: normalize(desktopInfo.osVersion),
|
||||||
arch: normalize(desktopInfo.arch),
|
arch: normalizeArchitectureValue(desktopInfo.arch),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[ClientInfo] Failed to load OS context', error);
|
console.warn('[ClientInfo] Failed to load OS context', error);
|
||||||
@ -150,7 +306,10 @@ async function getOsContext(): Promise<Partial<ClientInfo>> {
|
|||||||
|
|
||||||
export const getClientInfo = async (): Promise<ClientInfo> => {
|
export const getClientInfo = async (): Promise<ClientInfo> => {
|
||||||
const base = getClientInfoSync();
|
const base = getClientInfoSync();
|
||||||
if (!isDesktop()) return base;
|
if (!isDesktop()) {
|
||||||
|
const hintsArch = await detectArchitectureFromClientHints();
|
||||||
|
return {...base, arch: hintsArch ?? base.arch};
|
||||||
|
}
|
||||||
|
|
||||||
const [osContext, desktop] = await Promise.all([getOsContext(), getDesktopContext()]);
|
const [osContext, desktop] = await Promise.all([getOsContext(), getDesktopContext()]);
|
||||||
return {...base, ...osContext, ...desktop};
|
return {...base, ...osContext, ...desktop};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user