diff --git a/ansible/post_validate_aw_windows.yml b/ansible/post_validate_aw_windows.yml new file mode 100644 index 0000000..2f28634 --- /dev/null +++ b/ansible/post_validate_aw_windows.yml @@ -0,0 +1,90 @@ +--- +- name: Post-deploy validation for Windows/RDP AWatch-rus + hosts: aw_windows + gather_facts: false + + vars: + aw_windows_launch_task_pattern: "ActivityWatch Launch *" + aw_windows_recovery_task_name: "ActivityWatch Recovery" + aw_windows_force_task_restart: true + aw_windows_api_smoke_check_enabled: true + aw_windows_api_smoke_check_bucket: "" + aw_windows_api_smoke_check_limit: 10 + + tasks: + - name: Принудительно запустить ActivityWatch recovery и launch tasks + when: aw_windows_force_task_restart | bool + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + Start-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}" + Get-ScheduledTask | + Where-Object TaskName -like "{{ aw_windows_launch_task_pattern }}" | + ForEach-Object { Start-ScheduledTask -TaskName $_.TaskName } + + - name: Получить Windows hostname для AW smoke-check bucket + when: aw_windows_api_smoke_check_enabled | bool + ansible.windows.win_command: powershell.exe -NoProfile -Command "$env:COMPUTERNAME" + register: aw_windows_hostname_result + changed_when: false + + - name: Вычислить AW AFK smoke-check bucket + when: aw_windows_api_smoke_check_enabled | bool + ansible.builtin.set_fact: + aw_windows_api_smoke_check_bucket_effective: >- + {{ + aw_windows_api_smoke_check_bucket + if (aw_windows_api_smoke_check_bucket | default('') | string | length) > 0 + else 'aw-watcher-afk_' ~ (aw_windows_hostname_result.stdout | trim) + }} + + - name: Дождаться свежих AFK событий на AW server + when: aw_windows_api_smoke_check_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 + 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: Выполнить валидацию и сохранить отчёт на целевом Windows host + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" ` + -ConfigPath "{{ aw_windows_state_root }}\deployment-config.json" + $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 validation failed. Report: {{ aw_windows_validation_remote_path }}" + } + + - name: Создать локальный каталог для validation reports + ansible.builtin.file: + path: "{{ aw_windows_validation_local_dir }}" + state: directory + mode: "0755" + delegate_to: localhost + + - 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.debug: + msg: + - "Validation OK on {{ inventory_hostname }}." + - "Report: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json" + diff --git a/windows/ActivityWatch.Windows.Common.psm1 b/windows/ActivityWatch.Windows.Common.psm1 index 9c9002c..6be39bd 100755 --- a/windows/ActivityWatch.Windows.Common.psm1 +++ b/windows/ActivityWatch.Windows.Common.psm1 @@ -49,7 +49,9 @@ function Get-ActivityWatchArchive { } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}.zip" -f $Version.TrimStart('v')) + $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' + $suffix = ([guid]::NewGuid().Guid.Substring(0, 8)) + $archivePath = Join-Path $WorkingRoot ("activitywatch-{0}-{1}-{2}.zip" -f $Version.TrimStart('v'), $stamp, $suffix) Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath return $archivePath } @@ -85,6 +87,16 @@ function Install-ActivityWatchPackage { New-ActivityWatchDirectory -Path $WorkingRoot New-ActivityWatchDirectory -Path $BackupRoot + # Ensure nothing is holding locks inside InstallRoot during upgrade. + foreach ($procName in @('aw-watcher-afk', 'aw-watcher-window', 'aw-server', 'aw-qt')) { + try { + Get-Process -Name $procName -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue + } + catch { + } + } + Start-Sleep -Seconds 2 + $extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid) if (Test-Path -LiteralPath $extractRoot) { Remove-Item -LiteralPath $extractRoot -Recurse -Force @@ -614,6 +626,10 @@ function Start-CollectorScriptIfNeeded { [int]`$SessionId ) + if ([string]::IsNullOrWhiteSpace(`$ScriptPath)) { + return + } + if (-not (Test-Path -LiteralPath `$ScriptPath)) { return } @@ -634,12 +650,13 @@ function Start-CollectorScriptIfNeeded { `$config = Get-DeploymentConfig -Path `$ConfigPath `$sessionId = (Get-Process -Id `$PID).SessionId `$installRoot = [string]`$config.paths.installRoot +`$stateRoot = [string]`$config.paths.stateRoot `$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port `$script:Hostname = `$env:COMPUTERNAME `$script:KnownBuckets = @{} `$collectorScript = [string]`$config.paths.collectorScript -`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { '' } -`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { '' } +`$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' } `$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe' `$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe' `$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port)