feat(releases): add SelfUpdater.HandleReplaceSelfAsync
This commit is contained in:
@@ -56,4 +56,40 @@ public static partial class SelfUpdater
|
|||||||
InstallerAsset: match.Asset,
|
InstallerAsset: match.Asset,
|
||||||
ChecksumsAsset: checksums);
|
ChecksumsAsset: checksums);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> HandleReplaceSelfAsync(
|
||||||
|
string oldPath,
|
||||||
|
string currentExePath,
|
||||||
|
Func<string, bool> launchProcess,
|
||||||
|
int maxWaitMs = 5000)
|
||||||
|
{
|
||||||
|
var deadline = DateTime.UtcNow.AddMilliseconds(maxWaitMs);
|
||||||
|
while (DateTime.UtcNow < deadline)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(oldPath))
|
||||||
|
{
|
||||||
|
File.Delete(oldPath);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
await Task.Delay(100);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
await Task.Delay(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File.Exists(oldPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File.Copy(currentExePath, oldPath, overwrite: false);
|
||||||
|
return launchProcess(oldPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,3 +124,56 @@ public class SelfUpdaterDecisionTests
|
|||||||
Assert.Equal(SelfUpdateDecisionKind.NoUpdate, d.Kind);
|
Assert.Equal(SelfUpdateDecisionKind.NoUpdate, d.Kind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class SelfUpdaterReplaceSelfTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _tempDir;
|
||||||
|
|
||||||
|
public SelfUpdaterReplaceSelfTests()
|
||||||
|
{
|
||||||
|
_tempDir = Path.Combine(Path.GetTempPath(), "ClaudeDo.Releases.Tests-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(_tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() { try { Directory.Delete(_tempDir, true); } catch { } }
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Replace_DeletesOldAndCopiesCurrent()
|
||||||
|
{
|
||||||
|
var oldPath = Path.Combine(_tempDir, "old.exe");
|
||||||
|
var currentPath = Path.Combine(_tempDir, "current.exe");
|
||||||
|
await File.WriteAllTextAsync(oldPath, "OLD");
|
||||||
|
await File.WriteAllTextAsync(currentPath, "NEW");
|
||||||
|
|
||||||
|
var relaunchedWith = "";
|
||||||
|
var result = await SelfUpdater.HandleReplaceSelfAsync(
|
||||||
|
oldPath: oldPath,
|
||||||
|
currentExePath: currentPath,
|
||||||
|
launchProcess: path => { relaunchedWith = path; return true; },
|
||||||
|
maxWaitMs: 500);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.Equal(oldPath, relaunchedWith);
|
||||||
|
Assert.Equal("NEW", await File.ReadAllTextAsync(oldPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Replace_TimesOutWhenFileStaysLocked_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var oldPath = Path.Combine(_tempDir, "locked.exe");
|
||||||
|
var currentPath = Path.Combine(_tempDir, "current.exe");
|
||||||
|
await File.WriteAllTextAsync(oldPath, "OLD");
|
||||||
|
await File.WriteAllTextAsync(currentPath, "NEW");
|
||||||
|
|
||||||
|
// Hold an exclusive lock across the wait window.
|
||||||
|
using var lockStream = new FileStream(oldPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
|
||||||
|
|
||||||
|
var result = await SelfUpdater.HandleReplaceSelfAsync(
|
||||||
|
oldPath: oldPath,
|
||||||
|
currentExePath: currentPath,
|
||||||
|
launchProcess: _ => true,
|
||||||
|
maxWaitMs: 200);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user