-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathUpdateLoxone.ps1
More file actions
3165 lines (2836 loc) · 186 KB
/
Copy pathUpdateLoxone.ps1
File metadata and controls
3165 lines (2836 loc) · 186 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<#
.SYNOPSIS
Automatically checks for Loxone Config updates, downloads, installs them, and updates MSs.
.DESCRIPTION
This script performs the following actions:
- Checks for the latest Loxone Config version from the official update XML.
- Compares the latest version with the currently installed version.
- If an update is needed:
- Downloads the update ZIP file.
- Verifies the download using CRC32 checksum and file size.
- Extracts the installer.
- Verifies the installers digital signature.
- Optionally closes running Loxone applications (Config, Monitor, LiveView).
- Runs the installer silently.
- Updates all MSs listed in a configuration file.
- Provides notifications to logged-in users about the update status.
- Logs all actions to a file.
- Can be run interactively or as a scheduled task.
.PARAMETER Channel
Specifies the update channel ('Test' or 'Public'). Defaults to 'Test'.
.PARAMETER DebugMode
Enables verbose debug logging to the console and log file.
.PARAMETER EnableCRC
Enables CRC32 checksum verification for the downloaded ZIP file. Defaults to $true.
.PARAMETER InstallMode
Specifies the installer mode ('silent' or 'verysilent'). Defaults to 'verysilent'.
.PARAMETER CloseApplications
If specified, attempts to close Loxone Config, Monitor, and LiveView before installation.
.PARAMETER ScriptSaveFolder
Specifies the directory where the script saves downloads and logs. Defaults to the script's directory or "$env:USERPROFILE\UpdateLoxone".
.PARAMETER MaxLogFileSizeMB
The maximum size in MB for the log file before rotation. Defaults to 1 MB.
.PARAMETER ScheduledTaskIntervalMinutes
The interval in minutes for the scheduled task repetition. Defaults to 10. Used only during task registration.
.PARAMETER RegisterTask
If specified, the script will register/update the scheduled task and then exit. Requires Admin rights.
.PARAMETER SkipUpdateIfAnyProcessIsRunning
If specified, the script will skip the update if Loxone Config, Monitor, or LiveView is detected running, instead of closing them (even if -CloseApplications is set).
.EXAMPLE
.\UpdateLoxone.ps1 -Channel Public -DebugMode
.EXAMPLE
.\UpdateLoxone.ps1 -CloseApplications
.EXAMPLE
- Uses the BurntToast module for notifications. Installs it if not present (requires internet).
- MS list file ('UpdateLoxoneMSList.txt') should be in the ScriptSaveFolder, containing one entry per line (e.g., user:pass@192.168.1.77 or 192.168.1.78).
- Ensure the UpdateLoxoneUtils.psm1 module is in the same directory as this script.
#>
[CmdletBinding()]
param(
[ValidateSet('Test', 'Public')]
[string]$Channel = "Test",
[switch]$DebugMode,
[bool]$EnableCRC = $true, # Changed back to bool with default
[ValidateSet('SILENT', 'VERYSILENT')] # Changed to uppercase to match InnoSetup standard, ValidateSet is case-insensitive by default
[string]$InstallMode = "SILENT", # Changed default
[switch]$CloseApplications,
[string]$ScriptSaveFolder = $null, # Default determined later
[int]$MaxLogFileSizeMB = 1,
[int]$ScheduledTaskIntervalMinutes = 10,
[switch]$RegisterTask, # New switch to trigger task registration
[switch]$SkipUpdateIfAnyProcessIsRunning, # New switch
[bool]$UpdateLoxoneApp = $true, # Changed back to bool with default
[ValidateSet('Test', 'Beta', 'Release', 'Internal', 'InternalV2', 'Latest')]
[string]$UpdateLoxoneAppChannel = "Latest", # New parameter for App channel
$PassedLogFile = $null, # Internal: Used when re-launching elevated to specify the log file
[switch]$EnforceSSLCertificate, # New switch to enforce SSL/TLS certificate validation for MS connections (default is to skip validation)
[switch]$Parallel, # Enable parallel execution mode
[int]$MaxConcurrency = 10, # Maximum concurrent operations for downloads/installs
[int]$MaxMSConcurrency = 10, # Maximum concurrent Miniserver updates
# Monitor Testing Parameters
[switch]$TestMonitor, # Test Monitor functionality only (no update performed)
[int]$TestMonitorDurationSeconds = 120, # How long to run Monitor in test mode
[switch]$KeepMonitorRunning, # Don't stop Monitor automatically (for manual testing)
[switch]$MonitorDiscoveryMode, # Enable extended .lxmon path discovery
[switch]$SkipPostUpdateHook, # Skip the post-update hook that runs after the MS updates
[string]$PostUpdateHook, # Path to the post-update hook script (default: post-update-hook.ps1 beside this script, or $env:UPDATELOXONE_POST_UPDATE_HOOK)
[switch]$VerboseHook # Log EVERY line the hook prints at INFO, not just the notable ones
)
# XML Signature Verification Function removed - Test showed it's not feasible with current structure
# Set up trap handler to clean up ThreadJobs on script termination
trap {
Write-Host "ERROR: Script terminated unexpectedly: $_" -ForegroundColor Red
Write-Host "Cleaning up ThreadJobs..." -ForegroundColor Yellow
try {
# Clean up all ThreadJobs
$allJobs = @(Get-Job -ErrorAction SilentlyContinue | Where-Object {
$_.Name -match "ProgressWorker|MS Worker|Config Worker|App Worker|Download Worker|Install Worker" -or
$_.Location -match "UpdateLoxone|LoxoneUtils"
})
if ($allJobs.Count -gt 0) {
Write-Host "Found $($allJobs.Count) job(s) to clean up" -ForegroundColor Yellow
foreach ($job in $allJobs) {
Stop-Job -Job $job -Force -ErrorAction SilentlyContinue
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
}
}
if (Get-Command Remove-ThreadJobs -ErrorAction SilentlyContinue) {
Remove-ThreadJobs -Context "Trap Handler Cleanup"
}
} catch {
Write-Host "Error during trap cleanup: $_" -ForegroundColor Red
}
# Exit with error code
exit 1
}
# Determine script's own directory reliably
$script:MyScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Definition
# Configuration defaults (can be overridden by user settings)
$script:UseParallelExecution = $false # Default to sequential execution unless explicitly enabled
$script:PrecheckConfig = $null # Default: no prechecks configured
# Load user configuration if exists
$configPath = Join-Path $script:MyScriptRoot "UpdateLoxone.config.json"
if (Test-Path $configPath) {
try {
$userConfig = Get-Content $configPath -Raw | ConvertFrom-Json
if ($null -ne $userConfig.UseParallelExecution) {
$script:UseParallelExecution = $userConfig.UseParallelExecution
Write-Host "Loaded UseParallelExecution from config: $($script:UseParallelExecution)" -ForegroundColor Green
}
if ($null -ne $userConfig.Prechecks) {
$script:PrecheckConfig = $userConfig.Prechecks
Write-Host "Loaded Prechecks configuration from config" -ForegroundColor Green
}
}
catch {
Write-Warning "Failed to load configuration from ${configPath}: $_"
}
}
# --- Precheck Helper Functions ---
function Get-MSCredentialsFromList {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$TargetHost,
[Parameter(Mandatory=$true)][string]$MSListPath
)
if (-not (Test-Path $MSListPath)) { return $null }
$lines = @(Get-Content $MSListPath)
foreach ($line in $lines) {
if ([string]::IsNullOrWhiteSpace($line) -or $line.Trim().StartsWith('#')) { continue }
$url = $line.Split(',')[0].Trim()
if ($url -notmatch '^[a-zA-Z]+://') { $url = "http://" + $url }
# Extract host from URL (same regex as MiniserverCache.psm1)
if ($url -match '@([^/:]+)' -and $Matches[1] -eq $TargetHost) {
if ($url -match '^(?<scheme>[^:]+)://(?<credentials>[^@]+)@(?<hostinfo>.+)$') {
$scheme = $Matches.scheme
$credPart = $Matches.credentials
if ($credPart -match '^(?<user>[^:]+):(?<pass>.+)$') {
return @{
Username = $Matches.user
Password = $Matches.pass
Scheme = $scheme
}
}
}
}
}
return $null
}
function Test-UpdatePrechecks {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]$PrecheckConfig,
[Parameter(Mandatory=$true)][string]$MSListPath
)
$results = @()
# --- Time Window Check ---
if ($PrecheckConfig.TimeWindow -and $PrecheckConfig.TimeWindow.Start -and $PrecheckConfig.TimeWindow.End) {
try {
$now = Get-Date
$startTime = [datetime]::ParseExact($PrecheckConfig.TimeWindow.Start, "HH:mm", $null)
$endTime = [datetime]::ParseExact($PrecheckConfig.TimeWindow.End, "HH:mm", $null)
$currentMinutes = $now.Hour * 60 + $now.Minute
$startMinutes = $startTime.Hour * 60 + $startTime.Minute
$endMinutes = $endTime.Hour * 60 + $endTime.Minute
if ($startMinutes -le $endMinutes) {
# Same-day window (e.g., 01:00-06:00)
$inWindow = ($currentMinutes -ge $startMinutes) -and ($currentMinutes -lt $endMinutes)
} else {
# Midnight-crossing window (e.g., 23:00-05:00)
$inWindow = ($currentMinutes -ge $startMinutes) -or ($currentMinutes -lt $endMinutes)
}
$windowStr = "$($PrecheckConfig.TimeWindow.Start)-$($PrecheckConfig.TimeWindow.End)"
$nowStr = Get-Date -Format 'HH:mm'
$results += @{
Check = "TimeWindow"
Passed = $inWindow
Description = if ($inWindow) { "Current time $nowStr is within window $windowStr" } else { "Current time $nowStr is outside window $windowStr" }
Expected = $windowStr
Actual = $nowStr
Error = $null
}
}
catch {
$results += @{
Check = "TimeWindow"
Passed = $false
Description = "Failed to parse time window configuration"
Expected = "$($PrecheckConfig.TimeWindow.Start)-$($PrecheckConfig.TimeWindow.End)"
Actual = $null
Error = $_.Exception.Message
}
}
}
# --- Miniserver State Checks ---
if ($PrecheckConfig.MiniserverStates) {
foreach ($stateCheck in $PrecheckConfig.MiniserverStates) {
$checkResult = @{
Check = "MiniserverState"
Passed = $false
Description = $stateCheck.Description
Expected = $stateCheck.ExpectedValue
Actual = $null
Error = $null
}
try {
$creds = Get-MSCredentialsFromList -TargetHost $stateCheck.Host -MSListPath $MSListPath
if (-not $creds) {
$checkResult.Error = "No credentials found for $($stateCheck.Host) in MS list"
$results += $checkResult
continue
}
$encodedEndpoint = [Uri]::EscapeDataString($stateCheck.Endpoint)
$uri = "$($creds.Scheme)://$($stateCheck.Host)/jdev/sps/io/$encodedEndpoint"
$webClient = New-Object System.Net.WebClient
try {
$webClient.Encoding = [System.Text.Encoding]::UTF8
$authBytes = [System.Text.Encoding]::ASCII.GetBytes("$($creds.Username):$($creds.Password)")
$webClient.Headers.Add("Authorization", "Basic $([Convert]::ToBase64String($authBytes))")
$response = $webClient.DownloadString($uri)
}
finally {
$webClient.Dispose()
}
# Parse JSON response: {"LL": {"control": "...", "value": "0", "Code": "200"}}
$parsed = $response | ConvertFrom-Json
if ($parsed.LL.Code -ne "200") {
$checkResult.Error = "MS returned Code=$($parsed.LL.Code) for endpoint $($stateCheck.Endpoint)"
$results += $checkResult
continue
}
$actualValue = $parsed.LL.value
$checkResult.Actual = $actualValue
$checkResult.Passed = ($actualValue -eq $stateCheck.ExpectedValue)
}
catch {
$checkResult.Error = "Failed to query $($stateCheck.Host): $($_.Exception.Message)"
}
$results += $checkResult
}
}
# --- Build Return Value ---
$allPassed = ($results | Where-Object { -not $_.Passed }).Count -eq 0
$failureSummary = ""
if (-not $allPassed) {
$failedResults = @($results | Where-Object { -not $_.Passed })
$summaryLines = @()
foreach ($f in $failedResults) {
if ($f.Check -eq "TimeWindow") {
$summaryLines += "Time: $($f.Actual) not in $($f.Expected)"
} elseif ($f.Error) {
$summaryLines += "$($f.Description): $($f.Error)"
} else {
$summaryLines += "$($f.Description): expected=$($f.Expected), actual=$($f.Actual)"
}
}
$failureSummary = $summaryLines -join "`n"
}
return @{
Passed = $allPassed
Results = $results
FailureSummary = $failureSummary
}
}
$Global:PersistentToastInitialized = $false # Ensure toast is created fresh each run
$script:SystemRelaunchExitOccurred = $false # Flag to indicate if script is exiting due to system re-launch
# Clean up any leaked env vars from previous runs in same PowerShell session
# Worker threads set these but crashes/interrupts can prevent cleanup, causing toast init to fail on next run
Remove-Item env:LOXONE_PARALLEL_MODE -ErrorAction SilentlyContinue
Remove-Item env:LOXONE_PARALLEL_WORKER -ErrorAction SilentlyContinue
Remove-Item env:LOXONE_WORKER_NAME -ErrorAction SilentlyContinue
Remove-Item env:LOXONE_IS_WORKER -ErrorAction SilentlyContinue
# --- Early SYSTEM Context Check and Minimal Module Load for Re-launch ---
$script:IsRunningAsSystemEarlyCheck = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value -eq 'S-1-5-18'
$script:InitialSystemInvocation = $script:IsRunningAsSystemEarlyCheck -and (-not $PSBoundParameters.ContainsKey('PassedLogFile') -or [string]::IsNullOrWhiteSpace($PassedLogFile))
if ($script:InitialSystemInvocation) {
Write-Host "INFO: (UpdateLoxone.ps1) Initial SYSTEM context detected. Performing minimal module load for re-launch." -ForegroundColor Yellow
# Define minimal paths for essential modules
$LoggingModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'LoxoneUtils\LoxoneUtils.Logging.psm1'
$RunAsUserModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'LoxoneUtils\LoxoneUtils.RunAsUser.psm1'
if (-not (Test-Path $LoggingModulePath)) {
Write-Error "FATAL: Essential module LoxoneUtils.Logging.psm1 not found at '$LoggingModulePath'."
exit 1
}
if (-not (Test-Path $RunAsUserModulePath)) {
Write-Error "FATAL: Essential module LoxoneUtils.RunAsUser.psm1 not found at '$RunAsUserModulePath'."
exit 1
}
# Minimal Log Setup for SYSTEM context re-launch
$SystemLogDir = Join-Path -Path $PSScriptRoot -ChildPath "Logs" # Or a more appropriate SYSTEM log location
if (-not (Test-Path -Path $SystemLogDir -PathType Container)) {
try { New-Item -Path $SystemLogDir -ItemType Directory -Force -ErrorAction Stop | Out-Null }
catch { Write-Error "FATAL: Failed to create SYSTEM log directory '$SystemLogDir'."; exit 1 }
}
$Global:LogFile = Join-Path -Path $SystemLogDir -ChildPath "UpdateLoxone_SYSTEM_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
Add-Content -Path $Global:LogFile -Value "$(Get-Date -Format 'u') [INFO] SYSTEM context: Initializing for re-launch. LogFile: $Global:LogFile"
try {
Import-Module $LoggingModulePath -Force -ErrorAction Stop
Import-Module $RunAsUserModulePath -Force -ErrorAction Stop
Write-Log -Message "(UpdateLoxone.ps1 - SYSTEM) Minimal modules (Logging, RunAsUser) imported." -Level INFO
} catch {
$errMsg = "CRITICAL ERROR: (UpdateLoxone.ps1 - SYSTEM) Failed to import essential modules for re-launch. Error: $($_.Exception.Message)"
Write-Host $errMsg -ForegroundColor Red
Add-Content -Path $Global:LogFile -Value "$(Get-Date -Format 'u') $errMsg -- Original Error Record: ($($_ | Out-String))"
exit 1
}
# Re-launch Logic (copied and adapted from later in the script)
Write-Log -Message "(UpdateLoxone.ps1 - SYSTEM) Attempting to re-launch as current user..." -Level INFO
$forwardedArgs = @()
foreach ($key in $PSBoundParameters.Keys) {
if ($key -ne "WasLaunchedBySystem") {
$value = $PSBoundParameters[$key]
if ($value -is [switch]) {
if ($value.IsPresent) { $forwardedArgs += "-$key" }
} elseif ($null -ne $value) {
$escapedValue = $value -replace '''', ''''''
if ($value -match '[\s''`"]') { $forwardedArgs += "-$key '$escapedValue'" }
else { $forwardedArgs += "-$key $value" }
}
}
}
# DO NOT pass -PassedLogFile, so the user process creates its own log.
# $forwardedArgs += "-PassedLogFile '$($Global:LogFile -replace '''', '''''')'"
$argumentString = $forwardedArgs -join " "
$thisScriptPath = $MyInvocation.MyCommand.Definition
Write-Log -Message "(UpdateLoxone.ps1 - SYSTEM) Re-launching '$thisScriptPath' as user with arguments: $argumentString" -Level DEBUG
try {
$powershellExePath = Get-Command powershell.exe | Select-Object -ExpandProperty Source
$psArgsForUser = "-NoProfile -ExecutionPolicy Bypass -File `"$thisScriptPath`" $argumentString"
Write-Log -Message "(UpdateLoxone.ps1 - SYSTEM) Re-launch command: '$powershellExePath' $psArgsForUser" -Level DEBUG
Invoke-AsCurrentUser -FilePath $powershellExePath -Arguments $psArgsForUser -Visible:$false -Elevated:$true -ErrorAction Stop
Write-Log -Message "(UpdateLoxone.ps1 - SYSTEM) Successfully initiated script re-launch in user session. Exiting SYSTEM process." -Level INFO
} catch {
Write-Log -Message "(UpdateLoxone.ps1 - SYSTEM) CRITICAL: Failed to re-launch script as user. Error: $($_.Exception.Message). Exiting SYSTEM process." -Level ERROR
if ($Global:LogFile -and (Get-Command Invoke-LogFileRotation -ErrorAction SilentlyContinue)) {
Invoke-LogFileRotation -LogFilePath $Global:LogFile -MaxArchiveCount 24 -ErrorAction SilentlyContinue
}
$script:SystemRelaunchExitOccurred = $true # Set flag for main finally block
exit 1
}
# If Invoke-AsCurrentUser succeeded, rotate the SYSTEM log now.
if ($Global:LogFile -and (Get-Command Invoke-LogFileRotation -ErrorAction SilentlyContinue)) {
Write-Log -Message "(UpdateLoxone.ps1 - SYSTEM) Re-launch successful. Rotating SYSTEM log: $Global:LogFile" -Level INFO
Invoke-LogFileRotation -LogFilePath $Global:LogFile -MaxArchiveCount 24 -ErrorAction SilentlyContinue
}
$script:SystemRelaunchExitOccurred = $true # Set flag for main finally block
exit 0 # Exit SYSTEM process after successful re-launch initiation and its own log rotation
} else {
# --- Full Module Load (Not initial SYSTEM invocation or already re-launched) ---
# Record script start time for total runtime calculation
$script:ScriptStartTime = Get-Date
Write-Host "INFO: (UpdateLoxone.ps1) Proceeding with full LoxoneUtils module manifest import." -ForegroundColor Cyan
$UtilsModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'LoxoneUtils\LoxoneUtils.psd1'
if (-not (Test-Path $UtilsModulePath)) {
Write-Error "FATAL: Helper module manifest 'LoxoneUtils.psd1' not found at '$UtilsModulePath'. Script cannot continue."
exit 1
}
# Attempt to forcefully remove any pre-existing LoxoneUtils modules to ensure a clean import
Write-Host "INFO: (UpdateLoxone.ps1) Attempting to forcefully remove any existing LoxoneUtils modules before main import..." -ForegroundColor Cyan
# Remove all loaded LoxoneUtils modules
$loadedModules = Get-Module -Name "LoxoneUtils*"
if ($loadedModules) {
Write-Host "INFO: (UpdateLoxone.ps1) Found $($loadedModules.Count) loaded LoxoneUtils module(s) to remove." -ForegroundColor Yellow
$loadedModules | ForEach-Object {
Write-Host "DEBUG: (UpdateLoxone.ps1) Removing loaded module: $($_.Name)" -ForegroundColor Gray
Remove-Module -ModuleInfo $_ -Force -ErrorAction SilentlyContinue
}
}
# Also check for modules that might be available but not imported
$availableModules = Get-Module -Name "LoxoneUtils*" -ListAvailable | Where-Object { $_.ModuleBase -like "$PSScriptRoot*" }
if ($availableModules) {
Write-Host "INFO: (UpdateLoxone.ps1) Found $($availableModules.Count) available LoxoneUtils module(s) in script directory." -ForegroundColor Yellow
# Force PowerShell to forget about these modules
$availableModules | ForEach-Object {
$moduleName = $_.Name
Write-Host "DEBUG: (UpdateLoxone.ps1) Clearing module cache for: $moduleName" -ForegroundColor Gray
# Remove from module table if present
if ($ExecutionContext.SessionState.Module.GetExportedCommands().ContainsKey($moduleName)) {
$ExecutionContext.SessionState.Module.RemoveModule($moduleName)
}
}
}
# Clear any cached type data that might interfere
Write-Host "INFO: (UpdateLoxone.ps1) Clearing cached type data for System.Security.AccessControl.ObjectSecurity..." -ForegroundColor Cyan
try {
# Remove the problematic type data if it exists
$typeData = Get-TypeData -TypeName "System.Security.AccessControl.ObjectSecurity"
if ($typeData) {
Remove-TypeData -TypeData $typeData -ErrorAction SilentlyContinue
}
} catch {
Write-Host "DEBUG: (UpdateLoxone.ps1) Could not clear type data: $_" -ForegroundColor Gray
}
# Import BurntToast with timeout and graceful fallback (toast is optional, not critical)
Write-Host "INFO: (UpdateLoxone.ps1) Checking for BurntToast module..." -ForegroundColor Cyan
$burntToastReady = $false
$btTimeoutSec = 30
# Step 1: Install if not available
if (-not (Get-Module -ListAvailable -Name BurntToast)) {
Write-Host "INFO: (UpdateLoxone.ps1) BurntToast not found. Installing (timeout: ${btTimeoutSec}s)..." -ForegroundColor Yellow
try {
# Ensure NuGet provider is available (prevents interactive prompts that hang)
if (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) {
Write-Host "INFO: (UpdateLoxone.ps1) Installing NuGet provider..." -ForegroundColor Cyan
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser -ErrorAction Stop | Out-Null
}
# Trust PSGallery to prevent prompts
$gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') {
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue
}
$installJob = Start-Job -ScriptBlock {
Install-Module BurntToast -Scope CurrentUser -Force -Confirm:$false -SkipPublisherCheck -ErrorAction Stop
}
$completed = $installJob | Wait-Job -Timeout $btTimeoutSec
if ($completed) {
Receive-Job $installJob -ErrorAction Stop
Write-Host "INFO: (UpdateLoxone.ps1) BurntToast installed successfully." -ForegroundColor Green
} else {
Stop-Job $installJob -ErrorAction SilentlyContinue
Write-Host "WARN: (UpdateLoxone.ps1) BurntToast install timed out after ${btTimeoutSec}s." -ForegroundColor Yellow
}
Remove-Job $installJob -Force -ErrorAction SilentlyContinue
} catch {
Write-Host "WARN: (UpdateLoxone.ps1) BurntToast install failed: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
# Step 2: Import with timeout via ThreadJob (same-process, module loaded into shared AppDomain)
# ThreadJob is killable on timeout, unlike direct Import-Module which can't be interrupted
if (Get-Module -Name BurntToast) {
$btMod = Get-Module -Name BurntToast
Write-Host "INFO: (UpdateLoxone.ps1) BurntToast already loaded - version=$($btMod.Version), path=$($btMod.Path)" -ForegroundColor Green
$burntToastReady = $true
} elseif (Get-Module -ListAvailable -Name BurntToast) {
# Pick BurntToast version per host:
# - PS 7+ (pwsh, often MSIX-packaged): prefer 1.x (uses ToastNotificationManagerCompat,
# different code path that may behave better with WinRT under MSIX/Canary builds).
# - PS 5.1 (powershell.exe, unpackaged): prefer 0.x — supports -AppId on Submit/Update-BTNotification
# so toasts use Loxone Config branding.
# If preferred major isn't installed, fall back to whatever's available.
$preferredVersion = $null
$availableVersions = @(Get-Module -ListAvailable -Name BurntToast | Sort-Object Version)
if ($PSVersionTable.PSVersion.Major -ge 7) {
$cand = $availableVersions | Where-Object { $_.Version.Major -ge 1 } | Select-Object -Last 1
$rationale = '1.x preferred for PS 7+ (different WinRT code path)'
} else {
$cand = $availableVersions | Where-Object { $_.Version.Major -eq 0 } | Select-Object -Last 1
$rationale = '0.x preferred for PS 5.1 (supports -AppId for Loxone branding)'
}
if ($cand) {
$preferredVersion = $cand.Version
Write-Host "INFO: (UpdateLoxone.ps1) PSVersion=$($PSVersionTable.PSVersion) - $rationale. Selecting BurntToast $preferredVersion (available: $(($availableVersions | ForEach-Object { $_.Version }) -join ', '))" -ForegroundColor Cyan
} else {
Write-Host "INFO: (UpdateLoxone.ps1) PSVersion=$($PSVersionTable.PSVersion) - preferred major not installed, falling back to default (latest available: $($availableVersions[-1].Version))" -ForegroundColor Yellow
}
Write-Host "INFO: (UpdateLoxone.ps1) Importing BurntToast (timeout: ${btTimeoutSec}s)..." -ForegroundColor Cyan
try {
# Ensure ThreadJob module is available (PS7+ built-in, PS5.1 needs module)
if (-not (Get-Command Start-ThreadJob -ErrorAction SilentlyContinue)) {
Import-Module ThreadJob -ErrorAction SilentlyContinue
}
if (Get-Command Start-ThreadJob -ErrorAction SilentlyContinue) {
# ThreadJob runs in the same process - module assemblies shared with main session
$importJob = Start-ThreadJob -ScriptBlock {
param($Ver)
if ($Ver) { Import-Module BurntToast -RequiredVersion $Ver -ErrorAction Stop }
else { Import-Module BurntToast -ErrorAction Stop }
} -ArgumentList $preferredVersion
$completed = $importJob | Wait-Job -Timeout $btTimeoutSec
if ($completed) {
Receive-Job $importJob -ErrorAction Stop | Out-Null
Remove-Job $importJob -Force -ErrorAction SilentlyContinue
# ThreadJob loaded it in same process; verify it's accessible
if (Get-Module -Name BurntToast) {
$burntToastReady = $true
$btMod = Get-Module -Name BurntToast
Write-Host "INFO: (UpdateLoxone.ps1) BurntToast imported (via ThreadJob) - version=$($btMod.Version), path=$($btMod.Path)" -ForegroundColor Green
} else {
# ThreadJob loaded in its runspace only - try quick main-session import
# Since ThreadJob succeeded, assemblies are cached; main should be fast
if ($preferredVersion) { Import-Module BurntToast -RequiredVersion $preferredVersion -ErrorAction Stop }
else { Import-Module BurntToast -ErrorAction Stop }
$burntToastReady = $true
$btMod = Get-Module -Name BurntToast
Write-Host "INFO: (UpdateLoxone.ps1) BurntToast imported - version=$($btMod.Version), path=$($btMod.Path)" -ForegroundColor Green
}
} else {
Stop-Job $importJob -ErrorAction SilentlyContinue
Remove-Job $importJob -Force -ErrorAction SilentlyContinue
Write-Host "WARN: (UpdateLoxone.ps1) BurntToast import hung after ${btTimeoutSec}s - continuing without toasts." -ForegroundColor Yellow
}
} else {
# Fallback: no ThreadJob available, direct import (no timeout possible)
Write-Host "WARN: (UpdateLoxone.ps1) ThreadJob unavailable - doing direct import (no timeout protection)." -ForegroundColor Yellow
if ($preferredVersion) { Import-Module BurntToast -RequiredVersion $preferredVersion -ErrorAction Stop }
else { Import-Module BurntToast -ErrorAction Stop }
$burntToastReady = $true
$btMod = Get-Module -Name BurntToast
Write-Host "INFO: (UpdateLoxone.ps1) BurntToast imported - version=$($btMod.Version), path=$($btMod.Path)" -ForegroundColor Green
}
} catch {
Write-Host "WARN: (UpdateLoxone.ps1) BurntToast import failed: $($_.Exception.Message)" -ForegroundColor Yellow
}
} else {
Write-Host "WARN: (UpdateLoxone.ps1) BurntToast module not available after install attempt." -ForegroundColor Yellow
}
if (-not $burntToastReady) {
Write-Host "INFO: (UpdateLoxone.ps1) Toast notifications disabled for this session. Script continues without them." -ForegroundColor Yellow
$Global:SuppressLoxoneToastInit = $true
}
Write-Host "INFO: (UpdateLoxone.ps1) Attempting to import LoxoneUtils manifest: '$UtilsModulePath'..." -ForegroundColor Cyan
try {
# Remove any cached versions first
Get-Module LoxoneUtils* | Remove-Module -Force -ErrorAction SilentlyContinue
# Force fresh import with all flags to ensure no caching
Import-Module $UtilsModulePath -Force -DisableNameChecking -Global -ErrorAction Stop
Write-Host "INFO: (UpdateLoxone.ps1) LoxoneUtils manifest import command completed." -ForegroundColor Cyan
# Explicitly check if Write-Log is now available. This is a critical safeguard.
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
Write-Host "CRITICAL ERROR: (UpdateLoxone.ps1) Write-Log command is NOT available even after importing LoxoneUtils manifest. This suggests a profound problem within the LoxoneUtils module. Script cannot continue." -ForegroundColor Red
exit 1
}
Write-Log -Message "(UpdateLoxone.ps1) Successfully loaded LoxoneUtils module via manifest. Write-Log is available." -Level INFO
# Log invocation parameters dynamically
$paramStrings = @()
foreach ($key in ($PSBoundParameters.Keys | Sort-Object)) {
$val = $PSBoundParameters[$key]
if ($val -is [switch]) { $paramStrings += "-$key" }
elseif ($val -is [securestring]) { $paramStrings += "-$key ****" }
else { $paramStrings += "-$key '$val'" }
}
$invocationLine = if ($paramStrings.Count -gt 0) { $paramStrings -join ' ' } else { '(no explicit parameters)' }
Write-Log -Message "(UpdateLoxone.ps1) Invocation: .\UpdateLoxone.ps1 $invocationLine" -Level INFO
Write-Log -Message "(UpdateLoxone.ps1) PowerShell: $($PSVersionTable.PSVersion) | User: $env:USERNAME | Host: $env:COMPUTERNAME" -Level INFO
}
catch {
$errorMessage = "CRITICAL ERROR: (UpdateLoxone.ps1) Failed to import LoxoneUtils module manifest ('$UtilsModulePath'). Error details: $($_.Exception.Message)"
Write-Host $errorMessage -ForegroundColor Red
# Capture the full error record to a string first for safer inclusion in the log message
$errorRecordString = $($_ | Out-String)
# Attempt to log to a fallback file if $global:LogFile might have been set by a partial init
if ($global:LogFile) { Add-Content -Path $global:LogFile -Value "$(Get-Date -Format 'u') $errorMessage -- Original Error Record: $errorRecordString" }
else { Add-Content -Path (Join-Path $script:MyScriptRoot "UpdateLoxone_FallbackCritical.log") -Value "$(Get-Date -Format 'u') $errorMessage -- Original Error Record: $errorRecordString" }
exit 1
}
# --- Sanitize PassedLogFile if provided (BEFORE Initialize-ScriptWorkflow) ---
# This block is now part of the 'else' for full module load, as Write-Log is needed.
if ($PSBoundParameters.ContainsKey('PassedLogFile') -and $null -ne $PassedLogFile) {
Write-Log -Message "(UpdateLoxone.ps1) Initial PassedLogFile received: '$PassedLogFile'" -Level DEBUG
$OriginalPassedLogFile = $PassedLogFile
$CleanedLogFile = $PassedLogFile
while ($CleanedLogFile.StartsWith("'") -and $CleanedLogFile.EndsWith("'") -and $CleanedLogFile.Length -ge 2) {
$CleanedLogFile = $CleanedLogFile.Substring(1, $CleanedLogFile.Length - 2).Trim()
}
if ($CleanedLogFile -ne $OriginalPassedLogFile) {
Write-Log -Message "(UpdateLoxone.ps1) Sanitized PassedLogFile from '$OriginalPassedLogFile' to '$CleanedLogFile'." -Level INFO
$PassedLogFile = $CleanedLogFile
$PSBoundParameters['PassedLogFile'] = $CleanedLogFile
} else {
Write-Log -Message "(UpdateLoxone.ps1) PassedLogFile '$OriginalPassedLogFile' did not require sanitization." -Level DEBUG
}
}
# --- Initialize Script Workflow ---
Write-Log -Message "(UpdateLoxone.ps1) Calling Initialize-ScriptWorkflow..." -Level INFO
$scriptContext = Initialize-ScriptWorkflow -BoundParameters $PSBoundParameters -PSScriptRoot $script:MyScriptRoot -MyInvocation $MyInvocation
if (-not $scriptContext.Succeeded) {
Write-Log -Message "(UpdateLoxone.ps1) Initialize-ScriptWorkflow failed: $($scriptContext.Reason). Error: $($scriptContext.Error | Out-String)" -Level ERROR
if (Get-Command Show-FinalStatusToast -ErrorAction SilentlyContinue) {
Show-FinalStatusToast -StatusMessage "FATAL: Script initialization failed: $($scriptContext.Reason)" -Success $false -LogFileToShow $scriptContext.LogFile
}
exit 1
}
Write-Log -Message "(UpdateLoxone.ps1) Initialize-ScriptWorkflow completed. Reason: $($scriptContext.Reason)" -Level INFO
# Handle specific reasons from Initialize-ScriptWorkflow
# SystemRelaunchRequired should ideally not happen if this 'else' block is reached,
# as the initial SYSTEM check should have handled it. But keep for robustness.
if ($scriptContext.Reason -eq "SystemRelaunchRequired") {
Write-Log -Message "(UpdateLoxone.ps1) SystemRelaunchRequired detected (unexpectedly after full module load). Attempting to re-launch as current user..." -Level WARN
$forwardedArgs = @()
foreach ($key in $PSBoundParameters.Keys) {
if ($key -ne "WasLaunchedBySystem") {
$value = $PSBoundParameters[$key]
if ($value -is [switch]) {
if ($value.IsPresent) { $forwardedArgs += "-$key" }
} elseif ($null -ne $value) {
$escapedValue = $value -replace '''', ''''''
if ($value -match '[\s''`"]') { $forwardedArgs += "-$key '$escapedValue'" }
else { $forwardedArgs += "-$key $value" }
}
}
}
if ($scriptContext.LogFile) {
$forwardedArgs += "-PassedLogFile '$($scriptContext.LogFile -replace '''', '''''')'"
}
$argumentString = $forwardedArgs -join " "
$thisScriptPath = $MyInvocation.MyCommand.Definition
Write-Log -Message "(UpdateLoxone.ps1) Re-launching '$thisScriptPath' as user with arguments: $argumentString" -Level DEBUG
try {
$powershellExePath = Get-Command powershell.exe | Select-Object -ExpandProperty Source
$psArgsForUser = "-NoProfile -ExecutionPolicy Bypass -File `"$thisScriptPath`" $argumentString"
Write-Log -Message "(UpdateLoxone.ps1) Re-launch command: '$powershellExePath' $psArgsForUser" -Level DEBUG
Invoke-AsCurrentUser -FilePath $powershellExePath -Arguments $psArgsForUser -Visible:$false -Elevated:$true -ErrorAction Stop
Write-Log -Message "(UpdateLoxone.ps1) Successfully initiated script re-launch in user session. Exiting process." -Level INFO
} catch {
Write-Log -Message "(UpdateLoxone.ps1) CRITICAL: Failed to re-launch script as user (from full load context). Error: $($_.Exception.Message). Exiting process." -Level ERROR
if ($scriptContext.LogFile -and (Get-Command Invoke-LogFileRotation -ErrorAction SilentlyContinue)) {
Invoke-LogFileRotation -LogFilePath $scriptContext.LogFile -MaxArchiveCount 24 -ErrorAction SilentlyContinue
}
$script:SystemRelaunchExitOccurred = $true
exit 1
}
$script:SystemRelaunchExitOccurred = $true
exit 0
}
} # End of the main 'else' block for full module load vs minimal SYSTEM load
if ($scriptContext.Reason -eq "ActionRegisterTaskAndExit") {
Write-Log -Message "(UpdateLoxone.ps1) -RegisterTask specified and confirmed by Initialize-ScriptWorkflow." -Level INFO
if (-not $scriptContext.IsAdminRun) {
Write-Log -Level WARN -Message "(UpdateLoxone.ps1) Task registration requested via -RegisterTask, but script is not running as Admin. Please re-run as Admin."
Show-FinalStatusToast -StatusMessage "Task registration requires Admin rights." -Success $false -LogFileToShow $scriptContext.LogFile
exit 1
}
Write-Log -Message "(UpdateLoxone.ps1) Attempting to register/update scheduled task '$($scriptContext.TaskName)'..." -Level INFO
try {
Register-ScheduledTaskForScript -ScriptPath $MyInvocation.MyCommand.Definition -TaskName $scriptContext.TaskName -ScheduledTaskIntervalMinutes $scriptContext.Params.ScheduledTaskIntervalMinutes -ErrorAction Stop
Write-Log -Message "(UpdateLoxone.ps1) Task '$($scriptContext.TaskName)' registration/update successful. Exiting script." -Level INFO
Show-FinalStatusToast -StatusMessage "Scheduled task '$($scriptContext.TaskName)' registered/updated." -Success $true -LogFileToShow $scriptContext.LogFile
exit 0
} catch {
$taskRegErrorMsg = "(UpdateLoxone.ps1) Failed to register/update task '$($scriptContext.TaskName)': $($_.Exception.Message)"
Write-Log -Message $taskRegErrorMsg -Level ERROR
Write-Log -Message "Error Record: ($($_ | Out-String))" -Level DEBUG
Show-FinalStatusToast -StatusMessage $taskRegErrorMsg -Success $false -LogFileToShow $scriptContext.LogFile
exit 1
}
}
# ═══════════════════════════════════════════════════════════════════════════════
# TEST MONITOR MODE
# ═══════════════════════════════════════════════════════════════════════════════
if ($TestMonitor) {
Write-Log -Message "╔════════════════════════════════════════════════════════════╗" -Level INFO
Write-Log -Message "║ TEST MONITOR MODUS (Kein Update) ║" -Level INFO
Write-Log -Message "╚════════════════════════════════════════════════════════════╝" -Level INFO
try {
# 1. Lade MS-Liste
$msListPath = Join-Path $scriptContext.ScriptSaveFolder "UpdateLoxoneMSList.txt"
if (-not (Test-Path $msListPath)) {
Write-Log -Message "✗ MS-Liste nicht gefunden: $msListPath" -Level ERROR
Write-Log -Message "Bitte erstellen Sie UpdateLoxoneMSList.txt im Verzeichnis: $($scriptContext.ScriptSaveFolder)" -Level ERROR
Show-FinalStatusToast -StatusMessage "MS-Liste nicht gefunden" -Success $false -LogFileToShow $scriptContext.LogFile
exit 1
}
Write-Log -Message "Lade Miniserver-Liste: $msListPath" -Level INFO
# Einfacher Parser (wird später durch erweiterten ersetzt)
$lines = Get-Content $msListPath | Where-Object { $_ -notmatch '^\s*#' -and $_ -notmatch '^\s*$' }
$miniServers = @()
foreach ($line in $lines) {
$parts = $line -split ','
$uri = [System.Uri]$parts[0]
$msName = $uri.Host
$msEntry = @{
Url = $parts[0]
Name = $msName
EnableMonitor = if ($parts.Count -gt 3 -and $parts[3] -eq 'true') { $true } else { $false }
}
$miniServers += [PSCustomObject]$msEntry
}
$monitorMS = $miniServers | Where-Object { $_.EnableMonitor -eq $true }
if ($monitorMS.Count -eq 0) {
Write-Log -Message "✗ Keine Miniserver mit enable_monitor=true gefunden" -Level WARN
# Empty log line removed
Write-Log -Message "Bitte UpdateLoxoneMSList.txt anpassen:" -Level INFO
Write-Log -Message "Format: URL,version,timestamp,enable_monitor" -Level INFO
Write-Log -Message "Beispiel: https://admin:pass@192.168.1.77,,,true" -Level INFO
Show-FinalStatusToast -StatusMessage "Keine Miniserver für Monitor konfiguriert" -Success $false -LogFileToShow $scriptContext.LogFile
exit 0
}
Write-Log -Message "✓ Gefundene Miniserver für Monitor-Test: $($monitorMS.Count)" -Level INFO
foreach ($ms in $monitorMS) {
Write-Log -Message " - $($ms.Name)" -Level INFO
}
# Empty log line removed
# 2. Finde monitor.exe
Write-Log -Message "Suche loxonemonitor.exe..." -Level INFO
# Versuche Loxone Config Installation zu finden
$configPath = Get-InstalledApplicationPath -AppName "Loxone Config"
if (-not $configPath) {
Write-Log -Message "✗ Loxone Config nicht installiert" -Level ERROR
Show-FinalStatusToast -StatusMessage "Loxone Config nicht gefunden" -Success $false -LogFileToShow $scriptContext.LogFile
exit 1
}
Write-Log -Message "Loxone Config gefunden: $configPath" -Level DEBUG
$monitorExe = Find-LoxoneMonitorExe -LoxoneConfigInstallPath $configPath
if (-not $monitorExe) {
Write-Log -Message "✗ loxonemonitor.exe nicht gefunden in: $configPath" -Level ERROR
Show-FinalStatusToast -StatusMessage "loxonemonitor.exe nicht gefunden" -Success $false -LogFileToShow $scriptContext.LogFile
exit 1
}
Write-Log -Message "✓ loxonemonitor.exe: $monitorExe" -Level INFO
# Empty log line removed
# 3. Starte Monitor
Write-Log -Message "Starte Monitor-Prozess..." -Level INFO
$monitorProc = Start-LoxoneMonitorProcess -MonitorExePath $monitorExe -WorkingDirectory $scriptContext.ScriptSaveFolder
Write-Log -Message "✓ Monitor gestartet (PID: $($monitorProc.Id))" -Level INFO
# Empty log line removed
# 4. Aktiviere Logging auf MS
$localIP = Get-LocalIPAddress
Write-Log -Message "Lokale IP für MS-Logging: $localIP" -Level INFO
# Empty log line removed
foreach ($ms in $monitorMS) {
Write-Log -Message "Aktiviere Logging auf MS '$($ms.Name)'..." -Level INFO
$success = Enable-MiniserverLogging -MiniserverUrl $ms.Url -TargetIP $localIP
if ($success) {
Write-Log -Message "✓ MS '$($ms.Name)' sendet jetzt Logs an $localIP" -Level INFO
}
else {
Write-Log -Message "✗ Konnte Logging auf MS '$($ms.Name)' nicht aktivieren" -Level WARN
}
}
# Empty log line removed
Write-Log -Message "═══════════════════════════════════════════════════════════" -Level INFO
Write-Log -Message "Monitor läuft - suche nach .lxmon Dateien..." -Level INFO
Write-Log -Message "Dauer: $TestMonitorDurationSeconds Sekunden" -Level INFO
if ($MonitorDiscoveryMode) {
Write-Log -Message "Discovery-Modus: AKTIV (erweiterte Suche)" -Level INFO
}
Write-Log -Message "═══════════════════════════════════════════════════════════" -Level INFO
# Empty log line removed
# 5. Warte kurz bis Logs eintreffen
Write-Log -Message "Warte 10 Sekunden bis erste Logs eintreffen..." -Level INFO
Start-Sleep -Seconds 10
# 6. Discovery ausführen
Write-Log -Message "Starte .lxmon Discovery..." -Level INFO
$foundPath = Find-LxmonFiles -MonitorProcessId $monitorProc.Id -DiscoveryMode:$MonitorDiscoveryMode
if ($foundPath) {
# Empty log line removed
Write-Log -Message "═══════════════════════════════════════════════════════════" -Level INFO
Write-Log -Message "✓✓✓ .lxmon Speicherort gefunden!" -Level INFO
Write-Log -Message "═══════════════════════════════════════════════════════════" -Level INFO
Write-Log -Message "Pfad: $foundPath" -Level INFO
# Empty log line removed
Write-Log -Message "WICHTIG: Diesen Pfad für finalen Code notieren!" -Level INFO
# Empty log line removed
# Zeige gefundene Dateien
$files = Get-ChildItem -Path $foundPath -Filter "*.lxmon" -ErrorAction SilentlyContinue
if ($files) {
Write-Log -Message "Gefundene .lxmon Dateien: $($files.Count)" -Level INFO
foreach ($file in $files) {
$sizeKB = [math]::Round($file.Length / 1KB, 2)
Write-Log -Message " - $($file.Name) (${sizeKB} KB, LastWrite: $($file.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss')))" -Level INFO
}
}
else {
Write-Log -Message "⚠ Verzeichnis existiert, aber noch keine .lxmon Dateien" -Level WARN
Write-Log -Message "Tipp: Führe Aktionen auf dem Miniserver aus um Logs zu erzeugen" -Level INFO
}
}
else {
# Empty log line removed
Write-Log -Message "═══════════════════════════════════════════════════════════" -Level INFO
Write-Log -Message "✗✗✗ KEINE .lxmon Dateien gefunden!" -Level WARN
Write-Log -Message "═══════════════════════════════════════════════════════════" -Level INFO
# Empty log line removed
Write-Log -Message "Mögliche Ursachen:" -Level INFO
Write-Log -Message "1. Monitor hat noch keine Logs empfangen (Miniserver sendet nicht)" -Level INFO
Write-Log -Message "2. Logs werden in unbekanntem Verzeichnis gespeichert" -Level INFO
# Empty log line removed
Write-Log -Message "Bitte manuell im Dateisystem suchen:" -Level INFO
Write-Log -Message " - C:\Windows\Temp (und Unterordner)" -Level INFO
Write-Log -Message " - %USERPROFILE%\AppData\Local\Temp" -Level INFO
Write-Log -Message " - %USERPROFILE%\Documents\Loxone" -Level INFO
# Empty log line removed
if (-not $MonitorDiscoveryMode) {
Write-Log -Message "Tipp: Verwende -MonitorDiscoveryMode für erweiterte Suche" -Level INFO
}
}
# 7. Ggf. weiterlaufen lassen
if ($KeepMonitorRunning) {
# Empty log line removed
Write-Log -Message "═══════════════════════════════════════════════════════════" -Level INFO
Write-Log -Message "Monitor läuft weiter (KeepMonitorRunning aktiv)" -Level INFO
Write-Log -Message "Drücke STRG+C zum Beenden" -Level INFO
Write-Log -Message "═══════════════════════════════════════════════════════════" -Level INFO
while ($true) {
Start-Sleep -Seconds 10
# Zeige periodisch Status
$currentFiles = Get-ChildItem -Path $foundPath -Filter "*.lxmon" -ErrorAction SilentlyContinue
if ($currentFiles) {
Write-Log -Message "[$(Get-Date -Format 'HH:mm:ss')] Monitor aktiv - $($currentFiles.Count) .lxmon Datei(en)" -Level INFO
}
}
}
}
catch {
Write-Log -Message "FEHLER im Test-Monitor Modus: $($_.Exception.Message)" -Level ERROR
Write-Log -Message "Stack Trace: $($_.ScriptStackTrace)" -Level DEBUG
}
finally {
# 8. Cleanup
# Empty log line removed
Write-Log -Message "Beende Test-Monitor Modus..." -Level INFO
# Deaktiviere Logging auf allen MS
if ($monitorMS) {
foreach ($ms in $monitorMS) {
Write-Log -Message "Deaktiviere Logging auf MS '$($ms.Name)'..." -Level INFO
Disable-MiniserverLogging -MiniserverUrl $ms.Url
}
}
# Stoppe Monitor
Stop-LoxoneMonitorProcess
# Empty log line removed
Write-Log -Message "╔════════════════════════════════════════════════════════════╗" -Level INFO
Write-Log -Message "║ TEST MONITOR MODUS BEENDET ║" -Level INFO
Write-Log -Message "╚════════════════════════════════════════════════════════════╝" -Level INFO
if ($scriptContext -and $scriptContext.LogFile) {
Show-FinalStatusToast -StatusMessage "Test-Monitor Modus abgeschlossen" -Success $true -LogFileToShow $scriptContext.LogFile
}
}
exit 0
}
# ═══════════════════════════════════════════════════════════════════════════════
# END TEST MONITOR MODE
# ═══════════════════════════════════════════════════════════════════════════════
if ($scriptContext.IsInteractive -and -not $scriptContext.IsAdminRun -and -not $scriptContext.IsRunningAsSystem -and -not $scriptContext.Params.RegisterTask) {
Write-Log -Level DEBUG -Message "(UpdateLoxone.ps1) Checking if task '$($scriptContext.TaskName)' needs registration (interactive, non-admin)."
if (-not (Test-ScheduledTask -TaskName $scriptContext.TaskName -ErrorAction SilentlyContinue)) {
Write-Log -Message "(UpdateLoxone.ps1) Task '$($scriptContext.TaskName)' not found or inaccessible. Interactive non-admin run. Suggesting elevation or -RegisterTask." -Level INFO
Write-Host "INFO: (UpdateLoxone.ps1) Scheduled task '$($scriptContext.TaskName)' is not registered. To set it up, please run this script as Administrator with the -RegisterTask switch." -ForegroundColor Yellow
}
}
# Clean up any dead/stale jobs from previous runs before starting.
# Aggressive cleanup: a healthy previous run removes its own jobs, so anything not currently
# Running at startup is by definition stale. Generic Start-ThreadJob auto-named jobs (Job1234)
# don't match a Worker name regex, so we filter by STATE rather than name.
Write-Log -Message "(UpdateLoxone.ps1) Checking for and cleaning up any dead threads from previous runs..." -Level INFO
try {
$existingJobs = @(Get-Job -ErrorAction SilentlyContinue)
if ($existingJobs.Count -gt 0) {
$stateSummary = ($existingJobs | Group-Object State | ForEach-Object { "$($_.Name)=$($_.Count)" }) -join ', '
Write-Log -Message "Found $($existingJobs.Count) existing job(s). States: $stateSummary" -Level WARN
# Anything not Running is stale. Running jobs are only removed if they match our worker
# patterns OR have been running > 30 minutes (orphaned from a long-dead previous run).
$toRemove = $existingJobs | Where-Object {
($_.State -in @('Completed','Failed','Stopped','Disconnected','Suspended','Blocked')) -or
($_.State -eq 'Running' -and $_.Name -match "ProgressWorker|MS Worker|Config Worker|App Worker|Download Worker|Install Worker") -or
($_.State -eq 'Running' -and $_.PSBeginTime -and ((Get-Date) - $_.PSBeginTime).TotalMinutes -gt 30)
}
$removed = 0; $failed = 0
foreach ($job in $toRemove) {
try {
if ($job.State -eq 'Running') {
Stop-Job -Job $job -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 200
}
Remove-Job -Job $job -Force -ErrorAction Stop
$removed++
} catch {
$failed++
Write-Log -Message " Failed to remove job ID=$($job.Id) Name='$($job.Name)' State=$($job.State): $($_.Exception.Message)" -Level WARN
}
}
Write-Log -Message "Cleanup result: removed=$removed, failed=$failed" -Level INFO
$remaining = @(Get-Job -ErrorAction SilentlyContinue)
if ($remaining.Count -gt 0) {
Write-Log -Message "Remaining $($remaining.Count) job(s) (legitimately running or stuck):" -Level WARN
foreach ($job in $remaining) {
$rt = if ($job.PSBeginTime) { "$([Math]::Round(((Get-Date) - $job.PSBeginTime).TotalMinutes,1))min" } else { '?' }
Write-Log -Message " - ID=$($job.Id) Name='$($job.Name)' State=$($job.State) Runtime=$rt" -Level WARN
}
}
} else {
Write-Log -Message "No existing jobs found. Starting with clean slate." -Level INFO
}
} catch {
Write-Log -Message "Error during thread cleanup: $_. Continuing anyway..." -Level WARN
}
$Global:PersistentToastInitialized = $false
$script:ErrorOccurred = $false
$script:LastErrorLine = 0
$scriptGlobalState = [pscustomobject]@{