308 lines
11 KiB
PowerShell
308 lines
11 KiB
PowerShell
#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 ================" |