Use dmidecode to read cpuid in non ARM devices

Cpu id is set to null so far for non ARM devices (e.g. Intel NUC). This
parses the output of dmidecode to get the cpu id and system model.

Change-type: patch
This commit is contained in:
Felipe Lalanne 2022-01-06 20:32:50 +00:00
parent c7fc7aacf8
commit d06b8e053e
2 changed files with 187 additions and 64 deletions

View File

@ -1,5 +1,6 @@
import * as systeminformation from 'systeminformation';
import * as _ from 'lodash';
import * as memoizee from 'memoizee';
import { promises as fs } from 'fs';
import { exec } from './fs-utils';
@ -75,9 +76,9 @@ export async function getSystemId(): Promise<string | undefined> {
return buffer.toString('utf-8').replace(/\0/g, '');
} catch {
// Otherwise use dmidecode
const [baseBoardInfo] = (await dmidecode('baseboard')).filter(
(entry) => entry.type === 'Base Board Information',
);
const [baseBoardInfo] = (
await dmidecode('baseboard').catch(() => [] as DmiDecodeInfo[])
).filter((entry) => entry.type === 'Base Board Information');
return baseBoardInfo?.values?.['Serial Number'] || undefined;
}
}
@ -88,9 +89,9 @@ export async function getSystemModel(): Promise<string | undefined> {
// Remove the null byte at the end
return buffer.toString('utf-8').replace(/\0/g, '');
} catch {
const [baseBoardInfo] = (await dmidecode('baseboard')).filter(
(entry) => entry.type === 'Base Board Information',
);
const [baseBoardInfo] = (
await dmidecode('baseboard').catch(() => [] as DmiDecodeInfo[])
).filter((entry) => entry.type === 'Base Board Information');
// Join manufacturer and product name in a single string
return (
@ -104,51 +105,58 @@ export async function getSystemModel(): Promise<string | undefined> {
}
}
export type DmiDecodeInfo = { type: string; values: { [key: string]: string } };
/**
* Parse the output of dmidecode and return an array of
* objects {type: string, values: string[]}
*
* This only parses simple key,value pairs from the output
* of dmidecode, multiline strings and arrays are ignored
*
* The output of the command is memoized
*/
export async function dmidecode(t: string) {
const { stdout: info } = await exec(`dmidecode -t ${t}`);
return (
info
.toString()
.split(/\r?\n/) // Split by line jumps
// Split into groups by looking for empty lines
.reduce((groups, line) => {
const currentGroup = groups.pop() || [];
if (/^\s*$/.test(line)) {
// For each empty line create a new group
groups.push(currentGroup);
groups.push([]);
} else {
// Otherwise append the line to the group
currentGroup.push(line);
groups.push(currentGroup);
}
return groups;
}, [] as string[][])
// Only select the handles
.filter((group) => group.length > 1 && /^Handle/.test(group[0]))
.map(([, type, ...lines]) => ({
type,
values: lines
// Only select lines that match 'key: value', this will exclude multiline strings
// and arrays (we don't care about those for these purposes)
.filter((line) => /^\s+[^:]+: .+$/.test(line))
.map((line) => {
const [key, value] = line.split(':').map((s) => s.trim());
// Finally convert the lines into key value pairs
return { [key]: value };
})
// And merge
.reduce((vals, v) => ({ ...vals, ...v }), {}),
}))
);
}
export const dmidecode = memoizee(
async (t: string): Promise<DmiDecodeInfo[]> => {
const { stdout: info } = await exec(`dmidecode -t ${t}`);
return (
info
.toString()
.split(/\r?\n/) // Split by line jumps
// Split into groups by looking for empty lines
.reduce((groups, line) => {
const currentGroup = groups.pop() || [];
if (/^\s*$/.test(line)) {
// For each empty line create a new group
groups.push(currentGroup);
groups.push([]);
} else {
// Otherwise append the line to the group
currentGroup.push(line);
groups.push(currentGroup);
}
return groups;
}, [] as string[][])
// Only select the handles
.filter((group) => group.length > 1 && /^Handle/.test(group[0]))
.map(([, type, ...lines]) => ({
type,
values: lines
// Only select lines that match 'key: value', this will exclude multiline strings
// and arrays (we don't care about those for these purposes)
.filter((line) => /^\s+[^:]+: .+$/.test(line))
.map((line) => {
const [key, value] = line.split(':').map((s) => s.trim());
// Finally convert the lines into key value pairs
return { [key]: value };
})
// And merge
.reduce((vals, v) => ({ ...vals, ...v }), {}),
}))
);
},
{ promise: true },
);
const undervoltageRegex = /under.*voltage/i;
export async function undervoltageDetected(): Promise<boolean> {

View File

@ -95,11 +95,58 @@ describe('System information', () => {
// Make sure it's the right number given the mocked data
expect(tempInfo).to.equal(51);
});
});
it('gets CPU ID', async () => {
describe('baseboard information', () => {
afterEach(() => {
(fs.readFile as SinonStub).reset();
(fsUtils.exec as SinonStub).reset();
});
// Do these two tests first so the dmidecode call is not memoized yet
it('returns undefined system model if dmidecode throws', async () => {
(fs.readFile as SinonStub).rejects('Not found');
(fsUtils.exec as SinonStub).rejects('Something bad happened');
const systemModel = await sysInfo.getSystemModel();
expect(systemModel).to.be.undefined;
});
it('returns undefined system ID if dmidecode throws', async () => {
(fs.readFile as SinonStub).rejects('Not found');
(fsUtils.exec as SinonStub).rejects('Something bad happened');
const systemId = await sysInfo.getSystemId();
expect(systemId).to.be.undefined;
});
it('gets system ID', async () => {
(fs.readFile as SinonStub).resolves(mockCPU.idBuffer);
const cpuId = await sysInfo.getSystemId();
expect(cpuId).to.equal('1000000001b93f3f');
});
it('gets system ID from dmidecode if /proc/device-tree/serial-number is not available', async () => {
(fs.readFile as SinonStub).rejects('Not found');
(fsUtils.exec as SinonStub).resolves({
stdout: mockCPU.dmidecode,
});
const cpuId = await sysInfo.getSystemId();
expect(cpuId).to.equal('GEBN94600PWW');
});
it('gets system model', async () => {
(fs.readFile as SinonStub).resolves('Raspberry PI 4');
const systemModel = await sysInfo.getSystemModel();
expect(systemModel).to.equal('Raspberry PI 4');
});
it('gets system model from dmidecode if /proc/device-tree/model is not available', async () => {
(fs.readFile as SinonStub).rejects('Not found');
(fsUtils.exec as SinonStub).resolves({
stdout: mockCPU.dmidecode,
});
const systemModel = await sysInfo.getSystemModel();
expect(systemModel).to.equal('Intel Corporation NUC7i5BNB');
});
});
describe('getMemoryInformation', async () => {
@ -154,6 +201,51 @@ describe('System information', () => {
expect(await sysInfo.undervoltageDetected()).to.be.false;
});
});
describe('dmidecode', () => {
it('parses dmidecode output into json', async () => {
(fsUtils.exec as SinonStub).resolves({
stdout: mockCPU.dmidecode,
});
expect(await sysInfo.dmidecode('baseboard')).to.deep.equal([
{
type: 'Base Board Information',
values: {
Manufacturer: 'Intel Corporation',
'Product Name': 'NUC7i5BNB',
Version: 'J31144-313',
'Serial Number': 'GEBN94600PWW',
'Location In Chassis': 'Default string',
'Chassis Handle': '0x0003',
Type: 'Motherboard',
'Contained Object Handles': '0',
},
},
{
type: 'On Board Device 1 Information',
values: {
Type: 'Sound',
Status: 'Enabled',
Description: 'Realtek High Definition Audio Device',
},
},
{
type: 'Onboard Device',
values: {
'Reference Designation': 'Onboard - Other',
Type: 'Other',
Status: 'Enabled',
'Type Instance': '1',
'Bus Address': '0000',
},
},
]);
// Reset the stub
(fsUtils.exec as SinonStub).reset();
});
});
});
const mockCPU = {
@ -236,25 +328,48 @@ const mockCPU = {
},
],
},
idBuffer: Buffer.from([
0x31,
0x30,
0x30,
0x30,
0x30,
0x30,
0x30,
0x30,
0x30,
0x31,
0x62,
0x39,
0x33,
0x66,
0x33,
0x66,
0x00,
]),
idBuffer: Buffer.from('1000000001b93f3f'),
dmidecode: Buffer.from(`# dmidecode 3.3
Getting SMBIOS data from sysfs.
SMBIOS 3.1.1 present.
Handle 0x0002, DMI type 2, 15 bytes
Base Board Information
Manufacturer: Intel Corporation
Product Name: NUC7i5BNB
Version: J31144-313
Serial Number: GEBN94600PWW
Asset Tag:
Features:
Board is a hosting board
Board is replaceable
Location In Chassis: Default string
Chassis Handle: 0x0003
Type: Motherboard
Contained Object Handles: 0
Handle 0x000F, DMI type 10, 20 bytes
On Board Device 1 Information
Type: Video
Status: Enabled
Description: Intel(R) HD Graphics Device
On Board Device 2 Information
Type: Ethernet
Status: Enabled
Description: Intel(R) I219-V Gigabit Network Device
On Board Device 3 Information
Type: Sound
Status: Enabled
Description: Realtek High Definition Audio Device
Handle 0x003F, DMI type 41, 11 bytes
Onboard Device
Reference Designation: Onboard - Other
Type: Other
Status: Enabled
Type Instance: 1
Bus Address: 0000:00:00.0
`),
};
const mockFS = [
{