Utils/uptime.ps1

Register-GnuUtil @{
    Name        = 'uptime'
    Category    = 'User and system info'
    Synopsis    = 'Tell how long the system has been running.'
    Usage       = @('uptime [OPTION]...')
    Description = @(
        'Print the current time, the length of time the system has been up,',
        'the number of users on the system, and the average number of jobs',
        'in the run queue over the last 1, 5 and 15 minutes.'
    )
    Options     = @(
        @{ Short = 'p'; Long = 'pretty'; Help = 'show uptime in pretty format (procps)' }
        @{ Short = 's'; Long = 'since'; Help = 'system up since, in yyyy-mm-dd HH:MM:SS format (procps)' }
    )
    Notes       = @(
        'PowerShell note: -p and -s come from the procps uptime. Windows has no load averages, so',
        'they are omitted there; users are the interactive sessions (explorer.exe processes).'
    )
}



function script:Get-GnuUptime {
    <#
    .SYNOPSIS
    The time since boot (Environment.TickCount64, or the performance counter, which counts since boot on Windows).
    #>

    param([string]$Property = 'TickCount64')
    $tickCount = [System.Environment].GetProperty($Property)
    if ($null -ne $tickCount) { return [timespan]::FromMilliseconds([double]$tickCount.GetValue($null)) }
    [timespan]::FromSeconds([System.Diagnostics.Stopwatch]::GetTimestamp() / [double][System.Diagnostics.Stopwatch]::Frequency)
}

function script:Get-GnuUserCount {
    <#
    .SYNOPSIS
    Logged-in users: 'who' on Unix, interactive sessions (explorer.exe) on Windows.
    #>

    if (Test-GnuWindows) {
        return @(Get-Process -Name explorer -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SessionId -Unique).Count
    }
    $who = Get-Command -Name who -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($null -eq $who) { return 0 }
    @(& $who 2>$null).Count
}

function script:Get-GnuLoadAverage {
    <#
    .SYNOPSIS
    The 1, 5 and 15 minute load averages (Linux /proc/loadavg, macOS sysctl), or nothing on Windows.
    #>

    param([string]$Path = '/proc/loadavg')
    if (Test-GnuWindows) { return }
    $text = $null
    if ([System.IO.File]::Exists($Path)) { $text = [System.IO.File]::ReadAllText($Path) }
    else {
        $sysctl = Get-Command -Name sysctl -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($null -ne $sysctl) { $text = [string](& $sysctl -n vm.loadavg 2>$null) }
    }
    $numbers = @([regex]::Matches([string]$text, '\d+(\.\d+)?') | Select-Object -First 3 | ForEach-Object { [double]::Parse($_.Value, [System.Globalization.CultureInfo]::InvariantCulture) })
    if ($numbers.Count -eq 3) { return , [double[]]$numbers }
}

function script:Format-GnuUptimeLine {
    <#
    .SYNOPSIS
    The coreutils uptime line.
    #>

    param(
        [Parameter(Mandatory)][datetime]$Now,
        [Parameter(Mandatory)][timespan]$Uptime,
        [Parameter(Mandatory)][int]$Users,
        [AllowNull()][double[]]$Load
    )
    $invariant = [System.Globalization.CultureInfo]::InvariantCulture
    $line = ' ' + $Now.ToString('HH:mm:ss', $invariant) + ' '
    $days = [int][Math]::Floor($Uptime.TotalDays)
    $clock = '{0,2}:{1:00}' -f $Uptime.Hours, $Uptime.Minutes
    if ($days -eq 1) { $line += "up 1 day $clock, " }
    elseif ($days -gt 1) { $line += "up $days days $clock, " }
    else { $line += "up $clock, " }
    if ($Users -eq 1) { $line += '1 user' } else { $line += "$Users users" }
    if ($null -ne $Load -and $Load.Count -eq 3) {
        $line += ', load average: ' + (($Load | ForEach-Object { $_.ToString('0.00', $invariant) }) -join ', ')
    }
    $line
}

function script:Format-GnuUptimePretty {
    <#
    .SYNOPSIS
    The procps 'uptime -p' text.
    #>

    param([Parameter(Mandatory)][timespan]$Uptime)
    $parts = New-Object 'System.Collections.Generic.List[string]'
    $weeks = [int][Math]::Floor($Uptime.TotalDays / 7)
    $days = [int][Math]::Floor($Uptime.TotalDays) % 7
    foreach ($unit in @(@($weeks, 'week'), @($days, 'day'), @($Uptime.Hours, 'hour'), @($Uptime.Minutes, 'minute'))) {
        if ($unit[0] -eq 1) { $parts.Add("1 $($unit[1])") }
        elseif ($unit[0] -gt 1) { $parts.Add("$($unit[0]) $($unit[1])s") }
    }
    if ($parts.Count -eq 0) { return 'up 0 minutes' }
    'up ' + ($parts -join ', ')
}