Skip to content

Commit 744ff74

Browse files
committed
Improve cache file writer with more robust handling
1 parent fa66de0 commit 744ff74

File tree

6 files changed

+70
-6
lines changed

6 files changed

+70
-6
lines changed

doc/100-General/10-Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic
2929
* [#556](https://github.com/Icinga/icinga-powershell-framework/pull/556) Fixes the certificate folder not present during first installation, preventing permissions properly set from the start which might cause issues once required
3030
* [#562](https://github.com/Icinga/icinga-powershell-framework/pull/562) Fixes corrupt Performance Data, in case plugins were executed inside a JEA context without the REST-Api
3131
* [#563](https://github.com/Icinga/icinga-powershell-framework/pull/563) Fixes checks like MSSQL using arguments of type `SecureString` not being usable with the Icinga for Windows REST-Api
32+
* [#565](https://github.com/Icinga/icinga-powershell-framework/pull/565) Fixes internal cache file writer and reader to store changes inside a `.tmp` file first and validating the file state and content, before applying it to the actual file to prevent data corruption
3233

3334
### Enhancements
3435

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
function Copy-IcingaCacheTempFile()
2+
{
3+
param (
4+
[string]$CacheFile = '',
5+
[string]$CacheTmpFile = ''
6+
);
7+
8+
# Copy the new file over the old one
9+
Copy-ItemSecure -Path $CacheTmpFile -Destination $CacheFile -Force | Out-Null;
10+
# Remove the old file
11+
Remove-ItemSecure -Path $CacheTmpFile -Retries 5 -Force | Out-Null;
12+
}

lib/core/cache/Get-IcingaCacheData.psm1

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
.PARAMETER KeyName
1717
This is the actual cache file located under icinga-powershell-framework/cache/<space>/<CacheStore>/<KeyName>.json
1818
Please note to only provide the name without the '.json' apendix. This is done by the module itself
19+
.PARAMETER TempFile
20+
To safely write data, by default Icinga for Windows will write all content into a .tmp file at the same location with the same name
21+
before applying it to the proper file. Set this argument to read the content of a temp file instead
1922
.INPUTS
2023
System.String
2124
.OUTPUTS
@@ -29,13 +32,19 @@ function Get-IcingaCacheData()
2932
param(
3033
[string]$Space,
3134
[string]$CacheStore,
32-
[string]$KeyName
35+
[string]$KeyName,
36+
[switch]$TempFile = $FALSE
3337
);
3438

3539
$CacheFile = Join-Path -Path (Join-Path -Path (Join-Path -Path (Get-IcingaCacheDir) -ChildPath $Space) -ChildPath $CacheStore) -ChildPath ([string]::Format('{0}.json', $KeyName));
3640
[string]$Content = '';
3741
$cacheData = @{ };
3842

43+
# Read a tmp file if present
44+
if ($TempFile) {
45+
$CacheFile = [string]::Format('{0}.tmp', $CacheFile);
46+
}
47+
3948
if ((Test-Path $CacheFile) -eq $FALSE) {
4049
return $null;
4150
}
@@ -46,7 +55,12 @@ function Get-IcingaCacheData()
4655
return $null;
4756
}
4857

49-
$cacheData = ConvertFrom-Json -InputObject ([string]$Content);
58+
try {
59+
$cacheData = ConvertFrom-Json -InputObject ([string]$Content);
60+
} catch {
61+
Write-IcingaEventMessage -EventId 1104 -Namespace 'Framework' -ExceptionObject $_ -Objects $CacheFile;
62+
return $null;
63+
}
5064

5165
if ([string]::IsNullOrEmpty($KeyName)) {
5266
return $cacheData;

lib/core/cache/Set-IcingaCacheData.psm1

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,19 @@ function Set-IcingaCacheData()
3434
$Value
3535
);
3636

37-
$CacheFile = Join-Path -Path (Join-Path -Path (Join-Path -Path (Get-IcingaCacheDir) -ChildPath $Space) -ChildPath $CacheStore) -ChildPath ([string]::Format('{0}.json', $KeyName));
38-
$cacheData = @{ };
37+
$CacheFile = Join-Path -Path (Join-Path -Path (Join-Path -Path (Get-IcingaCacheDir) -ChildPath $Space) -ChildPath $CacheStore) -ChildPath ([string]::Format('{0}.json', $KeyName));
38+
$CacheTmpFile = [string]::Format('{0}.tmp', $CacheFile);
39+
$cacheData = @{ };
40+
41+
if ((Test-IcingaCacheDataTempFile -Space $Space -CacheStore $CacheStore)) {
42+
Copy-IcingaCacheTempFile -CacheFile $CacheFile -CacheTmpFile $CacheTmpFile;
43+
}
3944

4045
if ((Test-Path $CacheFile)) {
4146
$cacheData = Get-IcingaCacheData -Space $Space -CacheStore $CacheStore;
4247
} else {
4348
try {
44-
New-Item -ItemType File -Path $CacheFile -Force -ErrorAction Stop | Out-Null;
49+
New-Item -ItemType File -Path $CacheTmpFile -Force -ErrorAction Stop | Out-Null;
4550
} catch {
4651
Exit-IcingaThrowException -InputString $_.Exception -CustomMessage (Get-IcingaCacheDir) -StringPattern 'NewItemUnauthorizedAccessError' -ExceptionType 'Permission' -ExceptionThrown $IcingaExceptions.Permission.CacheFolder;
4752
Exit-IcingaThrowException -CustomMessage $_.Exception -ExceptionType 'Unhandled' -Force;
@@ -60,5 +65,14 @@ function Set-IcingaCacheData()
6065
}
6166
}
6267

63-
Write-IcingaFileSecure -File $CacheFile -Value (ConvertTo-Json -InputObject $cacheData -Depth 100);
68+
# First write all content to a tmp file at the same location, just with '.tmp' at the end
69+
Write-IcingaFileSecure -File $CacheTmpFile -Value (ConvertTo-Json -InputObject $cacheData -Depth 100);
70+
71+
# If something went wrong, remove the cache file again
72+
if ((Test-IcingaCacheDataTempFile -Space $Space -CacheStore $CacheStore) -eq $FALSE) {
73+
Remove-ItemSecure -Path $CacheTmpFile -Retries 5 -Force | Out-Null;
74+
return;
75+
}
76+
77+
Copy-IcingaCacheTempFile -CacheFile $CacheFile -CacheTmpFile $CacheTmpFile;
6478
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
function Test-IcingaCacheDataTempFile()
2+
{
3+
param (
4+
[string]$Space,
5+
[string]$CacheStore
6+
);
7+
8+
# Once the file is written successully, validate it is fine
9+
$tmpContent = Get-IcingaCacheData -Space $Space -CacheStore $CacheStore -TempFile;
10+
11+
if ($null -eq $tmpContent) {
12+
# File is corrupt or empty
13+
return $FALSE;
14+
}
15+
16+
return $TRUE;
17+
}

lib/core/logging/Icinga_EventLog_Enums.psm1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Framewo
3838
'Details' = 'Icinga for Windows was unable to run a specific command within the namespace content, to load additional extensions and component data into Icinga for Windows.';
3939
'EventId' = 1103;
4040
};
41+
1104 = @{
42+
'EntryType' = 'Error';
43+
'Message' = 'Unable to read Icinga for Windows cache file';
44+
'Details' = 'Icinga for Windows could not read the specified cache file, as the content seems to be corrupt. This happens mostly in case of unexpected shutdowns or terminations during the write process.';
45+
'EventId' = 1104;
46+
};
4147
1400 = @{
4248
'EntryType' = 'Error';
4349
'Message' = 'Icinga for Windows background daemon not found';

0 commit comments

Comments
 (0)