Case Study 1: Building a System Monitor
Overview
In this case study, we build a cross-platform system monitor that displays CPU information, memory usage, disk space, and running process count. The monitor demonstrates reading system information from OS-specific sources, using conditional compilation for cross-platform support, and running external commands to capture system data.
What the Monitor Displays
=== System Monitor ===
Platform: Linux (64-bit)
User: rosa
Hostname: rosa-laptop
CPU: Intel Core i7-10750H @ 2.60GHz
Cores: 6
Memory:
Total: 16,229 MB
Available: 8,432 MB
Used: 7,797 MB (48.0%)
Disk (/):
Total: 500 GB
Free: 234 GB
Used: 266 GB (53.2%)
Processes: 287
Uptime: 3 days, 7 hours
Implementation Strategy
Each metric requires a different source depending on the platform:
| Metric | Linux | Windows |
|---|---|---|
| CPU info | /proc/cpuinfo |
Registry or WMI |
| Memory | /proc/meminfo |
GlobalMemoryStatusEx API |
| Disk | df command via TProcess |
GetDiskFreeSpaceEx API |
| Processes | Count entries in /proc |
CreateToolhelp32Snapshot API |
| Uptime | /proc/uptime |
GetTickCount64 API |
The Cross-Platform Abstraction
unit SystemInfo;
{$mode objfpc}{$H+}
interface
type
TSystemReport = record
Platform: string;
UserName: string;
CPUModel: string;
CPUCores: Integer;
MemTotalMB: Int64;
MemAvailMB: Int64;
DiskTotalGB: Int64;
DiskFreeGB: Int64;
ProcessCount: Integer;
UptimeSeconds: Int64;
end;
function GatherSystemInfo: TSystemReport;
procedure DisplayReport(const R: TSystemReport);
implementation
uses
SysUtils, Classes, Process
{$IFDEF WINDOWS}, Windows{$ENDIF};
Linux Implementation
{$IFDEF LINUX}
function ReadProcFile(const Path: string): TStringList;
begin
Result := TStringList.Create;
if FileExists(Path) then
Result.LoadFromFile(Path);
end;
function GetLinuxCPUModel: string;
var
Lines: TStringList;
I: Integer;
begin
Result := 'Unknown';
Lines := ReadProcFile('/proc/cpuinfo');
try
for I := 0 to Lines.Count - 1 do
if Pos('model name', Lines[I]) = 1 then
begin
Result := Trim(Copy(Lines[I], Pos(':', Lines[I]) + 1, Length(Lines[I])));
Break;
end;
finally
Lines.Free;
end;
end;
function GetLinuxCPUCores: Integer;
var
Lines: TStringList;
I: Integer;
begin
Result := 0;
Lines := ReadProcFile('/proc/cpuinfo');
try
for I := 0 to Lines.Count - 1 do
if Pos('processor', Lines[I]) = 1 then
Inc(Result);
finally
Lines.Free;
end;
if Result = 0 then Result := 1;
end;
function GetLinuxMemInfo(const Key: string): Int64;
var
Lines: TStringList;
I: Integer;
Line, ValueStr: string;
begin
Result := 0;
Lines := ReadProcFile('/proc/meminfo');
try
for I := 0 to Lines.Count - 1 do
if Pos(Key, Lines[I]) = 1 then
begin
Line := Lines[I];
ValueStr := Trim(Copy(Line, Pos(':', Line) + 1, Length(Line)));
{ Remove ' kB' suffix }
ValueStr := Trim(Copy(ValueStr, 1, Pos(' ', ValueStr) - 1));
Result := StrToInt64Def(ValueStr, 0) div 1024; { Convert kB to MB }
Break;
end;
finally
Lines.Free;
end;
end;
function GetLinuxUptime: Int64;
var
Lines: TStringList;
UptimeStr: string;
begin
Result := 0;
Lines := ReadProcFile('/proc/uptime');
try
if Lines.Count > 0 then
begin
UptimeStr := Copy(Lines[0], 1, Pos(' ', Lines[0]) - 1);
Result := Trunc(StrToFloatDef(UptimeStr, 0));
end;
finally
Lines.Free;
end;
end;
{$ENDIF}
Assembling the Report
function GatherSystemInfo: TSystemReport;
begin
{ Platform }
{$IFDEF WINDOWS}
Result.Platform := 'Windows';
{$ENDIF}
{$IFDEF LINUX}
Result.Platform := 'Linux';
{$ENDIF}
{$IFDEF DARWIN}
Result.Platform := 'macOS';
{$ENDIF}
{$IFDEF CPU64}
Result.Platform := Result.Platform + ' (64-bit)';
{$ELSE}
Result.Platform := Result.Platform + ' (32-bit)';
{$ENDIF}
{ User }
{$IFDEF WINDOWS}
Result.UserName := GetEnvironmentVariable('USERNAME');
{$ELSE}
Result.UserName := GetEnvironmentVariable('USER');
{$ENDIF}
{ CPU }
{$IFDEF LINUX}
Result.CPUModel := GetLinuxCPUModel;
Result.CPUCores := GetLinuxCPUCores;
{$ELSE}
Result.CPUModel := 'Detection not implemented for this platform';
Result.CPUCores := 1;
{$ENDIF}
{ Memory }
{$IFDEF LINUX}
Result.MemTotalMB := GetLinuxMemInfo('MemTotal');
Result.MemAvailMB := GetLinuxMemInfo('MemAvailable');
{$ENDIF}
{ Uptime }
{$IFDEF LINUX}
Result.UptimeSeconds := GetLinuxUptime;
{$ELSE}
Result.UptimeSeconds := GetTickCount64 div 1000;
{$ENDIF}
end;
Display Function
procedure DisplayReport(const R: TSystemReport);
var
UsedMB: Int64;
UsedPct: Double;
Days, Hours, Minutes: Integer;
begin
WriteLn('=== System Monitor ===');
WriteLn('Platform: ', R.Platform);
WriteLn('User: ', R.UserName);
WriteLn;
WriteLn('CPU: ', R.CPUModel);
WriteLn('Cores: ', R.CPUCores);
WriteLn;
UsedMB := R.MemTotalMB - R.MemAvailMB;
if R.MemTotalMB > 0 then
UsedPct := UsedMB / R.MemTotalMB * 100
else
UsedPct := 0;
WriteLn('Memory:');
WriteLn(Format(' Total: %s MB', [FormatFloat('#,##0', R.MemTotalMB)]));
WriteLn(Format(' Available: %s MB', [FormatFloat('#,##0', R.MemAvailMB)]));
WriteLn(Format(' Used: %s MB (%.1f%%)', [FormatFloat('#,##0', UsedMB), UsedPct]));
WriteLn;
Days := R.UptimeSeconds div 86400;
Hours := (R.UptimeSeconds mod 86400) div 3600;
Minutes := (R.UptimeSeconds mod 3600) div 60;
WriteLn(Format('Uptime: %d days, %d hours, %d minutes', [Days, Hours, Minutes]));
end;
Lessons Learned
-
Abstract platform differences behind clean interfaces. The
TSystemReportrecord is the same on all platforms — only the gathering code differs. -
Linux's /proc filesystem is a goldmine. Almost any system metric is available as a text file. No special APIs needed.
-
Not all features are available everywhere. Gracefully handle missing information rather than crashing.
-
TProcess is versatile. When a direct API is not available, running a system command and parsing its output is a perfectly valid approach.
-
Test on each target platform. Conditional compilation means the compiler only checks the active platform's code. Errors in the other platform's code are hidden until you compile there.