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

  1. Abstract platform differences behind clean interfaces. The TSystemReport record is the same on all platforms — only the gathering code differs.

  2. Linux's /proc filesystem is a goldmine. Almost any system metric is available as a text file. No special APIs needed.

  3. Not all features are available everywhere. Gracefully handle missing information rather than crashing.

  4. TProcess is versatile. When a direct API is not available, running a system command and parsing its output is a perfectly valid approach.

  5. 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.