#Requires -Version 5.1 <# .SYNOPSIS Tracks user activity and reports working/stopped status to a central server. .DESCRIPTION This PowerShell script monitors user idle time and reports state changes to a central Flask API. It runs at user logon via Task Scheduler and maintains a state machine between "working" and "stopped" states based on a 5-minute inactivity threshold. .NOTES File Name : report.ps1 Author : IT Department Version : 1.2 #> # Hide the PowerShell console window Add-Type -Name Window -Namespace Console -MemberDefinition ' [DllImport("Kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow); ' $consolePtr = [Console.Window]::GetConsoleWindow() [void][Console.Window]::ShowWindow($consolePtr, 0) # 0 = hide # Configuration $ApiUrl = "http://localhost:5000/api/report" # Default value, can be overridden by environment variable $IdleThresholdMinutes = 5 $LogFilePath = Join-Path $env:TEMP "user_work_tracking_client.log" $pollIntervalSeconds = 60 # Check every minute $reportIntervalMinutes = 5 # Send reports every 5 minutes regardless of state changes $lastReportTime = [DateTime]::MinValue # Initialize last report time # Helper Function for Logging function Write-Log($Message) { $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' try { "$timestamp - $Message" | Out-File -Append -FilePath $LogFilePath -Encoding UTF8 -ErrorAction Stop } catch { Write-Warning "Failed to write to log file '$LogFilePath': $($_.Exception.Message)" # Optionally, write to console as fallback Write-Host "$timestamp [LOG FALLBACK] - $Message" } } Write-Log "================ Script Started ===================" # Try to read config from environment or a local config file try { # Check for environment variable first if ($env:API_ENDPOINT) { $ApiUrl = $env:API_ENDPOINT Write-Log "Using API URL from environment variable: $ApiUrl" } else { # Look for a config file $configPath = Join-Path ([System.IO.Path]::GetDirectoryName($PSCommandPath)) "config.env" if (Test-Path $configPath) { Write-Log "Found config file at $configPath" Get-Content $configPath | ForEach-Object { if ($_ -match '^API_ENDPOINT=(.*)$') { $ApiUrl = $matches[1].Trim('"') Write-Log "Using API URL from config file: $ApiUrl" } if ($_ -match '^IDLE_THRESHOLD_MINUTES=(\d+)$') { $IdleThresholdMinutes = [int]$matches[1] Write-Log "Using idle threshold from config file: $IdleThresholdMinutes minutes" } if ($_ -match '^POLL_INTERVAL_SECONDS=(\d+)$') { $pollIntervalSeconds = [int]$matches[1] Write-Log "Using poll interval from config file: $pollIntervalSeconds seconds" } if ($_ -match '^REPORT_INTERVAL_MINUTES=(\d+)$') { $reportIntervalMinutes = [int]$matches[1] Write-Log "Using report interval from config file: $reportIntervalMinutes minutes" } } } else { Write-Log "No config file found. Using default values." } } } catch { Write-Log "Error reading configuration: $_. Using default values." } Write-Log "Using API URL: $ApiUrl" Write-Log "Idle Threshold (Minutes): $IdleThresholdMinutes" Write-Log "Report Interval (Minutes): $reportIntervalMinutes" # Initiate state $currentState = "working" # Start in working state (user just logged in) $lastReportedState = $null # Function to get idle time using quser command function Get-IdleTime { try { # First try the Win32 API method (more reliable) if (-not ([System.Management.Automation.PSTypeName]'IdleTime').Type) { Add-Type @' using System; using System.Runtime.InteropServices; public class IdleTime { [DllImport("user32.dll")] public static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); public static TimeSpan GetIdleTime() { LASTINPUTINFO lastInput = new LASTINPUTINFO(); lastInput.cbSize = (uint)Marshal.SizeOf(lastInput); if (GetLastInputInfo(ref lastInput)) { uint currentTickCount = (uint)Environment.TickCount; uint idleTicks = currentTickCount - lastInput.dwTime; return TimeSpan.FromMilliseconds(idleTicks); } else { return TimeSpan.Zero; } } [StructLayout(LayoutKind.Sequential)] public struct LASTINPUTINFO { public uint cbSize; public uint dwTime; } } '@ } # Use the Win32 API method $idleTime = [IdleTime]::GetIdleTime() $idleMinutes = $idleTime.TotalMinutes Write-Log "Win32 API reports idle time: $idleMinutes minutes" return $idleTime } catch { Write-Log "Error using Win32 API for idle time: $_. Falling back to quser" # Fallback: Try the quser command method try { $quser = quser $env:USERNAME | Select-Object -Skip 1 $idleText = ($quser -replace '\s{2,}', ',').Split(',')[3] # If no idle time is reported, return 0 if ($idleText -eq "." -or $idleText -eq "none" -or $idleText -eq $null) { Write-Log "quser reports no idle time" return [TimeSpan]::Zero } # Parse HH:MM format if ($idleText -match '(\d+):(\d+)') { $hours = [int]$Matches[1] $minutes = [int]$Matches[2] $result = [TimeSpan]::FromMinutes(($hours * 60) + $minutes) Write-Log "quser parsed idle time: $($result.TotalMinutes) minutes" return $result } # Parse "MM+" format (represents minutes) if ($idleText -match '(\d+)\+') { $minutes = [int]$Matches[1] $result = [TimeSpan]::FromMinutes($minutes) Write-Log "quser parsed idle time: $($result.TotalMinutes) minutes" return $result } # Default to zero if we couldn't parse Write-Log "quser couldn't parse idle format: '$idleText'" return [TimeSpan]::Zero } catch { Write-Log "Error getting idle time via quser: $_" return [TimeSpan]::Zero } } } # Function to report state to the API function Send-StateReport { param ( [string]$State ) $payload = @{ user = $env:USERNAME state = $State ts = (Get-Date).ToUniversalTime().ToString("o") # ISO 8601 format } | ConvertTo-Json Write-Log "Preparing to send payload: $payload" try { Write-Log "Attempting API call to $ApiUrl" # Send to API $response = Invoke-RestMethod -Uri $ApiUrl ` -Method Post ` -Body $payload ` -ContentType "application/json" ` -ErrorAction Stop Write-Log "API call successful. Response: $($response | ConvertTo-Json -Depth 3)" # Update last report time $script:lastReportTime = Get-Date # Check for success field in response if ($response.success -eq $true) { Write-Log "State '$State' reported successfully." return $true } else { Write-Log "API indicated failure. Message: $($response.message)" return $false } } catch { # Log error details $errorMessage = "Failed to report state '$State' to API. Error: $($_.Exception.Message)" if ($_.Exception.Response) { $statusCode = $_.Exception.Response.StatusCode $statusDescription = $_.Exception.Response.StatusDescription $errorMessage += " Status Code: $statusCode ($statusDescription)" try { $responseBody = $_.Exception.Response.GetResponseStream() $streamReader = New-Object System.IO.StreamReader($responseBody) $errorBody = $streamReader.ReadToEnd() $streamReader.Close() $responseBody.Close() $errorMessage += " Response Body: $errorBody" } catch { $errorMessage += " (Could not read error response body)" } } Write-Log $errorMessage return $false } } # Initial state report at startup (user just logged in = working) if (Send-StateReport -State $currentState) { $lastReportedState = $currentState Write-Log "Initial state reported as '$currentState'" } else { Write-Log "Failed to report initial state. Will retry on next check." } # Main monitoring loop Write-Log "Starting activity monitoring with $IdleThresholdMinutes minute idle threshold" try { while ($true) { # Get current idle time $idleTime = Get-IdleTime $idleMinutes = $idleTime.TotalMinutes Write-Log "Idle time check: $idleMinutes minutes." # If idle time couldn't be determined, log and wait if ($idleTime -lt 0) { Write-Log "Error getting idle time. Waiting for next check." Start-Sleep -Seconds $pollIntervalSeconds continue } # Determine state based on idle time $newState = if ($idleMinutes -ge $IdleThresholdMinutes) { "stopped" } else { "working" } Write-Log "Determined state: $newState (Current: $currentState)" # Check if it's time to send a periodic report (every 5 minutes) $timeSinceLastReport = (Get-Date) - $lastReportTime $shouldSendPeriodicReport = $timeSinceLastReport.TotalMinutes -ge $reportIntervalMinutes Write-Log "Time since last report: $($timeSinceLastReport.TotalMinutes.ToString("F2")) minutes (threshold: $reportIntervalMinutes)" # If state changed or it's time for periodic report, send it if ($newState -ne $currentState -or $shouldSendPeriodicReport) { # If it's a state change, update current state if ($newState -ne $currentState) { Write-Log "State changed from '$currentState' to '$newState'. Reporting to API." $currentState = $newState } else { Write-Log "Sending periodic report (current state: $currentState)" } # Report to API try { if (Send-StateReport -State $currentState) { $lastReportedState = $currentState } } catch { Write-Log "Error reporting state: $_" } } # Sleep until next check Start-Sleep -Seconds $pollIntervalSeconds } } catch { Write-Log "Critical error in main loop: $_" # Try to report stopped state before exiting (if we were working) if ($currentState -eq "working") { Send-StateReport -State "stopped" } exit 1 } Write-Log "================ Script Ended Gracefully ================"