feat(releases): add SelfUpdater.HandleReplaceSelfAsync
This commit is contained in:
@@ -56,4 +56,40 @@ public static partial class SelfUpdater
|
||||
InstallerAsset: match.Asset,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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