-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathhelper-json.ps1
More file actions
91 lines (83 loc) · 4 KB
/
Copy pathhelper-json.ps1
File metadata and controls
91 lines (83 loc) · 4 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
function MergeJson ($jsons) {
$settings = New-Object -TypeName Newtonsoft.Json.Linq.JsonMergeSettings
$settings.MergeArrayHandling = [Newtonsoft.Json.Linq.MergeArrayHandling]::Replace;
# Use newtonsoft to parse json into object
$resultObject = $null;
$jsons | ForEach-Object {
$jsonObject = [Newtonsoft.Json.JsonConvert]::DeserializeObject($_);
if (!$resultObject) {
$resultObject = $jsonObject;
} else {
$resultObject.Merge($jsonObject, $settings);
}
}
$resultObject.ToString();
}
# Write text to a file by first writing to a temp file in the same directory and
# then atomically moving it into place. This avoids leaving a truncated/partial
# destination file if the write is interrupted or if two processes race to update
# the same file (which can, for example, wipe Windows Terminal's profiles.list and
# reset profiles to defaults).
function Out-FileAtomic ($content, $outFilePath, $encoding = "Utf8") {
$outDir = Split-Path -Parent $outFilePath;
if (!$outDir) { $outDir = "."; }
$resolvedDir = (Resolve-Path -LiteralPath $outDir).ProviderPath;
$destFull = Join-Path $resolvedDir (Split-Path -Leaf $outFilePath);
# Serialize writers across processes with a named mutex keyed on the destination
# path, so concurrent updates (e.g. several psmux hooks firing at once, or a hook
# racing the profile.ps1 startup pass) can't collide while replacing the file.
# Mutex names can't contain '\', so use a hash of the (case-insensitive) path.
$sha1 = [System.Security.Cryptography.SHA1]::Create();
try {
$hash = [BitConverter]::ToString($sha1.ComputeHash([Text.Encoding]::UTF8.GetBytes($destFull.ToLowerInvariant()))).Replace('-', '');
} finally { $sha1.Dispose(); }
$mutex = New-Object System.Threading.Mutex($false, "Global\PwshProfile-OutFileAtomic-$hash");
$held = $false;
try {
try { $held = $mutex.WaitOne(10000); } catch [System.Threading.AbandonedMutexException] { $held = $true; }
$tempPath = Join-Path $resolvedDir ([System.IO.Path]::GetRandomFileName());
try {
$content | Out-File $tempPath -Encoding $encoding;
# [IO.File]::Move(src,dst,$true) maps to Win32 MoveFileEx with
# MOVEFILE_REPLACE_EXISTING, overwriting atomically on the same volume
# (unlike Move-Item -Force, which can fail with "Cannot create a file when
# that file already exists"). The mutex prevents contention between our own
# writers; the retry rides out transient locks held by *external* processes
# (Windows Terminal watching settings.json, Defender, the search indexer),
# which surface as IOException / UnauthorizedAccessException.
$attempt = 0;
$maxAttempts = 8;
while ($true) {
try {
[System.IO.File]::Move($tempPath, $destFull, $true);
break;
} catch {
# A .NET method failure surfaces as a MethodInvocationException whose
# real cause is the InnerException, so unwrap before deciding to retry.
$ex = $_.Exception;
while ($ex.InnerException) { $ex = $ex.InnerException; }
$transient = ($ex -is [System.IO.IOException]) -or ($ex -is [System.UnauthorizedAccessException]);
if (!$transient -or ++$attempt -ge $maxAttempts) { throw; }
Start-Sleep -Milliseconds ([Math]::Min(250, 40 * $attempt));
}
}
} finally {
if (Test-Path -LiteralPath $tempPath) {
Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue;
}
}
} finally {
if ($held) { $mutex.ReleaseMutex(); }
$mutex.Dispose();
}
}
function MergeJsonFilesAndStrings ($inJsonFilePaths, $inJsonStrings, $outJsonFilePath, $encoding = "Utf8") {
$inJson = @($inJsonFilePaths | ForEach-Object {
Get-Content $_ -Raw;
}) + @($inJsonStrings);
$outJson = MergeJson $inJson;
Out-FileAtomic $outJson $outJsonFilePath $encoding;
}
function MergeJsonFiles ($inJsonFilePaths, $outJsonFilePath, $encoding = "Utf8") {
MergeJsonFilesAndStrings @($inJsonFilePaths) @() $outJsonFilePath $encoding;
}