Private/Kinds/OneDrive.ps1

# The OneDrive Kind: whether OneDrive is keeping up, read from what it says of itself.
#
# "OneDrive does not sync" is a ticket, and until now a Run said of OneDrive that it was
# installed, how much memory it held and how large its folder had grown. Whether a file
# is stuck it did not say. OneDrive writes that down itself: beside its other logs, which
# are packed and not for reading, each account has a small plain-text report,
# SyncDiagnostics.log, of names and numbers - how many files wait to go up or come down,
# how many failed, whether it found itself stalled. Observed 2026-10-13 on a workstation
# with a business account: 5621 files in 587 folders, nothing waiting, nothing failed,
# written a day before although OneDrive had been running for two - it is not written
# every minute, so a Finding says how old what it read is.
#
# What is read:
#
# - Which accounts are set up, from the user's registry: business or personal, where
# its folder is, whether Files On-Demand is on. Neither a mail address nor a name of
# a tenant is read.
# - The report of each account. Only the names listed below are kept; the report also
# holds identifiers of the device and of the account, and those stay in the file.
# - Which of Desktop, Documents and Pictures lie in an account's folder: that is what
# OneDrive calls the backup of the known folders, read where Windows keeps the paths.
#
# Readable without admin rights, and only for the User of the Run: the accounts of
# another User are in that User's registry.

# What a report says that a Finding goes by. Everything else in it stays in the file.
$script:OneDriveDiagnosticNames = @(
    'SyncProgressState', 'UtcNow', 'FilesToUpload', 'FilesToDownload', 'BytesToUpload', 'BytesToDownload',
    'ChangesToProcess', 'ChangesToSend', 'files', 'folders', 'drivesConnected', 'numDrives',
    'drivesWaitingForInitialSync', 'numFileFailedUploads', 'numFileFailedDownloads', 'numUploadErrorsReported',
    'numDownloadErrorsReported', 'numFileInWarning', 'conflictsFailed', 'conflictsHandled', 'syncStallDetected',
    'scanStateStallDetected', 'wasFileDBReset', 'numResyncs', 'placeholdersEnabled', 'uptimeSecs', 'clientVersion'
)

function ConvertFrom-OneDriveDiagnostic {
    <#
    .SYNOPSIS
        The text of a SyncDiagnostics.log as the names and values a Finding goes by. Pure.
    .DESCRIPTION
        The report is lines of "name = value", and near its top "name: value". A name is
        kept as the first of these writes it; the report repeats some in another case.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param([AllowNull()][AllowEmptyString()][string]$Text)

    $read = @{}
    foreach ($line in ("$Text" -split "`r?`n")) {
        if ($line -notmatch '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*[=:]\s*(.*?)\s*$') { continue }
        $name = $Matches[1]
        $kept = @($script:OneDriveDiagnosticNames | Where-Object { $_ -eq $name })[0]
        if ($kept -and -not $read.ContainsKey($kept)) { $read[$kept] = $Matches[2] }
    }
    $read
}

function Get-OneDriveAccount {
    <#
    .SYNOPSIS
        The OneDrive accounts set up for the User of the Run, from the registry. Asks
        only; reads no address and no name.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param()

    $root = 'HKCU:\Software\Microsoft\OneDrive\Accounts'
    if (-not (Test-Path -LiteralPath $root)) { return }
    foreach ($key in @(Get-ChildItem -LiteralPath $root -ErrorAction SilentlyContinue)) {
        $values = Get-ItemProperty -LiteralPath $key.PSPath -ErrorAction SilentlyContinue
        $folder = "$(Get-DataProperty $values 'UserFolder')".Trim()
        # A key without a folder is an account that was never signed in to.
        if (-not $folder) { continue }
        [pscustomobject]@{
            Key            = "$($key.PSChildName)"
            Business       = [bool](ConvertTo-Number (Get-DataProperty $values 'Business'))
            Folder         = $folder
            FolderExists   = [bool](Test-Path -LiteralPath $folder -PathType Container)
            FilesOnDemand  = ConvertTo-Number (Get-DataProperty $values 'LastKnownCloudFilesEnabled')
            SignInResult   = ConvertTo-Number (Get-DataProperty $values 'LastSignInResult')
        }
    }
}

function Get-OneDriveKnownFolder {
    <#
    .SYNOPSIS
        Where Windows keeps Desktop, Documents and Pictures of the User of the Run.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param()

    $values = Get-ItemProperty -LiteralPath 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders' -ErrorAction SilentlyContinue
    foreach ($pair in @(@('Desktop', 'Desktop'), @('Documents', 'Personal'), @('Pictures', 'My Pictures'))) {
        $path = "$(Get-DataProperty $values $pair[1])".Trim()
        if ($path) { [pscustomobject]@{ Name = $pair[0]; Path = [Environment]::ExpandEnvironmentVariables($path) } }
    }
}

# What Get-OneDriveData hands its Judge. See Private/DataShape.ps1.
$script:OneDriveDataShape = @{
    App          = 'Value'
    Accounts     = 'List'
    Running      = 'Value'
    Version      = 'Value'
    KnownFolders = 'List'
    GatheredAt   = 'Answered'
}

function Get-OneDriveData {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([hashtable]$Parameters = @{})

    $logRoot = [Environment]::ExpandEnvironmentVariables("$(Get-Parameter $Parameters 'LogRoot' '%LOCALAPPDATA%\Microsoft\OneDrive\logs')")

    $accounts = @(Get-OneDriveAccount | ForEach-Object {
        $account = $_
        $report  = Join-Path (Join-Path $logRoot $account.Key) 'SyncDiagnostics.log'
        $said    = $null
        $written = $null
        if (Test-Path -LiteralPath $report -PathType Leaf) {
            try {
                $said    = ConvertFrom-OneDriveDiagnostic -Text (Read-AppLogFile -Path $report)
                $written = (Get-Item -LiteralPath $report).LastWriteTime
            }
            catch { $said = $null }
        }
        [pscustomobject]@{
            Key           = $account.Key
            Business      = $account.Business
            Folder        = $account.Folder
            FolderExists  = $account.FolderExists
            FilesOnDemand = $account.FilesOnDemand
            SignInResult  = $account.SignInResult
            # $null where there is no report or it would not open: not "nothing waits".
            Diagnostic    = $said
            ReportWritten = $written
        }
    })

    $running = $null
    try { $running = [bool]@(Get-Process -Name 'OneDrive' -ErrorAction SilentlyContinue).Count } catch { $running = $null }

    [pscustomobject]@{
        PSTypeName   = 'Gutcheck.Data.OneDrive'
        App          = "$(Get-Parameter $Parameters 'App' 'OneDrive')"
        Accounts     = $accounts
        Running      = $running
        Version      = "$(Get-DataProperty (Get-ItemProperty -LiteralPath 'HKCU:\Software\Microsoft\OneDrive' -ErrorAction SilentlyContinue) 'Version')"
        KnownFolders = @(Get-OneDriveKnownFolder)
        GatheredAt   = Get-Date
    }
}

function Get-OneDriveNumber {
    # One number out of a report: 0 where the report does not name it. Pure.
    [CmdletBinding()]
    param([AllowNull()]$Diagnostic, [Parameter(Mandatory)][string]$Name)

    $value = ConvertTo-Number (Get-DataProperty $Diagnostic $Name)
    if ($null -eq $value) { return 0 }
    $value
}

function Get-OneDriveAccountTitle {
    # What an account is called in the Report: by its kind, and by its key where a
    # machine has several of a kind. Pure.
    [CmdletBinding()]
    param([AllowNull()]$Account)

    $kind = Get-Text 'Value.OneDrive.Account.Personal'
    if ([bool](Get-DataProperty $Account 'Business')) { $kind = Get-Text 'Value.OneDrive.Account.Business' }
    (Get-Text 'Value.OneDrive.Account') -f $kind, "$(Get-DataProperty $Account 'Key')"
}

function Get-OneDriveStateText {
    <#
    .SYNOPSIS
        What the number OneDrive writes for its state means, where that is known, with
        the number beside it. Pure.
    .DESCRIPTION
        Microsoft does not document SyncProgressState. Four values are read here the way
        those who read it do; of them only "up to date" was observed on a machine. A
        reading and no specification, so the number is always shown, and a Finding never
        decides by it alone: the counts beside it say the same thing in plain numbers.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()]$State)

    $number = ConvertTo-Number $State
    if ($null -eq $number) { return '' }
    $text = switch ([long]$number) {
        16777216 { Get-Text 'Value.OneDrive.State.UpToDate' }
        65536    { Get-Text 'Value.OneDrive.State.Paused' }
        8194     { Get-Text 'Value.OneDrive.State.NotSyncing' }
        1854     { Get-Text 'Value.OneDrive.State.Problems' }
        default  { Get-Text 'Value.OneDrive.State.Unknown' }
    }
    (Get-Text 'Value.OneDrive.State') -f $text, ([long]$number)
}

function New-OneDriveSyncFinding {
    <#
    .SYNOPSIS
        Whether one account is keeping up: what failed, what waits, and how old the
        report is that says so.
    .DESCRIPTION
        A WARN where OneDrive says that something failed or that it is stalled. What only
        waits is said and is no fault - a large upload waits for hours. A report that is
        days old says how things were, and that is said with it.
    #>

    [CmdletBinding()]
    param([AllowNull()]$Account, [AllowNull()]$Data, [hashtable]$Parameters)

    $check = (Get-Text 'Check.OneDrive.Sync') -f (Get-OneDriveAccountTitle -Account $Account)
    $staleDays = ConvertTo-Number (Get-Parameter $Parameters 'ReportStaleDays' 3)
    if ($null -eq $staleDays) { $staleDays = 3 }

    if ((Get-DataProperty $Data 'Running') -eq $false) {
        return New-Finding -Category Apps -Check $check -Severity WARN -Value (Get-Text 'Value.OneDrive.NotRunning') `
            -Meaning (Get-Text 'Meaning.OneDrive.NotRunning') -Hint (Get-Text 'Hint.OneDrive.NotRunning')
    }
    if ((Get-DataProperty $Account 'FolderExists') -eq $false) {
        return New-Finding -Category Apps -Check $check -Severity WARN -Value ((Get-Text 'Value.OneDrive.FolderMissing') -f "$(Get-DataProperty $Account 'Folder')") `
            -Meaning (Get-Text 'Meaning.OneDrive.FolderMissing') -Hint (Get-Text 'Hint.OneDrive.FolderMissing')
    }

    $said = Get-DataProperty $Account 'Diagnostic'
    if ($null -eq $said -or -not @($said.Keys).Count) {
        return New-Finding -Category Apps -Check $check -Severity INFO -Value (Get-Text 'Value.OneDrive.NoReport') -Hint (Get-Text 'Hint.OneDrive.NoReport')
    }

    $written = ConvertTo-DataTime (Get-DataProperty $Account 'ReportWritten')
    $now     = ConvertTo-DataTime (Get-DataProperty $Data 'GatheredAt')
    $stale   = $false
    if ($null -ne $written -and $null -ne $now) { $stale = ($now - $written).TotalDays -gt $staleDays }

    $failedUp   = (Get-OneDriveNumber $said 'numFileFailedUploads') + (Get-OneDriveNumber $said 'numUploadErrorsReported')
    $failedDown = (Get-OneDriveNumber $said 'numFileFailedDownloads') + (Get-OneDriveNumber $said 'numDownloadErrorsReported')
    $conflicts  = Get-OneDriveNumber $said 'conflictsFailed'
    $warned     = Get-OneDriveNumber $said 'numFileInWarning'
    $stalled    = [bool]((Get-OneDriveNumber $said 'syncStallDetected') + (Get-OneDriveNumber $said 'scanStateStallDetected'))
    $up         = Get-OneDriveNumber $said 'FilesToUpload'
    $down       = Get-OneDriveNumber $said 'FilesToDownload'
    $changes    = (Get-OneDriveNumber $said 'ChangesToProcess') + (Get-OneDriveNumber $said 'ChangesToSend')
    $initial    = Get-OneDriveNumber $said 'drivesWaitingForInitialSync'
    $reset      = [bool](Get-OneDriveNumber $said 'wasFileDBReset')

    $value = @()
    $wrong = @()
    if ($failedUp)   { $wrong += (Get-Text 'Value.OneDrive.FailedUp') -f $failedUp }
    if ($failedDown) { $wrong += (Get-Text 'Value.OneDrive.FailedDown') -f $failedDown }
    if ($conflicts)  { $wrong += (Get-Text 'Value.OneDrive.Conflicts') -f $conflicts }
    if ($stalled)    { $wrong += Get-Text 'Value.OneDrive.Stalled' }
    $waits = @()
    if ($up)      { $waits += (Get-Text 'Value.OneDrive.ToUpload') -f $up }
    if ($down)    { $waits += (Get-Text 'Value.OneDrive.ToDownload') -f $down }
    if ($changes) { $waits += (Get-Text 'Value.OneDrive.Changes') -f $changes }
    if ($initial) { $waits += (Get-Text 'Value.OneDrive.InitialSync') -f $initial }

    if ($wrong.Count)     { $value += (Get-Text 'Value.OneDrive.Wrong') -f ($wrong -join '; ') }
    if ($waits.Count)     { $value += (Get-Text 'Value.OneDrive.Waits') -f ($waits -join '; ') }
    if (-not $wrong.Count -and -not $waits.Count) { $value += Get-Text 'Value.OneDrive.UpToDate' }
    if ($warned)          { $value += (Get-Text 'Value.OneDrive.InWarning') -f $warned }
    if ($reset)           { $value += Get-Text 'Value.OneDrive.DatabaseReset' }
    $value += (Get-Text 'Value.OneDrive.Counts') -f (Get-OneDriveNumber $said 'files'), (Get-OneDriveNumber $said 'folders')
    $state = Get-OneDriveStateText -State (Get-DataProperty $said 'SyncProgressState')
    if ($state) { $value += $state }
    if ($null -ne $written) {
        if ($stale) { $value += (Get-Text 'Value.OneDrive.ReportStale') -f $written }
        else        { $value += (Get-Text 'Value.OneDrive.ReportAt') -f $written }
    }

    $reference = @(New-Reference -Section 'Title.OneDrive.Accounts')
    if ($wrong.Count) {
        return New-Finding -Category Apps -Check $check -Severity WARN -Value ($value -join ' | ') `
            -Meaning (Get-Text 'Meaning.OneDrive.Wrong') -Hint (Get-Text 'Hint.OneDrive.Wrong') -Reference $reference -LastSeen $written
    }
    if ($waits.Count -or $warned -or $reset) {
        return New-Finding -Category Apps -Check $check -Severity INFO -Value ($value -join ' | ') `
            -Meaning (Get-Text 'Meaning.OneDrive.Waits') -Hint (Get-Text 'Hint.OneDrive.Waits') -Reference $reference
    }
    if ($stale) {
        return New-Finding -Category Apps -Check $check -Severity INFO -Value ($value -join ' | ') `
            -Meaning (Get-Text 'Meaning.OneDrive.Stale') -Hint (Get-Text 'Hint.OneDrive.Stale') -Reference $reference
    }
    New-Finding -Category Apps -Check $check -Severity OK -Value ($value -join ' | ') -Reference $reference
}

function New-OneDriveSetupFinding {
    <#
    .SYNOPSIS
        How OneDrive is set up for the User of the Run: the accounts, Files On-Demand,
        and which of Desktop, Documents and Pictures it backs up.
    #>

    [CmdletBinding()]
    param([AllowNull()]$Data)

    $check    = Get-Text 'Check.OneDrive.Setup'
    $accounts = @((Get-DataCollection $Data 'Accounts') | Where-Object { $_ })
    if (-not $accounts.Count) {
        return New-Finding -Category Apps -Check $check -Severity INFO -Value (Get-Text 'Value.OneDrive.NoAccount') -Hint (Get-Text 'Hint.OneDrive.NoAccount')
    }

    $value = @((Get-Text 'Value.OneDrive.Accounts') -f (@($accounts | ForEach-Object { Get-OneDriveAccountTitle -Account $_ }) -join '; '))

    # A known folder is backed up where its path lies in the folder of an account.
    $folders = @((Get-DataCollection $Data 'KnownFolders') | Where-Object { $_ })
    $backed  = @($folders | Where-Object {
        $path = "$(Get-DataProperty $_ 'Path')"
        @($accounts | Where-Object { $root = "$(Get-DataProperty $_ 'Folder')".TrimEnd('\'); $root -and $path.StartsWith($root + '\', [StringComparison]::OrdinalIgnoreCase) }).Count
    } | ForEach-Object {
        switch ("$(Get-DataProperty $_ 'Name')") {
            'Desktop'   { Get-Text 'Value.OneDrive.Folder.Desktop' }
            'Documents' { Get-Text 'Value.OneDrive.Folder.Documents' }
            'Pictures'  { Get-Text 'Value.OneDrive.Folder.Pictures' }
        }
    })
    if ($backed.Count)      { $value += (Get-Text 'Value.OneDrive.Backup') -f ($backed -join ', ') }
    elseif ($folders.Count) { $value += Get-Text 'Value.OneDrive.BackupNone' }

    $off = @($accounts | Where-Object { (Get-DataProperty $_ 'FilesOnDemand') -eq 0 })
    if ($off.Count) { $value += Get-Text 'Value.OneDrive.OnDemandOff' } else { $value += Get-Text 'Value.OneDrive.OnDemandOn' }
    $version = "$(Get-DataProperty $Data 'Version')".Trim()
    if ($version) { $value += (Get-Text 'Value.OneDrive.Version') -f $version }

    if ($off.Count) {
        return New-Finding -Category Apps -Check $check -Severity INFO -Value ($value -join ' | ') `
            -Meaning (Get-Text 'Meaning.OneDrive.OnDemandOff') -Hint (Get-Text 'Hint.OneDrive.OnDemandOff')
    }
    New-Finding -Category Apps -Check $check -Severity OK -Value ($value -join ' | ')
}

function ConvertTo-OneDriveFinding {
    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [AllowNull()]$Data,
        [hashtable]$Parameters = @{}
    )

    New-OneDriveSetupFinding -Data $Data
    foreach ($account in @((Get-DataCollection $Data 'Accounts') | Where-Object { $_ })) {
        New-OneDriveSyncFinding -Account $account -Data $Data -Parameters $Parameters
    }
}

function ConvertTo-OneDriveSection {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()]$Data)

    $accounts = @((Get-DataCollection $Data 'Accounts') | Where-Object { $_ })
    if (-not $accounts.Count) { return }
    New-Section -Title (Get-Text 'Title.OneDrive.Accounts') -Help (Get-Text 'Help.OneDrive.Accounts') -Row @(
        $accounts | ForEach-Object {
            $said = Get-DataProperty $_ 'Diagnostic'
            $read = [bool]($null -ne $said -and @($said.Keys).Count)
            $n = { param($Name) if ($read) { Get-OneDriveNumber $said $Name } else { $null } }
            $row = [ordered]@{}
            $row[(Get-Text 'Column.OneDrive.Account')]    = Get-OneDriveAccountTitle -Account $_
            $row[(Get-Text 'Column.OneDrive.Folder')]     = "$(Get-DataProperty $_ 'Folder')"
            $row[(Get-Text 'Column.OneDrive.ReportAt')]   = Get-DataProperty $_ 'ReportWritten'
            $row[(Get-Text 'Column.OneDrive.State')]      = $(if ($read) { Get-OneDriveStateText -State (Get-DataProperty $said 'SyncProgressState') } else { Get-Text 'Value.OneDrive.NoReport' })
            $row[(Get-Text 'Column.OneDrive.Files')]      = & $n 'files'
            $row[(Get-Text 'Column.OneDrive.Folders')]    = & $n 'folders'
            $row[(Get-Text 'Column.OneDrive.ToUpload')]   = & $n 'FilesToUpload'
            $row[(Get-Text 'Column.OneDrive.ToDownload')] = & $n 'FilesToDownload'
            $row[(Get-Text 'Column.OneDrive.FailedUp')]   = $(if ($read) { (Get-OneDriveNumber $said 'numFileFailedUploads') + (Get-OneDriveNumber $said 'numUploadErrorsReported') })
            $row[(Get-Text 'Column.OneDrive.FailedDown')] = $(if ($read) { (Get-OneDriveNumber $said 'numFileFailedDownloads') + (Get-OneDriveNumber $said 'numDownloadErrorsReported') })
            $row[(Get-Text 'Column.OneDrive.Conflicts')]  = & $n 'conflictsFailed'
            $row[(Get-Text 'Column.OneDrive.InWarning')]  = & $n 'numFileInWarning'
            $row[(Get-Text 'Column.OneDrive.Stalled')]    = $(if ($read) { Get-TriStateText ([bool]((Get-OneDriveNumber $said 'syncStallDetected') + (Get-OneDriveNumber $said 'scanStateStallDetected'))) })
            $row[(Get-Text 'Column.OneDrive.OnDemand')]   = Get-TriStateText $(if ($null -eq (Get-DataProperty $_ 'FilesOnDemand')) { $null } else { [bool](Get-DataProperty $_ 'FilesOnDemand') })
            [pscustomobject]$row
        }
    )
}