fix(windows): disable outlook popup and enforce smtp-only email monitoring
This commit is contained in:
@@ -1,13 +1,37 @@
|
||||
# Copy to /etc/activitywatch/aw-server.env and fill with real values.
|
||||
|
||||
# Core AW Server Configuration
|
||||
AW_SERVER_VERSION=0.13.2
|
||||
AW_SERVER_DOWNLOAD_URL=https://github.com/ActivityWatch/aw-server-rust/releases/download/v0.13.2/aw-server-rust-linux-x86_64.zip
|
||||
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/pebble.db
|
||||
AW_SERVER_LOG_DIR=/var/log/activitywatch
|
||||
AW_SERVER_USER=activitywatch
|
||||
AW_SERVER_GROUP=activitywatch
|
||||
|
||||
# Worktime API Configuration
|
||||
AW_WORKTIME_REPORT_BASE=http://10.10.10.13:5610
|
||||
AW_WORKTIME_TZ=Europe/Moscow
|
||||
|
||||
# DLP IOC Configuration
|
||||
AW_DLP_IOC_DIR=/opt/activitywatch/dlp-ioc/output
|
||||
|
||||
# DLP Policy Engine Configuration
|
||||
AW_DLP_POLICY_ENGINE_BIND_HOST=0.0.0.0
|
||||
AW_DLP_POLICY_ENGINE_PORT=5601
|
||||
AW_DLP_POLICY_ENGINE_DB_PATH=/var/lib/activitywatch/dlp-policy-engine.sqlite
|
||||
|
||||
# Logging Configuration
|
||||
AW_LOG_LEVEL=info
|
||||
AW_LOG_TO_JOURNAL=true
|
||||
AW_LOG_TO_FILE=true
|
||||
|
||||
# Health Check Configuration
|
||||
AW_HEALTH_CHECK_ENABLED=true
|
||||
AW_HEALTH_CHECK_INTERVAL=60
|
||||
|
||||
# Integration Test Configuration
|
||||
AW_INTEGRATION_TEST_ENABLED=false
|
||||
|
||||
@@ -7,10 +7,15 @@ Wants=activitywatch-server.service
|
||||
Type=simple
|
||||
EnvironmentFile=/etc/activitywatch/aw-server.env
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/aw-worktime-api.py
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StartLimitBurst=3
|
||||
StartLimitIntervalSec=60
|
||||
User=activitywatch
|
||||
Group=activitywatch
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=aw-worktime-api
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -4,12 +4,19 @@ After=network-online.target activitywatch-server.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Type=simple
|
||||
Environment=AW_SERVER_URL=http://127.0.0.1:5600
|
||||
Environment=AW_WORKTIME_HOST=SHARKON2025
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/aw-worktime-ui-bridge.py
|
||||
User=root
|
||||
Group=root
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StartLimitBurst=3
|
||||
StartLimitIntervalSec=120
|
||||
User=activitywatch
|
||||
Group=activitywatch
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=aw-worktime-ui-bridge
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
Description=AW DLP Policy Engine
|
||||
After=network.target activitywatch-server.service
|
||||
Wants=activitywatch-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
EnvironmentFile=/etc/activitywatch/aw-server.env
|
||||
WorkingDirectory=/opt/activitywatch/dlp-policy-engine
|
||||
ExecStart=/opt/activitywatch/dlp-policy-engine/.venv/bin/uvicorn policy_service:app --host ${AW_DLP_POLICY_ENGINE_BIND_HOST} --port ${AW_DLP_POLICY_ENGINE_PORT}
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StartLimitBurst=3
|
||||
StartLimitIntervalSec=60
|
||||
User=activitywatch
|
||||
Group=activitywatch
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=aw-dlp-policy-engine
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def build_policy_bundle(record: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not record:
|
||||
return {
|
||||
"active": False,
|
||||
"policyId": None,
|
||||
"name": None,
|
||||
"version": None,
|
||||
"checksum": None,
|
||||
"updatedAtUtc": None,
|
||||
"policy": None,
|
||||
}
|
||||
|
||||
return {
|
||||
"active": True,
|
||||
"policyId": record["id"],
|
||||
"name": record["name"],
|
||||
"version": record["current_version"],
|
||||
"checksum": record["checksum"],
|
||||
"updatedAtUtc": record["updated_at"],
|
||||
"policy": record["policy"],
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
|
||||
class PolicyDocument(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
version: int = 1
|
||||
defaults: dict[str, Any] = Field(
|
||||
default_factory=lambda: {
|
||||
"enabled": True,
|
||||
"cooldownSeconds": 300,
|
||||
"action": "alert",
|
||||
"severity": "medium",
|
||||
}
|
||||
)
|
||||
endpoint: dict[str, list[dict[str, Any]]] = Field(
|
||||
default_factory=lambda: {
|
||||
"clipboard": [],
|
||||
"usb": [],
|
||||
"print": [],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class PolicyCreateRequest(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=128)
|
||||
description: str | None = Field(default=None, max_length=2048)
|
||||
policy: PolicyDocument
|
||||
activate: bool = False
|
||||
actor: str | None = Field(default="api")
|
||||
|
||||
|
||||
class PolicyUpdateRequest(BaseModel):
|
||||
name: str | None = Field(default=None, min_length=1, max_length=128)
|
||||
description: str | None = Field(default=None, max_length=2048)
|
||||
policy: PolicyDocument | None = None
|
||||
activate: bool = False
|
||||
actor: str | None = Field(default="api")
|
||||
|
||||
|
||||
class PolicyActivateRequest(BaseModel):
|
||||
actor: str | None = Field(default="api")
|
||||
|
||||
|
||||
class PolicyRecord(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
description: str | None
|
||||
is_active: bool
|
||||
current_version: int
|
||||
checksum: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class PolicyVersionRecord(BaseModel):
|
||||
policy_id: int
|
||||
version: int
|
||||
checksum: str
|
||||
created_at: datetime
|
||||
created_by: str | None
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
|
||||
from policy_distributor import build_policy_bundle
|
||||
from policy_schema import PolicyActivateRequest, PolicyCreateRequest, PolicyUpdateRequest
|
||||
from policy_storage import PolicyStorage
|
||||
|
||||
|
||||
def _env(name: str, default: str) -> str:
|
||||
value = os.environ.get(name)
|
||||
return value if value not in (None, "") else default
|
||||
|
||||
|
||||
APP_NAME = "aw-dlp-policy-engine"
|
||||
APP_VERSION = "0.1.0"
|
||||
DB_PATH = _env("AW_DLP_POLICY_ENGINE_DB_PATH", "/var/lib/activitywatch/dlp-policy-engine.sqlite")
|
||||
storage = PolicyStorage(DB_PATH)
|
||||
|
||||
app = FastAPI(title=APP_NAME, version=APP_VERSION)
|
||||
|
||||
|
||||
@app.get("/healthz")
|
||||
def healthz() -> dict[str, str]:
|
||||
return {
|
||||
"status": "ok",
|
||||
"service": APP_NAME,
|
||||
"db_path": DB_PATH,
|
||||
"db_exists": str(Path(DB_PATH).exists()).lower(),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/0/dlp/policies")
|
||||
def list_policies() -> dict[str, object]:
|
||||
return {"items": storage.list_policies()}
|
||||
|
||||
|
||||
@app.post("/api/0/dlp/policies", status_code=201)
|
||||
def create_policy(payload: PolicyCreateRequest) -> dict[str, object]:
|
||||
try:
|
||||
item = storage.create_policy(
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
policy=payload.policy.model_dump(mode="json"),
|
||||
activate=payload.activate,
|
||||
actor=payload.actor,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return {"item": item}
|
||||
|
||||
|
||||
@app.get("/api/0/dlp/policies/active")
|
||||
def get_active_policy() -> dict[str, object]:
|
||||
item = storage.get_active_policy()
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="no active policy configured")
|
||||
return build_policy_bundle(item)
|
||||
|
||||
|
||||
@app.post("/api/0/dlp/policies/rollback")
|
||||
def rollback_active_policy(payload: PolicyActivateRequest) -> dict[str, object]:
|
||||
item = storage.rollback_active_policy(actor=payload.actor)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="no active policy configured")
|
||||
return {"item": item}
|
||||
|
||||
|
||||
@app.get("/api/0/dlp/policies/{policy_id}")
|
||||
def get_policy(policy_id: int) -> dict[str, object]:
|
||||
item = storage.get_policy(policy_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="policy not found")
|
||||
return {"item": item}
|
||||
|
||||
|
||||
@app.put("/api/0/dlp/policies/{policy_id}")
|
||||
def update_policy(policy_id: int, payload: PolicyUpdateRequest) -> dict[str, object]:
|
||||
try:
|
||||
item = storage.update_policy(
|
||||
policy_id=policy_id,
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
policy=payload.policy.model_dump(mode="json") if payload.policy is not None else None,
|
||||
activate=payload.activate,
|
||||
actor=payload.actor,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="policy not found")
|
||||
return {"item": item}
|
||||
|
||||
|
||||
@app.post("/api/0/dlp/policies/{policy_id}/activate")
|
||||
def activate_policy(policy_id: int, payload: PolicyActivateRequest) -> dict[str, object]:
|
||||
item = storage.activate_policy(policy_id=policy_id, actor=payload.actor)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="policy not found")
|
||||
return {"item": item}
|
||||
|
||||
|
||||
@app.delete("/api/0/dlp/policies/{policy_id}")
|
||||
def delete_policy(policy_id: int) -> dict[str, bool]:
|
||||
try:
|
||||
deleted = storage.delete_policy(policy_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
|
||||
if not deleted:
|
||||
raise HTTPException(status_code=404, detail="policy not found")
|
||||
return {"deleted": True}
|
||||
@@ -0,0 +1,272 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import sqlite3
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def canonical_policy_json(policy: dict[str, Any]) -> str:
|
||||
return json.dumps(policy, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def checksum_policy(policy: dict[str, Any]) -> str:
|
||||
return hashlib.sha256(canonical_policy_json(policy).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
class PolicyStorage:
|
||||
def __init__(self, db_path: str) -> None:
|
||||
self.db_path = Path(db_path)
|
||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._init_schema()
|
||||
|
||||
@contextmanager
|
||||
def connect(self) -> Iterator[sqlite3.Connection]:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
yield conn
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _init_schema(self) -> None:
|
||||
with self.connect() as conn:
|
||||
conn.executescript(
|
||||
"""
|
||||
PRAGMA journal_mode=WAL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS policies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 0,
|
||||
current_version INTEGER NOT NULL DEFAULT 1,
|
||||
checksum TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS policy_versions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
policy_id INTEGER NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
policy_json TEXT NOT NULL,
|
||||
checksum TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_by TEXT,
|
||||
rollback_of_version INTEGER,
|
||||
FOREIGN KEY(policy_id) REFERENCES policies(id),
|
||||
UNIQUE(policy_id, version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_policies_active ON policies(is_active);
|
||||
CREATE INDEX IF NOT EXISTS idx_policy_versions_policy ON policy_versions(policy_id, version DESC);
|
||||
"""
|
||||
)
|
||||
|
||||
def list_policies(self) -> list[dict[str, Any]]:
|
||||
with self.connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, name, description, is_active, current_version, checksum, created_at, updated_at
|
||||
FROM policies
|
||||
ORDER BY is_active DESC, updated_at DESC, id DESC
|
||||
"""
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def get_policy(self, policy_id: int) -> dict[str, Any] | None:
|
||||
with self.connect() as conn:
|
||||
policy_row = conn.execute(
|
||||
"""
|
||||
SELECT id, name, description, is_active, current_version, checksum, created_at, updated_at
|
||||
FROM policies
|
||||
WHERE id = ?
|
||||
""",
|
||||
(policy_id,),
|
||||
).fetchone()
|
||||
if not policy_row:
|
||||
return None
|
||||
|
||||
version_row = conn.execute(
|
||||
"""
|
||||
SELECT version, policy_json, checksum, created_at, created_by
|
||||
FROM policy_versions
|
||||
WHERE policy_id = ? AND version = ?
|
||||
""",
|
||||
(policy_id, policy_row["current_version"]),
|
||||
).fetchone()
|
||||
if not version_row:
|
||||
return None
|
||||
|
||||
result = dict(policy_row)
|
||||
result["policy"] = json.loads(version_row["policy_json"])
|
||||
result["version_created_at"] = version_row["created_at"]
|
||||
result["version_created_by"] = version_row["created_by"]
|
||||
return result
|
||||
|
||||
def get_active_policy(self) -> dict[str, Any] | None:
|
||||
with self.connect() as conn:
|
||||
row = conn.execute("SELECT id FROM policies WHERE is_active = 1 ORDER BY updated_at DESC LIMIT 1").fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return self.get_policy(int(row["id"]))
|
||||
|
||||
def create_policy(self, name: str, description: str | None, policy: dict[str, Any], activate: bool, actor: str | None) -> dict[str, Any]:
|
||||
checksum = checksum_policy(policy)
|
||||
now = utc_now()
|
||||
policy_json = canonical_policy_json(policy)
|
||||
with self.connect() as conn:
|
||||
if activate:
|
||||
conn.execute("UPDATE policies SET is_active = 0")
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
INSERT INTO policies(name, description, is_active, current_version, checksum, created_at, updated_at)
|
||||
VALUES(?, ?, ?, 1, ?, ?, ?)
|
||||
""",
|
||||
(name, description, 1 if activate else 0, checksum, now, now),
|
||||
)
|
||||
policy_id = int(cursor.lastrowid)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO policy_versions(policy_id, version, policy_json, checksum, created_at, created_by, rollback_of_version)
|
||||
VALUES(?, 1, ?, ?, ?, ?, NULL)
|
||||
""",
|
||||
(policy_id, policy_json, checksum, now, actor),
|
||||
)
|
||||
return self.get_policy(policy_id) # type: ignore[return-value]
|
||||
|
||||
def update_policy(
|
||||
self,
|
||||
policy_id: int,
|
||||
name: str | None,
|
||||
description: str | None,
|
||||
policy: dict[str, Any] | None,
|
||||
activate: bool,
|
||||
actor: str | None,
|
||||
) -> dict[str, Any] | None:
|
||||
current = self.get_policy(policy_id)
|
||||
if not current:
|
||||
return None
|
||||
|
||||
with self.connect() as conn:
|
||||
new_name = name if name is not None else current["name"]
|
||||
new_description = description if description is not None else current["description"]
|
||||
new_version = int(current["current_version"])
|
||||
new_checksum = current["checksum"]
|
||||
|
||||
if policy is not None:
|
||||
new_version += 1
|
||||
new_checksum = checksum_policy(policy)
|
||||
policy_json = canonical_policy_json(policy)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO policy_versions(policy_id, version, policy_json, checksum, created_at, created_by, rollback_of_version)
|
||||
VALUES(?, ?, ?, ?, ?, ?, NULL)
|
||||
""",
|
||||
(policy_id, new_version, policy_json, new_checksum, utc_now(), actor),
|
||||
)
|
||||
|
||||
if activate:
|
||||
conn.execute("UPDATE policies SET is_active = 0")
|
||||
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE policies
|
||||
SET name = ?, description = ?, is_active = ?, current_version = ?, checksum = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
new_name,
|
||||
new_description,
|
||||
1 if activate else current["is_active"],
|
||||
new_version,
|
||||
new_checksum,
|
||||
utc_now(),
|
||||
policy_id,
|
||||
),
|
||||
)
|
||||
return self.get_policy(policy_id)
|
||||
|
||||
def activate_policy(self, policy_id: int, actor: str | None) -> dict[str, Any] | None:
|
||||
current = self.get_policy(policy_id)
|
||||
if not current:
|
||||
return None
|
||||
|
||||
with self.connect() as conn:
|
||||
conn.execute("UPDATE policies SET is_active = 0")
|
||||
conn.execute(
|
||||
"UPDATE policies SET is_active = 1, updated_at = ? WHERE id = ?",
|
||||
(utc_now(), policy_id),
|
||||
)
|
||||
return self.get_policy(policy_id)
|
||||
|
||||
def rollback_active_policy(self, actor: str | None) -> dict[str, Any] | None:
|
||||
active = self.get_active_policy()
|
||||
if not active:
|
||||
return None
|
||||
|
||||
with self.connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT version, policy_json
|
||||
FROM policy_versions
|
||||
WHERE policy_id = ?
|
||||
ORDER BY version DESC
|
||||
LIMIT 2
|
||||
""",
|
||||
(active["id"],),
|
||||
).fetchall()
|
||||
if len(rows) < 2:
|
||||
return active
|
||||
|
||||
previous_version = int(rows[1]["version"])
|
||||
previous_policy = json.loads(rows[1]["policy_json"])
|
||||
rollback_version = int(active["current_version"]) + 1
|
||||
rollback_checksum = checksum_policy(previous_policy)
|
||||
now = utc_now()
|
||||
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO policy_versions(policy_id, version, policy_json, checksum, created_at, created_by, rollback_of_version)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
active["id"],
|
||||
rollback_version,
|
||||
canonical_policy_json(previous_policy),
|
||||
rollback_checksum,
|
||||
now,
|
||||
actor,
|
||||
previous_version,
|
||||
),
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE policies
|
||||
SET current_version = ?, checksum = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(rollback_version, rollback_checksum, now, active["id"]),
|
||||
)
|
||||
return self.get_policy(int(active["id"]))
|
||||
|
||||
def delete_policy(self, policy_id: int) -> bool:
|
||||
current = self.get_policy(policy_id)
|
||||
if not current:
|
||||
return False
|
||||
if current["is_active"]:
|
||||
raise ValueError("cannot delete active policy")
|
||||
|
||||
with self.connect() as conn:
|
||||
conn.execute("DELETE FROM policy_versions WHERE policy_id = ?", (policy_id,))
|
||||
conn.execute("DELETE FROM policies WHERE id = ?", (policy_id,))
|
||||
return True
|
||||
@@ -0,0 +1,3 @@
|
||||
fastapi==0.115.12
|
||||
uvicorn==0.34.2
|
||||
pydantic==2.11.4
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Service reliability script for AW services
|
||||
# Fixes common issues and ensures proper configuration
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ENV_FILE="/etc/activitywatch/aw-server.env"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
}
|
||||
|
||||
check_env_file() {
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
log "ERROR: Environment file not found: $ENV_FILE"
|
||||
return 1
|
||||
fi
|
||||
log "✓ Environment file exists"
|
||||
}
|
||||
|
||||
fix_permissions() {
|
||||
log "Fixing permissions..."
|
||||
|
||||
# Ensure proper ownership
|
||||
chown -R activitywatch:activitywatch /var/lib/activitywatch
|
||||
chown -R activitywatch:activitywatch /var/log/activitywatch
|
||||
chown -R activitywatch:activitywatch /opt/activitywatch
|
||||
|
||||
# Ensure proper permissions
|
||||
chmod 755 /var/lib/activitywatch
|
||||
chmod 755 /var/log/activitywatch
|
||||
chmod 755 /opt/activitywatch
|
||||
|
||||
log "✓ Permissions fixed"
|
||||
}
|
||||
|
||||
restart_services() {
|
||||
log "Restarting services..."
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
# Stop all services
|
||||
systemctl stop aw-worktime-api aw-worktime-ui-bridge activitywatch-server || true
|
||||
|
||||
# Wait for stop
|
||||
sleep 2
|
||||
|
||||
# Start in dependency order
|
||||
systemctl start activitywatch-server
|
||||
sleep 3
|
||||
systemctl start aw-worktime-api
|
||||
sleep 2
|
||||
systemctl start aw-worktime-ui-bridge
|
||||
|
||||
log "✓ Services restarted"
|
||||
}
|
||||
|
||||
enable_services() {
|
||||
log "Enabling services..."
|
||||
|
||||
systemctl enable activitywatch-server
|
||||
systemctl enable aw-worktime-api
|
||||
systemctl enable aw-worktime-ui-bridge
|
||||
|
||||
log "✓ Services enabled"
|
||||
}
|
||||
|
||||
setup_logrotate() {
|
||||
local logrotate_file="/etc/logrotate.d/activitywatch"
|
||||
|
||||
if [[ ! -f "$logrotate_file" ]]; then
|
||||
log "Setting up log rotation..."
|
||||
cp "$SCRIPT_DIR/logrotate.conf" "$logrotate_file"
|
||||
log "✓ Log rotation configured"
|
||||
else
|
||||
log "✓ Log rotation already configured"
|
||||
fi
|
||||
}
|
||||
|
||||
setup_health_check() {
|
||||
local health_script="/usr/local/bin/aw-health-check"
|
||||
local health_timer="/etc/systemd/system/aw-health-check.timer"
|
||||
local health_service="/etc/systemd/system/aw-health-check.service"
|
||||
|
||||
if [[ ! -f "$health_script" ]]; then
|
||||
log "Setting up health check..."
|
||||
cp "$SCRIPT_DIR/health-check.sh" "$health_script"
|
||||
chmod +x "$health_script"
|
||||
|
||||
# Create systemd timer for health checks
|
||||
cat > "$health_timer" << 'EOF'
|
||||
[Unit]
|
||||
Description=AW Health Check Timer
|
||||
Requires=aw-health-check.service
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*:0/5:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
EOF
|
||||
|
||||
cat > "$health_service" << 'EOF'
|
||||
[Unit]
|
||||
Description=AW Health Check
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/aw-health-check
|
||||
User=root
|
||||
Group=root
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable aw-health-check.timer
|
||||
systemctl start aw-health-check.timer
|
||||
|
||||
log "✓ Health check configured"
|
||||
else
|
||||
log "✓ Health check already configured"
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
log "=== AW Service Reliability Fix ==="
|
||||
|
||||
check_env_file || exit 1
|
||||
fix_permissions
|
||||
setup_logrotate
|
||||
setup_health_check
|
||||
restart_services
|
||||
enable_services
|
||||
|
||||
log
|
||||
log "=== Reliability Fix Complete ==="
|
||||
log "Check status with: systemctl status activitywatch-server aw-worktime-api aw-worktime-ui-bridge"
|
||||
log "Check health with: /usr/local/bin/aw-health-check"
|
||||
log "View logs with: journalctl -u activitywatch-server -u aw-worktime-api -u aw-worktime-ui-bridge -f"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Health check script for AW services
|
||||
# Returns 0 if all services are healthy, 1 otherwise
|
||||
|
||||
SERVICES=("activitywatch-server" "aw-worktime-api" "aw-worktime-ui-bridge")
|
||||
UNHEALTHY_SERVICES=()
|
||||
|
||||
check_service() {
|
||||
local service=$1
|
||||
if systemctl is-active --quiet "$service"; then
|
||||
echo "✓ $service is running"
|
||||
else
|
||||
echo "✗ $service is not running"
|
||||
UNHEALTHY_SERVICES+=("$service")
|
||||
fi
|
||||
}
|
||||
|
||||
check_api_endpoint() {
|
||||
local url=$1
|
||||
local service_name=$2
|
||||
|
||||
if curl -s --max-time 5 "$url" >/dev/null 2>&1; then
|
||||
echo "✓ $service_name API endpoint is responding"
|
||||
else
|
||||
echo "✗ $service_name API endpoint is not responding"
|
||||
UNHEALTHY_SERVICES+=("$service_name-api")
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== AW Services Health Check ==="
|
||||
echo "Timestamp: $(date)"
|
||||
echo
|
||||
|
||||
# Check systemd services
|
||||
for service in "${SERVICES[@]}"; do
|
||||
check_service "$service"
|
||||
done
|
||||
|
||||
echo
|
||||
|
||||
# Check API endpoints
|
||||
check_api_endpoint "http://127.0.0.1:5600/api/0/info" "activitywatch-server"
|
||||
check_api_endpoint "http://127.0.0.1:5610/reports/worktime/today" "aw-worktime-api"
|
||||
|
||||
echo
|
||||
|
||||
if [ ${#UNHEALTHY_SERVICES[@]} -eq 0 ]; then
|
||||
echo "✓ All services are healthy"
|
||||
exit 0
|
||||
else
|
||||
echo "✗ Unhealthy services: ${UNHEALTHY_SERVICES[*]}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,28 @@
|
||||
# Log rotation for ActivityWatch services
|
||||
# Place in /etc/logrotate.d/
|
||||
|
||||
/var/log/activitywatch/*.log {
|
||||
daily
|
||||
missingok
|
||||
rotate 30
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 644 activitywatch activitywatch
|
||||
postrotate
|
||||
systemctl reload activitywatch-server >/dev/null 2>&1 || true
|
||||
systemctl reload aw-worktime-api >/dev/null 2>&1 || true
|
||||
endscript
|
||||
}
|
||||
|
||||
/var/log/journal/*activitywatch*.journal {
|
||||
daily
|
||||
missingok
|
||||
rotate 7
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
postrotate
|
||||
systemctl restart systemd-journald >/dev/null 2>&1 || true
|
||||
endscript
|
||||
}
|
||||
Reference in New Issue
Block a user