diff --git a/aw-server/aw-ru-patch.js b/aw-server/aw-ru-patch.js
index 0e7b585..2f8be73 100755
--- a/aw-server/aw-ru-patch.js
+++ b/aw-server/aw-ru-patch.js
@@ -344,7 +344,16 @@
'.aw-ru-pve-audit-value { font-size: 24px; font-weight: 700; }',
'.aw-ru-pve-audit-table { width: 100%; border-collapse: collapse; margin-top: 8px; }',
'.aw-ru-pve-audit-table th, .aw-ru-pve-audit-table td { padding: 6px 8px; border-bottom: 1px solid rgba(120,120,120,.18); vertical-align: top; text-align: left; font-size: 13px; }',
- '.aw-ru-pve-audit-muted { opacity: .72; font-size: 13px; }'
+ '.aw-ru-pve-audit-muted { opacity: .72; font-size: 13px; }',
+ '.aw-ru-rdp-center { margin: 16px 0; padding: 16px; border: 1px solid rgba(120,120,120,.35); border-radius: 8px; background: rgba(10,20,40,.04); }',
+ '.aw-ru-rdp-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin: 12px 0 16px; }',
+ '.aw-ru-rdp-card { border: 1px solid rgba(120,120,120,.22); border-radius: 8px; padding: 12px; background: rgba(255,255,255,.02); }',
+ '.aw-ru-rdp-card h5 { margin: 0 0 6px; font-size: 13px; opacity: .8; }',
+ '.aw-ru-rdp-value { font-size: 24px; font-weight: 700; }',
+ '.aw-ru-rdp-table { width: 100%; border-collapse: collapse; margin-top: 8px; }',
+ '.aw-ru-rdp-table th, .aw-ru-rdp-table td { padding: 6px 8px; border-bottom: 1px solid rgba(120,120,120,.18); vertical-align: top; text-align: left; font-size: 13px; }',
+ '.aw-ru-rdp-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }',
+ '.aw-ru-rdp-links a { display: inline-block; padding: 4px 8px; border-radius: 999px; background: rgba(90,140,255,.15); text-decoration: none; }'
].join("\n");
document.head.appendChild(style);
}
@@ -370,6 +379,12 @@
return "";
}
+ function getCurrentActivityDayFromHash() {
+ const hash = window.location.hash || "";
+ const match = hash.match(/#\/activity\/[^/]+\/day\/([^/?#]+)/i);
+ return match && match[1] ? decodeURIComponent(match[1]) : "today";
+ }
+
function isPveLikeHost(host) {
return /^pve[-_]/i.test(String(host || ""));
}
@@ -384,6 +399,230 @@
return true;
}
+ function isClientActivityRoute() {
+ const hash = window.location.hash || "";
+ const match = hash.match(/^#\/activity\/([^/]+)(?:\/day\/([^/]+))?\/view\/([^/?#]+)/i);
+ if (!match) return false;
+ const host = decodeURIComponent(match[1] || "");
+ return isLikelyClientHost(host) && !isPveLikeHost(host);
+ }
+
+ function getRdpReportBaseUrl() {
+ const url = new URL(window.location.href);
+ url.hash = "";
+ url.search = "";
+ url.pathname = "/reports/worktime/today";
+ url.port = "5610";
+ return url;
+ }
+
+ function buildRdpReportUrl(format, day) {
+ const url = getRdpReportBaseUrl();
+ url.searchParams.set("day", day || "today");
+ if (format) url.searchParams.set("format", format);
+ return url.toString();
+ }
+
+ function normalizeActivityDay(day) {
+ if (day && day !== "today") return day;
+ const now = new Date();
+ return [
+ now.getFullYear(),
+ String(now.getMonth() + 1).padStart(2, "0"),
+ String(now.getDate()).padStart(2, "0")
+ ].join("-");
+ }
+
+ function getActivityDayRange(day) {
+ const normalizedDay = normalizeActivityDay(day);
+ const start = new Date(normalizedDay + "T00:00:00");
+ const end = new Date(normalizedDay + "T23:59:59");
+ return { normalizedDay: normalizedDay, start: start, end: end };
+ }
+
+ function formatActiveHhmm(totalSeconds) {
+ const seconds = Math.max(0, Number(totalSeconds) || 0);
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ return String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0");
+ }
+
+ function isWorktimeRowActive(data) {
+ if (!data || typeof data !== "object") return false;
+ if (typeof data.active === "boolean") return data.active;
+ const state = String(data.state || "").trim().toLowerCase();
+ return state === "active" || state === "активно";
+ }
+
+ function formatDurationSeconds(totalSeconds) {
+ const seconds = Math.max(0, Number(totalSeconds) || 0);
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const secs = Math.floor(seconds % 60);
+ if (hours > 0) return hours + "ч " + String(minutes).padStart(2, "0") + "м";
+ if (minutes > 0) return minutes + "м " + String(secs).padStart(2, "0") + "с";
+ return secs + "с";
+ }
+
+ function formatIsoForUi(value) {
+ if (!value) return "—";
+ try {
+ return new Date(value).toLocaleString();
+ } catch (error) {
+ return value;
+ }
+ }
+
+ async function fetchRdpWorktimeReport(host, day) {
+ if (!host) return null;
+ const cacheKey = host + "|" + (day || "today");
+ if (!window.__awRuRdpReportCache) window.__awRuRdpReportCache = {};
+ if (window.__awRuRdpReportCache[cacheKey]) return window.__awRuRdpReportCache[cacheKey];
+ const range = getActivityDayRange(day);
+ const bucketId = "aw-worktime-sessions_" + host;
+ const params = new URLSearchParams();
+ params.set("start", range.start.toISOString());
+ params.set("end", new Date(range.end.getTime() + 1000).toISOString());
+ params.set("limit", "100000");
+ const response = await fetch("/api/0/buckets/" + encodeURIComponent(bucketId) + "/events?" + params.toString(), { credentials: "same-origin" });
+ if (!response.ok) throw new Error("rdp-report-fetch-failed");
+ const events = await response.json();
+ if (!Array.isArray(events)) return null;
+ const rowsByUser = new Map();
+ events.forEach(function (event) {
+ const data = event && event.data ? event.data : {};
+ const ts = event && event.timestamp ? String(event.timestamp) : "";
+ if (!ts) return;
+ const tsDate = new Date(ts);
+ if (Number.isNaN(tsDate.getTime())) return;
+ const tsDay = [
+ tsDate.getFullYear(),
+ String(tsDate.getMonth() + 1).padStart(2, "0"),
+ String(tsDate.getDate()).padStart(2, "0")
+ ].join("-");
+ if (tsDay !== range.normalizedDay) return;
+ const userId = String(data.userId || "");
+ const userName = String(data.username || userId || "").trim();
+ if (!userName) return;
+ const key = userId || userName;
+ if (!rowsByUser.has(key)) {
+ rowsByUser.set(key, {
+ user: userName,
+ user_id: userId || userName,
+ active_seconds: 0,
+ first_activity: "",
+ last_activity: "",
+ sessions_count: new Set(),
+ samples_count: 0,
+ active_samples: 0
+ });
+ }
+ const row = rowsByUser.get(key);
+ row.samples_count += 1;
+ if (data.sessionId !== undefined && data.sessionId !== null) row.sessions_count.add(String(data.sessionId));
+ if (isWorktimeRowActive(data)) {
+ const sampleSeconds = Math.max(0, Number(data.sampleSeconds || event.duration || 0));
+ row.active_seconds += sampleSeconds;
+ row.active_samples += 1;
+ if (!row.first_activity || ts < row.first_activity) row.first_activity = ts;
+ if (!row.last_activity || ts > row.last_activity) row.last_activity = ts;
+ }
+ });
+ const payload = {
+ host: host,
+ report_date: range.normalizedDay,
+ rows: Array.from(rowsByUser.values()).map(function (row) {
+ return {
+ user: row.user,
+ user_id: row.user_id,
+ active_seconds: row.active_seconds,
+ active_hhmm: formatActiveHhmm(row.active_seconds),
+ first_activity: row.first_activity,
+ last_activity: row.last_activity,
+ sessions_count: row.sessions_count.size,
+ samples_count: row.samples_count,
+ active_samples: row.active_samples
+ };
+ })
+ };
+ window.__awRuRdpReportCache[cacheKey] = payload;
+ return payload;
+ }
+
+ async function injectRdpWorktimeCenter(root) {
+ if (!isClientActivityRoute()) return;
+ const host = getCurrentHostFromHash();
+ const day = getCurrentActivityDayFromHash();
+ const report = await fetchRdpWorktimeReport(host, day);
+ if (!report || !Array.isArray(report.rows) || !report.rows.length) return;
+
+ const totalActiveSeconds = report.rows.reduce(function (sum, row) {
+ return sum + Math.max(0, Number(row && row.active_seconds || 0));
+ }, 0);
+ const activeUsers = report.rows.filter(function (row) {
+ return Number(row && row.active_seconds || 0) > 0;
+ });
+ const topRows = activeUsers
+ .slice()
+ .sort(function (left, right) {
+ return Number(right.active_seconds || 0) - Number(left.active_seconds || 0);
+ })
+ .slice(0, 5);
+
+ Array.from(root.querySelectorAll("li")).forEach(function (item) {
+ const text = (item.textContent || "").trim();
+ if (/^(?:Активное время|Time active):/i.test(text)) {
+ item.textContent = "Активное время: " + formatDurationSeconds(totalActiveSeconds);
+ }
+ });
+
+ const heading = root.querySelector("h3");
+ if (!heading || !heading.parentElement) return;
+
+ let center = root.querySelector("[data-aw-ru-rdp-center='1']");
+ if (!center) {
+ center = document.createElement("section");
+ center.className = "aw-ru-rdp-center";
+ center.setAttribute("data-aw-ru-rdp-center", "1");
+ const anchor = heading.parentElement.querySelector("img") || null;
+ heading.parentElement.insertBefore(center, anchor);
+ }
+
+ const latestActivity = topRows.reduce(function (latest, row) {
+ const value = row && row.last_activity ? String(row.last_activity) : "";
+ if (!value) return latest;
+ if (!latest) return value;
+ return value > latest ? value : latest;
+ }, "");
+
+ center.innerHTML =
+ '
RDP summary
' +
+ 'Этот блок строится из server-side worktime отчёта и не зависит от того, жива ли локальная интерактивная RDP-сессия.
' +
+ '' +
+ '
Активное время
' + escapeHtml(formatDurationSeconds(totalActiveSeconds)) + '
' +
+ '
Активных пользователей
' + escapeHtml(String(activeUsers.length)) + '
' +
+ '
Последняя активность
' + escapeHtml(formatIsoForUi(latestActivity)) + '
' +
+ '
' +
+ '' +
+ '| Пользователь | Активное время | Первая активность | Последняя активность |
' +
+ '' +
+ (topRows.length ? topRows.map(function (row) {
+ return '' +
+ '| ' + escapeHtml(row.user || row.user_id || "") + ' | ' +
+ '' + escapeHtml(row.active_hhmm || formatDurationSeconds(row.active_seconds || 0)) + ' | ' +
+ '' + escapeHtml(formatIsoForUi(row.first_activity || "")) + ' | ' +
+ '' + escapeHtml(formatIsoForUi(row.last_activity || "")) + ' | ' +
+ '
';
+ }).join("") : '| Нет активных пользователей в отчёте. |
') +
+ '' +
+ '
' +
+ '';
+ }
+
function enforceSafeActivityViewForPveHost() {
const hash = window.location.hash || "";
const match = hash.match(/^#\/activity\/([^/]+)(?:\/day\/([^/]+))?\/view\/([^/?#]+)/i);
@@ -1903,6 +2142,7 @@
staticPatchRouteKey = routeKey;
}
injectPveAuditCenter(document.body);
+ injectRdpWorktimeCenter(document.body).catch(function () {});
injectDlpNavigation(document.body);
if (isDlpSignalBucketRoute() && dlpOverlayFailureCount === 0) {
try {
diff --git a/aw-server/aw-worktime-ui-bridge.py b/aw-server/aw-worktime-ui-bridge.py
index 91ec1b3..8a853f0 100644
--- a/aw-server/aw-worktime-ui-bridge.py
+++ b/aw-server/aw-worktime-ui-bridge.py
@@ -13,11 +13,20 @@ STATE_PATH = os.environ.get(
"/var/lib/activitywatch/aw-worktime-ui-bridge-state.json",
)
TIMEOUT = float(os.environ.get("AW_WORKTIME_UI_BRIDGE_TIMEOUT", "20"))
+WATCHER_FALLBACK_ENABLED = os.environ.get("AW_WORKTIME_UI_BRIDGE_WATCHER_FALLBACK", "1").strip().lower() not in {
+ "0",
+ "false",
+ "no",
+ "off",
+}
+WATCHER_FALLBACK_STALE_SECONDS = float(os.environ.get("AW_WORKTIME_UI_BRIDGE_WATCHER_STALE_SECONDS", "600"))
SESSIONS_BUCKET = f"aw-worktime-sessions_{HOST}"
AFK_BUCKET = f"aw-rdp-afk_{HOST}"
WINDOW_BUCKET = f"aw-rdp-window_{HOST}"
+WATCHER_AFK_BUCKET = f"aw-watcher-afk_{HOST}"
+WATCHER_WINDOW_BUCKET = f"aw-watcher-window_{HOST}"
def _req(method: str, path: str, payload=None):
@@ -43,6 +52,31 @@ def ensure_bucket(bucket_id: str, event_type: str, client: str):
raise
+def get_latest_bucket_event_ts(bucket_id: str):
+ try:
+ events = _req("GET", f"/api/0/buckets/{bucket_id}/events?limit=1") or []
+ except urllib.error.HTTPError as e:
+ if e.code == 404:
+ return None
+ raise
+ if not events:
+ return None
+ ts = events[0].get("timestamp")
+ if not ts:
+ return None
+ try:
+ return parse_iso_utc(ts)
+ except Exception:
+ return None
+
+
+def bucket_needs_fallback(bucket_id: str, now_utc: datetime, stale_after_seconds: float):
+ latest_dt = get_latest_bucket_event_ts(bucket_id)
+ if latest_dt is None:
+ return True
+ return (now_utc - latest_dt).total_seconds() >= stale_after_seconds
+
+
def load_state():
try:
with open(STATE_PATH, "r", encoding="utf-8") as f:
@@ -198,6 +232,13 @@ def main():
_req("POST", f"/api/0/buckets/{AFK_BUCKET}/events", afk_events)
_req("POST", f"/api/0/buckets/{WINDOW_BUCKET}/events", win_events)
+ if WATCHER_FALLBACK_ENABLED:
+ if bucket_needs_fallback(WATCHER_AFK_BUCKET, now_utc, WATCHER_FALLBACK_STALE_SECONDS):
+ ensure_bucket(WATCHER_AFK_BUCKET, "afkstatus", "aw-watcher-afk")
+ _req("POST", f"/api/0/buckets/{WATCHER_AFK_BUCKET}/events", afk_events)
+ if bucket_needs_fallback(WATCHER_WINDOW_BUCKET, now_utc, WATCHER_FALLBACK_STALE_SECONDS):
+ ensure_bucket(WATCHER_WINDOW_BUCKET, "currentwindow", "aw-watcher-window")
+ _req("POST", f"/api/0/buckets/{WATCHER_WINDOW_BUCKET}/events", win_events)
save_state({"last_ts": new_last_ts})
print(f"posted_afk={len(afk_events)} posted_win={len(win_events)} last_ts={new_last_ts}")
diff --git a/aw-server/test_aw_worktime_ui_bridge.py b/aw-server/test_aw_worktime_ui_bridge.py
new file mode 100644
index 0000000..6b909d3
--- /dev/null
+++ b/aw-server/test_aw_worktime_ui_bridge.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+import importlib.util
+import unittest
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+from unittest import mock
+
+
+MODULE_PATH = Path(__file__).with_name("aw-worktime-ui-bridge.py")
+SPEC = importlib.util.spec_from_file_location("aw_worktime_ui_bridge", MODULE_PATH)
+MODULE = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(MODULE)
+
+
+class WorktimeUiBridgeTests(unittest.TestCase):
+ def test_bucket_needs_fallback_when_bucket_is_missing(self):
+ now = datetime(2026, 5, 22, 16, 0, 0, tzinfo=timezone.utc)
+ with mock.patch.object(MODULE, "get_latest_bucket_event_ts", return_value=None):
+ self.assertTrue(MODULE.bucket_needs_fallback("aw-watcher-window_SHARKON2025", now, 600))
+
+ def test_bucket_needs_fallback_when_bucket_is_recent(self):
+ now = datetime(2026, 5, 22, 16, 0, 0, tzinfo=timezone.utc)
+ recent = now - timedelta(seconds=120)
+ with mock.patch.object(MODULE, "get_latest_bucket_event_ts", return_value=recent):
+ self.assertFalse(MODULE.bucket_needs_fallback("aw-watcher-window_SHARKON2025", now, 600))
+
+ def test_bucket_needs_fallback_when_bucket_is_stale(self):
+ now = datetime(2026, 5, 22, 16, 0, 0, tzinfo=timezone.utc)
+ stale = now - timedelta(seconds=601)
+ with mock.patch.object(MODULE, "get_latest_bucket_event_ts", return_value=stale):
+ self.assertTrue(MODULE.bucket_needs_fallback("aw-watcher-afk_SHARKON2025", now, 600))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/windows/ActivityWatch.Windows.Common.psm1 b/windows/ActivityWatch.Windows.Common.psm1
index 2294465..d0380e5 100755
--- a/windows/ActivityWatch.Windows.Common.psm1
+++ b/windows/ActivityWatch.Windows.Common.psm1
@@ -1,4 +1,4 @@
-Set-StrictMode -Version Latest
+Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Assert-Administrator {
@@ -354,17 +354,81 @@ function Get-ActivityWatchLoggedOnUsers {
return @($users)
}
-function Test-ActivityWatchUserHasSession {
+function Get-ActivityWatchSessionRecords {
+ $sessions = New-Object System.Collections.Generic.List[object]
+
+ try {
+ $lines = & qwinsta.exe 2>$null
+ foreach ($line in @($lines)) {
+ $normalized = [string]$line
+ if ([string]::IsNullOrWhiteSpace($normalized)) {
+ continue
+ }
+
+ $normalized = $normalized.TrimStart(' ', '>')
+ if ([string]::IsNullOrWhiteSpace($normalized)) {
+ continue
+ }
+
+ if ($normalized -match '^(SESSIONNAME|ИМЯ СЕАНСА)\s+') {
+ continue
+ }
+
+ $columns = @(
+ (($normalized -replace '\s{2,}', '|') -split '\|') |
+ ForEach-Object { $_.Trim() } |
+ Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
+ )
+ if ($columns.Count -lt 3) {
+ continue
+ }
+
+ $sessionName = [string]$columns[0]
+ $userName = $null
+ $sessionIdIndex = 1
+
+ if ($columns[1] -notmatch '^\d+$') {
+ $userName = [string]$columns[1]
+ $sessionIdIndex = 2
+ }
+
+ if ($columns.Count -le $sessionIdIndex -or $columns[$sessionIdIndex] -notmatch '^\d+$') {
+ continue
+ }
+
+ $sessionId = [int]$columns[$sessionIdIndex]
+ $state = if ($columns.Count -gt ($sessionIdIndex + 1)) { [string]$columns[$sessionIdIndex + 1] } else { '' }
+ $isLive = $state -match '^(Active|Conn)$'
+
+ $sessions.Add([pscustomobject]@{
+ SessionName = $sessionName
+ UserName = $userName
+ SessionId = $sessionId
+ State = $state
+ IsLive = $isLive
+ }) | Out-Null
+ }
+ }
+ catch {
+ }
+
+ $explorerUsers = Get-ActivityWatchExplorerUsersBySession
+ foreach ($session in @($sessions.ToArray())) {
+ $sessionId = [int]$session.SessionId
+ if ($explorerUsers.ContainsKey($sessionId)) {
+ $session.UserName = [string]$explorerUsers[$sessionId]
+ }
+ }
+
+ return @($sessions.ToArray())
+}
+
+function Resolve-ActivityWatchUserCandidates {
param(
[Parameter(Mandatory = $true)]
- [string]$UserId,
- [string[]]$LoggedOnUsers
+ [string]$UserId
)
- if ([string]::IsNullOrWhiteSpace($UserId)) {
- return $false
- }
-
$candidateIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
[void]$candidateIds.Add($UserId)
@@ -379,7 +443,21 @@ function Test-ActivityWatchUserHasSession {
[void]$candidateIds.Add(('{0}\{1}' -f $env:USERDOMAIN, $leafUser))
}
- foreach ($candidate in @($candidateIds)) {
+ return @($candidateIds)
+}
+
+function Test-ActivityWatchUserHasSession {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$UserId,
+ [string[]]$LoggedOnUsers
+ )
+
+ if ([string]::IsNullOrWhiteSpace($UserId)) {
+ return $false
+ }
+
+ foreach ($candidate in @(Resolve-ActivityWatchUserCandidates -UserId $UserId)) {
if ($LoggedOnUsers -contains $candidate) {
return $true
}
@@ -388,6 +466,34 @@ function Test-ActivityWatchUserHasSession {
return $false
}
+function Test-ActivityWatchUserHasLiveSession {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$UserId,
+ [object[]]$SessionRecords
+ )
+
+ if ([string]::IsNullOrWhiteSpace($UserId)) {
+ return $false
+ }
+
+ foreach ($candidate in @(Resolve-ActivityWatchUserCandidates -UserId $UserId)) {
+ if (@($SessionRecords | Where-Object {
+ $_.IsLive -and
+ -not [string]::IsNullOrWhiteSpace([string]$_.UserName) -and
+ (
+ [string]$_.UserName -ieq $candidate -or
+ ('{0}\{1}' -f $env:COMPUTERNAME, [string]$_.UserName) -ieq $candidate -or
+ ((-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) -and ('{0}\{1}' -f $env:USERDOMAIN, [string]$_.UserName) -ieq $candidate)
+ )
+ }).Count -gt 0) {
+ return $true
+ }
+ }
+
+ return $false
+}
+
function Copy-ActivityWatchCollectorAssets {
param(
[Parameter(Mandatory = $true)]
@@ -1075,6 +1181,7 @@ function Write-ActivityWatchRecoveryScript {
[string]$ConfigPath
)
+ $modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1'
$content = @"
param(
[string]`$ConfigPath = '$ConfigPath'
@@ -1082,57 +1189,58 @@ param(
Set-StrictMode -Version Latest
`$ErrorActionPreference = 'Continue'
+Import-Module '$modulePath' -Force
+Invoke-ActivityWatchRecoveryLoop -ConfigPath `$ConfigPath
+"@
-function Get-DeploymentConfig {
- param([string]`$Path)
- return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json
+ Set-Content -LiteralPath $Path -Value $content -Encoding UTF8
}
-function Get-RecoveryConfigPaths {
- param([string]`$PrimaryConfigPath)
+function Get-ActivityWatchRecoveryConfigPaths {
+ param([string]$PrimaryConfigPath)
- `$paths = New-Object System.Collections.Generic.List[string]
- if (`$PrimaryConfigPath -and (Test-Path -LiteralPath `$PrimaryConfigPath)) {
- `$paths.Add((Resolve-Path -LiteralPath `$PrimaryConfigPath).Path)
+ $paths = New-Object System.Collections.Generic.List[string]
+ if ($PrimaryConfigPath -and (Test-Path -LiteralPath $PrimaryConfigPath)) {
+ $paths.Add((Resolve-Path -LiteralPath $PrimaryConfigPath).Path)
}
- `$searchRoot = `$env:ProgramData
- if (`$PrimaryConfigPath) {
- `$stateRoot = Split-Path -Path `$PrimaryConfigPath -Parent
- `$candidateRoot = Split-Path -Path `$stateRoot -Parent
- if (`$candidateRoot -and (Test-Path -LiteralPath `$candidateRoot)) {
- `$searchRoot = `$candidateRoot
+ $searchRoot = $env:ProgramData
+ if ($PrimaryConfigPath) {
+ $stateRoot = Split-Path -Path $PrimaryConfigPath -Parent
+ $candidateRoot = Split-Path -Path $stateRoot -Parent
+ if ($candidateRoot -and (Test-Path -LiteralPath $candidateRoot)) {
+ $searchRoot = $candidateRoot
}
}
- if (Test-Path -LiteralPath `$searchRoot) {
- Get-ChildItem -LiteralPath `$searchRoot -Directory -ErrorAction SilentlyContinue |
- Where-Object { `$_.Name -like 'ActivityWatch*' } |
+ if (Test-Path -LiteralPath $searchRoot) {
+ Get-ChildItem -LiteralPath $searchRoot -Directory -ErrorAction SilentlyContinue |
+ Where-Object { $_.Name -like 'ActivityWatch*' } |
ForEach-Object {
- `$candidate = Join-Path `$_.FullName 'deployment-config.json'
- if (Test-Path -LiteralPath `$candidate) {
- `$paths.Add(`$candidate)
+ $candidate = Join-Path $_.FullName 'deployment-config.json'
+ if (Test-Path -LiteralPath $candidate) {
+ $paths.Add($candidate)
}
}
}
- return @(`$paths | Sort-Object -Unique)
+ return @($paths | Sort-Object -Unique)
}
-function Get-RecoveryTaskDefinitions {
- param([string[]]`$ConfigPaths)
+function Get-ActivityWatchRecoveryTaskDefinitions {
+ param([string[]]$ConfigPaths)
- `$taskMap = [ordered]@{}
- foreach (`$candidatePath in @(`$ConfigPaths)) {
+ $taskMap = [ordered]@{}
+ foreach ($candidatePath in @($ConfigPaths)) {
try {
- `$config = Get-DeploymentConfig -Path `$candidatePath
- foreach (`$task in @(`$config.userTasks)) {
- `$taskName = [string]`$task.launchTaskName
- `$userId = [string]`$task.userId
- if (-not [string]::IsNullOrWhiteSpace(`$taskName) -and -not `$taskMap.Contains(`$taskName)) {
- `$taskMap[`$taskName] = [pscustomobject]@{
- taskName = `$taskName
- userId = `$userId
+ $config = Read-ActivityWatchDeploymentConfig -Path $candidatePath
+ foreach ($task in @($config.userTasks)) {
+ $taskName = [string]$task.launchTaskName
+ $userId = [string]$task.userId
+ if (-not [string]::IsNullOrWhiteSpace($taskName) -and -not $taskMap.Contains($taskName)) {
+ $taskMap[$taskName] = [pscustomobject]@{
+ taskName = $taskName
+ userId = $userId
}
}
}
@@ -1141,221 +1249,380 @@ function Get-RecoveryTaskDefinitions {
}
}
- return @(`$taskMap.Values)
+ return @($taskMap.Values)
}
-function New-RecoveryLock {
- param([string]`$PrimaryConfigPath)
+function New-ActivityWatchRecoveryLock {
+ param([string]$PrimaryConfigPath)
- `$stateRoot = if (`$PrimaryConfigPath) { Split-Path -Path `$PrimaryConfigPath -Parent } else { Join-Path `$env:ProgramData 'AWatch-rus' }
- if (-not (Test-Path -LiteralPath `$stateRoot)) {
- New-Item -Path `$stateRoot -ItemType Directory -Force | Out-Null
+ $stateRoot = if ($PrimaryConfigPath) { Split-Path -Path $PrimaryConfigPath -Parent } else { Join-Path $env:ProgramData 'AWatch-rus' }
+ if (-not (Test-Path -LiteralPath $stateRoot)) {
+ New-Item -Path $stateRoot -ItemType Directory -Force | Out-Null
}
- `$lockPath = Join-Path `$stateRoot 'recovery-loop.lock'
- if (Test-Path -LiteralPath `$lockPath) {
+ $lockPath = Join-Path $stateRoot 'recovery-loop.lock'
+ if (Test-Path -LiteralPath $lockPath) {
try {
- `$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json
- `$existingPid = [int]`$lockData.pid
- if (`$existingPid -gt 0 -and (Get-Process -Id `$existingPid -ErrorAction SilentlyContinue)) {
- return `$null
+ $lockData = Get-Content -LiteralPath $lockPath -Raw | ConvertFrom-Json
+ $existingPid = [int]$lockData.pid
+ if ($existingPid -gt 0 -and (Get-Process -Id $existingPid -ErrorAction SilentlyContinue)) {
+ return $null
}
}
catch {
}
}
- `$payload = @{
- pid = `$PID
+ $payload = @{
+ pid = $PID
createdAt = (Get-Date).ToUniversalTime().ToString('o')
} | ConvertTo-Json -Compress
- Set-Content -LiteralPath `$lockPath -Value `$payload -Encoding UTF8
- return `$lockPath
+ Set-Content -LiteralPath $lockPath -Value $payload -Encoding UTF8
+ return $lockPath
}
-function Start-TaskIfNotRunning {
- param(
- [string]`$TaskName,
- [string]`$UserId,
- [string[]]`$LoggedOnUsers
- )
- if ([string]::IsNullOrWhiteSpace(`$TaskName)) {
- return
- }
+function Test-ActivityWatchCollectorRunningGlobal {
+ param([string]$ScriptPath)
- if ([string]::IsNullOrWhiteSpace(`$UserId)) {
- return
- }
-
- if (-not (Test-UserHasSession -UserId `$UserId -LoggedOnUsers `$LoggedOnUsers)) {
- return
- }
-
- try {
- `$task = Get-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue
- if (-not `$task) {
- return
- }
- if ([string]`$task.State -eq 'Running') {
- return
- }
- Start-ScheduledTask -TaskName `$TaskName -ErrorAction SilentlyContinue
- }
- catch {
- }
-}
-
-function Get-LoggedOnUsers {
- `$users = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
-
- try {
- `$lines = & quser.exe 2>`$null
- foreach (`$line in @(`$lines)) {
- `$normalized = [string]`$line
- if ([string]::IsNullOrWhiteSpace(`$normalized)) {
- continue
- }
-
- `$normalized = `$normalized.TrimStart(' ', '>')
- if ([string]::IsNullOrWhiteSpace(`$normalized)) {
- continue
- }
-
- if (`$normalized -match '^(USERNAME|ПОЛЬЗОВАТЕЛЬ)\s+') {
- continue
- }
-
- `$parts = `$normalized -split '\s+'
- if (`$parts.Count -lt 1) {
- continue
- }
-
- `$user = [string]`$parts[0]
- if ([string]::IsNullOrWhiteSpace(`$user)) {
- continue
- }
-
- [void]`$users.Add(`$user)
- [void]`$users.Add(('{0}\{1}' -f `$env:COMPUTERNAME, `$user))
- if (-not [string]::IsNullOrWhiteSpace(`$env:USERDOMAIN)) {
- [void]`$users.Add(('{0}\{1}' -f `$env:USERDOMAIN, `$user))
- }
- }
- }
- catch {
- }
-
- return @(`$users)
-}
-
-function Test-UserHasSession {
- param(
- [string]`$UserId,
- [string[]]`$LoggedOnUsers
- )
-
- if ([string]::IsNullOrWhiteSpace(`$UserId)) {
- return `$false
- }
-
- `$candidateIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
- [void]`$candidateIds.Add(`$UserId)
-
- `$leafUser = `$UserId
- if (`$leafUser -match '^[^\\]+\\(.+)$') {
- `$leafUser = `$Matches[1]
- [void]`$candidateIds.Add(`$leafUser)
- }
-
- [void]`$candidateIds.Add(('{0}\{1}' -f `$env:COMPUTERNAME, `$leafUser))
- if (-not [string]::IsNullOrWhiteSpace(`$env:USERDOMAIN)) {
- [void]`$candidateIds.Add(('{0}\{1}' -f `$env:USERDOMAIN, `$leafUser))
- }
-
- foreach (`$candidate in @(`$candidateIds)) {
- if (`$LoggedOnUsers -contains `$candidate) {
- return `$true
- }
- }
-
- return `$false
-}
-
-function Test-CollectorRunningGlobal {
- param([string]`$ScriptPath)
- if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) {
- return `$false
+ if ([string]::IsNullOrWhiteSpace($ScriptPath)) {
+ return $false
}
return [bool]@(
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object {
- (`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and
- `$_.CommandLine -match [Regex]::Escape(`$ScriptPath)
+ ($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and
+ $_.CommandLine -match [Regex]::Escape($ScriptPath)
}
).Count
}
-function Start-CollectorScriptGlobalIfNeeded {
+function Start-ActivityWatchCollectorScriptGlobalIfNeeded {
param(
- [string]`$ScriptPath,
- [string]`$ConfigPath
+ [string]$ScriptPath,
+ [string]$ConfigPath
)
- if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) {
+ if ([string]::IsNullOrWhiteSpace($ScriptPath)) {
return
}
- if (-not (Test-Path -LiteralPath `$ScriptPath)) {
+ if (-not (Test-Path -LiteralPath $ScriptPath)) {
return
}
- if (Test-CollectorRunningGlobal -ScriptPath `$ScriptPath) {
+ if (Test-ActivityWatchCollectorRunningGlobal -ScriptPath $ScriptPath) {
return
}
- `$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
- `$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', `$ScriptPath, '-ConfigPath', `$ConfigPath)
- Start-Process -FilePath `$powershellExe -ArgumentList `$argumentList -WindowStyle Hidden
+ $powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
+ $argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass', '-File', $ScriptPath, '-ConfigPath', $ConfigPath)
+ Start-Process -FilePath $powershellExe -ArgumentList $argumentList -WindowStyle Hidden
}
-`$recoveryLockPath = New-RecoveryLock -PrimaryConfigPath `$ConfigPath
-if (-not `$recoveryLockPath) {
- return
+function Start-ActivityWatchTaskIfNotRunning {
+ param(
+ [string]$TaskName,
+ [string]$UserId,
+ [object[]]$SessionRecords
+ )
+
+ if ([string]::IsNullOrWhiteSpace($TaskName) -or [string]::IsNullOrWhiteSpace($UserId)) {
+ return $false
+ }
+
+ if (-not (Test-ActivityWatchUserHasLiveSession -UserId $UserId -SessionRecords $SessionRecords)) {
+ return $false
+ }
+
+ try {
+ $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
+ if (-not $task) {
+ return $false
+ }
+ if ([string]$task.State -eq 'Running') {
+ return $true
+ }
+ Start-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
+ return $true
+ }
+ catch {
+ return $false
+ }
}
-try {
- while (`$true) {
- `$sleepSeconds = 180
- try {
- `$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath
- `$config = Get-DeploymentConfig -Path `$ConfigPath
- `$loggedOnUsers = Get-LoggedOnUsers
- `$stateRoot = [string]`$config.paths.stateRoot
- `$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { Join-Path `$stateRoot 'worktime-session-collector.ps1' }
- Start-CollectorScriptGlobalIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath
- foreach (`$taskDef in Get-RecoveryTaskDefinitions -ConfigPaths `$configPaths) {
- Start-TaskIfNotRunning -TaskName `$taskDef.taskName -UserId `$taskDef.userId -LoggedOnUsers `$loggedOnUsers
- }
+function Get-ActivityWatchLiveInteractiveSessions {
+ param([object[]]$SessionRecords)
- if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) {
- `$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30)
+ return @(
+ $SessionRecords |
+ Where-Object {
+ $_.IsLive -and
+ $_.SessionId -gt 0 -and
+ -not [string]::IsNullOrWhiteSpace([string]$_.UserName)
+ } |
+ Sort-Object @{ Expression = { if ([string]$_.SessionName -ieq 'console') { 0 } else { 1 } } }, @{ Expression = { [int]$_.SessionId } }
+ )
+}
+
+function Resolve-ActivityWatchLiveSessionUserId {
+ param(
+ [Parameter(Mandatory = $true)]
+ [pscustomobject]$SessionRecord,
+ [pscustomobject[]]$TaskDefinitions
+ )
+
+ $rawUser = [string]$SessionRecord.UserName
+ if ([string]::IsNullOrWhiteSpace($rawUser)) {
+ return $null
+ }
+
+ foreach ($taskDef in @($TaskDefinitions)) {
+ foreach ($candidate in @(Resolve-ActivityWatchUserCandidates -UserId [string]$taskDef.userId)) {
+ if ($candidate -ieq $rawUser -or
+ $candidate -ieq ('{0}\{1}' -f $env:COMPUTERNAME, $rawUser) -or
+ ((-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) -and $candidate -ieq ('{0}\{1}' -f $env:USERDOMAIN, $rawUser))) {
+ return [string]$taskDef.userId
}
}
- catch {
+ }
+
+ if ($rawUser -match '^[^\\]+\\') {
+ return $rawUser
+ }
+
+ return ('{0}\{1}' -f $env:COMPUTERNAME, $rawUser)
+}
+
+function Get-ActivityWatchExplorerUsersBySession {
+ $map = @{}
+
+ try {
+ Get-Process explorer -IncludeUserName -ErrorAction SilentlyContinue |
+ Where-Object { $_.SessionId -gt 0 -and -not [string]::IsNullOrWhiteSpace([string]$_.UserName) } |
+ Sort-Object SessionId, StartTime |
+ ForEach-Object {
+ if (-not $map.ContainsKey([int]$_.SessionId)) {
+ $map[[int]$_.SessionId] = [string]$_.UserName
+ }
+ }
+ }
+ catch {
+ }
+
+ return $map
+}
+
+function Get-ActivityWatchDisconnectedInteractiveSessions {
+ param([object[]]$SessionRecords)
+
+ $explorerUsers = Get-ActivityWatchExplorerUsersBySession
+ $result = New-Object System.Collections.Generic.List[object]
+
+ foreach ($session in @($SessionRecords | Where-Object { -not $_.IsLive -and $_.SessionId -gt 0 })) {
+ $resolvedUser = [string]$session.UserName
+ if ([string]::IsNullOrWhiteSpace($resolvedUser) -and $explorerUsers.ContainsKey([int]$session.SessionId)) {
+ $resolvedUser = [string]$explorerUsers[[int]$session.SessionId]
}
- Start-Sleep -Seconds `$sleepSeconds
- }
-}
-finally {
- if (`$recoveryLockPath -and (Test-Path -LiteralPath `$recoveryLockPath)) {
- Remove-Item -LiteralPath `$recoveryLockPath -Force -ErrorAction SilentlyContinue
- }
-}
-"@
+ if ([string]::IsNullOrWhiteSpace($resolvedUser)) {
+ continue
+ }
- Set-Content -LiteralPath $Path -Value $content -Encoding UTF8
+ $result.Add([pscustomobject]@{
+ SessionName = [string]$session.SessionName
+ SessionId = [int]$session.SessionId
+ State = [string]$session.State
+ UserName = $resolvedUser
+ }) | Out-Null
+ }
+
+ return @($result | Sort-Object SessionId -Unique)
+}
+
+function Promote-ActivityWatchDisconnectedSessionToConsole {
+ param(
+ [pscustomobject[]]$TaskDefinitions,
+ [object[]]$SessionRecords
+ )
+
+ $candidates = Get-ActivityWatchDisconnectedInteractiveSessions -SessionRecords $SessionRecords
+ if (-not $candidates -or $candidates.Count -eq 0) {
+ return $false
+ }
+
+ $selected = $null
+ foreach ($taskDef in @($TaskDefinitions)) {
+ foreach ($candidate in @($candidates)) {
+ foreach ($knownUser in @(Resolve-ActivityWatchUserCandidates -UserId [string]$taskDef.userId)) {
+ if ($knownUser -ieq [string]$candidate.UserName -or
+ $knownUser -ieq ('{0}\{1}' -f $env:COMPUTERNAME, [string]$candidate.UserName) -or
+ ((-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) -and $knownUser -ieq ('{0}\{1}' -f $env:USERDOMAIN, [string]$candidate.UserName))) {
+ $selected = $candidate
+ break
+ }
+ }
+ if ($selected) { break }
+ }
+ if ($selected) { break }
+ }
+
+ if (-not $selected) {
+ $selected = $candidates | Select-Object -First 1
+ }
+
+ if (-not $selected) {
+ return $false
+ }
+
+ try {
+ & cmd.exe /c ("tscon {0} /dest:console" -f [int]$selected.SessionId) | Out-Null
+ return ($LASTEXITCODE -eq 0)
+ }
+ catch {
+ return $false
+ }
+}
+
+function Ensure-ActivityWatchLaunchTaskForUser {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$UserId,
+ [Parameter(Mandatory = $true)]
+ [string]$LaunchScriptPath,
+ [Parameter(Mandatory = $true)]
+ [string]$ConfigPath
+ )
+
+ if ([string]::IsNullOrWhiteSpace($UserId) -or -not (Test-Path -LiteralPath $LaunchScriptPath)) {
+ return $null
+ }
+
+ $taskName = "ActivityWatch Launch [$((Get-ActivityWatchTaskNameToken -UserId $UserId))]"
+ $launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath
+ Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath
+
+ $wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe'
+ $action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`""
+ $trigger = New-ScheduledTaskTrigger -AtLogOn -User $UserId
+ $principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType Interactive -RunLevel Highest
+ $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0)
+
+ try {
+ $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
+ if ($existingTask) {
+ $existingUserId = [string]$existingTask.Principal.UserId
+ $existingArgs = @($existingTask.Actions | ForEach-Object { [string]$_.Arguments }) -join ' '
+ if ($existingUserId -ieq $UserId -and $existingArgs -like "*$launcherPath*") {
+ return $taskName
+ }
+
+ Remove-ActivityWatchScheduledTask -TaskName $taskName
+ }
+
+ Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
+ return $taskName
+ }
+ catch {
+ return $null
+ }
+}
+
+function Start-ActivityWatchConsoleFallbackIfNeeded {
+ param(
+ [pscustomobject[]]$TaskDefinitions,
+ [object[]]$SessionRecords,
+ [pscustomobject]$Config,
+ [string]$ConfigPath,
+ [bool]$ConfiguredLiveTasksStarted
+ )
+
+ if ($ConfiguredLiveTasksStarted) {
+ return
+ }
+
+ $liveSessions = Get-ActivityWatchLiveInteractiveSessions -SessionRecords $SessionRecords
+ if (-not $liveSessions -or $liveSessions.Count -eq 0) {
+ if (Promote-ActivityWatchDisconnectedSessionToConsole -TaskDefinitions $TaskDefinitions -SessionRecords $SessionRecords) {
+ Start-Sleep -Seconds 3
+ $SessionRecords = Get-ActivityWatchSessionRecords
+ $liveSessions = Get-ActivityWatchLiveInteractiveSessions -SessionRecords $SessionRecords
+ }
+ }
+ if (-not $liveSessions -or $liveSessions.Count -eq 0) {
+ return
+ }
+
+ $launchScriptPath = if ($Config.paths.PSObject.Properties.Name -contains 'launchScript') { [string]$Config.paths.launchScript } else { $null }
+ if ([string]::IsNullOrWhiteSpace($launchScriptPath)) {
+ return
+ }
+
+ $preferredSession = $liveSessions | Select-Object -First 1
+ $userId = Resolve-ActivityWatchLiveSessionUserId -SessionRecord $preferredSession -TaskDefinitions $TaskDefinitions
+ if ([string]::IsNullOrWhiteSpace($userId)) {
+ return
+ }
+
+ $taskName = Ensure-ActivityWatchLaunchTaskForUser -UserId $userId -LaunchScriptPath $launchScriptPath -ConfigPath $ConfigPath
+ if ([string]::IsNullOrWhiteSpace($taskName)) {
+ return
+ }
+
+ try {
+ $task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
+ if ($task -and [string]$task.State -ne 'Running') {
+ Start-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
+ }
+ }
+ catch {
+ }
+}
+
+function Invoke-ActivityWatchRecoveryLoop {
+ param([string]$ConfigPath)
+
+ $recoveryLockPath = New-ActivityWatchRecoveryLock -PrimaryConfigPath $ConfigPath
+ if (-not $recoveryLockPath) {
+ return
+ }
+
+ try {
+ while ($true) {
+ $sleepSeconds = 180
+ try {
+ $configPaths = Get-ActivityWatchRecoveryConfigPaths -PrimaryConfigPath $ConfigPath
+ $config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
+ $taskDefs = Get-ActivityWatchRecoveryTaskDefinitions -ConfigPaths $configPaths
+ $sessionRecords = Get-ActivityWatchSessionRecords
+ $stateRoot = [string]$config.paths.stateRoot
+ $sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
+ Start-ActivityWatchCollectorScriptGlobalIfNeeded -ScriptPath $sessionCollectorScript -ConfigPath $ConfigPath
+
+ $configuredLiveTasksStarted = $false
+ foreach ($taskDef in $taskDefs) {
+ if (Start-ActivityWatchTaskIfNotRunning -TaskName $taskDef.taskName -UserId $taskDef.userId -SessionRecords $sessionRecords) {
+ $configuredLiveTasksStarted = $true
+ }
+ }
+
+ Start-ActivityWatchConsoleFallbackIfNeeded -TaskDefinitions $taskDefs -SessionRecords $sessionRecords -Config $config -ConfigPath $ConfigPath -ConfiguredLiveTasksStarted $configuredLiveTasksStarted
+
+ if ($config -and $config.recovery -and $config.recovery.intervalSeconds) {
+ $sleepSeconds = [Math]::Max([int]$config.recovery.intervalSeconds, 30)
+ }
+ }
+ catch {
+ }
+
+ Start-Sleep -Seconds $sleepSeconds
+ }
+ }
+ finally {
+ if ($recoveryLockPath -and (Test-Path -LiteralPath $recoveryLockPath)) {
+ Remove-Item -LiteralPath $recoveryLockPath -Force -ErrorAction SilentlyContinue
+ }
+ }
}
function Get-ActivityWatchHiddenLauncherPath {
@@ -1702,10 +1969,10 @@ function Start-ActivityWatchTasks {
[string]$RecoveryTaskName = 'ActivityWatch Recovery'
)
- $loggedOnUsers = Get-ActivityWatchLoggedOnUsers
+ $sessionRecords = Get-ActivityWatchSessionRecords
foreach ($definition in $TaskDefinitions) {
- if (Test-ActivityWatchUserHasSession -UserId $definition.UserId -LoggedOnUsers $loggedOnUsers) {
+ if (Test-ActivityWatchUserHasLiveSession -UserId $definition.UserId -SessionRecords $sessionRecords) {
Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue
}
}