Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc8988e5f7 | ||
|
|
230a9c6936 | ||
|
|
6a36febfeb | ||
|
|
e80272a55f | ||
|
|
1c3d7896cd | ||
|
|
211a6a6eac | ||
|
|
6f5e5eb751 | ||
|
|
e643576aa9 |
@@ -1,89 +0,0 @@
|
||||
# Copilot instructions for ActivityWatch-Russian
|
||||
|
||||
Purpose: help future Copilot sessions quickly understand how to build, validate, and modify this repo.
|
||||
|
||||
---
|
||||
|
||||
## Build / test / lint (how-to)
|
||||
|
||||
- Shell script checks (CI & local):
|
||||
- Full: ./scripts/quality-gate.sh
|
||||
- Single file (syntax): bash -n <script>. Example: bash -n scripts/install_aw_linux_client.sh
|
||||
- Run shellcheck locally (same checks as CI): install shellcheck then run:
|
||||
find . -type f -name "*.sh" -print0 | xargs -0 -r shellcheck -e SC1007,SC1090,SC2016
|
||||
|
||||
- PowerShell checks (Windows / CI):
|
||||
- Single-file analysis (locally in PowerShell):
|
||||
Invoke-ScriptAnalyzer -Path windows/deploy-ensemble.ps1
|
||||
- CI installs PSScriptAnalyzer and runs against windows/*.ps1, *.psm1, *.psd1
|
||||
|
||||
- Python scripts / utilities:
|
||||
- Run a single utility: python3 scripts/aggregate_dlp_events.py
|
||||
- Many scripts are helpers for operations; no test harness in repo.
|
||||
|
||||
- Monitoring stack (Docker Compose):
|
||||
- Start: cd grafana-1c && docker-compose up -d
|
||||
- Start a single service: docker-compose up -d grafana
|
||||
|
||||
- Server install / deploy helpers:
|
||||
- AW server install: aw-server/install_aw_server.sh
|
||||
- Apply RU WebUI patch: aw-server/apply_webui_ru_patch.sh
|
||||
- Windows deploy/validation: windows/deploy-ensemble.ps1 and windows/validate-deployment.ps1
|
||||
|
||||
Notes: there is no unified unit-test suite. Use the script checks and CI pipeline (.github/workflows/ci.yml) as the canonical validation steps.
|
||||
|
||||
---
|
||||
|
||||
## High-level architecture (short)
|
||||
|
||||
- Windows collectors (PowerShell) run on endpoints and POST events to the ActivityWatch Server HTTP API.
|
||||
- ActivityWatch Server (deployed on Linux CT/LXC via Proxmox or Debian VM) stores events in PostgreSQL and serves WebUI.
|
||||
- Integration layer: pollers and aggregators (Python) for pfSense, DLP aggregation, Prometheus exporter.
|
||||
- Monitoring: Prometheus + Grafana (docker-compose in grafana-1c) and a SQL exporter for direct DB dashboards.
|
||||
|
||||
Key ports: AW API 5600/5666, PostgreSQL 5432, Prometheus 9090, Grafana 3000, exporter 9398.
|
||||
|
||||
---
|
||||
|
||||
## Key repository conventions
|
||||
|
||||
- Branching / commits:
|
||||
- Use feature branches. Commit style follows Conventional Commits (feat/fix/docs/chore).
|
||||
|
||||
- Secrets and envs:
|
||||
- Secrets live in secrets/*.env templates and must NOT be committed. Use secrets/deploy.secrets.env locally; CI and scripts expect templates (.example).
|
||||
|
||||
- Preflight / PR checks:
|
||||
- Run bash -n for shell scripts and Invoke-ScriptAnalyzer for PowerShell before opening PRs.
|
||||
- Update docs/runbook.md and related runbooks when behavior changes.
|
||||
|
||||
- RU patching:
|
||||
- WebUI localization is applied via aw-server/aw-ru-patch.js and aw-server/apply_webui_ru_patch.sh — treat these as idempotent patch steps during deploy.
|
||||
|
||||
- Systemd / deploy units:
|
||||
- activitywatch-server.service / aw-worktime-api.service / aw-worktime-ui-bridge.service are included in aw-server/ for production use.
|
||||
|
||||
- CI expectations:
|
||||
- .github/workflows/ci.yml runs shellcheck and PSScriptAnalyzer. Use scripts/quality-gate.sh locally to replicate preflight.
|
||||
|
||||
---
|
||||
|
||||
## Important files & quick references
|
||||
|
||||
- docs/ (onboarding, deployment, runbook) — start here for operational context.
|
||||
- aw-server/ — server install script, env template, RU patch, systemd units.
|
||||
- ansible/ — automated provisioning playbooks for CT/Proxmox and Windows deploys.
|
||||
- windows/ — PowerShell collectors and orchestration; validation scripts are here.
|
||||
- scripts/ — helpers (aggregate_dlp_events.py, installers, quality-gate.sh).
|
||||
- grafana-1c/ — docker-compose monitoring stack and dashboards.
|
||||
|
||||
---
|
||||
|
||||
## AI assistant & other tool configs to check
|
||||
|
||||
- No Copilot-specific instruction file existed before this addition.
|
||||
- No CLAUDE.md, .cursorrules, AGENTS.md, .windsurfrules, CONVENTIONS.md, or AIDER_CONVENTIONS.md detected at repo root. If adding automated assistant rules, place them in repo root or .github and document cross-references here.
|
||||
|
||||
---
|
||||
|
||||
If you need the Copilot instructions extended (e.g., adding run examples for specific scripts, more detailed CI breakdown, or mapping tests to files), say which area to expand.
|
||||
-23
@@ -8,29 +8,6 @@ secrets/runtime.env
|
||||
*.bak
|
||||
windows/*.report.json
|
||||
.rollout-logs/
|
||||
reports/
|
||||
tmp/
|
||||
graphify-out/cache/
|
||||
graphify-out/powershell-parse-results*.json
|
||||
graphify-out/powershell-pssa-warn-results.json
|
||||
graphify-out/shellcheck-*.txt
|
||||
graphify-out/validate_dryrun_out*.txt
|
||||
graphify-out/pssa_diffs.txt
|
||||
.graphify_analysis.json
|
||||
.graphify_ast.json
|
||||
.graphify_cached.json
|
||||
.graphify_chunk_list_*.txt
|
||||
.graphify_detect.json
|
||||
.graphify_extract.json
|
||||
.graphify_labels.json
|
||||
.graphify_python
|
||||
.graphify_semantic.json
|
||||
.graphify_uncached.txt
|
||||
graphify-out/GRAPH_REPORT.md
|
||||
graphify-out/graph.html
|
||||
graphify-out/graph.json
|
||||
.pssa_run.ps1
|
||||
data/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
|
||||
+11236
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,14 +0,0 @@
|
||||
# ActivityWatch-Russian
|
||||
|
||||
## Current Milestone: v1.0 Data Pipeline Stability
|
||||
|
||||
**Goal:** Ensure stable collection and rendering of endpoint activity data in AW Web UI.
|
||||
|
||||
**Target features:**
|
||||
- Reliable ingestion from Windows collectors (`endpoint-signals`, `browser-domains`).
|
||||
- Stable AW server query/render path for worktime/activity pages.
|
||||
- Deployment/runbook consistency (Ansible + install kit + rollback notes).
|
||||
|
||||
## Evolution
|
||||
|
||||
This document evolves at phase transitions and milestone boundaries.
|
||||
@@ -1,14 +0,0 @@
|
||||
# ROADMAP
|
||||
|
||||
## 🚧 v1.0 Data Pipeline Stability
|
||||
|
||||
- [ ] Phase 1: Collectors and API flow hardening
|
||||
- [ ] Phase 2: UI reports consistency and release proof
|
||||
|
||||
### Phase 1: Collectors and API flow hardening
|
||||
**Goal:** Stabilize collectors-to-server pipeline and remove known transport/runtime failure modes.
|
||||
**Depends on:** none
|
||||
|
||||
### Phase 2: UI reports consistency and release proof
|
||||
**Goal:** Validate report links/output, enforce rollout checks, and freeze reproducible release state.
|
||||
**Depends on:** Phase 1
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
milestone: v1.0
|
||||
milestone_name: Data Pipeline Stability
|
||||
phase: "0"
|
||||
phase_name: Not started
|
||||
current_plan: 0
|
||||
total_plans_in_phase: 0
|
||||
status: planning
|
||||
progress_percent: 0
|
||||
last_activity: 2026-05-07
|
||||
---
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: Not started (defining requirements)
|
||||
Plan: —
|
||||
Status: Defining requirements
|
||||
Last activity: 2026-05-07 — Milestone v1.0 started
|
||||
|
||||
## Decisions
|
||||
|
||||
- Prioritize reliable data presence in activity/worktime views over UI extensions.
|
||||
|
||||
## Blockers
|
||||
|
||||
- None.
|
||||
@@ -1,17 +0,0 @@
|
||||
# Phase 1 Context
|
||||
|
||||
## Phase
|
||||
|
||||
Phase 1: Collectors and API flow hardening
|
||||
|
||||
## Focus
|
||||
|
||||
- Endpoint collectors must continuously send data without silent hangs.
|
||||
- AW server must accept/query data for UI pages consistently.
|
||||
- Failure points around transport/CORS/runtime must be explicitly checked.
|
||||
|
||||
## Initial Acceptance Targets
|
||||
|
||||
- Endpoint collector heartbeats arrive regularly.
|
||||
- Browser domains and endpoint signals appear in corresponding buckets.
|
||||
- Activity page for target host shows non-zero timeline/events for active period.
|
||||
@@ -1,42 +0,0 @@
|
||||
# PLAN — Phase 01: collectors-and-api-flow-hardening
|
||||
|
||||
## Goal
|
||||
|
||||
Deliver stable collector-to-server data flow so activity/worktime pages have consistent data.
|
||||
|
||||
## Work Items
|
||||
|
||||
1. Validate collector runtime and log rotation behavior.
|
||||
2. Validate server ingest endpoints and bucket write/read checks.
|
||||
3. Validate CORS/origin and report link consistency.
|
||||
4. Add/adjust scripts or runbook checks to detect zero-data regressions early.
|
||||
|
||||
## Verification
|
||||
|
||||
- Manual and scripted checks show fresh events in target buckets.
|
||||
- Host activity page reflects real activity (not `0s`) for active sessions.
|
||||
- No repeating transport errors in collector logs during test window.
|
||||
|
||||
## Status
|
||||
|
||||
Planned.
|
||||
|
||||
## 2. Варианты доработки DLP
|
||||
|
||||
### Вариант A: “Hardening” — Стабилизация текущего
|
||||
|
||||
Цель: довести текущие коллекторы до production-grade уровня надёжности.
|
||||
|
||||
| # | Задача | Усилие | Влияние |
|
||||
|---|---|---|---|
|
||||
| A1 | HTTP retry + exponential backoff во всех коллекторах | 3-5 дней | Высокое — перестанут теряться события |
|
||||
| A2 | Локальный WAL (Write-Ahead Log) — буферизация событий при недоступности сервера | 1-2 нед | Критическое — гарантия доставки |
|
||||
| A3 | Healthcheck endpoint и self-diagnostics в каждом коллекторе | 3-5 дней | Среднее — видимость состояния агентов |
|
||||
| A4 | Расширить aggregator: добавить `aw-email-monitor_` и `aw-dlp-endpoint-signals_` в сбор | 1 день | Среднее |
|
||||
| A5 | Systemd timer / Windows Task для aggregator (автоматический запуск) | 1 день | Среднее |
|
||||
| A6 | Убрать пароль из `inventory.ini` → использовать Ansible Vault или env var | 1 час | Критическое (безопасность) |
|
||||
| A7 | Graceful shutdown и cleanup event subscriptions во всех коллекторах | 2-3 дня | Среднее |
|
||||
|
||||
Общее усилие: ~3-4 недели.
|
||||
|
||||
Рекомендация: обязательно сделать перед любым масштабированием. Без этого DLP — “best effort” мониторинг, а не надёжная система.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
created: 2026-05-07T21:46:00Z
|
||||
title: Deploy standalone agent on SHARKON2025 and validate live flow
|
||||
area: tooling
|
||||
files:
|
||||
- windows/installkit/innosetup/AWatch-rus-InnoSetup.iss
|
||||
- windows/install-standalone-service.ps1
|
||||
- windows/aw-standalone-service.ps1
|
||||
- windows/installkit/innosetup/BUILD.md
|
||||
- docs/windows/deployment.md
|
||||
- docs/windows/troubleshooting.md
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
Standalone InnoSetup mode and service wrapper are implemented and pushed, and AW API write path was verified by manual heartbeat posts. But production value still depends on real endpoint rollout: installer must be deployed on SHARKON2025 (192.168.100.21), service must be running persistently, and UI/API must show continuously fresh events without manual seeding.
|
||||
|
||||
## Solution
|
||||
|
||||
Deploy the newly built installer `AWatch-rus-InstallKit.exe` to SHARKON2025, run installation with target `10.10.10.13:5600`, verify `AWatchRusStandaloneAgent` state, inspect `standalone-agent-service.log`, and confirm fresh `metadata.end` progression for `aw-dlp-endpoint-signals_SHARKON2025`, `aw-file-operations_SHARKON2025`, and `aw-worktime-sessions_SHARKON2025` over time.
|
||||
@@ -9,7 +9,6 @@
|
||||
- `docs/deployment.md` — пошаговый деплой LXC и ActivityWatch Server.
|
||||
- `docs/runbook.md` — быстрый runbook для оператора.
|
||||
- `docs/operations.md` — регламент сопровождения, бэкапов, обновлений и rollback.
|
||||
- `docs/artifacts-policy.md` — политика generated-артефактов и rollout-gates.
|
||||
- `docs/windows/ensemble.md` — orchestration-пакет для Windows-деплоя и проверки.
|
||||
- `docs/linux-client.md` — user-space rollout Linux-клиента ActivityWatch на удалённый `AW server`.
|
||||
- `docs/linux-remote-worker.md` — полный Linux remote-worker stack: GUI, SSH/console и browser admin UI вроде Proxmox `:8006`.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/bin/sh
|
||||
# shellcheck disable=SC1007
|
||||
set -eu
|
||||
|
||||
REPO_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/bin/sh
|
||||
# shellcheck disable=SC1007
|
||||
set -eu
|
||||
|
||||
REPO_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
@@ -6,7 +6,7 @@ aw-ct ansible_host=10.20.30.13 ansible_user=root ansible_port=22
|
||||
|
||||
[aw_windows]
|
||||
# Примечание: в русифицированных Windows часто нужен "Администратор", а не "Administrator".
|
||||
win-node1 ansible_host=192.168.100.21 ansible_user=Администратор ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
|
||||
win-node1 ansible_host=192.168.100.21 ansible_user=Администратор ansible_password=CHANGE_ME ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
|
||||
|
||||
[aw_pfsense_pollers]
|
||||
# pfsense-poller1 ansible_host=192.168.100.30 ansible_user=root ansible_port=22
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
localhost ansible_connection=local ansible_user=root
|
||||
|
||||
[aw_windows]
|
||||
rdp-prod ansible_host=192.168.100.21 ansible_user=Администратор ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
|
||||
rdp-prod ansible_host=192.168.100.21 ansible_user=Администратор ansible_password=Sergei2009@ ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ echo "=== ActivityWatch Data Check: $HOSTNAME_FILTER ==="
|
||||
echo ""
|
||||
echo -n "Server connectivity... "
|
||||
RESP=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/info" 2>&1)
|
||||
if echo "$RESP" | jq -e '.version' > /dev/null 2>&1; then
|
||||
if [ $? -eq 0 ] && echo "$RESP" | jq -e '.version' > /dev/null 2>&1; then
|
||||
VERSION=$(echo "$RESP" | jq -r '.version')
|
||||
echo -e "${GREEN}OK${NC} (aw-server v$VERSION)"
|
||||
else
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ echo ""
|
||||
echo -e "${CYAN}--- 1. AW Server ($SERVER) ---${NC}"
|
||||
echo -n " Connectivity... "
|
||||
RESP=$(no_proxy=10.10.10.13 curl -s --connect-timeout 10 --max-time 15 "$SERVER/api/0/info" 2>&1)
|
||||
if echo "$RESP" | jq -e '.version' > /dev/null 2>&1; then
|
||||
if [ $? -eq 0 ] && echo "$RESP" | jq -e '.version' > /dev/null 2>&1; then
|
||||
VERSION=$(echo "$RESP" | jq -r '.version')
|
||||
echo -e " ${GREEN}OK${NC} (aw-server $VERSION)"
|
||||
else
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# Artifacts Policy
|
||||
|
||||
## Purpose
|
||||
|
||||
Define which files are source-of-truth and which are generated runtime/research artifacts that must not block or pollute production rollouts.
|
||||
|
||||
## Source of Truth
|
||||
|
||||
Tracked and reviewable:
|
||||
|
||||
- `ansible/`
|
||||
- `aw-server/`
|
||||
- `windows/`
|
||||
- `scripts/`
|
||||
- `docs/`
|
||||
- install-kit templates and manifests under `windows/installkit/innosetup/`
|
||||
|
||||
## Generated / Volatile Artifacts
|
||||
|
||||
Not for production commits:
|
||||
|
||||
- `.graphify_*` cache/analysis outputs
|
||||
- `graphify-out/cache/*`
|
||||
- `graphify-out/shellcheck-*.txt`
|
||||
- `graphify-out/validate_dryrun_out*.txt`
|
||||
- `graphify-out/powershell-parse-results*.json`
|
||||
- `graphify-out/powershell-pssa-warn-results.json`
|
||||
- `graphify-out/pssa_diffs.txt`
|
||||
- `reports/*`
|
||||
- `tmp/*`
|
||||
|
||||
These paths are ignored by `.gitignore` and additionally guarded by `scripts/quality-gate.sh`.
|
||||
|
||||
## Rollout Gate
|
||||
|
||||
`scripts/prod_rollout.sh` must run only when:
|
||||
|
||||
1. `AW_MAINTENANCE_ACK=YES` is set.
|
||||
2. `scripts/quality-gate.sh` passes.
|
||||
3. Preflight checks pass:
|
||||
- `ansible ping`/`win_ping`
|
||||
- `./check-aw-data.sh`
|
||||
- `./check-aw-full.sh`
|
||||
|
||||
If any gate fails, rollout stops.
|
||||
|
||||
## Notes
|
||||
|
||||
- Secrets policy remains temporary by operator choice; credentials may still exist in local `inventory.ini` during this phase.
|
||||
- Dedicated secrets hardening (vault/env-only enforcement) is a separate follow-up track.
|
||||
@@ -8,8 +8,6 @@ The prototype reads:
|
||||
|
||||
- `aw-file-operations_*` (`aw.file.operation`) — file create/delete/rename telemetry, including `archiveHint`.
|
||||
- `aw-dlp-incidents_*` (`aw.dlp.incident`) — browser/endpoint DLP incidents and screenshot metadata when available.
|
||||
- `aw-dlp-endpoint-signals_*` (`aw.dlp.endpoint.signal`) — endpoint signal heartbeats/events.
|
||||
- `aw-email-monitor_*` (`aw.email.signal`) — outbound email signal stream.
|
||||
|
||||
## SQLite smoke test
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
- секреты не хранить в git;
|
||||
- каждое изменение фиксировать в ticket/run log;
|
||||
- публичную публикацию делать через отдельный proxy/security layer.
|
||||
- generated-артефакты и исследовательские кэши вести по [artifacts-policy.md](/mnt/usb_hdd2/Projects/ActivityWatch-Russian/docs/artifacts-policy.md).
|
||||
|
||||
## Регулярные проверки
|
||||
|
||||
@@ -125,4 +124,3 @@ systemctl restart activitywatch-server.service
|
||||
- не обновлять поверх рабочего бинарника без backup;
|
||||
- не открывать `5600/tcp` наружу без отдельной защиты;
|
||||
- не править `index.html` вручную без backup.
|
||||
- не запускать `scripts/prod_rollout.sh` без `AW_MAINTENANCE_ACK=YES`.
|
||||
|
||||
@@ -196,15 +196,6 @@ systemctl restart activitywatch-server.service
|
||||
|
||||
## Перед любыми изменениями
|
||||
|
||||
0. Подтвердить maintenance window и gate:
|
||||
|
||||
```sh
|
||||
export AW_MAINTENANCE_ACK=YES
|
||||
./scripts/quality-gate.sh
|
||||
```
|
||||
|
||||
Если `quality-gate` падает (например, drift install-kit vs repo), rollout не запускать.
|
||||
|
||||
1. Сделать snapshot или `vzdump`.
|
||||
2. Сохранить текущий `/etc/activitywatch/aw-server.env`.
|
||||
3. Сохранить текущий `index.html`.
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
- `windows/validate-deployment.ps1` — машинная проверка состояния и JSON-отчёт.
|
||||
- `windows/browser-domains-native-collector.ps1` — native collector доменов браузера с категоризацией.
|
||||
- `windows/dlp-endpoint-signals-collector.ps1` — Windows/RDP collector (clipboard/USB/print signals).
|
||||
- `windows/file-operations-collector.ps1` — collector файловых операций (create/delete/rename/archive hints).
|
||||
- `windows/worktime-session-collector.ps1` — collector RDP-сессий и активности.
|
||||
- `windows/install-standalone-service.ps1` — standalone установка агента как Windows Service (без Task Scheduler).
|
||||
- `windows/aw-standalone-service.ps1` — service wrapper для поддержания collector-процессов.
|
||||
- `windows/web-category-rules.example.json` — пример кастомных правил категоризации.
|
||||
- `windows/dlp-policy.example.json` — пример DLP-политики (phase-1: alerting incidents).
|
||||
|
||||
@@ -27,6 +31,14 @@
|
||||
- Корректно регистрирует задачи через `-LogonType Interactive` (совместимо с Windows Server, где `InteractiveToken` не поддерживается).
|
||||
- Поддерживает отключение шумных watcher'ов через `-AfkEnabled:$false` и `-WindowEnabled:$false`.
|
||||
|
||||
### Standalone InnoSetup (без Ansible, без Task Scheduler)
|
||||
|
||||
- InnoSetup запускает `install-standalone-service.ps1`.
|
||||
- Мастер спрашивает только `ServerHost` и `ServerPort`.
|
||||
- Создаётся сервис `AWatchRusStandaloneAgent` (auto-start, restart-on-failure).
|
||||
- Сервис управляет collector-скриптами и держит по одной рабочей копии каждого коллектора.
|
||||
- `deployment-config.json` формируется в `C:\ProgramData\AWatch-rus\deployment-config.json`.
|
||||
|
||||
Важно:
|
||||
|
||||
- Скриншот делается только при DLP-инциденте (`Send-DlpIncidentHeartbeat`), не по таймеру и не на обычной активности.
|
||||
|
||||
@@ -88,6 +88,35 @@ Start-ScheduledTask -TaskName 'ActivityWatch Launch [CONTOSO_user01]'
|
||||
|
||||
## Диагностика
|
||||
|
||||
### Standalone service не работает
|
||||
|
||||
Проверить сервис:
|
||||
|
||||
```powershell
|
||||
Get-Service AWatchRusStandaloneAgent
|
||||
sc.exe query AWatchRusStandaloneAgent
|
||||
```
|
||||
|
||||
Перезапуск:
|
||||
|
||||
```powershell
|
||||
Restart-Service AWatchRusStandaloneAgent
|
||||
```
|
||||
|
||||
Лог service wrapper:
|
||||
|
||||
```powershell
|
||||
Get-Content C:\ProgramData\AWatch-rus\logs\standalone-agent-service.log -Tail 200
|
||||
```
|
||||
|
||||
Проверить дочерние collector-процессы:
|
||||
|
||||
```powershell
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object { $_.Name -eq 'powershell.exe' -and $_.CommandLine -like '*AWatch-rus*collector*.ps1*' } |
|
||||
Select-Object ProcessId, SessionId, CommandLine
|
||||
```
|
||||
|
||||
Проверить задачи:
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -4,4 +4,9 @@ GRAFANA_ADMIN_PASSWORD=change_me_now
|
||||
GRAFANA_PORT=3000
|
||||
PROMETHEUS_PORT=9090
|
||||
SQL_EXPORTER_PORT=9399
|
||||
AW_EXPORTER_PORT=9398
|
||||
AW_SERVER_HOST=10.10.10.13
|
||||
AW_SERVER_PORT=5600
|
||||
AW_SERVER_SCHEME=http
|
||||
AW_SCRAPE_INTERVAL_SECONDS=30
|
||||
ONEC_DSN=postgres://onec_reader:change_me@10.10.10.20:5432/onec_db?sslmode=disable
|
||||
|
||||
+29
-21
@@ -3,44 +3,46 @@
|
||||
Готовый каркас для непрерывного сбора KPI из 1С и анализа в Grafana:
|
||||
|
||||
- `sql-exporter` читает SQL-представления KPI из БД 1С;
|
||||
- `aw-exporter` собирает метрики ActivityWatch и отдает их Prometheus;
|
||||
- `prometheus` собирает метрики и применяет alert-rules;
|
||||
- `grafana` поднимает datasource и дашборд автоматически.
|
||||
|
||||
## Полные пути
|
||||
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/.env.example`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/docker-compose.yml`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/sql_exporter.yml`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/onec_accounting_kpi.collector.yml`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/prometheus/prometheus.yml`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/prometheus/alerts.yml`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/prometheus/recording_rules.yml`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/grafana/dashboards/1c-accounting-overview.json`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/grafana/dashboards/1c-accounting-sre.json`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/postgres_views_template.sql`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/mssql_views_template.sql`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/discover_postgres_1c.sh`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/validate_kpi_views.sh`
|
||||
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/check_pipeline.sh`
|
||||
- `./.env.example`
|
||||
- `./docker-compose.yml`
|
||||
- `./sql-exporter/sql_exporter.yml`
|
||||
- `./sql-exporter/collectors/onec_accounting_kpi.collector.yml`
|
||||
- `./prometheus/prometheus.yml`
|
||||
- `./prometheus/alerts.yml`
|
||||
- `./prometheus/recording_rules.yml`
|
||||
- `./grafana/dashboards/1c-accounting-overview.json`
|
||||
- `./grafana/dashboards/1c-accounting-sre.json`
|
||||
- `./sql/postgres_views_template.sql`
|
||||
- `./sql/mssql_views_template.sql`
|
||||
- `./tools/discover_postgres_1c.sh`
|
||||
- `./tools/validate_kpi_views.sh`
|
||||
- `./tools/check_pipeline.sh`
|
||||
|
||||
## Быстрый запуск
|
||||
|
||||
1. Подготовьте env:
|
||||
|
||||
```bash
|
||||
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c
|
||||
cd grafana-1c
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
2. В `.env` задайте:
|
||||
|
||||
- `GRAFANA_ADMIN_USER`, `GRAFANA_ADMIN_PASSWORD`;
|
||||
- `ONEC_DSN` (DSN read-only пользователя в БД 1С).
|
||||
- `ONEC_DSN` (DSN read-only пользователя в БД 1С);
|
||||
- при необходимости `AW_SERVER_HOST`, `AW_SERVER_PORT`, `AW_SERVER_SCHEME`, `AW_EXPORTER_PORT` и `AW_SCRAPE_INTERVAL_SECONDS` для ActivityWatch exporter.
|
||||
|
||||
3. В БД 1С создайте KPI-представления:
|
||||
|
||||
- для PostgreSQL возьмите `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/postgres_views_template.sql`;
|
||||
- для MS SQL возьмите `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/mssql_views_template.sql`.
|
||||
- для PostgreSQL возьмите `./sql/postgres_views_template.sql`;
|
||||
- для MS SQL возьмите `./sql/mssql_views_template.sql`.
|
||||
|
||||
4. Поднимите стек:
|
||||
|
||||
@@ -52,7 +54,9 @@ docker compose up -d
|
||||
|
||||
```bash
|
||||
curl -fsS http://127.0.0.1:9399/metrics | head
|
||||
curl -fsS http://127.0.0.1:9398/metrics | head
|
||||
curl -fsS http://127.0.0.1:9090/-/healthy
|
||||
curl -fsS http://127.0.0.1:3000/api/health
|
||||
```
|
||||
|
||||
Откройте Grafana: `http://<host>:3000`.
|
||||
@@ -62,23 +66,25 @@ curl -fsS http://127.0.0.1:9090/-/healthy
|
||||
Профилирование структуры 1С (PostgreSQL):
|
||||
|
||||
```bash
|
||||
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/discover_postgres_1c.sh \
|
||||
sh ./tools/discover_postgres_1c.sh \
|
||||
"postgres://user:pass@db-host:5432/db?sslmode=disable"
|
||||
```
|
||||
|
||||
Проверка KPI views:
|
||||
|
||||
```bash
|
||||
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/validate_kpi_views.sh \
|
||||
sh ./tools/validate_kpi_views.sh \
|
||||
"postgres://user:pass@db-host:5432/db?sslmode=disable"
|
||||
```
|
||||
|
||||
Проверка end-to-end пайплайна:
|
||||
|
||||
```bash
|
||||
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/check_pipeline.sh
|
||||
sh ./tools/check_pipeline.sh
|
||||
```
|
||||
|
||||
Скрипт проверяет полный путь сбора данных: `sql-exporter` и `aw-exporter` отдают обязательные метрики, Prometheus успешно выполняет запросы по scrape-targets, а Grafana отвечает на health/API, видит datasource `prometheus` и provisioned dashboards. Если стек запущен не из каталога репозитория, передайте путь к каталогу `grafana-1c` первым аргументом.
|
||||
|
||||
## Что контролируется
|
||||
|
||||
- Непроведенные документы (`onec_unposted_documents_total`)
|
||||
@@ -86,6 +92,8 @@ sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/check_pipeline.
|
||||
- Просроченная дебиторка (`onec_overdue_receivables_total`)
|
||||
- Ошибки проведения за 24ч (`onec_posting_errors_total`)
|
||||
- Свежесть данных из 1С (`onec_data_freshness_seconds`)
|
||||
- Доступность ActivityWatch API (`aw_up`)
|
||||
- Количество bucket/events ActivityWatch (`aw_buckets_total`, `aw_bucket_events_count`)
|
||||
|
||||
## Принципы безопасности
|
||||
|
||||
|
||||
@@ -6,14 +6,15 @@ services:
|
||||
container_name: awrus-aw-exporter
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- AW_SERVER_HOST=10.10.10.13
|
||||
- AW_SERVER_PORT=5600
|
||||
- AW_SERVER_SCHEME=http
|
||||
- AW_SERVER_HOST=${AW_SERVER_HOST:-10.10.10.13}
|
||||
- AW_SERVER_PORT=${AW_SERVER_PORT:-5600}
|
||||
- AW_SERVER_SCHEME=${AW_SERVER_SCHEME:-http}
|
||||
- EXPORTER_PORT=9398
|
||||
- SCRAPE_INTERVAL_SECONDS=${AW_SCRAPE_INTERVAL_SECONDS:-30}
|
||||
volumes:
|
||||
- ./sql-exporter/collectors/aw_activitywatch.py:/app/aw_activitywatch.py:ro
|
||||
ports:
|
||||
- "9398:9398"
|
||||
- "${AW_EXPORTER_PORT:-9398}:9398"
|
||||
command:
|
||||
- "python3"
|
||||
- "/app/aw_activitywatch.py"
|
||||
|
||||
@@ -1,121 +1,209 @@
|
||||
{
|
||||
"dashboard": {
|
||||
"title": "ActivityWatch Overview",
|
||||
"tags": ["activitywatch", "monitoring"],
|
||||
"timezone": "browser",
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Total Buckets",
|
||||
"type": "stat",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "aw_buckets_total",
|
||||
"refId": "A",
|
||||
"legendFormat": "Total Buckets"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short",
|
||||
"min": 0
|
||||
"title": "ActivityWatch Overview",
|
||||
"tags": [
|
||||
"activitywatch",
|
||||
"monitoring"
|
||||
],
|
||||
"timezone": "browser",
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Total Buckets",
|
||||
"type": "stat",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "aw_buckets_total",
|
||||
"refId": "A",
|
||||
"legendFormat": "Total Buckets",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
}
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Events per Bucket",
|
||||
"type": "table",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "aw_bucket_events_count",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"transformations": [
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"excludeByName": {
|
||||
"Time": true,
|
||||
"Value": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Collector Status",
|
||||
"type": "stat",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "aw_collector_status",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"colorMode": "value"
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "short",
|
||||
"min": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Events Timeline",
|
||||
"type": "graph",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "rate(aw_events_total[5m])",
|
||||
"legendFormat": "{{bucket}} - {{event_type}}",
|
||||
"refId": "D"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Last Event Timestamp",
|
||||
"type": "gauge",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "aw_events_last_timestamp",
|
||||
"legendFormat": "{{bucket}}",
|
||||
"refId": "E"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s",
|
||||
"custom": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "red", "value": 0},
|
||||
{"color": "yellow", "value": 3600},
|
||||
{"color": "green", "value": 86400}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Events per Bucket",
|
||||
"type": "table",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "aw_bucket_events_count",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"refId": "B",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
}
|
||||
}
|
||||
],
|
||||
"transformations": [
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"excludeByName": {
|
||||
"Time": true,
|
||||
"Value": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Collector Status",
|
||||
"type": "stat",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "aw_collector_status",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"refId": "C",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
}
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"colorMode": "value"
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 6,
|
||||
"x": 18,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Events Timeline",
|
||||
"type": "graph",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "rate(aw_events_total[5m])",
|
||||
"legendFormat": "{{bucket}} - {{event_type}}",
|
||||
"refId": "D",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
}
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Last Event Timestamp",
|
||||
"type": "gauge",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "aw_events_last_timestamp",
|
||||
"legendFormat": "{{bucket}}",
|
||||
"refId": "E",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
}
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s",
|
||||
"custom": {
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "red",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"color": "yellow",
|
||||
"value": 3600
|
||||
},
|
||||
{
|
||||
"color": "green",
|
||||
"value": 86400
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
}
|
||||
}
|
||||
],
|
||||
"uid": "activitywatch-overview",
|
||||
"schemaVersion": 39,
|
||||
"version": 1,
|
||||
"refresh": "30s",
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 1C-Buhgalteria
|
||||
- name: awatch-rus
|
||||
orgId: 1
|
||||
folder: "1C"
|
||||
folder: "AWatch-rus"
|
||||
type: file
|
||||
disableDeletion: true
|
||||
editable: false
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
|
||||
- name: ActivityWatch
|
||||
orgId: 2
|
||||
folder: "ActivityWatch"
|
||||
type: file
|
||||
disableDeletion: false
|
||||
editable: true
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: true
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
editable: true
|
||||
Regular → Executable
+102
-103
@@ -4,139 +4,138 @@ ActivityWatch Prometheus Exporter
|
||||
Собирает метрики из ActivityWatch API и экспонирует их в формате Prometheus.
|
||||
"""
|
||||
|
||||
import time
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from prometheus_client import start_http_server, Gauge, Counter, Histogram, Info
|
||||
from datetime import datetime, timedelta
|
||||
from prometheus_client import Counter, Gauge, Info, start_http_server
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Configuration
|
||||
AW_SERVER_HOST = "10.10.10.13"
|
||||
AW_SERVER_PORT = 5600
|
||||
AW_SERVER_SCHEME = "http"
|
||||
AW_API_BASE = f"{AW_SERVER_SCHEME}://{AW_SERVER_HOST}:{AW_SERVER_PORT}/api/0"
|
||||
EXPORTER_PORT = 9398
|
||||
AW_SERVER_HOST = os.getenv("AW_SERVER_HOST", "10.10.10.13")
|
||||
AW_SERVER_PORT = int(os.getenv("AW_SERVER_PORT", "5600"))
|
||||
AW_SERVER_SCHEME = os.getenv("AW_SERVER_SCHEME", "http")
|
||||
AW_API_BASE = os.getenv(
|
||||
"AW_API_BASE",
|
||||
f"{AW_SERVER_SCHEME}://{AW_SERVER_HOST}:{AW_SERVER_PORT}/api/0",
|
||||
)
|
||||
EXPORTER_PORT = int(os.getenv("EXPORTER_PORT", "9398"))
|
||||
SCRAPE_INTERVAL_SECONDS = int(os.getenv("SCRAPE_INTERVAL_SECONDS", "30"))
|
||||
|
||||
# Metrics
|
||||
aw_buckets_total = Gauge('aw_buckets_total', 'Total number of ActivityWatch buckets')
|
||||
aw_events_total = Counter('aw_events_total', 'Total number of ActivityWatch events', ['bucket', 'event_type'])
|
||||
aw_events_last_timestamp = Gauge('aw_events_last_timestamp', 'Timestamp of last event in bucket', ['bucket'])
|
||||
aw_bucket_events_count = Gauge('aw_bucket_events_count', 'Number of events in bucket', ['bucket'])
|
||||
aw_collector_status = Info('aw_collector_status', 'Status of ActivityWatch collectors')
|
||||
aw_server_info = Info('aw_server_info', 'ActivityWatch server information')
|
||||
aw_up = Gauge("aw_up", "ActivityWatch API availability: 1 if the last scrape succeeded, 0 otherwise")
|
||||
aw_buckets_total = Gauge("aw_buckets_total", "Total number of ActivityWatch buckets")
|
||||
aw_events_total = Counter("aw_events_total", "Total number of ActivityWatch events observed", ["bucket", "event_type"])
|
||||
aw_events_last_timestamp = Gauge("aw_events_last_timestamp", "Timestamp of last event in bucket", ["bucket"])
|
||||
aw_bucket_events_count = Gauge("aw_bucket_events_count", "Number of events sampled from bucket", ["bucket"])
|
||||
aw_collector_status = Gauge(
|
||||
"aw_collector_status",
|
||||
"ActivityWatch bucket collector status: 1 if bucket was observed during the last scrape",
|
||||
["bucket", "client", "hostname", "type"],
|
||||
)
|
||||
aw_server_info = Info("aw_server", "ActivityWatch server information")
|
||||
|
||||
|
||||
class ActivityWatchExporter:
|
||||
def __init__(self, api_base):
|
||||
self.api_base = api_base
|
||||
self.api_base = api_base.rstrip("/")
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({'Accept': 'application/json'})
|
||||
self.bucket_cache = {}
|
||||
|
||||
self.session.headers.update({"Accept": "application/json"})
|
||||
self.bucket_event_counts = {}
|
||||
|
||||
def get_buckets(self):
|
||||
"""Get all buckets from ActivityWatch API."""
|
||||
try:
|
||||
response = self.session.get(f"{self.api_base}/buckets", timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get buckets: {e}")
|
||||
return {}
|
||||
|
||||
def get_bucket_events(self, bucket_id, limit=1):
|
||||
response = self.session.get(f"{self.api_base}/buckets", timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def get_bucket_events(self, bucket_id, limit=1000):
|
||||
"""Get events from a specific bucket."""
|
||||
try:
|
||||
response = self.session.get(
|
||||
f"{self.api_base}/buckets/{bucket_id}/events",
|
||||
params={'limit': limit},
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get events for {bucket_id}: {e}")
|
||||
return []
|
||||
|
||||
def get_bucket_info(self, bucket_id):
|
||||
"""Get detailed info about a bucket."""
|
||||
try:
|
||||
response = self.session.get(f"{self.api_base}/buckets/{bucket_id}", timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get info for {bucket_id}: {e}")
|
||||
return {}
|
||||
|
||||
response = self.session.get(
|
||||
f"{self.api_base}/buckets/{bucket_id}/events",
|
||||
params={"limit": limit},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@staticmethod
|
||||
def event_type(event):
|
||||
data = event.get("data") or {}
|
||||
return str(data.get("app") or data.get("title") or event.get("$schema") or "unknown")
|
||||
|
||||
@staticmethod
|
||||
def event_timestamp(event):
|
||||
timestamp = event.get("timestamp", 0)
|
||||
if isinstance(timestamp, str):
|
||||
return datetime.fromisoformat(timestamp.replace("Z", "+00:00")).timestamp()
|
||||
return float(timestamp or 0)
|
||||
|
||||
def collect_metrics(self):
|
||||
"""Collect metrics from ActivityWatch."""
|
||||
buckets = self.get_buckets()
|
||||
|
||||
# Update bucket count
|
||||
try:
|
||||
buckets = self.get_buckets()
|
||||
aw_up.set(1)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to get buckets: %s", exc)
|
||||
aw_up.set(0)
|
||||
return
|
||||
|
||||
aw_buckets_total.set(len(buckets))
|
||||
|
||||
# Server info
|
||||
aw_server_info.info({
|
||||
'host': AW_SERVER_HOST,
|
||||
'port': AW_SERVER_PORT,
|
||||
'scheme': AW_SERVER_SCHEME,
|
||||
'api_base': self.api_base
|
||||
})
|
||||
|
||||
# Collector status
|
||||
collectors = {}
|
||||
aw_server_info.info(
|
||||
{
|
||||
"host": AW_SERVER_HOST,
|
||||
"port": str(AW_SERVER_PORT),
|
||||
"scheme": AW_SERVER_SCHEME,
|
||||
"api_base": self.api_base,
|
||||
}
|
||||
)
|
||||
|
||||
aw_collector_status.clear()
|
||||
for bucket_id, bucket_data in buckets.items():
|
||||
client = bucket_data.get('client', 'unknown')
|
||||
hostname = bucket_data.get('hostname', 'unknown')
|
||||
bucket_type = bucket_data.get('type', 'unknown')
|
||||
|
||||
# Count events
|
||||
events = self.get_bucket_events(bucket_id, limit=1000)
|
||||
client = str(bucket_data.get("client", "unknown"))
|
||||
hostname = str(bucket_data.get("hostname", "unknown"))
|
||||
bucket_type = str(bucket_data.get("type", "unknown"))
|
||||
|
||||
try:
|
||||
events = self.get_bucket_events(bucket_id)
|
||||
except Exception as exc:
|
||||
logger.error("Failed to get events for %s: %s", bucket_id, exc)
|
||||
events = []
|
||||
|
||||
event_count = len(events)
|
||||
aw_bucket_events_count.labels(bucket=bucket_id).set(event_count)
|
||||
|
||||
# Last event timestamp
|
||||
aw_collector_status.labels(bucket=bucket_id, client=client, hostname=hostname, type=bucket_type).set(1)
|
||||
|
||||
previous_count = self.bucket_event_counts.get(bucket_id)
|
||||
if previous_count is not None and event_count > previous_count:
|
||||
for event in events[: event_count - previous_count]:
|
||||
aw_events_total.labels(bucket=bucket_id, event_type=self.event_type(event)).inc()
|
||||
self.bucket_event_counts[bucket_id] = event_count
|
||||
|
||||
if events:
|
||||
last_event = events[0]
|
||||
timestamp = last_event.get('timestamp', 0)
|
||||
try:
|
||||
# Convert to Unix timestamp if needed
|
||||
if isinstance(timestamp, str):
|
||||
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
||||
unix_ts = dt.timestamp()
|
||||
else:
|
||||
unix_ts = timestamp
|
||||
aw_events_last_timestamp.labels(bucket=bucket_id).set(unix_ts)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Collector status
|
||||
collector_key = f"{hostname}_{client}"
|
||||
collectors[collector_key] = {
|
||||
'status': 'active',
|
||||
'bucket': bucket_id,
|
||||
'type': bucket_type,
|
||||
'events': event_count
|
||||
}
|
||||
|
||||
aw_collector_status.info(collectors)
|
||||
aw_events_last_timestamp.labels(bucket=bucket_id).set(self.event_timestamp(events[0]))
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to parse last event timestamp for %s: %s", bucket_id, exc)
|
||||
|
||||
|
||||
def main():
|
||||
exporter = ActivityWatchExporter(AW_API_BASE)
|
||||
|
||||
# Initial collection
|
||||
exporter.collect_metrics()
|
||||
|
||||
# Start HTTP server
|
||||
|
||||
start_http_server(EXPORTER_PORT)
|
||||
logger.info(f"ActivityWatch exporter started on port {EXPORTER_PORT}")
|
||||
logger.info(f"Scraping ActivityWatch API at {AW_API_BASE}")
|
||||
|
||||
# Collect metrics every 30 seconds
|
||||
logger.info("ActivityWatch exporter started on port %s", EXPORTER_PORT)
|
||||
logger.info("Scraping ActivityWatch API at %s", AW_API_BASE)
|
||||
|
||||
while True:
|
||||
time.sleep(30)
|
||||
time.sleep(SCRAPE_INTERVAL_SECONDS)
|
||||
exporter.collect_metrics()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,20 +1,95 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
STACK_DIR="${1:-/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c}"
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
STACK_DIR="${1:-$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)}"
|
||||
ENV_FILE="$STACK_DIR/.env"
|
||||
|
||||
echo "[*] Checking endpoints"
|
||||
curl -fsS http://127.0.0.1:9399/metrics >/tmp/awrus-onec-metrics.out
|
||||
curl -fsS http://127.0.0.1:9090/-/healthy >/tmp/awrus-prom-healthy.out
|
||||
curl -fsS "http://127.0.0.1:9090/api/v1/query?query=up%7Bjob%3D%22onec_sql_exporter%22%7D" >/tmp/awrus-prom-up.json
|
||||
curl -fsS "http://127.0.0.1:9090/api/v1/query?query=onec_data_freshness_seconds" >/tmp/awrus-prom-freshness.json
|
||||
env_value() {
|
||||
key="$1"
|
||||
default="$2"
|
||||
current=$(eval "printf '%s' \"\${$key:-}\"")
|
||||
if [ -n "$current" ]; then
|
||||
printf '%s' "$current"
|
||||
return
|
||||
fi
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
value=$(sed -n "s/^$key=//p" "$ENV_FILE" | tail -n 1)
|
||||
if [ -n "$value" ]; then
|
||||
printf '%s' "$value"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
printf '%s' "$default"
|
||||
}
|
||||
|
||||
echo "[*] Checking container status"
|
||||
cd "$STACK_DIR"
|
||||
docker compose ps
|
||||
GRAFANA_PORT=$(env_value GRAFANA_PORT 3000)
|
||||
PROMETHEUS_PORT=$(env_value PROMETHEUS_PORT 9090)
|
||||
SQL_EXPORTER_PORT=$(env_value SQL_EXPORTER_PORT 9399)
|
||||
AW_EXPORTER_PORT=$(env_value AW_EXPORTER_PORT 9398)
|
||||
GRAFANA_ADMIN_USER=$(env_value GRAFANA_ADMIN_USER admin)
|
||||
GRAFANA_ADMIN_PASSWORD=$(env_value GRAFANA_ADMIN_PASSWORD change_me_now)
|
||||
|
||||
TMP_DIR="${TMPDIR:-/tmp}"
|
||||
METRICS_OUT="$TMP_DIR/awrus-onec-metrics.out"
|
||||
AW_METRICS_OUT="$TMP_DIR/awrus-aw-metrics.out"
|
||||
PROM_HEALTH_OUT="$TMP_DIR/awrus-prom-healthy.out"
|
||||
PROM_UP_OUT="$TMP_DIR/awrus-prom-up.json"
|
||||
PROM_FRESHNESS_OUT="$TMP_DIR/awrus-prom-freshness.json"
|
||||
GRAFANA_HEALTH_OUT="$TMP_DIR/awrus-grafana-health.json"
|
||||
GRAFANA_DS_OUT="$TMP_DIR/awrus-grafana-datasources.json"
|
||||
GRAFANA_DASH_OUT="$TMP_DIR/awrus-grafana-dashboards.json"
|
||||
|
||||
require_metric() {
|
||||
metric_name="$1"
|
||||
metrics_file="$2"
|
||||
if ! grep -q "^$metric_name" "$metrics_file"; then
|
||||
echo "[!] Required metric '$metric_name' was not found in $metrics_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_prometheus_success() {
|
||||
file="$1"
|
||||
if ! grep -q '"status":"success"' "$file"; then
|
||||
echo "[!] Prometheus query did not return status=success: $file" >&2
|
||||
cat "$file" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "[*] Checking exporter endpoints"
|
||||
curl -fsS "http://127.0.0.1:$SQL_EXPORTER_PORT/metrics" >"$METRICS_OUT"
|
||||
curl -fsS "http://127.0.0.1:$AW_EXPORTER_PORT/metrics" >"$AW_METRICS_OUT"
|
||||
require_metric "onec_data_freshness_seconds" "$METRICS_OUT"
|
||||
require_metric "aw_up" "$AW_METRICS_OUT"
|
||||
|
||||
echo "[*] Checking Prometheus health and scrape targets"
|
||||
curl -fsS "http://127.0.0.1:$PROMETHEUS_PORT/-/healthy" >"$PROM_HEALTH_OUT"
|
||||
curl -fsS "http://127.0.0.1:$PROMETHEUS_PORT/api/v1/query?query=up%7Bjob%3D~%22onec_sql_exporter%7Caw_activitywatch_exporter%22%7D" >"$PROM_UP_OUT"
|
||||
curl -fsS "http://127.0.0.1:$PROMETHEUS_PORT/api/v1/query?query=onec_data_freshness_seconds" >"$PROM_FRESHNESS_OUT"
|
||||
require_prometheus_success "$PROM_UP_OUT"
|
||||
require_prometheus_success "$PROM_FRESHNESS_OUT"
|
||||
|
||||
echo "[*] Checking Grafana health, datasource and dashboards"
|
||||
curl -fsS "http://127.0.0.1:$GRAFANA_PORT/api/health" >"$GRAFANA_HEALTH_OUT"
|
||||
curl -fsS -u "$GRAFANA_ADMIN_USER:$GRAFANA_ADMIN_PASSWORD" "http://127.0.0.1:$GRAFANA_PORT/api/datasources/uid/prometheus" >"$GRAFANA_DS_OUT"
|
||||
curl -fsS -u "$GRAFANA_ADMIN_USER:$GRAFANA_ADMIN_PASSWORD" "http://127.0.0.1:$GRAFANA_PORT/api/search?type=dash-db&query=" >"$GRAFANA_DASH_OUT"
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
echo "[*] Checking container status"
|
||||
cd "$STACK_DIR"
|
||||
docker compose ps
|
||||
else
|
||||
echo "[*] docker command not found; skipping container status"
|
||||
fi
|
||||
|
||||
echo "[+] Pipeline health artifacts:"
|
||||
echo " /tmp/awrus-onec-metrics.out"
|
||||
echo " /tmp/awrus-prom-healthy.out"
|
||||
echo " /tmp/awrus-prom-up.json"
|
||||
echo " /tmp/awrus-prom-freshness.json"
|
||||
echo " $METRICS_OUT"
|
||||
echo " $AW_METRICS_OUT"
|
||||
echo " $PROM_HEALTH_OUT"
|
||||
echo " $PROM_UP_OUT"
|
||||
echo " $PROM_FRESHNESS_OUT"
|
||||
echo " $GRAFANA_HEALTH_OUT"
|
||||
echo " $GRAFANA_DS_OUT"
|
||||
echo " $GRAFANA_DASH_OUT"
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
# ActivityWatch-Russian Knowledge Graph Report
|
||||
|
||||
## Overview
|
||||
- **Total Nodes**: 404
|
||||
- **Total Edges**: 933
|
||||
- **Communities**: 27
|
||||
- **Source**: AST extraction (code-only corpus)
|
||||
|
||||
## Communities by Size
|
||||
|
||||
### Community 1 (62 nodes)
|
||||
- install_kit_awindows_20260427_211240_windows_dlp_endpoint_signals_collector_ps1
|
||||
- dlp_endpoint_signals_collector_get_deploymentconfig
|
||||
- dlp_endpoint_signals_collector_write_endpointlog
|
||||
- dlp_endpoint_signals_collector_invoke_awjsonpost
|
||||
- dlp_endpoint_signals_collector_ensure_bucket
|
||||
- ... and 57 more
|
||||
|
||||
### Community 2 (56 nodes)
|
||||
- aw_server_aw_ru_patch_js
|
||||
- aw_ru_patch_injectstyles
|
||||
- aw_ru_patch_hidenoisenavigation
|
||||
- aw_ru_patch_getcurrenthostfromhash
|
||||
- aw_ru_patch_ispvelikehost
|
||||
- ... and 51 more
|
||||
|
||||
### Community 3 (54 nodes)
|
||||
- install_kit_awindows_20260427_211240_windows_browser_domains_native_collector_ps1
|
||||
- browser_domains_native_collector_get_deploymentconfig
|
||||
- browser_domains_native_collector_write_collectorlog
|
||||
- browser_domains_native_collector_write_dlpincidentlog
|
||||
- browser_domains_native_collector_test_domainmatch
|
||||
- ... and 49 more
|
||||
|
||||
### Community 0 (38 nodes)
|
||||
- scripts_aggregate_dlp_events_py
|
||||
- aggregate_dlp_events_bucket
|
||||
- aggregate_dlp_events_awevent
|
||||
- aggregate_dlp_events_psycopgconnection
|
||||
- protocol
|
||||
- ... and 33 more
|
||||
|
||||
### Community 5 (34 nodes)
|
||||
- aw_ru_patch_isdlpsignalbucketroute
|
||||
- aw_ru_patch_getdlphostfrombucketid
|
||||
- aw_ru_patch_builddlpkey
|
||||
- aw_ru_patch_loadbucketevents
|
||||
- aw_ru_patch_serializerulematch
|
||||
- ... and 29 more
|
||||
|
||||
### Community 4 (34 nodes)
|
||||
- install_kit_awindows_20260427_211240_windows_email_outbound_collector_ps1
|
||||
- email_outbound_collector_get_deploymentconfig
|
||||
- email_outbound_collector_write_collectorlog
|
||||
- email_outbound_collector_invoke_awjsonpost
|
||||
- email_outbound_collector_ensure_bucket
|
||||
- ... and 29 more
|
||||
|
||||
### Community 8 (28 nodes)
|
||||
- aw_ru_patch_replacetext
|
||||
- aw_ru_patch_walk
|
||||
- aw_ru_patch_translateattributes
|
||||
- aw_ru_patch_ishomeroute
|
||||
- aw_ru_patch_getdefaulthostgroupsconfig
|
||||
- ... and 23 more
|
||||
|
||||
### Community 9 (18 nodes)
|
||||
- aw_ru_patch_getdlpbucketidfromhash
|
||||
- aw_ru_patch_generatedlpid
|
||||
- aw_ru_patch_awapijson
|
||||
- aw_ru_patch_ensureawbucket
|
||||
- aw_ru_patch_saveawheartbeat
|
||||
- ... and 13 more
|
||||
|
||||
### Community 10 (14 nodes)
|
||||
- install_kit_awindows_20260427_211240_windows_migrate_awatch_rus_paths_ps1
|
||||
- migrate_awatch_rus_paths_copy_directorycontents
|
||||
- migrate_awatch_rus_paths_copy_ifexists
|
||||
- migrate_awatch_rus_paths_convert_pathvalue
|
||||
- migrate_awatch_rus_paths_stop_awatchtaskset
|
||||
- ... and 9 more
|
||||
|
||||
### Community 6 (12 nodes)
|
||||
- grafana_1c_sql_exporter_collectors_aw_activitywatch_py
|
||||
- aw_activitywatch_activitywatchexporter
|
||||
- aw_activitywatch_activitywatchexporter_init
|
||||
- aw_activitywatch_activitywatchexporter_get_buckets
|
||||
- aw_activitywatch_activitywatchexporter_get_bucket_events
|
||||
- ... and 7 more
|
||||
|
||||
## File Types
|
||||
The graph was built from code files including:
|
||||
- PowerShell scripts (.ps1)
|
||||
- Python scripts (.py)
|
||||
- JavaScript patches (.js)
|
||||
- Configuration files
|
||||
|
||||
## Notes
|
||||
- This is a structural (AST-based) graph showing code relationships
|
||||
- No semantic extraction was performed (no docs/papers in corpus)
|
||||
- Communities represent clusters of related functions and modules
|
||||
File diff suppressed because one or more lines are too long
+11645
File diff suppressed because it is too large
Load Diff
@@ -31,15 +31,6 @@ cd ansible
|
||||
ansible-playbook -i inventory.ini deploy_aw_server.yml
|
||||
```
|
||||
|
||||
## Секреты (пароли) безопасно
|
||||
|
||||
Рекомендуемый способ не хранить пароли в репозитории — перед запуском экспортировать их в переменные окружения:
|
||||
|
||||
- Linux `aw_server` (SSH пароль root): `AW_SSH_PASSWORD`
|
||||
- Windows `aw_windows` (WinRM пароль): `AW_WINRM_PASSWORD`
|
||||
|
||||
В `group_vars/aw_server.yml` и `group_vars/windows.yml` они читаются через `lookup('env', ...)`.
|
||||
|
||||
## Полный установочный playbook (всё за один запуск)
|
||||
|
||||
Если нужно прогнать полный цикл одной командой:
|
||||
@@ -158,13 +149,3 @@ Playbook:
|
||||
- Для полного сценария CT создаётся автоматически через `pct create`.
|
||||
- На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector.
|
||||
- Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`.
|
||||
|
||||
## Prod rollout одной командой
|
||||
|
||||
Для ручного запуска с dry-run и логированием используйте:
|
||||
|
||||
```bash
|
||||
bash scripts/prod_rollout.sh
|
||||
```
|
||||
|
||||
Скрипт попросит `AW_SSH_PASSWORD` и `AW_WINRM_PASSWORD` интерактивно (ввод скрыт) и сложит логи в `.rollout-logs/`.
|
||||
|
||||
@@ -54,11 +54,6 @@
|
||||
- "{{ aw_server_webui_dir }}"
|
||||
- "{{ aw_server_webui_dir }}/js"
|
||||
- "{{ aw_server_data_dir }}"
|
||||
- "{{ aw_server_db_path | dirname }}"
|
||||
- "{{ aw_server_data_dir }}/.config"
|
||||
- "{{ aw_server_data_dir }}/.config/activitywatch"
|
||||
- "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust"
|
||||
- "{{ aw_server_data_dir }}/backups"
|
||||
- "{{ aw_server_log_dir }}"
|
||||
- /etc/activitywatch
|
||||
- "{{ aw_bootstrap_dir }}"
|
||||
@@ -79,147 +74,100 @@
|
||||
- "{{ aw_server_webui_dir }}"
|
||||
- "{{ aw_server_webui_dir }}/js"
|
||||
- "{{ aw_server_data_dir }}"
|
||||
- "{{ aw_server_db_path | dirname }}"
|
||||
- "{{ aw_server_data_dir }}/.config"
|
||||
- "{{ aw_server_data_dir }}/.config/activitywatch"
|
||||
- "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust"
|
||||
- "{{ aw_server_data_dir }}/backups"
|
||||
- "{{ aw_server_log_dir }}"
|
||||
|
||||
- name: (Check mode) Пропустить установку релиза ActivityWatch
|
||||
ansible.builtin.debug:
|
||||
msg: "ansible_check_mode=true: download/unarchive/install of ActivityWatch release is skipped."
|
||||
when: ansible_check_mode
|
||||
- name: Скачать архив релиза ActivityWatch
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ aw_server_download_url }}"
|
||||
dest: "{{ aw_archive_path }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Установить релиз ActivityWatch (download/unarchive/install)
|
||||
when: not ansible_check_mode
|
||||
block:
|
||||
- name: Скачать архив релиза ActivityWatch
|
||||
ansible.builtin.get_url:
|
||||
url: "{{ aw_server_download_url }}"
|
||||
dest: "{{ aw_archive_path }}"
|
||||
mode: "0644"
|
||||
- name: Распаковать релиз ActivityWatch
|
||||
ansible.builtin.unarchive:
|
||||
src: "{{ aw_archive_path }}"
|
||||
dest: "{{ aw_release_dir }}"
|
||||
remote_src: true
|
||||
extra_opts: ["-o"]
|
||||
|
||||
- name: Распаковать релиз ActivityWatch
|
||||
ansible.builtin.unarchive:
|
||||
src: "{{ aw_archive_path }}"
|
||||
dest: "{{ aw_release_dir }}"
|
||||
remote_src: true
|
||||
extra_opts: ["-o"]
|
||||
- name: Найти распакованный каталог ActivityWatch
|
||||
ansible.builtin.find:
|
||||
paths: "{{ aw_release_dir }}"
|
||||
file_type: directory
|
||||
patterns: "activitywatch*"
|
||||
register: aw_release_find
|
||||
|
||||
- name: Найти распакованный каталог ActivityWatch
|
||||
ansible.builtin.find:
|
||||
paths: "{{ aw_release_dir }}"
|
||||
recurse: true
|
||||
file_type: directory
|
||||
patterns: "activitywatch*"
|
||||
register: aw_release_find
|
||||
- name: Найти бинарный файл AW server
|
||||
ansible.builtin.find:
|
||||
paths: "{{ aw_release_dir }}"
|
||||
file_type: file
|
||||
patterns:
|
||||
- aw-server-rust
|
||||
- aw-server
|
||||
register: aw_server_binary_find
|
||||
|
||||
- name: Найти бинарный файл AW server
|
||||
ansible.builtin.find:
|
||||
paths: "{{ aw_release_dir }}"
|
||||
recurse: true
|
||||
file_type: file
|
||||
patterns:
|
||||
- aw-server-rust
|
||||
- aw-server
|
||||
register: aw_server_binary_find
|
||||
- name: Найти каталог WebUI
|
||||
ansible.builtin.find:
|
||||
paths: "{{ aw_release_dir }}"
|
||||
file_type: directory
|
||||
patterns:
|
||||
- aw-webui
|
||||
- webui
|
||||
register: aw_webui_dir_find
|
||||
|
||||
- name: Найти index.html WebUI
|
||||
ansible.builtin.find:
|
||||
paths: "{{ aw_release_dir }}"
|
||||
recurse: true
|
||||
file_type: file
|
||||
patterns:
|
||||
- index.html
|
||||
register: aw_webui_index_find
|
||||
- name: Сохранить пути распакованного релиза
|
||||
ansible.builtin.set_fact:
|
||||
aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
|
||||
aw_server_binary_path: "{{ (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
|
||||
aw_webui_source_path: "{{ (aw_webui_dir_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
|
||||
|
||||
- name: Сохранить пути распакованного релиза (binary + webui index)
|
||||
ansible.builtin.set_fact:
|
||||
aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
|
||||
aw_server_binary_path: >-
|
||||
{{
|
||||
(
|
||||
(
|
||||
(aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list)
|
||||
| select('match', '.*/aw-server-rust$') | list | first
|
||||
)
|
||||
| default(
|
||||
(
|
||||
(aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first)
|
||||
),
|
||||
true
|
||||
)
|
||||
) | default('')
|
||||
}}
|
||||
aw_webui_index_path: >-
|
||||
{{
|
||||
(
|
||||
(
|
||||
(aw_webui_index_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list)
|
||||
| select('search', '/static/index\\.html$') | list | first
|
||||
)
|
||||
| default(
|
||||
(
|
||||
(aw_webui_index_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first)
|
||||
),
|
||||
true
|
||||
)
|
||||
) | default('')
|
||||
}}
|
||||
- name: Проверить, что компоненты релиза найдены
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- aw_release_extracted is defined
|
||||
- aw_release_extracted | length > 0
|
||||
- aw_server_binary_path is defined
|
||||
- aw_server_binary_path | length > 0
|
||||
- aw_webui_source_path is defined
|
||||
- aw_webui_source_path | length > 0
|
||||
fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch."
|
||||
|
||||
- name: Сохранить каталог WebUI (dirname index.html)
|
||||
ansible.builtin.set_fact:
|
||||
aw_webui_source_path: "{{ aw_webui_index_path | dirname }}"
|
||||
- name: Создать каталог установленного релиза
|
||||
ansible.builtin.file:
|
||||
path: "{{ aw_release_install_dir }}"
|
||||
state: directory
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Проверить, что компоненты релиза найдены
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- aw_release_extracted is defined
|
||||
- aw_release_extracted | length > 0
|
||||
- aw_server_binary_path is defined
|
||||
- aw_server_binary_path | length > 0
|
||||
- aw_webui_source_path is defined
|
||||
- aw_webui_source_path | length > 0
|
||||
fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch."
|
||||
- name: Установить бинарный файл AW server
|
||||
ansible.builtin.copy:
|
||||
remote_src: true
|
||||
src: "{{ aw_server_binary_path }}"
|
||||
dest: "{{ aw_release_install_dir }}/aw-server-rust"
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Создать каталог установленного релиза
|
||||
ansible.builtin.file:
|
||||
path: "{{ aw_release_install_dir }}"
|
||||
state: directory
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
mode: "0755"
|
||||
- name: Создать ссылку на активный бинарный файл AW server
|
||||
ansible.builtin.file:
|
||||
src: "{{ aw_release_install_dir }}/aw-server-rust"
|
||||
dest: /opt/activitywatch/bin/aw-server-rust
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
state: link
|
||||
force: true
|
||||
|
||||
- name: Установить бинарный файл AW server
|
||||
ansible.builtin.copy:
|
||||
remote_src: true
|
||||
src: "{{ aw_server_binary_path }}"
|
||||
dest: "{{ aw_release_install_dir }}/aw-server-rust"
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
mode: "0755"
|
||||
- name: Синхронизировать WebUI в RU каталог
|
||||
ansible.builtin.command:
|
||||
cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/"
|
||||
|
||||
- name: Создать ссылку на активный бинарный файл AW server
|
||||
ansible.builtin.file:
|
||||
src: "{{ aw_release_install_dir }}/aw-server-rust"
|
||||
dest: /opt/activitywatch/bin/aw-server-rust
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
state: link
|
||||
force: true
|
||||
|
||||
- name: Синхронизировать WebUI в RU каталог
|
||||
ansible.builtin.command:
|
||||
cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/"
|
||||
|
||||
- name: Настроить владельца файлов /opt/activitywatch
|
||||
ansible.builtin.file:
|
||||
path: /opt/activitywatch
|
||||
state: directory
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
recurse: true
|
||||
- name: Настроить владельца файлов /opt/activitywatch
|
||||
ansible.builtin.file:
|
||||
path: /opt/activitywatch
|
||||
state: directory
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
recurse: true
|
||||
|
||||
- name: Установить systemd service из шаблона репозитория
|
||||
ansible.builtin.copy:
|
||||
@@ -236,401 +184,143 @@
|
||||
- Перезагрузить systemd
|
||||
- Перезапустить activitywatch
|
||||
|
||||
- name: (Check mode) Пропустить WebUI patch и запуск сервиса
|
||||
ansible.builtin.debug:
|
||||
msg: "ansible_check_mode=true: WebUI patch + service start + API checks are skipped."
|
||||
when: ansible_check_mode
|
||||
|
||||
- name: Применить WebUI RU patch и запустить сервис
|
||||
when: not ansible_check_mode
|
||||
block:
|
||||
- name: Скопировать RU patch файлы WebUI из репозитория
|
||||
ansible.builtin.copy:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ item.dest }}"
|
||||
mode: "{{ item.mode }}"
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
loop:
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" }
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" }
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "{{ aw_server_webui_dir }}/js/aw-worktime-panel.js", mode: "0644" }
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" }
|
||||
|
||||
- name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh
|
||||
ansible.builtin.file:
|
||||
path: /root/bootstrap
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Скопировать RU patch файлы для apply_webui_ru_patch.sh (хотфиксы compiled JS чанков)
|
||||
ansible.builtin.copy:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ item.dest }}"
|
||||
mode: "{{ item.mode }}"
|
||||
loop:
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" }
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" }
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "/root/bootstrap/aw-worktime-panel.js", mode: "0644" }
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" }
|
||||
|
||||
- name: Скопировать apply_webui_ru_patch.sh скрипт
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/apply_webui_ru_patch.sh"
|
||||
dest: /opt/activitywatch/aw-server/apply_webui_ru_patch.sh
|
||||
mode: "0755"
|
||||
|
||||
- name: Записать /etc/activitywatch/aw-server.env перед хотфиксами
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/activitywatch/aw-server.env
|
||||
mode: "0640"
|
||||
owner: root
|
||||
group: root
|
||||
content: |
|
||||
AW_SERVER_BIND_HOST={{ aw_server_bind_host }}
|
||||
AW_SERVER_PORT={{ aw_server_port }}
|
||||
AW_SERVER_DATA_DIR={{ aw_server_data_dir }}
|
||||
AW_SERVER_DB_PATH={{ aw_server_db_path }}
|
||||
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
|
||||
AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }}
|
||||
AW_SERVER_USER={{ aw_server_user }}
|
||||
AW_SERVER_GROUP={{ aw_server_group }}
|
||||
AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }}
|
||||
AW_WORKTIME_TZ={{ aw_worktime_timezone }}
|
||||
XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share
|
||||
XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config
|
||||
|
||||
- name: Установить скрипт AW worktime API
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.py"
|
||||
dest: /usr/local/bin/aw-worktime-api.py
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Установить systemd unit AW worktime API
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.service"
|
||||
dest: /etc/systemd/system/aw-worktime-api.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Установить скрипт AW worktime UI bridge
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.py"
|
||||
dest: /usr/local/bin/aw-worktime-ui-bridge.py
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Установить systemd unit AW worktime UI bridge
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.service"
|
||||
dest: /etc/systemd/system/aw-worktime-ui-bridge.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Установить systemd timer AW worktime UI bridge
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.timer"
|
||||
dest: /etc/systemd/system/aw-worktime-ui-bridge.timer
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Перезагрузить systemd после установки AW worktime API
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Включить и перезапустить AW worktime API
|
||||
ansible.builtin.systemd:
|
||||
name: aw-worktime-api.service
|
||||
enabled: true
|
||||
state: restarted
|
||||
|
||||
- name: Отключить legacy timer aw-worktime-afk-bridge (если есть)
|
||||
ansible.builtin.systemd:
|
||||
name: aw-worktime-afk-bridge.timer
|
||||
enabled: false
|
||||
state: stopped
|
||||
failed_when: false
|
||||
|
||||
- name: Включить и перезапустить AW worktime UI bridge timer
|
||||
ansible.builtin.systemd:
|
||||
name: aw-worktime-ui-bridge.timer
|
||||
enabled: true
|
||||
state: restarted
|
||||
|
||||
- name: Выполнить разовый прогон AW worktime UI bridge
|
||||
ansible.builtin.systemd:
|
||||
name: aw-worktime-ui-bridge.service
|
||||
state: started
|
||||
failed_when: false
|
||||
|
||||
- name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper)
|
||||
ansible.builtin.command:
|
||||
cmd: "/opt/activitywatch/aw-server/apply_webui_ru_patch.sh"
|
||||
register: apply_ru_patch_result
|
||||
failed_when: false
|
||||
|
||||
- name: Вывести результат применения хотфиксов
|
||||
ansible.builtin.debug:
|
||||
msg: "apply_webui_ru_patch.sh: {{ apply_ru_patch_result.stdout }}"
|
||||
|
||||
- name: Проверить наличие index.html после копирования
|
||||
ansible.builtin.stat:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
register: aw_webui_ru_index
|
||||
|
||||
- name: Проверить, что index.html доступен для RU patch
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- aw_webui_ru_index.stat.exists
|
||||
fail_msg: "Не найден index.html WebUI для применения RU patch."
|
||||
|
||||
- name: Удалить старые теги RU patch из index.html
|
||||
ansible.builtin.replace:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
regexp: '<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*></script>'
|
||||
replace: ''
|
||||
|
||||
- name: Добавить cleanup script RU patch в index.html
|
||||
ansible.builtin.replace:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
regexp: '</head>'
|
||||
replace: '<script src="/js/sw-cleanup.js?v={{ aw_sw_cleanup_cache_bust }}"></script></head>'
|
||||
|
||||
- name: Добавить загрузчик RU patch перед закрытием body
|
||||
ansible.builtin.replace:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
regexp: '</body>'
|
||||
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v={{ aw_ru_patch_cache_bust }}"></script></body>'
|
||||
|
||||
- name: Скопировать merge script AW DB на сервер
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/scripts/merge_aw_server_dbs.py"
|
||||
dest: /usr/local/bin/merge_aw_server_dbs.py
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Проверить наличие legacy root DB
|
||||
ansible.builtin.stat:
|
||||
path: /root/.local/share/activitywatch/aw-server-rust/sqlite.db
|
||||
register: aw_legacy_root_db
|
||||
|
||||
- name: Проверить наличие target DB
|
||||
ansible.builtin.stat:
|
||||
path: "{{ aw_server_db_path }}"
|
||||
register: aw_target_db
|
||||
|
||||
- name: Остановить сервис перед merge server DB
|
||||
ansible.builtin.systemd:
|
||||
name: activitywatch-server.service
|
||||
state: stopped
|
||||
when: aw_legacy_root_db.stat.exists | default(false)
|
||||
|
||||
- name: Создать backup каталоги server DB
|
||||
ansible.builtin.file:
|
||||
path: "{{ aw_server_data_dir }}/backups/db"
|
||||
state: directory
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
mode: "0755"
|
||||
when: aw_legacy_root_db.stat.exists | default(false)
|
||||
|
||||
- name: Backup target DB перед merge
|
||||
ansible.builtin.copy:
|
||||
remote_src: true
|
||||
src: "{{ aw_server_db_path }}"
|
||||
dest: "{{ aw_server_data_dir }}/backups/db/target-before-merge-{{ ansible_date_time.iso8601_basic_short }}.sqlite.db"
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
mode: "0644"
|
||||
when:
|
||||
- aw_legacy_root_db.stat.exists | default(false)
|
||||
- aw_target_db.stat.exists | default(false)
|
||||
|
||||
- name: Backup legacy root DB перед merge
|
||||
ansible.builtin.copy:
|
||||
remote_src: true
|
||||
src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db
|
||||
dest: "{{ aw_server_data_dir }}/backups/db/legacy-root-{{ ansible_date_time.iso8601_basic_short }}.sqlite.db"
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
mode: "0644"
|
||||
when: aw_legacy_root_db.stat.exists | default(false)
|
||||
|
||||
- name: Merge legacy root DB в target DB
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- python3
|
||||
- /usr/local/bin/merge_aw_server_dbs.py
|
||||
- --base
|
||||
- /root/.local/share/activitywatch/aw-server-rust/sqlite.db
|
||||
- --overlay
|
||||
- "{{ aw_server_db_path }}"
|
||||
- --output
|
||||
- "{{ aw_server_db_path }}.merged"
|
||||
when:
|
||||
- aw_legacy_root_db.stat.exists | default(false)
|
||||
- aw_target_db.stat.exists | default(false)
|
||||
|
||||
- name: Install merged DB as active target DB
|
||||
ansible.builtin.copy:
|
||||
remote_src: true
|
||||
src: "{{ aw_server_db_path }}.merged"
|
||||
dest: "{{ aw_server_db_path }}"
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
mode: "0644"
|
||||
when:
|
||||
- aw_legacy_root_db.stat.exists | default(false)
|
||||
- aw_target_db.stat.exists | default(false)
|
||||
|
||||
- name: Скопировать legacy root DB в target DB если target ещё не существует
|
||||
ansible.builtin.copy:
|
||||
remote_src: true
|
||||
src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db
|
||||
dest: "{{ aw_server_db_path }}"
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
mode: "0644"
|
||||
when:
|
||||
- aw_legacy_root_db.stat.exists | default(false)
|
||||
- not (aw_target_db.stat.exists | default(false))
|
||||
|
||||
- name: Записать aw-server-rust config.toml с разрешёнными CORS origin
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust/config.toml"
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
mode: "0644"
|
||||
content: |
|
||||
cors = [
|
||||
{% for origin in aw_server_cors_origins | default([]) %}
|
||||
"{{ origin }}"{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
- name: Включить и запустить сервис
|
||||
ansible.builtin.systemd:
|
||||
name: activitywatch-server.service
|
||||
enabled: true
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
|
||||
- name: Дождаться ответа API
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info"
|
||||
method: GET
|
||||
status_code: 200
|
||||
register: aw_api
|
||||
retries: 10
|
||||
delay: 3
|
||||
until: aw_api.status == 200
|
||||
|
||||
- name: Считать текущие server-side settings
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/"
|
||||
method: GET
|
||||
status_code: 200
|
||||
register: aw_settings_current
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
|
||||
- name: Считать текущие server-side views
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/views"
|
||||
method: GET
|
||||
status_code: 200
|
||||
register: aw_views_current
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
|
||||
- name: Считать текущие server-side classes
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/classes"
|
||||
method: GET
|
||||
status_code: 200
|
||||
register: aw_classes_current
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
|
||||
- name: Создать backup текущих server-side settings/views/classes
|
||||
- name: Скопировать RU patch файлы WebUI из репозитория
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ aw_server_data_dir }}/backups/{{ item.name }}-{{ ansible_date_time.iso8601_basic_short }}.json"
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ item.dest }}"
|
||||
mode: "{{ item.mode }}"
|
||||
owner: "{{ aw_server_user }}"
|
||||
group: "{{ aw_server_group }}"
|
||||
mode: "0644"
|
||||
content: "{{ item.payload | to_nice_json }}"
|
||||
loop:
|
||||
- name: settings
|
||||
payload: "{{ aw_settings_current.json | default({}) }}"
|
||||
- name: views
|
||||
payload: "{{ aw_views_current.json | default(none) }}"
|
||||
- name: classes
|
||||
payload: "{{ aw_classes_current.json | default(none) }}"
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" }
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" }
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "{{ aw_server_webui_dir }}/js/aw-worktime-panel.js", mode: "0644" }
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" }
|
||||
|
||||
- name: Настроить DLP Aggregator (Phase 2)
|
||||
block:
|
||||
- name: Создать каталог для скриптов
|
||||
ansible.builtin.file:
|
||||
path: "/opt/activitywatch/scripts"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
- name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh
|
||||
ansible.builtin.file:
|
||||
path: /root/bootstrap
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Скопировать агрегатор событий DLP
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/scripts/aggregate_dlp_events.py"
|
||||
dest: "/opt/activitywatch/scripts/aggregate_dlp_events.py"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
- name: Скопировать RU patch файлы для apply_webui_ru_patch.sh (хотфиксы compiled JS чанков)
|
||||
ansible.builtin.copy:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ item.dest }}"
|
||||
mode: "{{ item.mode }}"
|
||||
loop:
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" }
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" }
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "/root/bootstrap/aw-worktime-panel.js", mode: "0644" }
|
||||
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" }
|
||||
|
||||
- name: Установить systemd unit для агрегатора
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/activitywatch-dlp-aggregator.service
|
||||
content: |
|
||||
[Unit]
|
||||
Description=ActivityWatch DLP Event Aggregator
|
||||
After=activitywatch-server.service
|
||||
- name: Скопировать apply_webui_ru_patch.sh скрипт
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/apply_webui_ru_patch.sh"
|
||||
dest: /opt/activitywatch/aw-server/apply_webui_ru_patch.sh
|
||||
mode: "0755"
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User={{ aw_server_user }}
|
||||
WorkingDirectory={{ aw_server_data_dir }}
|
||||
ExecStart=/usr/bin/python3 /opt/activitywatch/scripts/aggregate_dlp_events.py \
|
||||
--aw-url http://127.0.0.1:{{ aw_server_port }}/api/0 \
|
||||
--sqlite-path {{ aw_server_data_dir }}/dlp_warehouse.sqlite \
|
||||
--state-path {{ aw_server_data_dir }}/dlp-aggregator-state.json
|
||||
- name: Записать /etc/activitywatch/aw-server.env перед хотфиксами
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/activitywatch/aw-server.env
|
||||
mode: "0640"
|
||||
owner: root
|
||||
group: root
|
||||
content: |
|
||||
AW_SERVER_BIND_HOST={{ aw_server_bind_host }}
|
||||
AW_SERVER_PORT={{ aw_server_port }}
|
||||
AW_SERVER_DATA_DIR={{ aw_server_data_dir }}
|
||||
AW_SERVER_DB_PATH={{ aw_server_db_path }}
|
||||
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
|
||||
AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }}
|
||||
AW_SERVER_USER={{ aw_server_user }}
|
||||
AW_SERVER_GROUP={{ aw_server_group }}
|
||||
AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }}
|
||||
AW_WORKTIME_TZ={{ aw_worktime_timezone }}
|
||||
XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share
|
||||
XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
- name: Установить скрипт AW worktime API
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.py"
|
||||
dest: /usr/local/bin/aw-worktime-api.py
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Установить systemd timer для агрегатора
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/system/activitywatch-dlp-aggregator.timer
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Run ActivityWatch DLP Aggregator every 5 minutes
|
||||
- name: Установить systemd unit AW worktime API
|
||||
ansible.builtin.copy:
|
||||
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.service"
|
||||
dest: /etc/systemd/system/aw-worktime-api.service
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1min
|
||||
OnUnitActiveSec=5min
|
||||
AccuracySec=1s
|
||||
- name: Перезагрузить systemd после установки AW worktime API
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
- name: Включить и перезапустить AW worktime API
|
||||
ansible.builtin.systemd:
|
||||
name: aw-worktime-api.service
|
||||
enabled: true
|
||||
state: restarted
|
||||
|
||||
- name: Включить и запустить таймер агрегатора
|
||||
ansible.builtin.systemd:
|
||||
name: activitywatch-dlp-aggregator.timer
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
- name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper)
|
||||
ansible.builtin.command:
|
||||
cmd: "/opt/activitywatch/aw-server/apply_webui_ru_patch.sh"
|
||||
register: apply_ru_patch_result
|
||||
failed_when: false
|
||||
|
||||
- name: Вывести результат применения хотфиксов
|
||||
ansible.builtin.debug:
|
||||
msg: "apply_webui_ru_patch.sh: {{ apply_ru_patch_result.stdout }}"
|
||||
|
||||
- name: Проверить наличие index.html после копирования
|
||||
ansible.builtin.stat:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
register: aw_webui_ru_index
|
||||
|
||||
- name: Проверить, что index.html доступен для RU patch
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- aw_webui_ru_index.stat.exists
|
||||
fail_msg: "Не найден index.html WebUI для применения RU patch."
|
||||
|
||||
- name: Удалить старые теги RU patch из index.html
|
||||
ansible.builtin.replace:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
regexp: '<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*></script>'
|
||||
replace: ''
|
||||
|
||||
- name: Добавить cleanup script RU patch в index.html
|
||||
ansible.builtin.replace:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
regexp: '</head>'
|
||||
replace: '<script src="/js/sw-cleanup.js?v={{ aw_sw_cleanup_cache_bust }}"></script></head>'
|
||||
|
||||
- name: Добавить загрузчик RU patch перед закрытием body
|
||||
ansible.builtin.replace:
|
||||
path: "{{ aw_server_webui_dir }}/index.html"
|
||||
regexp: '</body>'
|
||||
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v={{ aw_ru_patch_cache_bust }}"></script></body>'
|
||||
|
||||
- name: Включить и запустить сервис
|
||||
ansible.builtin.systemd:
|
||||
name: activitywatch-server.service
|
||||
enabled: true
|
||||
state: restarted
|
||||
daemon_reload: true
|
||||
|
||||
- name: Дождаться ответа API
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info"
|
||||
method: GET
|
||||
status_code: 200
|
||||
register: aw_api
|
||||
retries: 10
|
||||
delay: 3
|
||||
until: aw_api.status == 200
|
||||
|
||||
- name: Применить базовые worktime settings (classes)
|
||||
ansible.builtin.uri:
|
||||
@@ -652,12 +342,16 @@
|
||||
|
||||
- name: Вычислить worktime durationDefault из aw_worktime_from/to
|
||||
ansible.builtin.set_fact:
|
||||
aw_worktime_from_h: "{{ (aw_worktime_from | default('08:00')).split(':')[0] | int }}"
|
||||
aw_worktime_from_m: "{{ (aw_worktime_from | default('08:00')).split(':')[1] | int }}"
|
||||
aw_worktime_to_h: "{{ (aw_worktime_to | default('17:00')).split(':')[0] | int }}"
|
||||
aw_worktime_to_m: "{{ (aw_worktime_to | default('17:00')).split(':')[1] | int }}"
|
||||
aw_worktime_duration_default_derived: >-
|
||||
{{
|
||||
(
|
||||
(
|
||||
(((aw_worktime_to | default('17:00')).split(':')[0] | int) * 60 + ((aw_worktime_to | default('17:00')).split(':')[1] | int)) -
|
||||
(((aw_worktime_from | default('08:00')).split(':')[0] | int) * 60 + ((aw_worktime_from | default('08:00')).split(':')[1] | int))
|
||||
((aw_worktime_to_h | int) * 60 + (aw_worktime_to_m | int)) -
|
||||
((aw_worktime_from_h | int) * 60 + (aw_worktime_from_m | int))
|
||||
) * 60
|
||||
)
|
||||
}}
|
||||
@@ -685,46 +379,20 @@
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/startOfDay"
|
||||
method: POST
|
||||
body: "\"{{ aw_worktime_start_of_day }}\""
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
status_code: [200, 201]
|
||||
body: "{{ aw_worktime_start_of_day }}"
|
||||
body_format: json
|
||||
status_code: 200
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
|
||||
- name: Применить базовый период worktime (durationDefault seconds)
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/durationDefault"
|
||||
method: POST
|
||||
body: "{{ aw_worktime_duration_default_effective | string }}"
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
status_code: [200, 201]
|
||||
body: "{{ aw_worktime_duration_default_effective }}"
|
||||
body_format: json
|
||||
status_code: 200
|
||||
when: aw_apply_worktime_settings | default(false) | bool
|
||||
|
||||
- name: Применить always_active_pattern для fallback без AFK
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/always_active_pattern"
|
||||
method: POST
|
||||
body: "\"{{ aw_server_always_active_pattern }}\""
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
status_code: [200, 201]
|
||||
when:
|
||||
- aw_apply_worktime_settings | default(false) | bool
|
||||
- (aw_server_always_active_pattern | default('') | string | length) > 0
|
||||
|
||||
- name: Применить landingpage профиля
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/landingpage"
|
||||
method: POST
|
||||
body: "\"{{ aw_server_landingpage }}\""
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
status_code: [200, 201]
|
||||
when:
|
||||
- aw_apply_worktime_settings | default(false) | bool
|
||||
- (aw_server_landingpage | default('') | string | length) > 0
|
||||
|
||||
handlers:
|
||||
- name: Перезагрузить systemd
|
||||
ansible.builtin.systemd:
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
||||
aw_windows_afk_enabled: true
|
||||
aw_windows_window_enabled: true
|
||||
aw_windows_file_ops_enabled: true
|
||||
aw_windows_local_agent_logs_enabled: false
|
||||
aw_windows_incident_capture_enabled: true
|
||||
aw_windows_incident_screenshot_enabled: true
|
||||
@@ -79,7 +78,6 @@
|
||||
- browser-domains-native-collector.ps1
|
||||
- dlp-endpoint-signals-collector.ps1
|
||||
- email-outbound-collector.ps1
|
||||
- file-operations-collector.ps1
|
||||
- worktime-session-collector.ps1
|
||||
- migrate-awatch-rus-paths.ps1
|
||||
- deploy-domain-users.ps1
|
||||
@@ -89,18 +87,6 @@
|
||||
- web-category-rules.example.json
|
||||
- dlp-policy.example.json
|
||||
|
||||
- name: Нормализовать кодировку PowerShell файлов (UTF-8 BOM для Windows PowerShell)
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$toolkitDir = "{{ aw_windows_deploy_root }}\windows"
|
||||
$encIn = New-Object System.Text.UTF8Encoding($false)
|
||||
$encOut = New-Object System.Text.UTF8Encoding($true)
|
||||
Get-ChildItem -LiteralPath $toolkitDir -File -Include *.ps1,*.psm1,*.psd1 | ForEach-Object {
|
||||
$text = [System.IO.File]::ReadAllText($_.FullName, $encIn)
|
||||
[System.IO.File]::WriteAllText($_.FullName, $text, $encOut)
|
||||
}
|
||||
|
||||
- name: Загрузить список пользователей для доменного развёртывания
|
||||
ansible.windows.win_copy:
|
||||
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
|
||||
@@ -145,7 +131,6 @@
|
||||
StateRoot = "{{ aw_windows_state_root }}"
|
||||
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
|
||||
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
|
||||
FileOpsEnabled = {{ '$true' if (aw_windows_file_ops_enabled | bool) else '$false' }}
|
||||
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
|
||||
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
|
||||
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
|
||||
@@ -165,19 +150,6 @@
|
||||
{% endif %}
|
||||
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
|
||||
|
||||
- name: Удалить лишние ActivityWatch Launch tasks вне текущего deployment-config
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$config = Get-Content -Raw -LiteralPath "{{ aw_windows_state_root }}\deployment-config.json" | ConvertFrom-Json
|
||||
$desired = @($config.userTasks | ForEach-Object { [string]$_.LaunchTaskName })
|
||||
foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch *' })) {
|
||||
if ($desired -notcontains [string]$task.TaskName) {
|
||||
Unregister-ScheduledTask -TaskName $task.TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
& cmd.exe /c "schtasks /Delete /TN `"$($task.TaskName)`" /F >nul 2>&1" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
- name: Принудительно запустить ActivityWatch recovery и launch tasks
|
||||
when: aw_windows_force_task_restart | bool
|
||||
ansible.windows.win_powershell:
|
||||
@@ -200,7 +172,6 @@
|
||||
when:
|
||||
- aw_windows_api_smoke_check_enabled | bool
|
||||
- aw_windows_afk_enabled | bool
|
||||
- aw_windows_hostname_result.stdout is defined
|
||||
ansible.builtin.set_fact:
|
||||
aw_windows_api_smoke_check_bucket_effective: >-
|
||||
{{
|
||||
@@ -209,51 +180,54 @@
|
||||
else 'aw-watcher-afk_' ~ (aw_windows_hostname_result.stdout | trim)
|
||||
}}
|
||||
|
||||
- name: Выполнить AW API smoke-check (проверка наличия свежих событий в AFK бакете)
|
||||
- name: Дождаться свежих AFK событий на AW server
|
||||
when:
|
||||
- aw_windows_api_smoke_check_enabled | bool
|
||||
- aw_windows_afk_enabled | bool
|
||||
delegate_to: localhost
|
||||
ansible.builtin.uri:
|
||||
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
|
||||
method: GET
|
||||
status_code: 200
|
||||
register: aw_windows_api_smoke_result
|
||||
until: aw_windows_api_smoke_result.json | length > 0
|
||||
retries: 5
|
||||
delay: 5
|
||||
ignore_errors: true
|
||||
return_content: true
|
||||
register: aw_windows_api_smoke
|
||||
until: >
|
||||
aw_windows_api_smoke.status == 200 and
|
||||
(aw_windows_api_smoke.json | length) > 0 and
|
||||
(
|
||||
aw_windows_api_smoke.json
|
||||
| selectattr('data.status', 'equalto', 'not-afk')
|
||||
| list
|
||||
| length
|
||||
) > 0
|
||||
retries: 10
|
||||
delay: 6
|
||||
|
||||
- name: Валидировать развёртывание на эндпоинте
|
||||
- name: Выполнить валидацию и сохранить отчёт на целевом Windows host
|
||||
ansible.windows.win_powershell:
|
||||
script: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$result = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
|
||||
$report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
|
||||
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
|
||||
$result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
|
||||
return $result
|
||||
$report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
|
||||
if ({{ '$true' if (aw_windows_fail_on_validation_error | bool) else '$false' }} -and -not [bool]$report.overallOk) {
|
||||
throw "Проверка развёртывания ActivityWatch завершилась ошибкой. Отчёт: {{ aw_windows_validation_remote_path }}"
|
||||
}
|
||||
|
||||
- name: Создать локальную директорию для отчётов валидации
|
||||
- name: Создать локальный каталог для validation reports
|
||||
ansible.builtin.file:
|
||||
path: "{{ aw_windows_validation_local_dir }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Стянуть отчёт валидации с эндпоинта
|
||||
- name: Забрать validation report
|
||||
ansible.builtin.fetch:
|
||||
src: "{{ aw_windows_validation_remote_path }}"
|
||||
dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
|
||||
flat: true
|
||||
|
||||
- name: Проверить статус валидации
|
||||
ansible.builtin.shell: |
|
||||
python3 - <<'PY'
|
||||
import json, sys
|
||||
with open('{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json', 'r') as f:
|
||||
data = json.load(f)
|
||||
if not data.get('overallOk', False):
|
||||
print(f"Validation failed for {{ inventory_hostname }}: {data.get('summary', 'Unknown error')}")
|
||||
sys.exit(1)
|
||||
PY
|
||||
delegate_to: localhost
|
||||
when: aw_windows_fail_on_validation_error | bool
|
||||
- name: Показать путь к отчёту
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Windows/RDP развёртывание завершено на {{ inventory_hostname }}."
|
||||
- "Отчёт проверки: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
|
||||
|
||||
@@ -4,7 +4,6 @@ aw_server_bind_host: "0.0.0.0"
|
||||
aw_server_port: 5600
|
||||
aw_server_webui_dir: "/opt/activitywatch/webui-ru"
|
||||
aw_server_data_dir: "/var/lib/activitywatch"
|
||||
aw_server_db_path: "/var/lib/activitywatch/.local/share/activitywatch/aw-server-rust/sqlite.db"
|
||||
aw_server_log_dir: "/var/log/activitywatch"
|
||||
aw_server_user: "activitywatch"
|
||||
aw_server_group: "activitywatch"
|
||||
@@ -13,17 +12,9 @@ aw_worktime_timezone: "Europe/Moscow"
|
||||
|
||||
aw_repo_root: "{{ playbook_dir | dirname }}"
|
||||
|
||||
# Применить базовые категории и views для рабочего времени через AW settings API.
|
||||
# При прод-обновлениях это нужно оставлять включённым, иначе UI остаётся без views/classes.
|
||||
aw_apply_worktime_settings: true
|
||||
|
||||
# Дополнительные origin для aw-server-rust CORS.
|
||||
# Обязательно включите тот origin, с которого реально открывается Web UI.
|
||||
aw_server_cors_origins:
|
||||
- "http://127.0.0.1:5600"
|
||||
- "http://localhost:5600"
|
||||
- "http://10.10.10.13:5600"
|
||||
- "http://aw-server:5600"
|
||||
# Опционально: применить базовые категории и views для рабочего времени через AW settings API.
|
||||
# Внимание: это перезаписывает существующие server-side settings/classes/views.
|
||||
aw_apply_worktime_settings: false
|
||||
|
||||
# Опциональные значения периода рабочего времени в Web UI.
|
||||
# startOfDay задаёт границу дня и стартовое время окна отчёта.
|
||||
@@ -33,5 +24,3 @@ aw_server_cors_origins:
|
||||
aw_worktime_from: "08:00"
|
||||
aw_worktime_to: "17:00"
|
||||
aw_worktime_start_of_day: "{{ aw_worktime_from }}"
|
||||
aw_server_always_active_pattern: "aw-watcher-window"
|
||||
aw_server_landingpage: "/activity/SHARKON2025/view/"
|
||||
|
||||
@@ -23,7 +23,6 @@ aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
|
||||
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
||||
aw_windows_afk_enabled: true
|
||||
aw_windows_window_enabled: true
|
||||
aw_windows_file_ops_enabled: true
|
||||
aw_windows_local_agent_logs_enabled: false
|
||||
aw_windows_incident_capture_enabled: true
|
||||
aw_windows_incident_screenshot_enabled: true
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
- activitywatch-server.service
|
||||
- aw-worktime-api.py
|
||||
- aw-worktime-api.service
|
||||
- aw-worktime-ui-bridge.py
|
||||
- aw-worktime-ui-bridge.service
|
||||
- aw-worktime-ui-bridge.timer
|
||||
- aw-worktime-panel.js
|
||||
- aw-server.env.example
|
||||
- aw-ru-patch.js
|
||||
|
||||
-3
@@ -11,9 +11,6 @@
|
||||
- activitywatch-server.service
|
||||
- aw-worktime-api.py
|
||||
- aw-worktime-api.service
|
||||
- aw-worktime-ui-bridge.py
|
||||
- aw-worktime-ui-bridge.service
|
||||
- aw-worktime-ui-bridge.timer
|
||||
- aw-worktime-panel.js
|
||||
- aw-server.env.example
|
||||
- aw-ru-patch.js
|
||||
|
||||
@@ -9,7 +9,7 @@ EnvironmentFile=/etc/activitywatch/aw-server.env
|
||||
User=__AW_SERVER_USER__
|
||||
Group=__AW_SERVER_GROUP__
|
||||
WorkingDirectory=__AW_SERVER_DATA_DIR__
|
||||
ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT" --dbpath "$AW_SERVER_DB_PATH" --webpath "$AW_SERVER_WEBUI_DIR"'
|
||||
ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT"'
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
StateDirectory=activitywatch
|
||||
@@ -17,7 +17,7 @@ LogsDirectory=activitywatch
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
ProtectHome=read-only
|
||||
ProtectHome=true
|
||||
LimitNOFILE=65535
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -29,21 +29,6 @@
|
||||
{ "label": "DLP", "type": "bucket", "bucket_prefix": "aw-dlp-endpoint-signals_" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "linux-remote",
|
||||
"name": "Linux remote workers",
|
||||
"description": "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.",
|
||||
"patterns": [
|
||||
"^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)"
|
||||
],
|
||||
"links": [
|
||||
{ "label": "Активность", "type": "activity" },
|
||||
{ "label": "SSH сессии", "type": "bucket", "bucket_prefix": "aw-ssh-sessions_" },
|
||||
{ "label": "Команды shell", "type": "bucket", "bucket_prefix": "aw-console-commands_" },
|
||||
{ "label": "Web категории", "type": "bucket", "bucket_prefix": "aw-detmir-web-category_" },
|
||||
{ "label": "Все бакеты", "type": "buckets" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "virtual-infra",
|
||||
"name": "Virtual servers + Proxmox",
|
||||
|
||||
@@ -370,16 +370,6 @@
|
||||
return /^pve[-_]/i.test(String(host || ""));
|
||||
}
|
||||
|
||||
function isLikelyClientHost(host) {
|
||||
const value = String(host || "").trim();
|
||||
if (!value) return false;
|
||||
if (/^(?:unknown|undefined|null)$/i.test(value)) return false;
|
||||
if (/^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1)$/i.test(value)) return false;
|
||||
if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)) return false;
|
||||
if (value.indexOf(":") !== -1 && /^[0-9a-f:\[\]]+$/i.test(value)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function enforceSafeActivityViewForPveHost() {
|
||||
const hash = window.location.hash || "";
|
||||
const match = hash.match(/^#\/activity\/([^/]+)\/day\/([^/]+)\/view\/([^/?#]+)/i);
|
||||
@@ -396,9 +386,9 @@
|
||||
|
||||
function getDlpHostFromSettings(settings) {
|
||||
const routeHost = getCurrentHostFromHash();
|
||||
if (isLikelyClientHost(routeHost)) return routeHost;
|
||||
if (routeHost) return routeHost;
|
||||
const bucketHost = getDlpHostFromBucketId(getDlpBucketIdFromHash());
|
||||
if (isLikelyClientHost(bucketHost)) return bucketHost;
|
||||
if (bucketHost) return bucketHost;
|
||||
return getTrendsHostFromSettings(settings);
|
||||
}
|
||||
|
||||
@@ -690,19 +680,6 @@
|
||||
{ label: "DLP", type: "bucket", bucket_prefix: "aw-dlp-endpoint-signals_" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "linux-remote",
|
||||
name: "Linux remote workers",
|
||||
description: "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.",
|
||||
patterns: ["^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)"],
|
||||
links: [
|
||||
{ label: "Активность", type: "activity" },
|
||||
{ label: "SSH сессии", type: "bucket", bucket_prefix: "aw-ssh-sessions_" },
|
||||
{ label: "Команды shell", type: "bucket", bucket_prefix: "aw-console-commands_" },
|
||||
{ label: "Web категории", type: "bucket", bucket_prefix: "aw-detmir-web-category_" },
|
||||
{ label: "Все бакеты", type: "buckets" }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "virtual-infra",
|
||||
name: "Virtual servers + Proxmox",
|
||||
@@ -763,15 +740,7 @@
|
||||
const prefixes = [
|
||||
"aw-watcher-window_",
|
||||
"aw-watcher-afk_",
|
||||
"aw-console-commands_",
|
||||
"aw-ssh-sessions_",
|
||||
"aw-linux-web-context_",
|
||||
"aw-detmir-web-category_",
|
||||
"aw-dlp-endpoint-signals_",
|
||||
"aw-session-events_",
|
||||
"aw-worktime-sessions_",
|
||||
"aw-pve-webadmin-events_",
|
||||
"aw-pve-task-events_",
|
||||
"aw-dlp-incidents_",
|
||||
"aw-pfsense-health_",
|
||||
"aw-pfsense-gateways_",
|
||||
@@ -801,27 +770,7 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
function hostHasBucketPrefix(hostBuckets, prefix) {
|
||||
return (hostBuckets || []).some(function (bucketId) {
|
||||
return String(bucketId || "").indexOf(prefix) === 0;
|
||||
});
|
||||
}
|
||||
|
||||
function matchHostGroup(host, groups, hostBuckets) {
|
||||
const bucketList = hostBuckets || [];
|
||||
if (hostHasBucketPrefix(bucketList, "aw-dlp-endpoint-signals_") || hostHasBucketPrefix(bucketList, "aw-session-events_")) {
|
||||
return "windows-rdp";
|
||||
}
|
||||
if (
|
||||
hostHasBucketPrefix(bucketList, "aw-console-commands_") ||
|
||||
hostHasBucketPrefix(bucketList, "aw-ssh-sessions_") ||
|
||||
hostHasBucketPrefix(bucketList, "aw-linux-web-context_") ||
|
||||
hostHasBucketPrefix(bucketList, "aw-detmir-web-category_")
|
||||
) {
|
||||
if (!hostHasBucketPrefix(bucketList, "aw-pve-webadmin-events_") && !hostHasBucketPrefix(bucketList, "aw-pve-task-events_")) {
|
||||
return "linux-remote";
|
||||
}
|
||||
}
|
||||
function matchHostGroup(host, groups) {
|
||||
for (const group of groups) {
|
||||
const patterns = Array.isArray(group.patterns) ? group.patterns : [];
|
||||
for (const pattern of patterns) {
|
||||
@@ -864,7 +813,7 @@
|
||||
grouped.set("__ungrouped__", []);
|
||||
|
||||
Array.from(hostBuckets.keys()).sort().forEach(function (host) {
|
||||
const groupId = matchHostGroup(host, groups, hostBuckets.get(host) || []) || "__ungrouped__";
|
||||
const groupId = matchHostGroup(host, groups) || "__ungrouped__";
|
||||
grouped.get(groupId).push(host);
|
||||
});
|
||||
|
||||
@@ -920,7 +869,7 @@
|
||||
center.setAttribute("data-aw-ru-host-groups", "1");
|
||||
center.innerHTML =
|
||||
'<h4>Разделы хостов</h4>' +
|
||||
'<p>Здесь хосты разделены на Windows RDP, Linux remote workers и инфраструктурные узлы.</p>' +
|
||||
'<p>Здесь хосты разделены на пользовательские Windows RDP и инфраструктурные виртуальные серверы/Proxmox.</p>' +
|
||||
'<div class="aw-ru-host-groups-grid" data-aw-ru-host-groups-grid><section class="aw-ru-host-group-card"><p>Загрузка...</p></section></div>';
|
||||
heading.parentElement.insertBefore(center, heading.nextSibling);
|
||||
}
|
||||
@@ -1476,8 +1425,7 @@
|
||||
if (!settings || typeof settings !== "object") return "";
|
||||
const landingpage = typeof settings.landingpage === "string" ? settings.landingpage : "";
|
||||
const match = landingpage.match(/\/activity\/([^/]+)/);
|
||||
const host = match && match[1] ? decodeURIComponent(match[1]) : "";
|
||||
return isLikelyClientHost(host) ? host : "";
|
||||
return match && match[1] ? match[1] : "";
|
||||
}
|
||||
|
||||
function getTrendsPath(hash) {
|
||||
@@ -1544,7 +1492,8 @@
|
||||
.map(function (bucketId) { return bucketId.replace(/^aw-watcher-window_/i, ""); })
|
||||
.filter(Boolean)
|
||||
.filter(function (host) { return !/^unknown$/i.test(host); });
|
||||
if (isLikelyClientHost(settingsHost) && hosts.indexOf(settingsHost) >= 0) return settingsHost;
|
||||
if (settingsHost && hosts.indexOf(settingsHost) >= 0) return settingsHost;
|
||||
if (settingsHost) return settingsHost;
|
||||
hosts.sort();
|
||||
return hosts[0] || "";
|
||||
}
|
||||
@@ -1584,8 +1533,7 @@
|
||||
window.fetch = function (input, init) {
|
||||
try {
|
||||
const url = typeof input === "string" ? input : String(input && input.url || "");
|
||||
const isCategoryBuilderRoute = /^#\/settings\/category-builder(?:[/?#]|$)/i.test(window.location.hash || "");
|
||||
if (isCategoryBuilderRoute && /\/api\/0\/query\/?$/i.test(url) && init && typeof init.body === "string") {
|
||||
if (/\/api\/0\/query\/?$/i.test(url) && init && typeof init.body === "string") {
|
||||
init = Object.assign({}, init, {
|
||||
body: rewriteUnknownCategoryBuilderQueryBody(init.body)
|
||||
});
|
||||
@@ -1609,8 +1557,7 @@
|
||||
proto.send = function (body) {
|
||||
try {
|
||||
const url = String(this.__awRuUrl || "");
|
||||
const isCategoryBuilderRoute = /^#\/settings\/category-builder(?:[/?#]|$)/i.test(window.location.hash || "");
|
||||
if (isCategoryBuilderRoute && /\/api\/0\/query\/?$/i.test(url) && typeof body === "string") {
|
||||
if (/\/api\/0\/query\/?$/i.test(url) && typeof body === "string") {
|
||||
body = rewriteUnknownCategoryBuilderQueryBody(body);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -26,9 +26,6 @@ VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json"
|
||||
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
|
||||
WORKTIME_API_SRC="$BOOTSTRAP_DIR/aw-worktime-api.py"
|
||||
WORKTIME_API_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-api.service"
|
||||
WORKTIME_UI_BRIDGE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.py"
|
||||
WORKTIME_UI_BRIDGE_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.service"
|
||||
WORKTIME_UI_BRIDGE_TIMER_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.timer"
|
||||
|
||||
for var_name in "${required_vars[@]}"; do
|
||||
if [[ -z "${!var_name:-}" ]]; then
|
||||
@@ -106,24 +103,6 @@ if [[ -f "$WORKTIME_API_SERVICE_SRC" ]]; then
|
||||
systemctl --no-pager --full status aw-worktime-api.service || true
|
||||
fi
|
||||
|
||||
if [[ -f "$WORKTIME_UI_BRIDGE_SRC" ]]; then
|
||||
install -m 0755 "$WORKTIME_UI_BRIDGE_SRC" /usr/local/bin/aw-worktime-ui-bridge.py
|
||||
fi
|
||||
|
||||
if [[ -f "$WORKTIME_UI_BRIDGE_SERVICE_SRC" ]]; then
|
||||
install -m 0644 "$WORKTIME_UI_BRIDGE_SERVICE_SRC" /etc/systemd/system/aw-worktime-ui-bridge.service
|
||||
fi
|
||||
|
||||
if [[ -f "$WORKTIME_UI_BRIDGE_TIMER_SRC" ]]; then
|
||||
install -m 0644 "$WORKTIME_UI_BRIDGE_TIMER_SRC" /etc/systemd/system/aw-worktime-ui-bridge.timer
|
||||
systemctl daemon-reload
|
||||
systemctl disable --now aw-worktime-afk-bridge.timer >/dev/null 2>&1 || true
|
||||
systemctl enable aw-worktime-ui-bridge.timer
|
||||
systemctl restart aw-worktime-ui-bridge.timer
|
||||
systemctl start aw-worktime-ui-bridge.service || true
|
||||
systemctl --no-pager --full status aw-worktime-ui-bridge.timer || true
|
||||
fi
|
||||
|
||||
for _ in $(seq 1 20); do
|
||||
if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then
|
||||
break
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"name": ["Работа", "Документы"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64|libreoffice|writer|calc)\\.exe\\b|LibreOffice|OnlyOffice|Adobe Reader|Acrobat",
|
||||
"regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64)\\.exe\\b|Adobe Reader|Acrobat",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": { "color": "#2E7D32" }
|
||||
@@ -40,7 +40,7 @@
|
||||
"name": ["Работа", "Администрирование"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd|gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\.exe\\b|\\b(gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\b|Proxmox Virtual Environment|\\bpfSense\\b|\\bGrafana\\b|\\bKibana\\b|\\bPortainer\\b",
|
||||
"regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd)\\.exe\\b",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": { "color": "#6D4C41" }
|
||||
@@ -56,7 +56,7 @@
|
||||
"name": ["Интернет", "Браузер"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser|chromium)\\.exe\\b|\\b(chrome|chromium|firefox|opera|brave|vivaldi)\\b",
|
||||
"regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser)\\.exe\\b",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": { "color": "#00897B" }
|
||||
@@ -82,7 +82,7 @@
|
||||
"name": ["ActivityWatch"],
|
||||
"rule": {
|
||||
"type": "regex",
|
||||
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b|\\baw-(watcher|qt)\\b",
|
||||
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b",
|
||||
"ignore_case": true
|
||||
},
|
||||
"data": {}
|
||||
|
||||
@@ -306,9 +306,6 @@ function Copy-ActivityWatchCollectorAssets {
|
||||
$resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop
|
||||
Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force
|
||||
}
|
||||
else {
|
||||
Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force
|
||||
}
|
||||
|
||||
if ($CustomPolicySource) {
|
||||
$resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop
|
||||
@@ -537,7 +534,11 @@ function Get-CollectorPowerShellProcessCount {
|
||||
function New-LaunchLock {
|
||||
param([string]`$StateRoot, [int]`$SessionId)
|
||||
|
||||
`$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId)
|
||||
if (-not (Test-Path -LiteralPath `$StateRoot)) {
|
||||
New-Item -Path `$StateRoot -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
`$lockPath = Join-Path `$StateRoot ("launch-watchers-session-{0}.lock" -f `$SessionId)
|
||||
if (Test-Path -LiteralPath `$lockPath) {
|
||||
try {
|
||||
`$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json
|
||||
@@ -730,11 +731,13 @@ function Start-CollectorScriptIfNeeded {
|
||||
return
|
||||
}
|
||||
|
||||
`$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null }
|
||||
`$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass')
|
||||
if (`$staParam) { `$argumentList += `$staParam }
|
||||
`$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath)
|
||||
Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden
|
||||
Start-Process -FilePath `$PowerShellExe -ArgumentList @(
|
||||
'-NoProfile',
|
||||
'-WindowStyle', 'Hidden',
|
||||
'-ExecutionPolicy', 'Bypass',
|
||||
'-File', `$ScriptPath,
|
||||
'-ConfigPath', `$ConfigPath
|
||||
) -WindowStyle Hidden
|
||||
}
|
||||
|
||||
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
||||
|
||||
+5
-3354
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ServerHost,
|
||||
@@ -112,8 +112,8 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||
|
||||
Write-Output 'ActivityWatch развёрнут для пользователей:'
|
||||
$targetUsers | ForEach-Object { Write-Output " - $_" }
|
||||
Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||
Write-Output "Каталог данных: $StateRoot"
|
||||
Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||
Write-Host 'ActivityWatch развёрнут для пользователей:'
|
||||
$targetUsers | ForEach-Object { Write-Host " - $_" }
|
||||
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||
Write-Host "Каталог данных: $StateRoot"
|
||||
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ServerHost,
|
||||
@@ -136,6 +136,6 @@ if ($reportDirectory) {
|
||||
|
||||
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8
|
||||
|
||||
Write-Output 'Комплексное развёртывание ActivityWatch завершено.'
|
||||
Write-Output "Пользователи: $($resolvedUsers -join ', ')"
|
||||
Write-Output "Отчёт: $effectiveReportPath"
|
||||
Write-Host 'Комплексное развёртывание ActivityWatch завершено.'
|
||||
Write-Host "Пользователи: $($resolvedUsers -join ', ')"
|
||||
Write-Host "Отчёт: $effectiveReportPath"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ServerHost,
|
||||
@@ -104,9 +104,9 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||
|
||||
Write-Output "ActivityWatch развёрнут для пользователя: $TargetUser"
|
||||
Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||
Write-Output "Каталог установки: $InstallRoot"
|
||||
Write-Output "Каталог данных: $StateRoot"
|
||||
Write-Output "Файл правил: $($assetResult.ActiveRules)"
|
||||
Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||
Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser"
|
||||
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||
Write-Host "Каталог установки: $InstallRoot"
|
||||
Write-Host "Каталог данных: $StateRoot"
|
||||
Write-Host "Файл правил: $($assetResult.ActiveRules)"
|
||||
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$ServerHost,
|
||||
@@ -151,6 +151,6 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||
|
||||
Write-Output 'Укрепление и восстановление ActivityWatch завершены.'
|
||||
Write-Output "Конфигурация: $effectiveConfigPath"
|
||||
Write-Output "Пользователи восстановлены: $($effectiveUsers -join ', ')"
|
||||
Write-Host 'Укрепление и восстановление ActivityWatch завершены.'
|
||||
Write-Host "Конфигурация: $effectiveConfigPath"
|
||||
Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2',
|
||||
[string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2',
|
||||
@@ -154,36 +154,7 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Миграция ActivityWatch W
|
||||
@{ Source = $NewStateRoot; Name = 'new-state' }
|
||||
)) {
|
||||
if (Test-Path -LiteralPath $item.Source) {
|
||||
$backupDest = Join-Path $backupRoot $item.Name
|
||||
New-ActivityWatchDirectory -Path $backupDest
|
||||
|
||||
$excludeDirs = @()
|
||||
if ($item.Source -eq $NewStateRoot) {
|
||||
# Avoid infinite recursion: backupRoot is inside NewStateRoot by default.
|
||||
$excludeDirs += $backupRoot
|
||||
}
|
||||
|
||||
$robocopyArgs = @(
|
||||
$item.Source,
|
||||
$backupDest,
|
||||
'/E',
|
||||
'/R:1',
|
||||
'/W:1',
|
||||
'/NFL',
|
||||
'/NDL',
|
||||
'/NJH',
|
||||
'/NJS',
|
||||
'/NP'
|
||||
)
|
||||
if ($excludeDirs.Count -gt 0) {
|
||||
$robocopyArgs += '/XD'
|
||||
$robocopyArgs += $excludeDirs
|
||||
}
|
||||
|
||||
& robocopy @robocopyArgs | Out-Null
|
||||
if ($LASTEXITCODE -ge 8) {
|
||||
throw "Backup robocopy failed (exit=$LASTEXITCODE) for source '$($item.Source)' to '$backupDest'"
|
||||
}
|
||||
Copy-Item -LiteralPath $item.Source -Destination (Join-Path $backupRoot $item.Name) -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json'
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -13,26 +13,49 @@ $config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
||||
$installRoot = [string]$config.paths.installRoot
|
||||
$stateRoot = [string]$config.paths.stateRoot
|
||||
$collectorScript = [string]$config.paths.collectorScript
|
||||
$endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' }
|
||||
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
|
||||
$rulesPath = [string]$config.paths.rulesPath
|
||||
$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' }
|
||||
$launchScript = [string]$config.paths.launchScript
|
||||
$recoveryScript = [string]$config.paths.recoveryScript
|
||||
|
||||
$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true }
|
||||
$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true }
|
||||
$requiredFiles = @(
|
||||
(Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'),
|
||||
(Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'),
|
||||
$collectorScript,
|
||||
$endpointCollectorScript,
|
||||
$sessionCollectorScript,
|
||||
$rulesPath,
|
||||
$policyPath,
|
||||
$launchScript,
|
||||
$recoveryScript,
|
||||
$ConfigPath
|
||||
)
|
||||
if ($afkExpected) {
|
||||
$requiredFiles += (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe')
|
||||
}
|
||||
if ($windowExpected) {
|
||||
$requiredFiles += (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe')
|
||||
}
|
||||
|
||||
$missingFiles = @(
|
||||
$requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) }
|
||||
)
|
||||
|
||||
$processNames = @('aw-watcher-afk', 'aw-watcher-window')
|
||||
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
|
||||
$processNames = @()
|
||||
if ($afkExpected) { $processNames += 'aw-watcher-afk' }
|
||||
if ($windowExpected) { $processNames += 'aw-watcher-window' }
|
||||
$runningProcesses = @()
|
||||
if ($processNames.Count -gt 0) {
|
||||
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
|
||||
}
|
||||
$sessionCollectorProcesses = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and
|
||||
$_.CommandLine -match [Regex]::Escape($sessionCollectorScript)
|
||||
} |
|
||||
Select-Object Name, ProcessId, SessionId, CommandLine
|
||||
|
||||
$taskNames = @()
|
||||
if ($config.userTasks) {
|
||||
@@ -42,7 +65,7 @@ $taskNames += [string]$config.recovery.taskName
|
||||
$taskNames = $taskNames | Sort-Object -Unique
|
||||
|
||||
$tasks = foreach ($taskName in $taskNames) {
|
||||
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
$task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1
|
||||
if ($task) {
|
||||
[pscustomobject]@{
|
||||
taskName = $task.TaskName
|
||||
@@ -53,7 +76,7 @@ $tasks = foreach ($taskName in $taskNames) {
|
||||
else {
|
||||
[pscustomobject]@{
|
||||
taskName = $taskName
|
||||
state = 'Missing'
|
||||
state = 'Отсутствует'
|
||||
present = $false
|
||||
}
|
||||
}
|
||||
@@ -76,8 +99,16 @@ $result = [ordered]@{
|
||||
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present }))
|
||||
}
|
||||
processes = [ordered]@{
|
||||
expected = $processNames
|
||||
list = @($runningProcesses)
|
||||
ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2)
|
||||
sessionCollectors = @($sessionCollectorProcesses)
|
||||
ok = [bool](
|
||||
(
|
||||
($processNames.Count -eq 0) -or
|
||||
(($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge $processNames.Count)
|
||||
) -and
|
||||
($sessionCollectorProcesses.Count -ge 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,4 @@
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$Hostname,
|
||||
[int]$PollSeconds = 30
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-Config {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "Конфигурация не найдена: $Path"
|
||||
}
|
||||
|
||||
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json
|
||||
)
|
||||
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||
}
|
||||
|
||||
function Ensure-Bucket {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||
)
|
||||
|
||||
try {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
return
|
||||
}
|
||||
catch { Write-Error param(
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$Hostname,
|
||||
[int]$PollSeconds = 30
|
||||
@@ -87,12 +47,7 @@ function Ensure-Bucket {
|
||||
hostname = $HostnameValue
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||
}
|
||||
catch {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
}
|
||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||
}
|
||||
|
||||
function Get-SessionRecords {
|
||||
@@ -204,458 +159,3 @@ while ($true) {
|
||||
|
||||
Start-Sleep -Seconds $sleepSec
|
||||
}
|
||||
; }
|
||||
|
||||
$body = @{
|
||||
client = 'aw-worktime-session-collector'
|
||||
type = 'aw.worktime.session'
|
||||
hostname = $HostnameValue
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||
}
|
||||
catch {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SessionRecords {
|
||||
$records = @()
|
||||
|
||||
try {
|
||||
$lines = quser 2>$null
|
||||
if (-not $lines) {
|
||||
return @()
|
||||
}
|
||||
|
||||
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||
if (-not $clean) {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $clean -split '\s+'
|
||||
if ($parts.Count -lt 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
$sessionName = ''
|
||||
$sessionIdIndex = 2
|
||||
if ($parts[1] -match '^\d+$') {
|
||||
$sessionIdIndex = 1
|
||||
}
|
||||
else {
|
||||
$sessionName = $parts[1]
|
||||
}
|
||||
|
||||
$sessionId = 0
|
||||
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||
$sessionId = [int]$parts[$sessionIdIndex]
|
||||
}
|
||||
|
||||
$records += [pscustomobject]@{
|
||||
username = $parts[0]
|
||||
sessionName = $sessionName
|
||||
sessionId = $sessionId
|
||||
state = $parts[$sessionIdIndex + 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { Write-Error param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$Hostname,
|
||||
[int]$PollSeconds = 30
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-Config {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "Конфигурация не найдена: $Path"
|
||||
}
|
||||
|
||||
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json
|
||||
)
|
||||
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||
}
|
||||
|
||||
function Ensure-Bucket {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||
)
|
||||
|
||||
try {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
return
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
$body = @{
|
||||
client = 'aw-worktime-session-collector'
|
||||
type = 'aw.worktime.session'
|
||||
hostname = $HostnameValue
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||
}
|
||||
catch {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SessionRecords {
|
||||
$records = @()
|
||||
|
||||
try {
|
||||
$lines = quser 2>$null
|
||||
if (-not $lines) {
|
||||
return @()
|
||||
}
|
||||
|
||||
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||
if (-not $clean) {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $clean -split '\s+'
|
||||
if ($parts.Count -lt 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
$sessionName = ''
|
||||
$sessionIdIndex = 2
|
||||
if ($parts[1] -match '^\d+$') {
|
||||
$sessionIdIndex = 1
|
||||
}
|
||||
else {
|
||||
$sessionName = $parts[1]
|
||||
}
|
||||
|
||||
$sessionId = 0
|
||||
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||
$sessionId = [int]$parts[$sessionIdIndex]
|
||||
}
|
||||
|
||||
$records += [pscustomobject]@{
|
||||
username = $parts[0]
|
||||
sessionName = $sessionName
|
||||
sessionId = $sessionId
|
||||
state = $parts[$sessionIdIndex + 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
return $records
|
||||
}
|
||||
|
||||
function Test-SessionIsActive {
|
||||
param([AllowNull()][string]$State)
|
||||
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||
$s = $State.Trim().ToLowerInvariant()
|
||||
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||
}
|
||||
|
||||
$cfg = Get-Config -Path $ConfigPath
|
||||
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||
$pulse = 120
|
||||
$sleepSec = if ($PollSeconds -gt 0) {
|
||||
$PollSeconds
|
||||
}
|
||||
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||
[int]$cfg.collector.pollSeconds
|
||||
}
|
||||
else {
|
||||
30
|
||||
}
|
||||
|
||||
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||
|
||||
while ($true) {
|
||||
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
$records = Get-SessionRecords
|
||||
if (-not $records -or $records.Count -eq 0) {
|
||||
$records = @([pscustomobject]@{
|
||||
username = $env:USERNAME
|
||||
sessionName = ''
|
||||
sessionId = (Get-Process -Id $PID).SessionId
|
||||
state = 'Unknown'
|
||||
})
|
||||
}
|
||||
|
||||
foreach ($rec in $records) {
|
||||
$payload = @{
|
||||
timestamp = $now
|
||||
duration = 0
|
||||
data = @{
|
||||
username = [string]$rec.username
|
||||
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||
sessionId = [int]$rec.sessionId
|
||||
sessionName = [string]$rec.sessionName
|
||||
state = [string]$rec.state
|
||||
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||
hostname = $hostValue
|
||||
source = 'worktime-session-collector'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleepSec
|
||||
}
|
||||
; }
|
||||
|
||||
return $records
|
||||
}
|
||||
|
||||
function Test-SessionIsActive {
|
||||
param([AllowNull()][string]$State)
|
||||
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||
$s = $State.Trim().ToLowerInvariant()
|
||||
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||
}
|
||||
|
||||
$cfg = Get-Config -Path $ConfigPath
|
||||
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||
$pulse = 120
|
||||
$sleepSec = if ($PollSeconds -gt 0) {
|
||||
$PollSeconds
|
||||
}
|
||||
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||
[int]$cfg.collector.pollSeconds
|
||||
}
|
||||
else {
|
||||
30
|
||||
}
|
||||
|
||||
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||
|
||||
while ($true) {
|
||||
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
$records = Get-SessionRecords
|
||||
if (-not $records -or $records.Count -eq 0) {
|
||||
$records = @([pscustomobject]@{
|
||||
username = $env:USERNAME
|
||||
sessionName = ''
|
||||
sessionId = (Get-Process -Id $PID).SessionId
|
||||
state = 'Unknown'
|
||||
})
|
||||
}
|
||||
|
||||
foreach ($rec in $records) {
|
||||
$payload = @{
|
||||
timestamp = $now
|
||||
duration = 0
|
||||
data = @{
|
||||
username = [string]$rec.username
|
||||
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||
sessionId = [int]$rec.sessionId
|
||||
sessionName = [string]$rec.sessionName
|
||||
state = [string]$rec.state
|
||||
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||
hostname = $hostValue
|
||||
source = 'worktime-session-collector'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||
}
|
||||
catch { Write-Error param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$Hostname,
|
||||
[int]$PollSeconds = 30
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-Config {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "Конфигурация не найдена: $Path"
|
||||
}
|
||||
|
||||
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json
|
||||
)
|
||||
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||
}
|
||||
|
||||
function Ensure-Bucket {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||
)
|
||||
|
||||
try {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
return
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
$body = @{
|
||||
client = 'aw-worktime-session-collector'
|
||||
type = 'aw.worktime.session'
|
||||
hostname = $HostnameValue
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||
}
|
||||
catch {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SessionRecords {
|
||||
$records = @()
|
||||
|
||||
try {
|
||||
$lines = quser 2>$null
|
||||
if (-not $lines) {
|
||||
return @()
|
||||
}
|
||||
|
||||
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||
if (-not $clean) {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $clean -split '\s+'
|
||||
if ($parts.Count -lt 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
$sessionName = ''
|
||||
$sessionIdIndex = 2
|
||||
if ($parts[1] -match '^\d+$') {
|
||||
$sessionIdIndex = 1
|
||||
}
|
||||
else {
|
||||
$sessionName = $parts[1]
|
||||
}
|
||||
|
||||
$sessionId = 0
|
||||
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||
$sessionId = [int]$parts[$sessionIdIndex]
|
||||
}
|
||||
|
||||
$records += [pscustomobject]@{
|
||||
username = $parts[0]
|
||||
sessionName = $sessionName
|
||||
sessionId = $sessionId
|
||||
state = $parts[$sessionIdIndex + 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
return $records
|
||||
}
|
||||
|
||||
function Test-SessionIsActive {
|
||||
param([AllowNull()][string]$State)
|
||||
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||
$s = $State.Trim().ToLowerInvariant()
|
||||
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||
}
|
||||
|
||||
$cfg = Get-Config -Path $ConfigPath
|
||||
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||
$pulse = 120
|
||||
$sleepSec = if ($PollSeconds -gt 0) {
|
||||
$PollSeconds
|
||||
}
|
||||
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||
[int]$cfg.collector.pollSeconds
|
||||
}
|
||||
else {
|
||||
30
|
||||
}
|
||||
|
||||
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||
|
||||
while ($true) {
|
||||
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
$records = Get-SessionRecords
|
||||
if (-not $records -or $records.Count -eq 0) {
|
||||
$records = @([pscustomobject]@{
|
||||
username = $env:USERNAME
|
||||
sessionName = ''
|
||||
sessionId = (Get-Process -Id $PID).SessionId
|
||||
state = 'Unknown'
|
||||
})
|
||||
}
|
||||
|
||||
foreach ($rec in $records) {
|
||||
$payload = @{
|
||||
timestamp = $now
|
||||
duration = 0
|
||||
data = @{
|
||||
username = [string]$rec.username
|
||||
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||
sessionId = [int]$rec.sessionId
|
||||
sessionName = [string]$rec.sessionName
|
||||
state = [string]$rec.state
|
||||
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||
hostname = $hostValue
|
||||
source = 'worktime-session-collector'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleepSec
|
||||
}
|
||||
; }
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleepSec
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/bin/sh
|
||||
# shellcheck disable=SC1007
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/bin/sh
|
||||
# shellcheck disable=SC1007
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
@@ -18,12 +18,7 @@ JsonScalar: TypeAlias = str | int | float | bool | None
|
||||
JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]
|
||||
|
||||
|
||||
DEFAULT_BUCKET_PREFIXES = (
|
||||
"aw-file-operations_",
|
||||
"aw-dlp-incidents_",
|
||||
"aw-dlp-endpoint-signals_",
|
||||
"aw-email-monitor_",
|
||||
)
|
||||
DEFAULT_BUCKET_PREFIXES = ("aw-file-operations_", "aw-dlp-incidents_")
|
||||
DEFAULT_SQLITE_PATH = "data/dlp-events.sqlite3"
|
||||
EVENT_COLUMNS = (
|
||||
"bucket_id",
|
||||
@@ -139,10 +134,6 @@ def bucket_stream_type(bucket: Bucket) -> str | None:
|
||||
return "file_operation"
|
||||
if bucket.id.startswith("aw-dlp-incidents_") or bucket.type == "aw.dlp.incident":
|
||||
return "dlp_incident"
|
||||
if bucket.id.startswith("aw-dlp-endpoint-signals_") or bucket.type == "aw.dlp.endpoint.signal":
|
||||
return "dlp_endpoint_signal"
|
||||
if bucket.id.startswith("aw-email-monitor_") or bucket.type == "aw.email.signal":
|
||||
return "email_monitor"
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -4,26 +4,11 @@ set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
KIT_DIR="install-kit-awindows-20260427-211240"
|
||||
|
||||
PY_BIN="${PY_BIN:-}"
|
||||
if [[ -z "$PY_BIN" ]]; then
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
PY_BIN="python3"
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
PY_BIN="python"
|
||||
else
|
||||
echo "ERROR: python3/python not found"
|
||||
exit 127
|
||||
fi
|
||||
fi
|
||||
|
||||
"$PY_BIN" - <<'PY'
|
||||
python - <<'PY'
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
root=Path('.')
|
||||
kit=Path('install-kit-awindows-20260427-211240')
|
||||
@@ -39,13 +24,6 @@ missing_in_repo=[]
|
||||
|
||||
for kp in sorted(p for p in kit.rglob('*') if p.is_file() and p.name!='MANIFEST.txt'):
|
||||
rel=kp.relative_to(kit)
|
||||
rel_s=str(rel)
|
||||
if rel_s.startswith("server-configs-192.168.100.21/"):
|
||||
continue
|
||||
if rel_s == "README-INSTALL-KIT.txt":
|
||||
continue
|
||||
if "__pycache__" in kp.parts or kp.suffix == ".pyc":
|
||||
continue
|
||||
rp=root/rel
|
||||
if not rp.exists():
|
||||
missing_in_repo.append(str(rel))
|
||||
@@ -72,9 +50,4 @@ if ps_mismatches:
|
||||
print('--- PowerShell mismatches ---')
|
||||
for p in ps_mismatches:
|
||||
print(p)
|
||||
|
||||
strict = os.getenv("ALLOW_KIT_DRIFT", "").lower() not in {"1", "true", "yes"}
|
||||
if strict and (missing_in_repo or mismatches):
|
||||
print("ERROR: install-kit drift detected. Set ALLOW_KIT_DRIFT=1 to bypass.")
|
||||
sys.exit(1)
|
||||
PY
|
||||
|
||||
@@ -49,7 +49,6 @@ while [ "$#" -gt 0 ]; do
|
||||
esac
|
||||
done
|
||||
|
||||
# shellcheck disable=SC1007
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
sh "${SCRIPT_DIR}/install_aw_linux_client.sh" \
|
||||
|
||||
+2
-14
@@ -21,11 +21,9 @@ prompt_secret() {
|
||||
if [[ -n "${!var_name:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
local _val
|
||||
read -r -s -p "${prompt}: " _val
|
||||
read -r -s -p "${prompt}: " "$var_name"
|
||||
echo
|
||||
printf -v "$var_name" '%s' "$_val"
|
||||
declare -gx "$var_name"
|
||||
export "$var_name"
|
||||
}
|
||||
|
||||
require_cmd git
|
||||
@@ -35,12 +33,6 @@ require_cmd ansible
|
||||
log "Repo: ${ROOT_DIR}"
|
||||
log "Branch: $(git branch --show-current)"
|
||||
|
||||
if [[ "${AW_MAINTENANCE_ACK:-}" != "YES" ]]; then
|
||||
log "ERROR: maintenance window is required."
|
||||
log "Set AW_MAINTENANCE_ACK=YES to proceed."
|
||||
exit 4
|
||||
fi
|
||||
|
||||
log "Running local quality gate..."
|
||||
./scripts/quality-gate.sh | tee -a "${LOG_DIR}/quality-gate.log"
|
||||
|
||||
@@ -73,10 +65,6 @@ log "Preflight connectivity..."
|
||||
ansible -i ansible/inventory.ini aw_server -m ping | tee -a "${LOG_DIR}/ping_aw_server.log"
|
||||
ansible -i ansible/inventory.ini aw_windows -m win_ping | tee -a "${LOG_DIR}/ping_aw_windows.log"
|
||||
|
||||
log "Preflight ActivityWatch API/data checks..."
|
||||
./check-aw-data.sh | tee -a "${LOG_DIR}/check_aw_data.log"
|
||||
./check-aw-full.sh | tee -a "${LOG_DIR}/check_aw_full.log"
|
||||
|
||||
log "Dry-run aw_server..."
|
||||
ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_server.yml --check --diff | tee -a "${LOG_DIR}/check_aw_server.log"
|
||||
|
||||
|
||||
+6
-15
@@ -4,42 +4,33 @@ set -euo pipefail
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
echo "[1/6] Bash syntax check"
|
||||
echo "[1/4] Bash syntax check"
|
||||
find aw-server proxmox -type f -name "*.sh" -print0 | xargs -0 -r -n1 bash -n
|
||||
|
||||
echo "[2/6] Shellcheck (if available)"
|
||||
echo "[2/4] Shellcheck (if available)"
|
||||
if command -v shellcheck >/dev/null 2>&1; then
|
||||
find aw-server proxmox -type f -name "*.sh" -print0 | xargs -0 -r shellcheck -e SC1007,SC1090,SC2016
|
||||
else
|
||||
echo "shellcheck not found, skipping."
|
||||
fi
|
||||
|
||||
echo "[3/6] PowerShell parse check (if pwsh available)"
|
||||
echo "[3/4] PowerShell parse check (if pwsh available)"
|
||||
if command -v pwsh >/dev/null 2>&1; then
|
||||
if ! pwsh -NoLogo -NoProfile -Command '
|
||||
pwsh -NoLogo -NoProfile -Command '
|
||||
$ErrorActionPreference = "Stop"
|
||||
Get-ChildItem windows -Filter *.ps1 | ForEach-Object {
|
||||
[void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName,[ref]$null,[ref]$null)
|
||||
}
|
||||
[void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path "windows/ActivityWatch.Windows.Common.psm1"),[ref]$null,[ref]$null)
|
||||
[void][System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path "windows/ActivityWatch.Windows.Common.psd1"),[ref]$null,[ref]$null)
|
||||
'; then
|
||||
echo "pwsh parse check failed due runtime environment; skipping."
|
||||
fi
|
||||
'
|
||||
else
|
||||
echo "pwsh not found, skipping."
|
||||
fi
|
||||
|
||||
echo "[4/6] Install-kit consistency check"
|
||||
./scripts/check_install_kit_vs_repo.sh
|
||||
|
||||
echo "[5/6] Generated-artifacts guard"
|
||||
if git status --short | grep -E '^(\\?\\?| M|M ) (\\.graphify_|graphify-out/|reports/|tmp/|data/)'; then
|
||||
echo "ERROR: generated artifacts detected in working tree. Clean or ignore them before rollout."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[6/6] Ansible syntax check (if ansible-playbook available)"
|
||||
echo "[4/4] Ansible syntax check (if ansible-playbook available)"
|
||||
if command -v ansible-playbook >/dev/null 2>&1; then
|
||||
for playbook in ansible/*.yml; do
|
||||
ansible-playbook --syntax-check "$playbook" -i ansible/inventory.example.ini >/dev/null
|
||||
|
||||
@@ -6,10 +6,7 @@ cd "$ROOT_DIR"
|
||||
|
||||
KIT_DIR="install-kit-awindows-20260427-211240"
|
||||
MANIFEST="$KIT_DIR/MANIFEST.txt"
|
||||
# ZIP/TAR variables are declared for archive checks in this script; keep them for clarity
|
||||
# shellcheck disable=SC2034
|
||||
ZIP_ARCHIVE="install-kit-awindows-20260427-211240.zip"
|
||||
# shellcheck disable=SC2034
|
||||
TAR_ARCHIVE="install-kit-awindows-20260427-211240.tar.gz"
|
||||
|
||||
required_files=(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$ServerHost,
|
||||
@@ -59,7 +59,6 @@ $resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $P
|
||||
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\AWatch-rus\logs' }
|
||||
$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) }
|
||||
$resolvedIncidentLogPath = if ($IncidentLogPath) { $IncidentLogPath } else { Join-Path $resolvedLogsRoot ("dlp-incidents-{0}.log" -f $env:USERNAME) }
|
||||
$resolvedHealthPath = Join-Path $resolvedLogsRoot ("health-browser-domains-{0}.json" -f $env:USERNAME)
|
||||
$resolvedLocalAgentLogsEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'logging' -and $deploymentConfig.logging.PSObject.Properties.Name -contains 'localAgentLogsEnabled') { [bool]$deploymentConfig.logging.localAgentLogsEnabled } else { $true }
|
||||
$resolvedIncidentArtifactsRoot = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { [string]$deploymentConfig.incidentCapture.artifactsRoot } else { Join-Path $env:LOCALAPPDATA 'AWatch-rus\\incident-artifacts' }
|
||||
$resolvedIncidentScreenshotEnabled = if ($deploymentConfig -and $deploymentConfig.PSObject.Properties.Name -contains 'incidentCapture' -and $deploymentConfig.incidentCapture.PSObject.Properties.Name -contains 'screenshotEnabled') { [bool]$deploymentConfig.incidentCapture.screenshotEnabled } else { $true }
|
||||
@@ -87,7 +86,6 @@ $script:DlpDefaults = [ordered]@{
|
||||
action = 'log'
|
||||
severity = 'low'
|
||||
}
|
||||
$script:HealthPath = $resolvedHealthPath
|
||||
$script:BrowserMap = @{
|
||||
msedge = 'edge'
|
||||
chrome = 'chrome'
|
||||
@@ -137,23 +135,6 @@ function Write-DlpIncidentLog {
|
||||
}
|
||||
}
|
||||
|
||||
function Write-CollectorHealth {
|
||||
param([string]$Status = 'running')
|
||||
try {
|
||||
$health = @{
|
||||
collector = 'browser-domains-native'
|
||||
hostname = $script:Hostname
|
||||
sessionId = $script:SessionId
|
||||
status = $Status
|
||||
apiBase = $script:ApiBase
|
||||
ts = (Get-Date).ToUniversalTime().ToString('o')
|
||||
} | ConvertTo-Json -Depth 4
|
||||
Set-Content -LiteralPath $script:HealthPath -Value $health -Encoding UTF8
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
function Test-DomainMatch {
|
||||
param(
|
||||
[string]$DomainHost,
|
||||
@@ -810,52 +791,46 @@ Load-CustomCategoryRules -Path $resolvedRulesPath
|
||||
Load-DlpPolicy -Path $resolvedPolicyPath
|
||||
Write-CollectorLog ("коллектор запущен для {0}" -f $script:ApiBase)
|
||||
|
||||
try {
|
||||
while ($true) {
|
||||
try {
|
||||
Write-CollectorHealth -Status 'running'
|
||||
$context = Get-ForegroundWindowContext
|
||||
if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) {
|
||||
$url = Get-BrowserUrlFromWindow -Handle $context.Handle
|
||||
if ($url) {
|
||||
$browserKey = $script:BrowserMap[$context.ProcessName]
|
||||
$domain = Get-HostFromUrl -Url $url
|
||||
if (-not $domain) {
|
||||
$domain = 'unknown'
|
||||
}
|
||||
while ($true) {
|
||||
try {
|
||||
$context = Get-ForegroundWindowContext
|
||||
if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) {
|
||||
$url = Get-BrowserUrlFromWindow -Handle $context.Handle
|
||||
if ($url) {
|
||||
$browserKey = $script:BrowserMap[$context.ProcessName]
|
||||
$domain = Get-HostFromUrl -Url $url
|
||||
if (-not $domain) {
|
||||
$domain = 'unknown'
|
||||
}
|
||||
|
||||
$rootDomain = Get-RootDomain -DomainHost $domain
|
||||
if (-not $rootDomain) {
|
||||
$rootDomain = $domain
|
||||
}
|
||||
$rootDomain = Get-RootDomain -DomainHost $domain
|
||||
if (-not $rootDomain) {
|
||||
$rootDomain = $domain
|
||||
}
|
||||
|
||||
$category = Get-WebCategory -DomainHost $domain
|
||||
$bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname
|
||||
Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey)
|
||||
Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName
|
||||
Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule
|
||||
$category = Get-WebCategory -DomainHost $domain
|
||||
$bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname
|
||||
Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey)
|
||||
Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName
|
||||
Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule
|
||||
|
||||
$decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group
|
||||
if ($decision) {
|
||||
$fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME
|
||||
$cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30)
|
||||
if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) {
|
||||
Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url)
|
||||
if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) {
|
||||
Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group
|
||||
}
|
||||
$decision = Get-DlpDecision -Domain $domain -RootDomain $rootDomain -Url $url -Title $context.Title -BrowserKey $browserKey -Category $category.Name -CategoryGroup $category.Group
|
||||
if ($decision) {
|
||||
$fingerprint = '{0}|{1}|{2}|{3}' -f $decision.id, $browserKey, $rootDomain, $env:USERNAME
|
||||
$cooldown = [Math]::Max([int]$decision.cooldownSeconds, 30)
|
||||
if (Should-EmitIncident -Fingerprint $fingerprint -CooldownSeconds $cooldown) {
|
||||
Write-DlpIncidentLog ("{0} {1} {2} {3}" -f $decision.severity, $decision.action, $decision.id, $url)
|
||||
if (@('alert', 'block', 'quarantine') -contains ([string]$decision.action).ToLowerInvariant()) {
|
||||
Send-DlpIncidentHeartbeat -Decision $decision -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $resolvedPollSeconds
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Write-CollectorHealth -Status 'stopped'
|
||||
catch {
|
||||
Write-CollectorLog ("ошибка коллектора: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $resolvedPollSeconds
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ServerHost,
|
||||
@@ -114,8 +114,8 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||
|
||||
Write-Output 'ActivityWatch развёрнут для пользователей:'
|
||||
$targetUsers | ForEach-Object { Write-Output " - $_" }
|
||||
Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||
Write-Output "Каталог данных: $StateRoot"
|
||||
Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||
Write-Host 'ActivityWatch развёрнут для пользователей:'
|
||||
$targetUsers | ForEach-Object { Write-Host " - $_" }
|
||||
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||
Write-Host "Каталог данных: $StateRoot"
|
||||
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ServerHost,
|
||||
@@ -139,6 +139,6 @@ if ($reportDirectory) {
|
||||
|
||||
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8
|
||||
|
||||
Write-Output 'Комплексное развёртывание ActivityWatch завершено.'
|
||||
Write-Output "Пользователи: $($resolvedUsers -join ', ')"
|
||||
Write-Output "Отчёт: $effectiveReportPath"
|
||||
Write-Host 'Комплексное развёртывание ActivityWatch завершено.'
|
||||
Write-Host "Пользователи: $($resolvedUsers -join ', ')"
|
||||
Write-Host "Отчёт: $effectiveReportPath"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ServerHost,
|
||||
@@ -106,9 +106,9 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||
|
||||
Write-Output "ActivityWatch развёрнут для пользователя: $TargetUser"
|
||||
Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||
Write-Output "Каталог установки: $InstallRoot"
|
||||
Write-Output "Каталог данных: $StateRoot"
|
||||
Write-Output "Файл правил: $($assetResult.ActiveRules)"
|
||||
Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||
Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser"
|
||||
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||
Write-Host "Каталог установки: $InstallRoot"
|
||||
Write-Host "Каталог данных: $StateRoot"
|
||||
Write-Host "Файл правил: $($assetResult.ActiveRules)"
|
||||
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,79 +53,13 @@ function Write-CollectorLog {
|
||||
catch { }
|
||||
}
|
||||
|
||||
function Add-WalEntry {
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json
|
||||
)
|
||||
if ([string]::IsNullOrWhiteSpace($script:WalPath)) { return }
|
||||
try {
|
||||
$entry = @{ ts = (Get-Date).ToUniversalTime().ToString('o'); uri = $Uri; json = $Json } | ConvertTo-Json -Compress
|
||||
Add-Content -LiteralPath $script:WalPath -Value $entry -Encoding UTF8
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function Flush-Wal {
|
||||
if ([string]::IsNullOrWhiteSpace($script:WalPath) -or -not (Test-Path -LiteralPath $script:WalPath)) { return }
|
||||
$remaining = New-Object System.Collections.Generic.List[string]
|
||||
try {
|
||||
$script:WalFlushing = $true
|
||||
foreach ($line in (Get-Content -LiteralPath $script:WalPath -ErrorAction SilentlyContinue)) {
|
||||
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
||||
try {
|
||||
$entry = $line | ConvertFrom-Json
|
||||
if ($null -eq $entry -or -not $entry.uri -or -not $entry.json) { continue }
|
||||
if (-not (Invoke-AwJsonPost -Uri ([string]$entry.uri) -Json ([string]$entry.json))) { $remaining.Add($line) }
|
||||
} catch { $remaining.Add($line) }
|
||||
}
|
||||
if ($remaining.Count -eq 0) {
|
||||
Remove-Item -LiteralPath $script:WalPath -Force -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
Set-Content -LiteralPath $script:WalPath -Value ($remaining -join [Environment]::NewLine) -Encoding UTF8
|
||||
}
|
||||
} finally {
|
||||
$script:WalFlushing = $false
|
||||
}
|
||||
}
|
||||
|
||||
function Write-CollectorHealth {
|
||||
param([string]$Status = 'running')
|
||||
if ([string]::IsNullOrWhiteSpace($script:HealthPath)) { return }
|
||||
try {
|
||||
$walDepth = 0
|
||||
if ($script:WalPath -and (Test-Path -LiteralPath $script:WalPath)) { $walDepth = @((Get-Content -LiteralPath $script:WalPath)).Count }
|
||||
$health = @{
|
||||
collector = 'email-outbound'; hostname = $script:Hostname; sessionId = $script:SessionId;
|
||||
status = $Status; apiBase = $script:ApiBase; walDepth = $walDepth; ts = (Get-Date).ToUniversalTime().ToString('o')
|
||||
} | ConvertTo-Json -Depth 5
|
||||
Set-Content -LiteralPath $script:HealthPath -Value $health -Encoding UTF8
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json,
|
||||
[int]$MaxAttempts = 5,
|
||||
[int]$InitialBackoffMs = 500
|
||||
)
|
||||
$attempt = 1
|
||||
$backoff = [Math]::Max(100, $InitialBackoffMs)
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||
while ($attempt -le $MaxAttempts) {
|
||||
try {
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||
return $true
|
||||
} catch {
|
||||
if ($attempt -ge $MaxAttempts) {
|
||||
if (-not $script:WalFlushing) { Add-WalEntry -Uri $Uri -Json $Json }
|
||||
return $false
|
||||
}
|
||||
Start-Sleep -Milliseconds $backoff
|
||||
$backoff = [Math]::Min($backoff * 2, 10000)
|
||||
$attempt++
|
||||
}
|
||||
}
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||
}
|
||||
|
||||
function Ensure-Bucket {
|
||||
@@ -583,10 +517,6 @@ $script:SeenSmtpConnections = @{}
|
||||
$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30)
|
||||
$script:LocalAgentLogsEnabled = $resolvedLocalAgentLogsEnabled
|
||||
$script:LogPath = $resolvedLogPath
|
||||
$stateRoot = if ($deploymentConfig -and $deploymentConfig.paths -and $deploymentConfig.paths.stateRoot) { [string]$deploymentConfig.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' }
|
||||
$script:WalPath = Join-Path $stateRoot 'wal-email-outbound.ndjson'
|
||||
$script:HealthPath = Join-Path $stateRoot 'health-email-outbound.json'
|
||||
$script:WalFlushing = $false
|
||||
$script:OutlookApp = $null
|
||||
$script:OutlookNamespace = $null
|
||||
$script:SentFolder = $null
|
||||
@@ -610,65 +540,43 @@ if ($useOutlook) {
|
||||
# Main loop
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
try {
|
||||
while ($true) {
|
||||
try {
|
||||
Flush-Wal
|
||||
Write-CollectorHealth -Status 'running'
|
||||
if (-not $script:Policy.defaults.enabled) {
|
||||
Start-Sleep -Seconds $resolvedPollSeconds
|
||||
continue
|
||||
}
|
||||
while ($true) {
|
||||
try {
|
||||
if (-not $script:Policy.defaults.enabled) {
|
||||
Start-Sleep -Seconds $resolvedPollSeconds
|
||||
continue
|
||||
}
|
||||
|
||||
if ($useOutlook) {
|
||||
if (-not $outlookReady) {
|
||||
$outlookReady = Initialize-OutlookCom
|
||||
}
|
||||
if ($outlookReady) {
|
||||
try {
|
||||
Poll-OutlookSentItems
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("outlook poll error: {0}" -f $_.Exception.Message)
|
||||
$outlookReady = $false
|
||||
$script:OutlookApp = $null
|
||||
$script:OutlookNamespace = $null
|
||||
$script:SentFolder = $null
|
||||
}
|
||||
}
|
||||
if ($useOutlook) {
|
||||
if (-not $outlookReady) {
|
||||
$outlookReady = Initialize-OutlookCom
|
||||
}
|
||||
|
||||
if ($useSmtp) {
|
||||
if ($outlookReady) {
|
||||
try {
|
||||
Poll-SmtpConnections
|
||||
Poll-OutlookSentItems
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("smtp poll error: {0}" -f $_.Exception.Message)
|
||||
Write-CollectorLog ("outlook poll error: {0}" -f $_.Exception.Message)
|
||||
$outlookReady = $false
|
||||
$script:OutlookApp = $null
|
||||
$script:OutlookNamespace = $null
|
||||
$script:SentFolder = $null
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $resolvedPollSeconds
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Write-CollectorHealth -Status 'stopped'
|
||||
try {
|
||||
if ($null -ne $script:SentFolder) {
|
||||
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($script:SentFolder)
|
||||
$script:SentFolder = $null
|
||||
}
|
||||
if ($null -ne $script:OutlookNamespace) {
|
||||
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($script:OutlookNamespace)
|
||||
$script:OutlookNamespace = $null
|
||||
}
|
||||
if ($null -ne $script:OutlookApp) {
|
||||
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($script:OutlookApp)
|
||||
$script:OutlookApp = $null
|
||||
if ($useSmtp) {
|
||||
try {
|
||||
Poll-SmtpConnections
|
||||
}
|
||||
catch {
|
||||
Write-CollectorLog ("smtp poll error: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
catch {
|
||||
Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message)
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $resolvedPollSeconds
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$ServerHost,
|
||||
@@ -22,9 +22,6 @@ Add-Type -AssemblyName System.Net.Http
|
||||
$script:KnownBuckets = @{}
|
||||
$script:Hostname = $env:COMPUTERNAME
|
||||
$script:SessionId = [System.Diagnostics.Process]::GetCurrentProcess().SessionId
|
||||
$script:WalPath = $null
|
||||
$script:HealthPath = $null
|
||||
$script:WalFlushing = $false
|
||||
|
||||
# Настройка логирования
|
||||
$script:LogPath = $LogPath
|
||||
@@ -46,88 +43,28 @@ function Write-FileCollectorLog {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function Add-WalEntry {
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json
|
||||
)
|
||||
if ([string]::IsNullOrWhiteSpace($script:WalPath)) { return }
|
||||
$httpClient = $null
|
||||
try {
|
||||
$entry = @{ ts = (Get-Date).ToUniversalTime().ToString('o'); uri = $Uri; json = $Json } | ConvertTo-Json -Compress
|
||||
Add-Content -LiteralPath $script:WalPath -Value $entry -Encoding UTF8
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function Flush-Wal {
|
||||
if ([string]::IsNullOrWhiteSpace($script:WalPath) -or -not (Test-Path -LiteralPath $script:WalPath)) { return }
|
||||
$remaining = New-Object System.Collections.Generic.List[string]
|
||||
try {
|
||||
$script:WalFlushing = $true
|
||||
foreach ($line in (Get-Content -LiteralPath $script:WalPath -ErrorAction SilentlyContinue)) {
|
||||
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
||||
try {
|
||||
$entry = $line | ConvertFrom-Json
|
||||
if ($null -eq $entry -or -not $entry.uri -or -not $entry.json) { continue }
|
||||
if (-not (Invoke-AwJsonPost -Uri ([string]$entry.uri) -Json ([string]$entry.json))) { $remaining.Add($line) }
|
||||
} catch { $remaining.Add($line) }
|
||||
}
|
||||
if ($remaining.Count -eq 0) {
|
||||
Remove-Item -LiteralPath $script:WalPath -Force -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
Set-Content -LiteralPath $script:WalPath -Value ($remaining -join [Environment]::NewLine) -Encoding UTF8
|
||||
$httpClient = New-Object System.Net.Http.HttpClient
|
||||
$content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json")
|
||||
$response = $httpClient.PostAsync($Uri, $content).Result
|
||||
if (-not $response.IsSuccessStatusCode) {
|
||||
$status = [int]$response.StatusCode
|
||||
$reason = [string]$response.ReasonPhrase
|
||||
$body = $response.Content.ReadAsStringAsync().Result
|
||||
Write-FileCollectorLog ("POST failed: uri={0} status={1} reason={2} body={3}" -f $Uri, $status, $reason, $body)
|
||||
}
|
||||
} catch {
|
||||
Write-FileCollectorLog "POST Error: $($_.Exception.Message)"
|
||||
} finally {
|
||||
$script:WalFlushing = $false
|
||||
}
|
||||
}
|
||||
|
||||
function Write-CollectorHealth {
|
||||
param([string]$Status = 'running')
|
||||
if ([string]::IsNullOrWhiteSpace($script:HealthPath)) { return }
|
||||
try {
|
||||
$walDepth = 0
|
||||
if ($script:WalPath -and (Test-Path -LiteralPath $script:WalPath)) { $walDepth = @((Get-Content -LiteralPath $script:WalPath)).Count }
|
||||
$health = @{
|
||||
collector = 'file-operations'; hostname = $script:Hostname; sessionId = $script:SessionId;
|
||||
status = $Status; apiBase = $script:ApiBase; walDepth = $walDepth; ts = (Get-Date).ToUniversalTime().ToString('o')
|
||||
} | ConvertTo-Json -Depth 5
|
||||
Set-Content -LiteralPath $script:HealthPath -Value $health -Encoding UTF8
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json,
|
||||
[int]$MaxAttempts = 5,
|
||||
[int]$InitialBackoffMs = 500
|
||||
)
|
||||
$attempt = 1
|
||||
$backoff = [Math]::Max(100, $InitialBackoffMs)
|
||||
while ($attempt -le $MaxAttempts) {
|
||||
$httpClient = $null
|
||||
try {
|
||||
$httpClient = New-Object System.Net.Http.HttpClient
|
||||
$content = New-Object System.Net.Http.StringContent($Json, [System.Text.Encoding]::UTF8, "application/json")
|
||||
$response = $httpClient.PostAsync($Uri, $content).Result
|
||||
if ($response.IsSuccessStatusCode) { return $true }
|
||||
if ($attempt -ge $MaxAttempts) {
|
||||
if (-not $script:WalFlushing) { Add-WalEntry -Uri $Uri -Json $Json }
|
||||
return $false
|
||||
}
|
||||
} catch {
|
||||
if ($attempt -ge $MaxAttempts) {
|
||||
if (-not $script:WalFlushing) { Add-WalEntry -Uri $Uri -Json $Json }
|
||||
return $false
|
||||
}
|
||||
} finally {
|
||||
if ($null -ne $httpClient) {
|
||||
$httpClient.Dispose()
|
||||
}
|
||||
if ($null -ne $httpClient) {
|
||||
$httpClient.Dispose()
|
||||
}
|
||||
Start-Sleep -Milliseconds $backoff
|
||||
$backoff = [Math]::Min($backoff * 2, 10000)
|
||||
$attempt++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,9 +151,6 @@ $scheme = if ($ServerScheme) { $ServerScheme } elseif ($config.server.scheme) {
|
||||
$hostName = if ($ServerHost) { $ServerHost } elseif ($config.server.host) { $config.server.host } else { 'localhost' }
|
||||
$port = if ($ServerPort) { $ServerPort } elseif ($config.server.port) { $config.server.port } else { 5600 }
|
||||
$script:ApiBase = "{0}://{1}:{2}/api/0" -f $scheme, $hostName, $port
|
||||
$stateRoot = if ($config.paths -and $config.paths.stateRoot) { [string]$config.paths.stateRoot } else { 'C:\ProgramData\AWatch-rus' }
|
||||
$script:WalPath = Join-Path $stateRoot 'wal-file-operations.ndjson'
|
||||
$script:HealthPath = Join-Path $stateRoot 'health-file-operations.json'
|
||||
|
||||
$bucketId = 'aw-file-operations_' + $script:Hostname
|
||||
Ensure-Bucket -BucketId $bucketId -ClientName 'aw-file-operations' -BucketType 'aw.file.operation'
|
||||
@@ -273,14 +207,11 @@ Write-FileCollectorLog "Collector started. Waiting for events..."
|
||||
|
||||
try {
|
||||
while ($true) {
|
||||
Flush-Wal
|
||||
Write-CollectorHealth -Status 'running'
|
||||
Start-Sleep -Seconds $PollSeconds
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Write-FileCollectorLog "Stopping collector..."
|
||||
Write-CollectorHealth -Status 'stopped'
|
||||
foreach ($sub in @($subscriptions)) {
|
||||
try {
|
||||
if ($sub -and $sub.Id) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding()]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$ServerHost,
|
||||
@@ -154,6 +154,6 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||
|
||||
Write-Output 'Укрепление и восстановление ActivityWatch завершены.'
|
||||
Write-Output "Конфигурация: $effectiveConfigPath"
|
||||
Write-Output "Пользователи восстановлены: $($effectiveUsers -join ', ')"
|
||||
Write-Host 'Укрепление и восстановление ActivityWatch завершены.'
|
||||
Write-Host "Конфигурация: $effectiveConfigPath"
|
||||
Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')"
|
||||
|
||||
@@ -25,11 +25,21 @@ The resulting installer `AWatch-rus-InstallKit.exe` is written to the same direc
|
||||
./build_with_wine.sh
|
||||
```
|
||||
|
||||
## Install-time parameters
|
||||
## Install-time parameters (Standalone agent mode)
|
||||
|
||||
The installer wizard asks for:
|
||||
The installer wizard asks only for:
|
||||
|
||||
- `ServerHost` / `ServerPort` (defaults to our AW server `10.10.10.13:5600`)
|
||||
- `Users` (CSV)
|
||||
- Whether to use offline payload (auto-enabled when the ZIP exists at compile time)
|
||||
- Whether to validate after deploy (`-ValidateAfterDeploy`, report written to `C:\ProgramData\AWatch-rus\ensemble-report-*.json`)
|
||||
- `ServerHost` / `ServerPort` (defaults to `10.10.10.13:5600`)
|
||||
|
||||
All other values are taken from defaults embedded in installer scripts.
|
||||
|
||||
## Runtime mode
|
||||
|
||||
- Installer runs `windows\install-standalone-service.ps1`.
|
||||
- A Windows service `AWatchRusStandaloneAgent` is created with auto-start and restart-on-failure.
|
||||
- Service wrapper (`windows\aw-standalone-service.ps1`) keeps DLP collectors running:
|
||||
- `browser-domains-native-collector.ps1`
|
||||
- `dlp-endpoint-signals-collector.ps1`
|
||||
- `file-operations-collector.ps1`
|
||||
- `email-outbound-collector.ps1` (if present)
|
||||
- `worktime-session-collector.ps1` (if present)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2',
|
||||
[string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json'
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -13,26 +13,70 @@ $config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
||||
$installRoot = [string]$config.paths.installRoot
|
||||
$stateRoot = [string]$config.paths.stateRoot
|
||||
$collectorScript = [string]$config.paths.collectorScript
|
||||
$endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' }
|
||||
$fileCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]$config.paths.fileCollectorScript } else { Join-Path $stateRoot 'file-operations-collector.ps1' }
|
||||
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
|
||||
$rulesPath = [string]$config.paths.rulesPath
|
||||
$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' }
|
||||
$launchScript = [string]$config.paths.launchScript
|
||||
$recoveryScript = [string]$config.paths.recoveryScript
|
||||
|
||||
$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true }
|
||||
$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true }
|
||||
$fileOpsExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]$config.collectors.fileOpsEnabled } else { $true }
|
||||
$printServiceOperationalEnabled = $false
|
||||
try {
|
||||
$printServiceLog = Get-WinEvent -ListLog 'Microsoft-Windows-PrintService/Operational' -ErrorAction Stop
|
||||
$printServiceOperationalEnabled = [bool]$printServiceLog.IsEnabled
|
||||
}
|
||||
catch {
|
||||
}
|
||||
$printJobTitlePolicyEnabled = $false
|
||||
try {
|
||||
$printPolicy = Get-ItemProperty -LiteralPath 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' -Name 'ShowJobTitleInEventLogs' -ErrorAction Stop
|
||||
$printJobTitlePolicyEnabled = ([int]$printPolicy.ShowJobTitleInEventLogs -eq 1)
|
||||
}
|
||||
catch {
|
||||
}
|
||||
$requiredFiles = @(
|
||||
(Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'),
|
||||
(Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'),
|
||||
$collectorScript,
|
||||
$endpointCollectorScript,
|
||||
$sessionCollectorScript,
|
||||
$rulesPath,
|
||||
$policyPath,
|
||||
$launchScript,
|
||||
$recoveryScript,
|
||||
$ConfigPath
|
||||
)
|
||||
if ($fileOpsExpected) {
|
||||
$requiredFiles += $fileCollectorScript
|
||||
}
|
||||
if ($afkExpected) {
|
||||
$requiredFiles += (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe')
|
||||
}
|
||||
if ($windowExpected) {
|
||||
$requiredFiles += (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe')
|
||||
}
|
||||
|
||||
$missingFiles = @(
|
||||
$requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) }
|
||||
)
|
||||
|
||||
$processNames = @('aw-watcher-afk', 'aw-watcher-window')
|
||||
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
|
||||
$processNames = @()
|
||||
if ($afkExpected) { $processNames += 'aw-watcher-afk' }
|
||||
if ($windowExpected) { $processNames += 'aw-watcher-window' }
|
||||
$runningProcesses = @()
|
||||
if ($processNames.Count -gt 0) {
|
||||
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
|
||||
}
|
||||
$sessionCollectorProcesses = @(
|
||||
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and
|
||||
$_.CommandLine -match [Regex]::Escape($sessionCollectorScript)
|
||||
} |
|
||||
Select-Object Name, ProcessId, SessionId, CommandLine
|
||||
)
|
||||
|
||||
$taskNames = @()
|
||||
if ($config.userTasks) {
|
||||
@@ -41,25 +85,28 @@ if ($config.userTasks) {
|
||||
$taskNames += [string]$config.recovery.taskName
|
||||
$taskNames = $taskNames | Sort-Object -Unique
|
||||
|
||||
$tasks = foreach ($taskName in $taskNames) {
|
||||
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
if ($task) {
|
||||
[pscustomobject]@{
|
||||
taskName = $task.TaskName
|
||||
state = [string]$task.State
|
||||
present = $true
|
||||
$tasks = @(
|
||||
foreach ($taskName in $taskNames) {
|
||||
$task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1
|
||||
if ($task) {
|
||||
[pscustomobject]@{
|
||||
taskName = $task.TaskName
|
||||
state = [string]$task.State
|
||||
present = $true
|
||||
}
|
||||
}
|
||||
else {
|
||||
[pscustomobject]@{
|
||||
taskName = $taskName
|
||||
state = 'Отсутствует'
|
||||
present = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
[pscustomobject]@{
|
||||
taskName = $taskName
|
||||
state = 'Missing'
|
||||
present = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$serverUrl = '{0}://{1}:{2}' -f [string]$config.server.scheme, [string]$config.server.host, [int]$config.server.port
|
||||
$uniqueRunningProcessNames = @($runningProcesses | Select-Object -ExpandProperty Name -Unique)
|
||||
$result = [ordered]@{
|
||||
generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
|
||||
configPath = $ConfigPath
|
||||
@@ -76,11 +123,24 @@ $result = [ordered]@{
|
||||
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present }))
|
||||
}
|
||||
processes = [ordered]@{
|
||||
expected = $processNames
|
||||
list = @($runningProcesses)
|
||||
ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2)
|
||||
sessionCollectors = @($sessionCollectorProcesses)
|
||||
ok = [bool](
|
||||
(
|
||||
($processNames.Count -eq 0) -or
|
||||
($uniqueRunningProcessNames.Count -ge $processNames.Count)
|
||||
) -and
|
||||
($sessionCollectorProcesses.Count -ge 1)
|
||||
)
|
||||
}
|
||||
printTelemetry = [ordered]@{
|
||||
operationalLogEnabled = $printServiceOperationalEnabled
|
||||
jobTitlePolicyEnabled = $printJobTitlePolicyEnabled
|
||||
ok = [bool]($printServiceOperationalEnabled -and $printJobTitlePolicyEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
$result.overallOk = [bool]($result.files.ok -and $result.tasks.ok -and $result.processes.ok)
|
||||
$result.overallOk = [bool]($result.files.ok -and $result.tasks.ok -and $result.processes.ok -and $result.printTelemetry.ok)
|
||||
|
||||
$result
|
||||
|
||||
@@ -1,20 +1,69 @@
|
||||
param(
|
||||
param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$Hostname,
|
||||
[int]$PollSeconds = 30
|
||||
)
|
||||
|
||||
# Force UTF-8 for console I/O
|
||||
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
|
||||
try { [Console]::InputEncoding = [System.Text.Encoding]::UTF8 } catch {}
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
function Decode-Bytes-Auto {
|
||||
param([byte[]]$Bytes)
|
||||
if (-not $Bytes) { return '' }
|
||||
|
||||
$candidates = @()
|
||||
|
||||
# Try strict UTF8 first (detect invalid sequences)
|
||||
try {
|
||||
$utf8Strict = New-Object System.Text.UTF8Encoding($false,$true)
|
||||
$txt = $utf8Strict.GetString($Bytes)
|
||||
$candidates += @{enc='utf8'; text=$txt}
|
||||
}
|
||||
catch {
|
||||
# invalid UTF8 sequences; ignore
|
||||
}
|
||||
|
||||
# Try CP866 and CP1251
|
||||
try { $cp866 = [System.Text.Encoding]::GetEncoding(866); $txt866 = $cp866.GetString($Bytes); $candidates += @{enc='cp866'; text=$txt866} } catch {}
|
||||
try { $cp1251 = [System.Text.Encoding]::GetEncoding(1251); $txt1251 = $cp1251.GetString($Bytes); $candidates += @{enc='cp1251'; text=$txt1251} } catch {}
|
||||
|
||||
# If nothing decoded yet, fallback to UTF8 permissive
|
||||
if ($candidates.Count -eq 0) {
|
||||
try { $txt = [System.Text.Encoding]::UTF8.GetString($Bytes); return $txt } catch { return '' }
|
||||
}
|
||||
|
||||
# Score decodings by count of Cyrillic letters; prefer highest
|
||||
$best = $null; $bestScore = -1
|
||||
foreach ($c in $candidates) {
|
||||
$t = $c.text
|
||||
if (-not $t) { continue }
|
||||
$score = 0
|
||||
try { $score = ([regex]::Matches($t,'\p{IsCyrillic}')).Count } catch { $score = 0 }
|
||||
if ($score -gt $bestScore) { $best = $c; $bestScore = $score }
|
||||
}
|
||||
|
||||
if ($best -ne $null) { return $best.text }
|
||||
|
||||
# Final fallback: first candidate text
|
||||
return $candidates[0].text
|
||||
}
|
||||
|
||||
function Get-Config {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "Конфигурация не найдена: $Path"
|
||||
throw "Config not found: $Path"
|
||||
}
|
||||
try {
|
||||
$bytes = [System.IO.File]::ReadAllBytes($Path)
|
||||
$text = Decode-Bytes-Auto -Bytes $bytes
|
||||
return $text | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
throw "Failed to read config: $Path - $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
@@ -22,9 +71,15 @@ function Invoke-AwJsonPost {
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json
|
||||
)
|
||||
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||
try {
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Json)
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes -ErrorAction Stop | Out-Null
|
||||
return $true
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "POST error: $($_.Exception.Message)"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-Bucket {
|
||||
@@ -33,629 +88,136 @@ function Ensure-Bucket {
|
||||
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||
)
|
||||
try { Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" -ErrorAction Stop | Out-Null; return } catch { Write-Verbose "Bucket not found, creating: $BucketId" }
|
||||
|
||||
try {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
return
|
||||
$body = @{ client='aw-worktime-session-collector'; type='aw.worktime.session'; hostname=$HostnameValue } | ConvertTo-Json -Compress
|
||||
$attempts = 0
|
||||
while ($attempts -lt 3) {
|
||||
$attempts++
|
||||
$ok = Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||
if ($ok) { return }
|
||||
Start-Sleep -Seconds (2 * $attempts)
|
||||
}
|
||||
catch { Write-Error param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$Hostname,
|
||||
[int]$PollSeconds = 30
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-Config {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "Конфигурация не найдена: $Path"
|
||||
}
|
||||
|
||||
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
try { Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" -ErrorAction Stop | Out-Null } catch { Write-Verbose "Ensure-Bucket final check failed: $BucketId" }
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json
|
||||
)
|
||||
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||
}
|
||||
|
||||
function Ensure-Bucket {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||
)
|
||||
|
||||
try {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
return
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
$body = @{
|
||||
client = 'aw-worktime-session-collector'
|
||||
type = 'aw.worktime.session'
|
||||
hostname = $HostnameValue
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||
}
|
||||
catch {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SessionRecords {
|
||||
$records = @()
|
||||
|
||||
try {
|
||||
$lines = quser 2>$null
|
||||
if (-not $lines) {
|
||||
return @()
|
||||
}
|
||||
|
||||
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||
if (-not $clean) {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $clean -split '\s+'
|
||||
if ($parts.Count -lt 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
$sessionName = ''
|
||||
$sessionIdIndex = 2
|
||||
if ($parts[1] -match '^\d+$') {
|
||||
$sessionIdIndex = 1
|
||||
}
|
||||
else {
|
||||
$sessionName = $parts[1]
|
||||
}
|
||||
|
||||
$sessionId = 0
|
||||
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||
$sessionId = [int]$parts[$sessionIdIndex]
|
||||
}
|
||||
|
||||
$records += [pscustomobject]@{
|
||||
username = $parts[0]
|
||||
sessionName = $sessionName
|
||||
sessionId = $sessionId
|
||||
state = $parts[$sessionIdIndex + 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
return $records
|
||||
}
|
||||
|
||||
function Test-SessionIsActive {
|
||||
param([AllowNull()][string]$State)
|
||||
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||
$s = $State.Trim().ToLowerInvariant()
|
||||
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||
}
|
||||
|
||||
$cfg = Get-Config -Path $ConfigPath
|
||||
$hostValue = if ($Hostname) { $Hostname } elseif ($cfg.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$cfg.awHostname)) { [string]$cfg.awHostname } else { [string]$env:COMPUTERNAME }
|
||||
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||
$pulse = 120
|
||||
$sleepSec = if ($PollSeconds -gt 0) {
|
||||
$PollSeconds
|
||||
}
|
||||
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||
[int]$cfg.collector.pollSeconds
|
||||
}
|
||||
else {
|
||||
30
|
||||
}
|
||||
|
||||
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||
|
||||
while ($true) {
|
||||
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
$records = Get-SessionRecords
|
||||
if (-not $records -or $records.Count -eq 0) {
|
||||
$records = @([pscustomobject]@{
|
||||
username = $env:USERNAME
|
||||
sessionName = ''
|
||||
sessionId = (Get-Process -Id $PID).SessionId
|
||||
state = 'Unknown'
|
||||
})
|
||||
}
|
||||
|
||||
foreach ($rec in $records) {
|
||||
$payload = @{
|
||||
timestamp = $now
|
||||
duration = 0
|
||||
data = @{
|
||||
username = [string]$rec.username
|
||||
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||
sessionId = [int]$rec.sessionId
|
||||
sessionName = [string]$rec.sessionName
|
||||
state = [string]$rec.state
|
||||
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||
hostname = $hostValue
|
||||
source = 'worktime-session-collector'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
|
||||
function Run-QueryUser {
|
||||
$tries = @(@{File='quser';Args=''},@{File='query';Args='user'})
|
||||
foreach ($t in $tries) {
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $t.File
|
||||
if ($t.Args) { $psi.Arguments = $t.Args }
|
||||
$psi.RedirectStandardOutput = $true
|
||||
$psi.RedirectStandardError = $true
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.CreateNoWindow = $true
|
||||
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
$stream = $proc.StandardOutput.BaseStream
|
||||
$ms = New-Object System.IO.MemoryStream
|
||||
$buffer = New-Object byte[] 4096
|
||||
while (($read = $stream.Read($buffer,0,$buffer.Length)) -gt 0) { $ms.Write($buffer,0,$read) }
|
||||
$proc.WaitForExit()
|
||||
$bytes = $ms.ToArray()
|
||||
|
||||
$text = Decode-Bytes-Auto -Bytes $bytes
|
||||
if ($text -and $text.Trim()) { return ($text -split "\r?\n") | Where-Object { $_ -ne '' } }
|
||||
}
|
||||
catch {
|
||||
# try next
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleepSec
|
||||
}
|
||||
; }
|
||||
|
||||
$body = @{
|
||||
client = 'aw-worktime-session-collector'
|
||||
type = 'aw.worktime.session'
|
||||
hostname = $HostnameValue
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||
}
|
||||
catch {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
}
|
||||
return @()
|
||||
}
|
||||
|
||||
function Get-SessionRecords {
|
||||
function Parse-SessionLines {
|
||||
param([string[]]$Lines)
|
||||
$records = @()
|
||||
if (-not $Lines) { return $records }
|
||||
|
||||
try {
|
||||
$lines = quser 2>$null
|
||||
if (-not $lines) {
|
||||
return @()
|
||||
$startIndex = 0
|
||||
if ($Lines.Count -gt 0 -and $Lines[0] -match '\b(USERNAME|Имя|Имя пользователя|Имя_пользователя)\b') { $startIndex = 1 }
|
||||
|
||||
for ($i = $startIndex; $i -lt $Lines.Count; $i++) {
|
||||
$line = $Lines[$i].Trim()
|
||||
if (-not $line) { continue }
|
||||
|
||||
$m = [regex]::Match($line, '^\s*(?<user>\S+)\s+(?<sess>\S+)?\s+(?<id>\d+)\s+(?<state>\S+)', [System.Text.RegularExpressions.RegexOptions]::None)
|
||||
if ($m.Success) {
|
||||
$user = $m.Groups['user'].Value; $sess = $m.Groups['sess'].Value; $id = [int]$m.Groups['id'].Value; $state = $m.Groups['state'].Value
|
||||
}
|
||||
else {
|
||||
$parts = $line -split '\s+'
|
||||
if ($parts.Count -lt 4) { continue }
|
||||
$user = $parts[0]
|
||||
if ($parts[1] -match '^\d+$') { $sess = ''; $id = [int]$parts[1]; $state = $parts[2] } else { $sess = $parts[1]; $id = [int]$parts[2]; $state = $parts[3] }
|
||||
}
|
||||
|
||||
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||
if (-not $clean) {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $clean -split '\s+'
|
||||
if ($parts.Count -lt 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
$sessionName = ''
|
||||
$sessionIdIndex = 2
|
||||
if ($parts[1] -match '^\d+$') {
|
||||
$sessionIdIndex = 1
|
||||
}
|
||||
else {
|
||||
$sessionName = $parts[1]
|
||||
}
|
||||
|
||||
$sessionId = 0
|
||||
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||
$sessionId = [int]$parts[$sessionIdIndex]
|
||||
}
|
||||
|
||||
$records += [pscustomobject]@{
|
||||
username = $parts[0]
|
||||
sessionName = $sessionName
|
||||
sessionId = $sessionId
|
||||
state = $parts[$sessionIdIndex + 1]
|
||||
}
|
||||
}
|
||||
$records += [pscustomobject]@{ username=$user; sessionName=$sess; sessionId=$id; state=$state }
|
||||
}
|
||||
catch { Write-Error param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$Hostname,
|
||||
[int]$PollSeconds = 30
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-Config {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "Конфигурация не найдена: $Path"
|
||||
}
|
||||
|
||||
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json
|
||||
)
|
||||
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||
}
|
||||
|
||||
function Ensure-Bucket {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||
)
|
||||
|
||||
try {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
return
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
$body = @{
|
||||
client = 'aw-worktime-session-collector'
|
||||
type = 'aw.worktime.session'
|
||||
hostname = $HostnameValue
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||
}
|
||||
catch {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SessionRecords {
|
||||
$records = @()
|
||||
|
||||
try {
|
||||
$lines = quser 2>$null
|
||||
if (-not $lines) {
|
||||
return @()
|
||||
}
|
||||
|
||||
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||
if (-not $clean) {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $clean -split '\s+'
|
||||
if ($parts.Count -lt 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
$sessionName = ''
|
||||
$sessionIdIndex = 2
|
||||
if ($parts[1] -match '^\d+$') {
|
||||
$sessionIdIndex = 1
|
||||
}
|
||||
else {
|
||||
$sessionName = $parts[1]
|
||||
}
|
||||
|
||||
$sessionId = 0
|
||||
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||
$sessionId = [int]$parts[$sessionIdIndex]
|
||||
}
|
||||
|
||||
$records += [pscustomobject]@{
|
||||
username = $parts[0]
|
||||
sessionName = $sessionName
|
||||
sessionId = $sessionId
|
||||
state = $parts[$sessionIdIndex + 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
return $records
|
||||
}
|
||||
|
||||
function Test-SessionIsActive {
|
||||
param([AllowNull()][string]$State)
|
||||
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||
param([string]$State)
|
||||
if (-not $State) { return $false }
|
||||
$s = $State.Trim().ToLowerInvariant()
|
||||
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||
return ($s -match 'active') -or ($s -match 'актив')
|
||||
}
|
||||
|
||||
# Main
|
||||
$cfg = Get-Config -Path $ConfigPath
|
||||
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||
$hostValue = if ($Hostname -and $Hostname.Trim()) { $Hostname.Trim() } elseif ($cfg -and $cfg.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$cfg.awHostname)) { [string]$cfg.awHostname } elseif ($cfg -and $cfg.awHostname) { [string]$cfg.awHostname } else { [string]$env:COMPUTERNAME }
|
||||
try { $apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port } catch { throw 'Invalid server configuration in config file.' }
|
||||
|
||||
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||
$pulse = 120
|
||||
$sleepSec = if ($PollSeconds -gt 0) {
|
||||
$PollSeconds
|
||||
}
|
||||
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||
[int]$cfg.collector.pollSeconds
|
||||
}
|
||||
else {
|
||||
30
|
||||
}
|
||||
$sleepSec = if ($PollSeconds -gt 0) { $PollSeconds } elseif ($cfg.collector -and $cfg.collector.pollSeconds) { [int]$cfg.collector.pollSeconds } else { 30 }
|
||||
|
||||
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||
|
||||
while ($true) {
|
||||
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
$records = Get-SessionRecords
|
||||
try {
|
||||
$lines = Run-QueryUser
|
||||
$records = Parse-SessionLines -Lines $lines
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "Session parse error: $($_.Exception.Message)"
|
||||
$records = @()
|
||||
}
|
||||
|
||||
if (-not $records -or $records.Count -eq 0) {
|
||||
$records = @([pscustomobject]@{
|
||||
username = $env:USERNAME
|
||||
sessionName = ''
|
||||
sessionId = (Get-Process -Id $PID).SessionId
|
||||
state = 'Unknown'
|
||||
})
|
||||
$records = @([pscustomobject]@{ username=$env:USERNAME; sessionName=''; sessionId=(Get-Process -Id $PID).SessionId; state='Unknown' })
|
||||
}
|
||||
|
||||
foreach ($rec in $records) {
|
||||
$payload = @{
|
||||
$payloadObj = [PSCustomObject]@{
|
||||
timestamp = $now
|
||||
duration = 0
|
||||
data = @{
|
||||
data = [PSCustomObject]@{
|
||||
username = [string]$rec.username
|
||||
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||
userId = "${env:USERDOMAIN}\$($rec.username)"
|
||||
sessionId = [int]$rec.sessionId
|
||||
sessionName = [string]$rec.sessionName
|
||||
state = [string]$rec.state
|
||||
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||
active = Test-SessionIsActive -State ([string]$rec.state)
|
||||
hostname = $hostValue
|
||||
source = 'worktime-session-collector'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
}
|
||||
|
||||
$payload = $payloadObj | ConvertTo-Json -Depth 6 -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||
$ok = Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||
if (-not $ok) { Write-Verbose "Heartbeat not confirmed for user $($rec.username)" }
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "Heartbeat error: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleepSec
|
||||
}
|
||||
; }
|
||||
|
||||
return $records
|
||||
}
|
||||
|
||||
function Test-SessionIsActive {
|
||||
param([AllowNull()][string]$State)
|
||||
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||
$s = $State.Trim().ToLowerInvariant()
|
||||
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||
}
|
||||
|
||||
$cfg = Get-Config -Path $ConfigPath
|
||||
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||
$pulse = 120
|
||||
$sleepSec = if ($PollSeconds -gt 0) {
|
||||
$PollSeconds
|
||||
}
|
||||
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||
[int]$cfg.collector.pollSeconds
|
||||
}
|
||||
else {
|
||||
30
|
||||
}
|
||||
|
||||
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||
|
||||
while ($true) {
|
||||
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
$records = Get-SessionRecords
|
||||
if (-not $records -or $records.Count -eq 0) {
|
||||
$records = @([pscustomobject]@{
|
||||
username = $env:USERNAME
|
||||
sessionName = ''
|
||||
sessionId = (Get-Process -Id $PID).SessionId
|
||||
state = 'Unknown'
|
||||
})
|
||||
}
|
||||
|
||||
foreach ($rec in $records) {
|
||||
$payload = @{
|
||||
timestamp = $now
|
||||
duration = 0
|
||||
data = @{
|
||||
username = [string]$rec.username
|
||||
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||
sessionId = [int]$rec.sessionId
|
||||
sessionName = [string]$rec.sessionName
|
||||
state = [string]$rec.state
|
||||
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||
hostname = $hostValue
|
||||
source = 'worktime-session-collector'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||
}
|
||||
catch { Write-Error param(
|
||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||
[string]$Hostname,
|
||||
[int]$PollSeconds = 30
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-Config {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "Конфигурация не найдена: $Path"
|
||||
}
|
||||
|
||||
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Invoke-AwJsonPost {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$Json
|
||||
)
|
||||
|
||||
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||
}
|
||||
|
||||
function Ensure-Bucket {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||
)
|
||||
|
||||
try {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
return
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
$body = @{
|
||||
client = 'aw-worktime-session-collector'
|
||||
type = 'aw.worktime.session'
|
||||
hostname = $HostnameValue
|
||||
} | ConvertTo-Json -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||
}
|
||||
catch {
|
||||
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SessionRecords {
|
||||
$records = @()
|
||||
|
||||
try {
|
||||
$lines = quser 2>$null
|
||||
if (-not $lines) {
|
||||
return @()
|
||||
}
|
||||
|
||||
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||
if (-not $clean) {
|
||||
continue
|
||||
}
|
||||
|
||||
$parts = $clean -split '\s+'
|
||||
if ($parts.Count -lt 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
$sessionName = ''
|
||||
$sessionIdIndex = 2
|
||||
if ($parts[1] -match '^\d+$') {
|
||||
$sessionIdIndex = 1
|
||||
}
|
||||
else {
|
||||
$sessionName = $parts[1]
|
||||
}
|
||||
|
||||
$sessionId = 0
|
||||
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||
$sessionId = [int]$parts[$sessionIdIndex]
|
||||
}
|
||||
|
||||
$records += [pscustomobject]@{
|
||||
username = $parts[0]
|
||||
sessionName = $sessionName
|
||||
sessionId = $sessionId
|
||||
state = $parts[$sessionIdIndex + 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
}
|
||||
|
||||
return $records
|
||||
}
|
||||
|
||||
function Test-SessionIsActive {
|
||||
param([AllowNull()][string]$State)
|
||||
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||
$s = $State.Trim().ToLowerInvariant()
|
||||
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||
}
|
||||
|
||||
$cfg = Get-Config -Path $ConfigPath
|
||||
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||
$pulse = 120
|
||||
$sleepSec = if ($PollSeconds -gt 0) {
|
||||
$PollSeconds
|
||||
}
|
||||
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||
[int]$cfg.collector.pollSeconds
|
||||
}
|
||||
else {
|
||||
30
|
||||
}
|
||||
|
||||
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||
|
||||
while ($true) {
|
||||
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||
$records = Get-SessionRecords
|
||||
if (-not $records -or $records.Count -eq 0) {
|
||||
$records = @([pscustomobject]@{
|
||||
username = $env:USERNAME
|
||||
sessionName = ''
|
||||
sessionId = (Get-Process -Id $PID).SessionId
|
||||
state = 'Unknown'
|
||||
})
|
||||
}
|
||||
|
||||
foreach ($rec in $records) {
|
||||
$payload = @{
|
||||
timestamp = $now
|
||||
duration = 0
|
||||
data = @{
|
||||
username = [string]$rec.username
|
||||
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||
sessionId = [int]$rec.sessionId
|
||||
sessionName = [string]$rec.sessionName
|
||||
state = [string]$rec.state
|
||||
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||
hostname = $hostValue
|
||||
source = 'worktime-session-collector'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
|
||||
try {
|
||||
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||
}
|
||||
catch {
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleepSec
|
||||
}
|
||||
; }
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleepSec
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user