Fix Squirrel CLI migrations by adding --updateSelf

This commit is contained in:
Caelan Sayler
2025-05-13 08:59:46 +01:00
committed by Caelan
parent 1af5c1cf3a
commit c1f33c19a0
2 changed files with 131 additions and 5 deletions

View File

@@ -56,6 +56,13 @@ fn root_command() -> Command {
.about("Remove all app shortcuts, files, and registry entries.")
.long_flag_alias("uninstall")
);
#[cfg(target_os = "windows")]
let cmd = cmd.subcommand(Command::new("update-self")
.about("Copy the currently executing Update.exe into the default location.")
.long_flag_alias("updateSelf")
.hide(true)
);
cmd
}
@@ -155,6 +162,8 @@ fn main() -> Result<()> {
let result = match subcommand {
#[cfg(target_os = "windows")]
"uninstall" => uninstall(subcommand_matches).map_err(|e| anyhow!("Uninstall error: {}", e)),
#[cfg(target_os = "windows")]
"update-self" => update_self(subcommand_matches).map_err(|e| anyhow!("Update-self error: {}", e)),
"start" => start(subcommand_matches).map_err(|e| anyhow!("Start error: {}", e)),
"apply" => apply(subcommand_matches).map_err(|e| anyhow!("Apply error: {}", e)),
"patch" => patch(subcommand_matches).map_err(|e| anyhow!("Patch error: {}", e)),
@@ -243,6 +252,47 @@ fn uninstall(_matches: &ArgMatches) -> Result<()> {
commands::uninstall(&locator, true)
}
#[cfg(target_os = "windows")]
fn update_self(_matches: &ArgMatches) -> Result<()> {
info!("Command: Update Self");
let my_path = env::current_exe()?;
const RETRY_DELAY: i32 = 500;
const RETRY_COUNT: i32 = 20;
match auto_locate_app_manifest(LocationContext::IAmUpdateExe) {
Ok(locator) => {
let target_update_path = locator.get_update_path();
if same_file::is_same_file(&target_update_path, &my_path)? {
bail!("Update.exe is already in the default location. No need to update.");
} else {
info!("Copying Update.exe to the default location: {:?}", target_update_path);
shared::retry_io_ex(|| std::fs::copy(&my_path, &target_update_path), RETRY_DELAY, RETRY_COUNT)?;
info!("Update.exe copied successfully.");
}
}
Err(e) => {
warn!("Failed to initialise locator: {}", e);
// search for an Update.exe in parent directories (at least 2 levels up)
let mut current_dir = env::current_dir()?;
let mut found = false;
for _ in 0..2 {
current_dir.pop();
let target_update_path = current_dir.join("Update.exe");
if target_update_path.exists() {
info!("Found Update.exe in parent directory: {:?}", target_update_path);
shared::retry_io_ex(|| std::fs::copy(&my_path, &target_update_path), RETRY_DELAY, RETRY_COUNT)?;
info!("Update.exe copied successfully.");
found = true;
break;
}
}
if !found {
bail!("Failed to locate Update.exe in parent directories, so it could not be updated.");
}
}
}
Ok(())
}
#[cfg(target_os = "windows")]
#[test]
fn test_cli_parse_handles_equals_spaces() {

View File

@@ -253,7 +253,7 @@ public class WindowsPackTests
Assert.False(File.Exists(appPath));
using var key2 = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default)
.OpenSubKey(uninstallRegSubKey + "\\" + id, RegistryKeyPermissionCheck.ReadSubTree);
.OpenSubKey(uninstallRegSubKey + "\\" + id, RegistryKeyPermissionCheck.ReadSubTree);
Assert.Null(key2);
}
@@ -508,12 +508,74 @@ public class WindowsPackTests
logger.Info("TEST: uninstalled / complete");
}
[SkippableTheory]
[InlineData("LegacyTestApp-ClowdV2-Setup.exe", "app-1.0.0")]
[InlineData("LegacyTestApp-SquirrelWinV2-Setup.exe", "app-1.0.0")]
public void LegacyAppCanMigrateUsingCli(string fixture, string origDirName)
{
Skip.IfNot(VelopackRuntimeInfo.IsWindows);
using var logger = _output.BuildLoggerFor<WindowsPackTests>();
var rootDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LegacyTestApp");
if (Directory.Exists(rootDir)) {
IoUtil.Retry(() => IoUtil.DeleteFileOrDirectoryHard(rootDir), 10, 1000);
}
var setup = PathHelper.GetFixture(fixture);
var p = Process.Start(setup);
p!.WaitForExit();
var currentDir = Path.Combine(rootDir, origDirName);
var appExe = Path.Combine(currentDir, "LegacyTestApp.exe");
var stubExe = Path.Combine(rootDir, "LegacyTestApp.exe");
var updateExe = Path.Combine(rootDir, "Update.exe");
var assertAppExe = appExe;
IoUtil.Retry(
() => {
Assert.True(File.Exists(assertAppExe));
Assert.True(File.Exists(updateExe));
},
retries: 10,
retryDelay: 1000);
using var _1 = TempUtil.GetTempDirectory(out var releaseDir);
PackTestApp("LegacyTestApp", "2.0.0", "hello!", releaseDir, logger, assemblyNameOverride: "LegacyTestApp");
RunNoCoverage(updateExe, ["--update", releaseDir], currentDir, logger, exitCode: 0);
Thread.Sleep(2000); // update.exe does a self update after
RunNoCoverage(stubExe, [], currentDir, logger, exitCode: 0);
Thread.Sleep(8000); // update.exe will do migration here
string logContents = ReadFileWithRetry(Path.Combine(rootDir, "Velopack.log"), logger);
logger.Info("Velopack.log:" + Environment.NewLine + logContents);
if (origDirName != "current") {
Assert.False(Directory.Exists(currentDir));
currentDir = Path.Combine(rootDir, "current");
}
Assert.True(Directory.Exists(currentDir));
appExe = Path.Combine(currentDir, "LegacyTestApp.exe");
Assert.True(File.Exists(appExe));
Assert.False(Directory.EnumerateDirectories(rootDir, "app-*").Any());
Assert.False(Directory.Exists(Path.Combine(rootDir, "staging")));
// this is the file written by TestApp when it's detected the squirrel restart. if this is here, everything went smoothly.
Assert.True(File.Exists(Path.Combine(rootDir, "restarted")));
var chk3version = RunNoCoverage(appExe, ["version"], currentDir, logger);
Assert.EndsWith(Environment.NewLine + "2.0.0", chk3version);
}
[SkippableTheory]
[InlineData("LegacyTestApp-ClowdV2-Setup.exe", "app-1.0.0")]
[InlineData("LegacyTestApp-ClowdV3-Setup.exe", "current")]
[InlineData("LegacyTestApp-SquirrelWinV2-Setup.exe", "app-1.0.0")]
[InlineData("LegacyTestApp-Velopack0084-Setup.exe", "current")]
public void LegacyAppCanSuccessfullyMigrate(string fixture, string origDirName)
public void LegacyAppCanMigrate(string fixture, string origDirName)
{
Skip.IfNot(VelopackRuntimeInfo.IsWindows);
using var logger = _output.BuildLoggerFor<WindowsPackTests>();
@@ -807,7 +869,8 @@ public class WindowsPackTests
return RunImpl(psi, logger, exitCode);
}
private static void PackTestApp(string id, string version, string testString, string releaseDir, ILogger logger, bool addNewFile = false)
private static void PackTestApp(string id, string version, string testString, string releaseDir, ILogger logger,
bool addNewFile = false, string assemblyNameOverride = null)
{
var projDir = PathHelper.GetTestRootPath("TestApp");
var testStringFile = Path.Combine(projDir, "Const.cs");
@@ -815,7 +878,10 @@ public class WindowsPackTests
try {
File.WriteAllText(testStringFile, $"class Const {{ public const string TEST_STRING = \"{testString}\"; }}");
var args = new string[] { "publish", "--no-self-contained", "-c", "Release", "-r", "win-x64", "-o", "publish" };
var args = new string[] {
"publish", "--no-self-contained", "-c", "Release", "-r", "win-x64", "-o", "publish",
$"-p:PublishSingleFile=true", "--tl:off"
};
var psi = new ProcessStartInfo("dotnet");
psi.WorkingDirectory = projDir;
@@ -839,10 +905,20 @@ public class WindowsPackTests
}
}
var publishDir = Path.Combine(projDir, "publish");
if (assemblyNameOverride != null) {
var targetExe = Path.Combine(publishDir, assemblyNameOverride + ".exe");
if (File.Exists(targetExe)) {
File.Delete(targetExe);
}
File.Move(Path.Combine(publishDir, "TestApp.exe"), targetExe);
}
//RunNoCoverage("dotnet", args, projDir, logger);
var options = new WindowsPackOptions {
EntryExecutableName = "TestApp.exe",
EntryExecutableName = assemblyNameOverride + ".exe",
ReleaseDir = new DirectoryInfo(releaseDir),
PackId = id,
PackVersion = version,