diff --git a/AGENTS.md b/AGENTS.md index 7da3583..ff33b57 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ Project type: Ansible-driven infrastructure, workstation/server provisioning, an - Void desktops: `ikaros`, `nymph` - Ubuntu workstation: `deadalus` - Ubuntu server: `prometheus` -- Workstation topology now supports Linux host + Ubuntu dev and Windows host + Ubuntu WSL dev as separate layers +- Workstation topology now supports Linux host + Ubuntu dev and Windows 11 host + Ubuntu WSL dev as separate layers - The WSL dev environment is intended to be managed by running Ansible locally from inside the distro, while the Windows host is managed remotely via PSRP - Most hosts use `ansible_connection: local` - Current playbook layering: `all:!workstation_host_windows -> dotfiles_common`, `void -> packages_void + services_runit + profile_desktop_common + profile_desktop_i3 + profile_desktop_sway + profile_desktop_hyprland + profile_desktop_host`, `workstation_dev_ubuntu -> packages_ubuntu + services_systemd + profile_workstation_dev_common`, `workstation_host_linux -> profile_workstation_gnome`, `workstation_dev_wsl -> packages_ubuntu + services_systemd + profile_workstation_dev_common + profile_workstation_dev_wsl`, `workstation_host_windows -> profile_workstation_host_windows`, `ubuntu_server -> packages_ubuntu + services_systemd + profile_server` @@ -156,7 +156,7 @@ Use the narrowest command matching the changed area. - `profile_workstation_dev_common` carries the Ubuntu dev layer shared by native workstation and WSL Ubuntu - `profile_workstation_gnome` carries Linux host-only GNOME setup, extensions, and UFW - `profile_workstation_dev_wsl` carries WSL-specific Ubuntu tweaks such as `systemd` and PSRP Python dependencies -- `profile_workstation_host_windows` manages the Windows host via PSRP over HTTPS using `negotiate` by default and installs host applications via `winget` +- `profile_workstation_host_windows` manages the Windows 11 host via PSRP over HTTPS using `negotiate` by default and installs host applications via `winget` - `deadalus-wsl` is modeled as a local inventory target intended to be run from inside the Ubuntu WSL distro - Future Windows taskbar pinning work should be done from a real Windows session after discovering installed app identifiers on that host, then applied via a Windows 11 taskbar layout policy with `PinListPlacement="Replace"` - Do not auto-restart `emptty` during playbook runs on active desktop hosts; prefer a manual restart from SSH or another TTY after the run diff --git a/README.md b/README.md index 5122beb..0519ce1 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Lo stato attuale del profilo desktop include, tra le altre cose: Sistemi operativi supportati: - Ubuntu LTS nativa -- Windows host + Ubuntu WSL +- Windows 11 host + Ubuntu WSL Desktop environment host Linux: @@ -113,7 +113,7 @@ Il profilo workstation e agganciato al playbook principale e ora distingue: - layer dev Ubuntu condiviso tra workstation Linux nativa e Ubuntu in WSL - layer host Linux GNOME -- layer host Windows con bootstrap WSL, remoting `PSRP` su `HTTPS/5986`, gestione app via `winget` e VS Code lato Windows +- layer host Windows 11 con bootstrap WSL, remoting `PSRP` su `HTTPS/5986`, gestione app via `winget` e VS Code lato Windows - layer WSL dedicato per sviluppo con `systemd` Lo stato attuale del profilo workstation include: diff --git a/ansible/roles/profile_workstation_host_windows/tasks/main.yml b/ansible/roles/profile_workstation_host_windows/tasks/main.yml index 775890d..9b45ad6 100644 --- a/ansible/roles/profile_workstation_host_windows/tasks/main.yml +++ b/ansible/roles/profile_workstation_host_windows/tasks/main.yml @@ -16,14 +16,20 @@ $buildNumber = [int]$os.OsBuildNumber $Ansible.Result = @{ product_name = $os.WindowsProductName - version = $os.WindowsVersion build_number = $buildNumber - is_windows_11 = ($os.WindowsProductName -like 'Windows 11*') -or ($buildNumber -ge 22000) - is_windows_10 = ($os.WindowsProductName -like 'Windows 10*') -and ($buildNumber -lt 22000) + is_windows_11 = $buildNumber -ge 22000 } $Ansible.Changed = $false register: windows_host_version_state +- name: Fail when Windows host is not Windows 11 + tags: [packages] + ansible.builtin.fail: + msg: >- + workstation_host_windows is supported only on Windows 11. Detected {{ windows_host_version_state.result.product_name }} + build {{ windows_host_version_state.result.build_number }}. + when: not (windows_host_version_state.result.is_windows_11 | default(false)) + - name: Enable dark mode for Windows apps tags: [packages] ansible.windows.win_regedit: @@ -62,9 +68,7 @@ data: 0 type: dword register: windows_11_widgets_state - when: - - windows_hide_taskbar_widgets | default(false) - - windows_host_version_state.result.is_windows_11 | default(false) + when: windows_hide_taskbar_widgets | default(false) - name: Disable Windows 11 widgets via policy tags: [packages] @@ -74,21 +78,7 @@ data: 0 type: dword register: windows_11_widgets_policy_state - when: - - windows_hide_taskbar_widgets | default(false) - - windows_host_version_state.result.is_windows_11 | default(false) - -- name: Hide Windows 10 news and interests taskbar widget - tags: [packages] - ansible.windows.win_regedit: - path: HKCU:\Software\Microsoft\Windows\CurrentVersion\Feeds - name: ShellFeedsTaskbarViewMode - data: 2 - type: dword - register: windows_10_widgets_state - when: - - windows_hide_taskbar_widgets | default(false) - - windows_host_version_state.result.is_windows_10 | default(false) + when: windows_hide_taskbar_widgets | default(false) - name: Note when Windows shell settings may require sign out tags: [packages] @@ -104,7 +94,6 @@ or (windows_taskbar_search_state is changed) or (windows_11_widgets_state is changed) or (windows_11_widgets_policy_state is changed) - or (windows_10_widgets_state is changed) - name: Ensure winget is available on Windows host tags: [packages] @@ -140,6 +129,7 @@ $packageId = '{{ item.id }}' $packageName = '{{ item.name | default(item.id) }}' $packageScope = '{{ item.scope | default('' ) }}' + $packageIdRegex = [regex]::Escape($packageId) $installArgs = @( 'install' '--id', $packageId @@ -154,10 +144,12 @@ $installArgs += @('--scope', $packageScope) } - $installed = & winget list --id $packageId --exact --accept-source-agreements --disable-interactivity 2>$null - if ($LASTEXITCODE -eq 0 -and $installed -match [regex]::Escape($packageId)) { - $Ansible.Changed = $false - return + if ($packageScope -ne 'user') { + $installed = & winget list --id $packageId --exact --accept-source-agreements --disable-interactivity 2>$null + if ($LASTEXITCODE -eq 0 -and $installed -match $packageIdRegex) { + $Ansible.Changed = $false + return + } } if ($packageScope -eq 'user') { @@ -170,11 +162,31 @@ $taskScriptPath = Join-Path $env:TEMP "$taskName.ps1" $stdoutPath = Join-Path $env:TEMP "$taskName.stdout.log" $stderrPath = Join-Path $env:TEMP "$taskName.stderr.log" + $resultPath = Join-Path $env:TEMP "$taskName.result.json" $quotedArgs = ($installArgs | ForEach-Object { '"' + ($_ -replace '"', '""') + '"' }) -join ', ' $taskScript = @( + "`$ErrorActionPreference = 'Stop'", + "function Test-PackageInstalled {", + " `$installed = & winget.exe list --id '$packageId' --exact --accept-source-agreements --disable-interactivity 2>`$null", + " return (`$LASTEXITCODE -eq 0 -and `$installed -match '$packageIdRegex')", + "}", + "`$result = @{ changed = `$false; installed = `$false }", + "if (-not (Test-PackageInstalled)) {", "`$installArgs = @($quotedArgs)", "`$process = Start-Process -FilePath 'winget.exe' -ArgumentList `$installArgs -Wait -PassThru -NoNewWindow -RedirectStandardOutput '$stdoutPath' -RedirectStandardError '$stderrPath'", - "exit `$process.ExitCode" + " if (`$process.ExitCode -ne 0) { exit `$process.ExitCode }", + " `$deadline = (Get-Date).AddMinutes(2)", + " do {", + " Start-Sleep -Seconds 2", + " `$result.installed = Test-PackageInstalled", + " } while ((Get-Date) -lt `$deadline -and -not `$result.installed)", + " if (-not `$result.installed) { throw 'Package was not detected after the user-scoped install completed.' }", + " `$result.changed = `$true", + "}", + "else {", + " `$result.installed = `$true", + "}", + "`$result | ConvertTo-Json -Compress | Set-Content -Path '$resultPath' -Encoding Ascii" ) -join [Environment]::NewLine Set-Content -Path $taskScriptPath -Value $taskScript -Encoding Ascii @@ -203,10 +215,22 @@ $stdout = if (Test-Path $stdoutPath) { Get-Content -Path $stdoutPath -Raw } else { '' } throw "Failed to install $packageName with winget. Exit code: $($taskInfo.LastTaskResult). Stdout: $stdout Stderr: $stderr" } + + if (-not (Test-Path $resultPath)) { + throw "Failed to install $packageName with winget: no result file was produced by the user-scoped scheduled task." + } + + $result = Get-Content -Path $resultPath -Raw | ConvertFrom-Json + if (-not $result.installed) { + throw "Failed to install $packageName with winget: the package is still not detected after the user-scoped install task finished." + } + + $Ansible.Changed = [bool]$result.changed + return } finally { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue - Remove-Item -Path $taskScriptPath, $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue + Remove-Item -Path $taskScriptPath, $stdoutPath, $stderrPath, $resultPath -Force -ErrorAction SilentlyContinue } } else {