Compare commits

...
Author SHA1 Message Date
igor04091968 e658510442 Stabilize Windows logical host guard
CI / Rust checks (push) Canceled after 0s
CI / Docs and registry checks (push) Canceled after 0s
CI / Smoke checks (push) Canceled after 0s
Coverage / Coverage baseline (push) Canceled after 0s
Security / Cargo audit (push) Canceled after 0s
Security / Cargo deny (push) Canceled after 0s
Security / Secret pattern check (push) Canceled after 0s
Security / Dependency review (push) Canceled after 0s
2026-07-01 06:06:42 +03:00
14 changed files with 314 additions and 68 deletions
@@ -6,7 +6,7 @@ use std::{
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
process::Command,
sync::mpsc,
sync::{Arc, Mutex, mpsc},
time::{Duration, SystemTime},
};
@@ -16,7 +16,7 @@ use chrono::{DateTime, Timelike, Utc};
use clap::{Parser, Subcommand};
use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher, event::RenameMode};
use regex::Regex;
use reqwest::blocking::Client;
use reqwest::{StatusCode, blocking::Client};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use sha2::{Digest, Sha256};
@@ -1462,13 +1462,10 @@ fn ensure_aw_bucket(
hostname: &str,
) -> Result<()> {
let url = format!("{}/buckets/{bucket_id}", api_base.trim_end_matches('/'));
if client
.get(&url)
.send()
.map(|response| response.status().is_success())
.unwrap_or(false)
{
return Ok(());
if let Ok(response) = client.get(&url).send() {
if aw_bucket_status_accepts_existing(response.status()) {
return Ok(());
}
}
let response = client
.post(&url)
@@ -1479,7 +1476,7 @@ fn ensure_aw_bucket(
}))
.send()
.with_context(|| format!("POST {url}"))?;
if !response.status().is_success() {
if !aw_bucket_status_accepts_existing(response.status()) {
bail!(
"bucket create failed {} status={}",
bucket_id,
@@ -1489,6 +1486,10 @@ fn ensure_aw_bucket(
Ok(())
}
fn aw_bucket_status_accepts_existing(status: StatusCode) -> bool {
status.is_success() || status == StatusCode::NOT_MODIFIED
}
fn append_file_ops_log(runtime: &FileOpsRuntime, message: &str) -> Result<()> {
if !runtime.local_logs_enabled {
return Ok(());
@@ -1599,6 +1600,7 @@ struct RustCollectorRuntime {
rules_path: PathBuf,
policy_path: PathBuf,
incident_screenshot_enabled: bool,
ensured_buckets: Arc<Mutex<HashSet<String>>>,
}
#[derive(Debug, Default, Clone, Serialize)]
@@ -2045,6 +2047,7 @@ fn build_rust_collector_runtime(
rules_path,
policy_path,
incident_screenshot_enabled,
ensured_buckets: Arc::new(Mutex::new(HashSet::new())),
})
}
@@ -3127,14 +3130,7 @@ fn send_collector_aw_event(
return Ok(());
}
let client = Client::builder().timeout(Duration::from_secs(15)).build()?;
ensure_aw_bucket(
&client,
&runtime.api_base,
bucket_id,
client_name,
bucket_type,
&runtime.hostname,
)?;
ensure_runtime_aw_bucket(runtime, &client, bucket_id, client_name, bucket_type)?;
let url = format!(
"{}/buckets/{bucket_id}/heartbeat?pulsetime={}",
runtime.api_base.trim_end_matches('/'),
@@ -3155,6 +3151,37 @@ fn send_collector_aw_event(
Ok(())
}
fn ensure_runtime_aw_bucket(
runtime: &RustCollectorRuntime,
client: &Client,
bucket_id: &str,
client_name: &str,
bucket_type: &str,
) -> Result<()> {
if runtime
.ensured_buckets
.lock()
.map(|cache| cache.contains(bucket_id))
.unwrap_or(false)
{
return Ok(());
}
ensure_aw_bucket(
client,
&runtime.api_base,
bucket_id,
client_name,
bucket_type,
&runtime.hostname,
)?;
if let Ok(mut cache) = runtime.ensured_buckets.lock() {
cache.insert(bucket_id.to_string());
}
Ok(())
}
fn collector_state(
schema: &str,
runtime: &RustCollectorRuntime,
@@ -6029,6 +6056,17 @@ Connect=Srvr="srv";Ref="x";
assert_eq!(value.get("ok").and_then(Value::as_bool), Some(true));
}
#[test]
fn bucket_status_accepts_only_success_or_not_modified() {
assert!(aw_bucket_status_accepts_existing(StatusCode::OK));
assert!(aw_bucket_status_accepts_existing(StatusCode::NO_CONTENT));
assert!(aw_bucket_status_accepts_existing(StatusCode::NOT_MODIFIED));
assert!(!aw_bucket_status_accepts_existing(StatusCode::NOT_FOUND));
assert!(!aw_bucket_status_accepts_existing(
StatusCode::SERVICE_UNAVAILABLE
));
}
#[test]
fn dlp_evidence_sync_accepts_only_dlp_screenshot_names() {
assert!(is_dlp_evidence_screenshot_name(
+73 -9
View File
@@ -18,7 +18,13 @@
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
aw_windows_domain: "SHARKON2025"
# Windows account domain is the physical/local Windows logon domain. It may
# change when the RDP server is renamed. Do not use it as the ActivityWatch
# bucket identity.
aw_windows_domain: ""
# Stable ActivityWatch identity used in bucket ids, dashboards and reports.
# Keep this stable across Windows/RDP host renames.
aw_windows_logical_host_id: ""
aw_windows_builtin_administrator_name: "Администратор"
aw_windows_users:
- Администратор
@@ -143,13 +149,44 @@
)
}}
- name: Получить Windows COMPUTERNAME для account-domain fallback
ansible.windows.win_command: powershell.exe -NoProfile -Command "$env:COMPUTERNAME"
register: aw_windows_computername_result
changed_when: false
- name: Вычислить effective Windows account domain
ansible.builtin.set_fact:
aw_windows_domain_effective: >-
{{
aw_windows_domain
if (
(aw_windows_domain | default('') | string | length) > 0
and (aw_windows_domain | string) != 'HOST-EXAMPLE'
)
else (aw_windows_computername_result.stdout | trim)
}}
- name: Вычислить effective File1C upload principal
ansible.builtin.set_fact:
aw_windows_file_1c_auto_upload_run_as_user_effective: >-
{{
aw_windows_file_1c_auto_upload_run_as_user
if (
(aw_windows_file_1c_auto_upload_run_as_user | default('') | string | length) > 0
and 'HOST-EXAMPLE' not in (aw_windows_file_1c_auto_upload_run_as_user | string)
and not ((aw_windows_file_1c_auto_upload_run_as_user | string).startswith('\\'))
)
else (aw_windows_domain_effective ~ '\\' ~ aw_windows_builtin_administrator_name)
}}
- name: Проверить обязательные переменные
ansible.builtin.assert:
that:
- aw_windows_server_host_effective | length > 0
- aw_windows_server_port is defined
- aw_windows_server_scheme is defined
- aw_windows_domain is defined
- aw_windows_domain_effective is defined
- aw_windows_domain_effective | string | length > 0
- aw_windows_builtin_administrator_name is defined
- aw_windows_builtin_administrator_name | length > 0
- aw_windows_users_effective | length > 0
@@ -158,6 +195,25 @@
- (not (aw_windows_file_1c_auto_upload_enabled | bool)) or (aw_windows_file_1c_target_host_effective | length > 0)
fail_msg: "Не заданы обязательные переменные Windows-развёртывания."
- name: Вычислить stable ActivityWatch host identity
ansible.builtin.set_fact:
aw_windows_logical_host_id_effective: >-
{{
aw_windows_logical_host_id
if (
(aw_windows_logical_host_id | default('') | string | length) > 0
and (aw_windows_logical_host_id | string) != 'HOST-EXAMPLE'
)
else (
aw_windows_hostname_override
if (
(aw_windows_hostname_override | default('') | string | length) > 0
and (aw_windows_hostname_override | string) != 'HOST-EXAMPLE'
)
else ''
)
}}
- name: Нормализовать effective флаги collector'ов и smoke-check
ansible.builtin.set_fact:
aw_windows_afk_enabled_effective: "{{ (aw_windows_afk_enabled | default(aw_windows_afk_enabled_default)) | bool }}"
@@ -286,7 +342,7 @@
ServerHost = "{{ aw_windows_server_host_effective }}"
ServerPort = {{ aw_windows_server_port }}
Version = "{{ aw_windows_package_version }}"
Domain = "{{ aw_windows_domain }}"
Domain = "{{ aw_windows_domain_effective }}"
UserListPath = "{{ aw_windows_deploy_root }}\windows\users.txt"
InstallRoot = "{{ aw_windows_install_root }}"
StateRoot = "{{ aw_windows_state_root }}"
@@ -316,7 +372,7 @@
File1CAutoUploadEnabled = {{ '$true' if (aw_windows_file_1c_auto_upload_enabled | bool) else '$false' }}
File1CAutoUploadIntervalMinutes = {{ aw_windows_file_1c_auto_upload_interval_minutes | int }}
File1CAutoUploadTaskName = "{{ aw_windows_file_1c_auto_upload_task_name }}"
File1CAutoUploadRunAsUser = "{{ aw_windows_file_1c_auto_upload_run_as_user }}"
File1CAutoUploadRunAsUser = "{{ aw_windows_file_1c_auto_upload_run_as_user_effective }}"
File1CTargetHost = "{{ aw_windows_file_1c_target_host_effective }}"
File1CTargetUser = "{{ aw_windows_file_1c_target_user }}"
File1CRegistryWorkbookPath = "{{ aw_windows_file_1c_registry_workbook_path }}"
@@ -336,8 +392,8 @@
{% endfor %}
)
{% endif %}
{% if (aw_windows_hostname_override | default('') | string | length) > 0 %}
$params.AwHostname = "{{ aw_windows_hostname_override }}"
{% if (aw_windows_logical_host_id_effective | default('') | string | length) > 0 %}
$params.AwHostname = "{{ aw_windows_logical_host_id_effective }}"
{% endif %}
{% if aw_windows_skip_hardening | bool %}
$params.SkipHardening = $true
@@ -465,7 +521,7 @@
}
& "{{ aw_windows_deploy_root }}\windows\install-collector-guard-service.ps1" @guardParams
- name: Получить Windows hostname для AW smoke-check bucket
- name: Получить Windows hostname для fallback AW smoke-check bucket
when:
- aw_windows_api_smoke_check_enabled | bool
ansible.windows.win_command: powershell.exe -NoProfile -Command "$env:COMPUTERNAME"
@@ -481,7 +537,11 @@
{{
aw_windows_api_smoke_check_bucket
if (aw_windows_api_smoke_check_bucket | default('') | string | length) > 0
else 'aw-worktime-sessions_' ~ (aw_windows_hostname_result.stdout | trim)
else 'aw-worktime-sessions_' ~ (
aw_windows_logical_host_id_effective
if (aw_windows_logical_host_id_effective | default('') | string | length) > 0
else (aw_windows_hostname_result.stdout | trim)
)
}}
- name: Вычислить AW Window smoke-check bucket
@@ -495,7 +555,11 @@
{{
aw_windows_api_smoke_check_window_bucket
if (aw_windows_api_smoke_check_window_bucket | default('') | string | length) > 0
else 'aw-watcher-window_' ~ (aw_windows_hostname_result.stdout | trim)
else 'aw-watcher-window_' ~ (
aw_windows_logical_host_id_effective
if (aw_windows_logical_host_id_effective | default('') | string | length) > 0
else (aw_windows_hostname_result.stdout | trim)
)
}}
- name: Выполнить AW API smoke-check (worktime bucket должен получать события)
+4
View File
@@ -15,6 +15,10 @@ aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases
aw_windows_package_zip_path: ""
aw_windows_domain: "HOST-EXAMPLE"
# Stable ActivityWatch host identity for bucket ids, Grafana variables and
# ClickHouse workforce keys. This is separate from aw_windows_domain.
# Production host_vars must override both values explicitly.
aw_windows_logical_host_id: "HOST-EXAMPLE"
aw_windows_builtin_administrator_name: "Администратор"
aw_windows_users:
- Администратор
+8 -3
View File
@@ -14,6 +14,11 @@ aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
aw_windows_domain: "HOST-EXAMPLE"
# Stable ActivityWatch host identity for bucket ids and dashboards.
# This is not necessarily the Windows computer name. Keep it stable across
# Windows/RDP host renames, and change aw_windows_domain separately when the
# local Windows logon domain changes.
aw_windows_logical_host_id: "HOST-EXAMPLE"
# Localized name of the built-in local Administrator account (SID ending in -500).
# On the current Russian Windows host this must stay "Администратор";
# do not replace it with "Administrator" unless the target OS account is actually named that way.
@@ -33,7 +38,7 @@ aw_windows_extra_users: []
# Единые Windows/RDP пути: те же, что использует InnoSetup.
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
aw_windows_hostname_override: "" # Например: HOST-EXAMPLE
aw_windows_hostname_override: "" # Legacy alias; prefer aw_windows_logical_host_id.
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
aw_windows_file_ops_enabled: true
@@ -74,8 +79,8 @@ aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
# По умолчанию AFK bucket вычисляется как aw-watcher-afk_<COMPUTERNAME>.
# Задайте явное значение только если watcher пишет в нестандартный bucket.
# По умолчанию smoke-check bucket вычисляется как aw-watcher-*_<aw_windows_logical_host_id>.
# Если logical id не задан, используется физический COMPUTERNAME как fallback.
aw_windows_api_smoke_check_enabled: true
aw_windows_api_smoke_check_bucket: ""
aw_windows_api_smoke_check_limit: 10
+13 -3
View File
@@ -1,5 +1,15 @@
---
# SHARKON2025 uses aw-windows-telemetry browser-domains-collector as the
# per-user currentwindow source. The legacy aw-watcher-window process emits
# no-user duplicate rows in this RDP setup, so keep it disabled for this host.
# Production DetMir keeps SHARKON2025 as the stable ActivityWatch logical host
# id for historical buckets, Grafana variables and ClickHouse workforce keys.
# This value is intentionally independent from the physical Windows computer
# name, which may change during RDP server maintenance.
aw_windows_logical_host_id: "SHARKON2025"
aw_windows_hostname_override: "{{ aw_windows_logical_host_id }}"
# Set aw_windows_domain to the current Windows local/domain logon prefix after
# a server rename. Do not use aw_windows_logical_host_id as a logon domain.
# This host uses aw-windows-telemetry browser-domains-collector as the per-user
# currentwindow source. The legacy aw-watcher-window process emits no-user
# duplicate rows in this RDP setup, so keep it disabled for this host.
aw_windows_window_enabled: false
+4 -4
View File
@@ -42,13 +42,13 @@ aw_windows_builtin_administrator_name: "Администратор"
Назначение: явно фиксировать локализованное имя встроенной учетной записи Administrator с SID `*-500`.
Для текущего Windows host `SHARKON2025` task name должен строиться как:
Task name должен строиться из stable ActivityWatch logical host id и локализованного имени пользователя. Для текущего DetMir production historical logical id остаётся `SHARKON2025`, даже если физический `COMPUTERNAME` RDP-сервера изменён:
```text
ActivityWatch Launch [SHARKON2025_Администратор]
```
Если task по `SHARKON2025_Administrator` не найден, recovery/deploy path обязан пробовать кириллическое имя `Администратор`. Это зафиксировано через:
Если task по `<logical-host>_Administrator` не найден, recovery/deploy path обязан пробовать кириллическое имя `Администратор`. Это зафиксировано через:
- default vars в `ansible/deploy_aw_windows.yml`;
- `ansible/group_vars/aw_windows.yml`;
@@ -60,9 +60,9 @@ ActivityWatch Launch [SHARKON2025_Администратор]
`ActivityWatch.Windows.Common.psm1` усилил recovery path:
- `Get-ActivityWatchBuiltInAdministratorName` сначала смотрит env override, затем SID-500 lookup, затем host-specific fallback `SHARKON2025 -> Администратор`;
- `Get-ActivityWatchBuiltInAdministratorName` сначала смотрит env override, затем SID-500 lookup, затем общий fallback `Administrator`;
- `Normalize-ActivityWatchUsers` стабилизирован для pipeline/list cases;
- удаление scheduled tasks стало устойчивее к частично удаленным task definitions;
- recovery task может ориентироваться на live interactive session и запускаться в interactive logon context, когда это безопаснее для watcher'ов.
Операционный вывод: для RDP/console telemetry нельзя полагаться на task name с английским `Administrator` на русифицированной Windows. Локализованное имя должно быть частью deploy vars.
Операционный вывод: для RDP/console telemetry нельзя полагаться на task name с английским `Administrator` на русифицированной Windows. Локализованное имя должно быть частью deploy vars, а `awHostname` должен оставаться stable logical id и не обязан совпадать с физическим именем Windows.
+4 -2
View File
@@ -191,7 +191,8 @@ Ansible playbook `ansible/deploy_aw_windows.yml` выполняет этот mig
.\windows\deploy-domain-users.ps1 `
-ServerHost <AW_SERVER_HOST> `
-ServerPort 5600 `
-Domain SHARKON2025 `
-Domain <WINDOWS_ACCOUNT_DOMAIN_OR_COMPUTERNAME> `
-AwHostname <STABLE_AW_LOGICAL_HOST_ID> `
-Users user2,user3,user4,user5 `
-InstallRoot 'C:\Program Files\AWatch-rus\bin' `
-StateRoot 'C:\ProgramData\AWatch-rus' `
@@ -205,7 +206,8 @@ Single-user pilot в таком же стиле:
.\windows\deploy-single-user.ps1 `
-ServerHost <AW_SERVER_HOST> `
-ServerPort 5600 `
-TargetUser 'SHARKON2025\user1' `
-TargetUser '<WINDOWS_ACCOUNT_DOMAIN_OR_COMPUTERNAME>\user1' `
-AwHostname <STABLE_AW_LOGICAL_HOST_ID> `
-InstallRoot 'C:\Program Files\AWatch-rus\bin' `
-StateRoot 'C:\ProgramData\AWatch-rus' `
-CustomRulesPath C:\Program Files\AWatch-rus\windows\web-category-rules.example.json `
+4 -2
View File
@@ -40,10 +40,12 @@ Get-ScheduledTask -TaskName 'ActivityWatch*' |
- по одной задаче `ActivityWatch Launch [...]` на пользователя;
- одна задача `ActivityWatch Recovery`.
Точечная проверка:
Точечная проверка. В имени `ActivityWatch Launch [...]` используется stable ActivityWatch logical host id из `deployment-config.json` (`awHostname`), а не обязательно физический `COMPUTERNAME`:
```powershell
Get-ScheduledTask | Where-Object TaskName -eq 'ActivityWatch Launch [SHARKON2025_user1]'
$cfg = Get-Content 'C:\ProgramData\AWatch-rus\deployment-config.json' -Raw | ConvertFrom-Json
$logicalHost = if ($cfg.awHostname) { [string]$cfg.awHostname } else { [string]$env:COMPUTERNAME }
Get-ScheduledTask | Where-Object TaskName -eq "ActivityWatch Launch [$($logicalHost)_user1]"
Get-ScheduledTask | Where-Object TaskName -eq 'ActivityWatch Recovery'
```
+118 -13
View File
@@ -2,6 +2,7 @@ using System;
using System.Diagnostics;
using System.IO;
using System.ServiceProcess;
using System.Threading;
namespace AWatchRus
{
@@ -9,6 +10,12 @@ namespace AWatchRus
{
private Process child;
private readonly ServiceOptions options;
private readonly object sync = new object();
private bool stopping;
private string childFileName;
private string childArguments;
private DateTime restartWindowStartedUtc = DateTime.UtcNow;
private int restartCountInWindow;
public CollectorGuardService(ServiceOptions options)
{
@@ -21,29 +28,113 @@ namespace AWatchRus
protected override void OnStart(string[] args)
{
Directory.CreateDirectory(Path.GetDirectoryName(options.LogPath));
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " service starting" + Environment.NewLine);
Log("service starting");
var fileName = string.IsNullOrWhiteSpace(options.ExecPath) ? options.PowerShellPath : options.ExecPath;
var arguments = string.IsNullOrWhiteSpace(options.ExecPath)
childFileName = string.IsNullOrWhiteSpace(options.ExecPath) ? options.PowerShellPath : options.ExecPath;
childArguments = string.IsNullOrWhiteSpace(options.ExecPath)
? string.Format(
"-NoProfile -ExecutionPolicy Bypass -File \"{0}\" -ConfigPath \"{1}\" -Mode {2} -LoopSeconds {3}",
options.ScriptPath,
options.ConfigPath,
options.Mode,
options.LoopSeconds)
: options.ExecArgs;
: (options.ExecArgs ?? string.Empty);
StartChild("initial start");
}
private void StartChild(string reason)
{
lock (sync)
{
if (stopping)
{
return;
}
if (child != null && !child.HasExited)
{
return;
}
}
var psi = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
FileName = childFileName,
Arguments = childArguments,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = false,
RedirectStandardError = false,
};
child = Process.Start(psi);
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " child pid=" + child.Id + " exec=" + fileName + Environment.NewLine);
var process = new Process
{
StartInfo = psi,
EnableRaisingEvents = true,
};
process.Exited += ChildExited;
process.Start();
lock (sync)
{
child = process;
}
Log("child pid=" + process.Id + " exec=" + childFileName + " reason=" + reason);
}
private void ChildExited(object sender, EventArgs args)
{
var process = sender as Process;
var exitCode = "unknown";
try
{
if (process != null)
{
exitCode = process.ExitCode.ToString();
}
}
catch
{
}
lock (sync)
{
if (stopping)
{
Log("child exited during stop exitCode=" + exitCode);
return;
}
}
Log("child exited unexpectedly exitCode=" + exitCode);
if (!RegisterChildRestart())
{
Log("child restart budget exhausted; exiting service for SCM recovery");
Environment.Exit(1);
return;
}
var restartThread = new Thread(new ThreadStart(delegate
{
Thread.Sleep(Math.Max(1, options.ChildRestartDelaySeconds) * 1000);
StartChild("child-exit restart");
}));
restartThread.IsBackground = true;
restartThread.Start();
}
private bool RegisterChildRestart()
{
lock (sync)
{
var now = DateTime.UtcNow;
if ((now - restartWindowStartedUtc).TotalSeconds > options.ChildRestartWindowSeconds)
{
restartWindowStartedUtc = now;
restartCountInWindow = 0;
}
restartCountInWindow++;
Log("child restart budget count=" + restartCountInWindow + " windowSeconds=" + options.ChildRestartWindowSeconds);
return restartCountInWindow <= options.MaxChildRestartsInWindow;
}
}
protected override void OnStop()
@@ -60,24 +151,35 @@ namespace AWatchRus
{
try
{
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " " + reason + Environment.NewLine);
if (child != null && !child.HasExited)
Log(reason);
Process process;
lock (sync)
{
child.Kill();
child.WaitForExit(10000);
stopping = true;
process = child;
}
if (process != null && !process.HasExited)
{
process.Kill();
process.WaitForExit(10000);
}
}
catch (Exception ex)
{
try
{
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " stop error: " + ex.Message + Environment.NewLine);
Log("stop error: " + ex.Message);
}
catch
{
}
}
}
private void Log(string message)
{
File.AppendAllText(options.LogPath, DateTime.Now.ToString("s") + " " + message + Environment.NewLine);
}
}
public sealed class ServiceOptions
@@ -91,6 +193,9 @@ namespace AWatchRus
public string LogPath = @"C:\ProgramData\AWatch-rus\logs\collector-guard-service.log";
public string ExecPath = null;
public string ExecArgs = null;
public int ChildRestartDelaySeconds = 5;
public int MaxChildRestartsInWindow = 5;
public int ChildRestartWindowSeconds = 600;
}
internal static class Program
@@ -312,11 +312,6 @@ function Get-ActivityWatchBuiltInAdministratorName {
catch {
}
if ([string]$env:COMPUTERNAME -ieq 'SHARKON2025') {
$script:ActivityWatchBuiltInAdministratorName = 'Администратор'
return $script:ActivityWatchBuiltInAdministratorName
}
$script:ActivityWatchBuiltInAdministratorName = 'Administrator'
return $script:ActivityWatchBuiltInAdministratorName
}
+4 -4
View File
@@ -322,18 +322,18 @@ function Invoke-GuardSelfTest {
$oldComputerName = $env:COMPUTERNAME
try {
$env:COMPUTERNAME = 'SHARKON2025'
$env:COMPUTERNAME = 'HOST-EXAMPLE'
$sessionRecords = @(
[pscustomobject]@{ SessionName = 'USER5'; UserName = 'USER5'; SessionId = 2; State = 'Disc'; IsLive = $false },
[pscustomobject]@{ SessionName = 'console'; UserName = ''; SessionId = 1; State = 'Conn'; IsLive = $true }
)
$taskDefs = @(
[pscustomobject]@{ taskName = 'ActivityWatch Launch [SHARKON2025_user5]'; userId = 'SHARKON2025\user5' }
[pscustomobject]@{ taskName = 'ActivityWatch Launch [HOST-EXAMPLE_user5]'; userId = 'HOST-EXAMPLE\user5' }
)
if (-not (Test-ActivityWatchUserHasManagedSession -UserId 'SHARKON2025\user5' -SessionRecords $sessionRecords -IncludeDisconnected)) {
if (-not (Test-ActivityWatchUserHasManagedSession -UserId 'HOST-EXAMPLE\user5' -SessionRecords $sessionRecords -IncludeDisconnected)) {
throw 'expected disconnected managed session to match task user'
}
if (Test-ActivityWatchUserHasManagedSession -UserId 'SHARKON2025\user5' -SessionRecords $sessionRecords -IncludeLive) {
if (Test-ActivityWatchUserHasManagedSession -UserId 'HOST-EXAMPLE\user5' -SessionRecords $sessionRecords -IncludeLive) {
throw 'disconnected managed session should not match live-only filter'
}
$managed = @(Get-ActivityWatchManagedInteractiveSessions -TaskDefinitions $taskDefs -SessionRecords $sessionRecords -IncludeDisconnected)
@@ -79,6 +79,7 @@ else {
New-Service -Name $ServiceName -BinaryPathName $binPath -DisplayName 'AWatch-rus Collector Guard' -StartupType Automatic | Out-Null
sc.exe description $ServiceName "Session-aware ActivityWatch collector guard for AWatch-rus" | Out-Null
sc.exe failure $ServiceName reset= 300 actions= restart/5000/restart/15000/restart/60000 | Out-Null
sc.exe failureflag $ServiceName 1 | Out-Null
if ($DisableRecoveryTask) {
Write-Warning 'DisableRecoveryTask is deprecated and ignored: ActivityWatch Recovery must remain enabled as collector guard fallback.'
@@ -5,7 +5,7 @@
#define AwDefaultServerHost "aw-server"
#define AwDefaultServerPort "5600"
#define AwDefaultWorktimeReportBase "http://aw-server:5610"
#define AwDefaultWorktimeHost "SHARKON2025"
#define AwDefaultWorktimeHost "HOST-EXAMPLE"
#define AwDefaultUsers "user1,user2,user3,user4,user5"
#define AwDefaultInstallRoot "C:\\Program Files\\AWatch-rus\\bin"
#define AwDefaultStateRoot "C:\\ProgramData\\AWatch-rus"
@@ -13,7 +13,7 @@
; This installer wraps the standalone-service path.
; It is suitable for standalone/headless deployment and must not be treated
; as the canonical multi-user RDP deployment path used on SHARKON2025.
; as the canonical multi-user RDP deployment path used on the configured logical host id.
[Setup]
AppId={{6D6A1F74-0F4F-4A57-B5E3-1C2C2F56C0E9}
+23 -3
View File
@@ -1,6 +1,9 @@
[CmdletBinding()]
param(
[string]$UserId = 'SHARKON2025\user1'
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
[string]$User = 'user1',
[string]$UserId,
[string]$AwHostname
)
Set-StrictMode -Version Latest
@@ -13,13 +16,30 @@ Start-Sleep -Seconds 10
Get-Process notepad -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
'@ | Set-Content -LiteralPath $probeScriptPath -Encoding UTF8
schtasks /Run /TN 'ActivityWatch Launch [SHARKON2025_user1]' | Out-Null
$config = $null
if (Test-Path -LiteralPath $ConfigPath) {
$config = Get-Content -Raw -LiteralPath $ConfigPath | ConvertFrom-Json
}
$logicalHost = if (-not [string]::IsNullOrWhiteSpace($AwHostname)) {
$AwHostname
}
elseif ($config -and $config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$config.awHostname)) {
[string]$config.awHostname
}
else {
[string]$env:COMPUTERNAME
}
$accountDomain = if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) { [string]$env:USERDOMAIN } else { [string]$env:COMPUTERNAME }
$effectiveUserId = if (-not [string]::IsNullOrWhiteSpace($UserId)) { $UserId } else { '{0}\{1}' -f $accountDomain, $User }
$launchTaskName = 'ActivityWatch Launch [{0}_{1}]' -f $logicalHost, $User
schtasks /Run /TN $launchTaskName | Out-Null
Start-Sleep -Seconds 3
$taskName = 'AW User1 Notepad Probe'
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
$action = New-ScheduledTaskAction -Execute (Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe') -Argument "-NoProfile -ExecutionPolicy Bypass -File $probeScriptPath"
$principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType Interactive -RunLevel Highest
$principal = New-ScheduledTaskPrincipal -UserId $effectiveUserId -LogonType Interactive -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings | Out-Null
Start-ScheduledTask -TaskName $taskName