work-tracing/report.ps1
ilia-gurielidze-autstand 9e6d0a6911 first commit
2025-05-05 12:12:46 +04:00

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 ================"