mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Add legacy migration code & tests
This commit is contained in:
@@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionLevel", "SolutionLe
|
||||
.editorconfig = .editorconfig
|
||||
.github\workflows\build.yml = .github\workflows\build.yml
|
||||
src\Directory.Build.props = src\Directory.Build.props
|
||||
test\Directory.Build.props = test\Directory.Build.props
|
||||
README.md = README.md
|
||||
Velopack.entitlements = Velopack.entitlements
|
||||
version.json = version.json
|
||||
|
||||
@@ -18,10 +18,12 @@ pub fn apply<'a>(
|
||||
noelevate: bool,
|
||||
) -> Result<()> {
|
||||
if wait_for_parent {
|
||||
let _ = shared::wait_for_parent_to_exit(60_000); // 1 minute
|
||||
if let Err(e) = shared::wait_for_parent_to_exit(60_000) {
|
||||
warn!("Failed to wait for parent process to exit ({}).", e);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = apply_package(&root_path, &app, restart, package, exe_args.clone(), noelevate) {
|
||||
if let Err(e) = apply_package_impl(&root_path, &app, restart, package, exe_args.clone(), noelevate) {
|
||||
error!("Error applying package: {}", e);
|
||||
if !restart {
|
||||
return Err(e);
|
||||
@@ -36,7 +38,7 @@ pub fn apply<'a>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_package<'a>(
|
||||
fn apply_package_impl<'a>(
|
||||
root_path: &PathBuf,
|
||||
app: &Manifest,
|
||||
restart: bool,
|
||||
@@ -84,7 +86,11 @@ fn apply_package<'a>(
|
||||
|
||||
let found_version = (&package_manifest.version).to_owned();
|
||||
if found_version <= app.version {
|
||||
bail!("Latest package found is {}, which is not newer than current version {}.", found_version, app.version);
|
||||
if package.is_none() {
|
||||
bail!("Latest package found is {}, which is not newer than current version {}.", found_version, app.version);
|
||||
} else {
|
||||
warn!("Provided package is {}, which is not newer than current version {}.", found_version, app.version);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
use crate::shared;
|
||||
use anyhow::{bail, Result};
|
||||
use std::path::Path;
|
||||
use crate::{
|
||||
dialogs,
|
||||
shared::{self, bundle},
|
||||
windows,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use winsafe::{self as w, co};
|
||||
|
||||
pub fn start(wait_for_parent: bool, exe_name: Option<&String>, exe_args: Option<Vec<&str>>, legacy_args: Option<&String>) -> Result<()> {
|
||||
if legacy_args.is_some() && exe_args.is_some() {
|
||||
@@ -8,16 +16,47 @@ pub fn start(wait_for_parent: bool, exe_name: Option<&String>, exe_args: Option<
|
||||
}
|
||||
|
||||
if wait_for_parent {
|
||||
shared::wait_for_parent_to_exit(60_000)?; // 1 minute
|
||||
if let Err(e) = shared::wait_for_parent_to_exit(60_000) {
|
||||
warn!("Failed to wait for parent process to exit ({}).", e);
|
||||
}
|
||||
}
|
||||
|
||||
let (root_path, app) = shared::detect_current_manifest()?;
|
||||
let (root_dir, app) = shared::detect_current_manifest()?;
|
||||
|
||||
let current = app.get_current_path(&root_path);
|
||||
match shared::has_app_prefixed_folder(&root_dir) {
|
||||
Ok(has_prefix) => {
|
||||
if has_prefix {
|
||||
info!("This is a legacy app. Will try and upgrade it now.");
|
||||
|
||||
// if started by legacy Squirrel, the working dir of Update.exe may be inside the app-* folder,
|
||||
// meaning we can not clean up properly.
|
||||
std::env::set_current_dir(&root_dir)?;
|
||||
|
||||
if let Err(e) = try_legacy_migration(&root_dir, &app) {
|
||||
warn!("Failed to migrate legacy app ({}).", e);
|
||||
dialogs::show_error(
|
||||
&app.title,
|
||||
Some("Unable to start app"),
|
||||
"This app installation has been corrupted and cannot be started. Please reinstall the app.",
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// we can't run the normal start command, because legacy squirrel might provide an "exe name" to restart
|
||||
// which no longer exists in the package
|
||||
let (root_dir, app) = shared::detect_current_manifest()?;
|
||||
shared::start_package(&app, &root_dir, exe_args, Some("VELOPACK_RESTART"))?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("Failed legacy check ({}).", e),
|
||||
}
|
||||
|
||||
let current = app.get_current_path(&root_dir);
|
||||
let exe_to_execute = if let Some(exe) = exe_name {
|
||||
Path::new(¤t).join(exe)
|
||||
} else {
|
||||
let exe = app.get_main_exe_path(&root_path);
|
||||
let exe = app.get_main_exe_path(&root_dir);
|
||||
Path::new(&exe).to_path_buf()
|
||||
};
|
||||
|
||||
@@ -39,3 +78,42 @@ pub fn start(wait_for_parent: bool, exe_name: Option<&String>, exe_args: Option<
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_legacy_migration(root_dir: &PathBuf, app: &bundle::Manifest) -> Result<()> {
|
||||
let package = shared::find_latest_full_package(&root_dir).ok_or_else(|| anyhow!("Unable to find latest full package."))?;
|
||||
let bundle = bundle::load_bundle_from_file(&package.file_path)?;
|
||||
let _bundle_manifest = bundle.read_manifest()?; // this verifies it's a bundle we support
|
||||
warn!("This application is installed in a folder prefixed with 'app-'. Attempting to migrate...");
|
||||
let _ = shared::force_stop_package(&root_dir);
|
||||
let current_dir = app.get_current_path(&root_dir);
|
||||
let main_exe_path = app.get_main_exe_path(&root_dir);
|
||||
|
||||
if !Path::new(¤t_dir).exists() {
|
||||
info!("Renaming latest app-* folder to current.");
|
||||
if let Some((latest_app_dir, _latest_ver)) = shared::get_latest_app_version_folder(&root_dir)? {
|
||||
fs::rename(&latest_app_dir, ¤t_dir)?;
|
||||
}
|
||||
}
|
||||
|
||||
info!("Applying latest full package...");
|
||||
let buf = Path::new(&package.file_path).to_path_buf();
|
||||
super::apply(&root_dir, &app, false, false, Some(&buf), None, true)?;
|
||||
|
||||
info!("Removing old app-* folders...");
|
||||
shared::delete_app_prefixed_folders(&root_dir)?;
|
||||
let _ = remove_dir_all::remove_dir_all(root_dir.join("staging"));
|
||||
|
||||
info!("Removing old shortcuts...");
|
||||
if let Err(e) = windows::remove_all_shortcuts_for_root_dir(&root_dir) {
|
||||
warn!("Failed to remove shortcuts ({}).", e);
|
||||
}
|
||||
|
||||
info!("Creating start menu shortcut...");
|
||||
let startmenu = w::SHGetKnownFolderPath(&co::KNOWNFOLDERID::StartMenu, co::KF::DONT_UNEXPAND, None)?;
|
||||
let lnk_path = Path::new(&startmenu).join("Programs").join(format!("{}.lnk", &app.title));
|
||||
if let Err(e) = windows::create_lnk(&lnk_path.to_string_lossy(), &main_exe_path, ¤t_dir, None) {
|
||||
warn!("Failed to create start menu shortcut: {}", e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -78,8 +78,9 @@ pub struct BundleInfo<'a> {
|
||||
file_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub fn load_bundle_from_file<'a>(file_name: &PathBuf) -> Result<BundleInfo<'a>> {
|
||||
debug!("Loading bundle from file '{}'...", file_name.display());
|
||||
pub fn load_bundle_from_file<'a, P: AsRef<Path>>(file_name: P) -> Result<BundleInfo<'a>> {
|
||||
let file_name = file_name.as_ref();
|
||||
debug!("Loading bundle from file '{}'...", file_name.to_string_lossy());
|
||||
let file = super::retry_io(|| File::open(&file_name))?;
|
||||
let cursor: Box<dyn ReadSeek> = Box::new(file);
|
||||
let zip = ZipArchive::new(cursor)?;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use regex::Regex;
|
||||
use semver::Version;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command as Process,
|
||||
};
|
||||
use winsafe::{self as w, co, prelude::*};
|
||||
|
||||
use super::bundle::{self, Manifest, EntryNameInfo};
|
||||
use super::bundle::{self, EntryNameInfo, Manifest};
|
||||
|
||||
pub fn wait_for_parent_to_exit(ms_to_wait: u32) -> Result<()> {
|
||||
info!("Reading parent process information.");
|
||||
@@ -105,9 +108,7 @@ fn kill_pid(pid: u32) -> Result<()> {
|
||||
|
||||
pub fn force_stop_package<P: AsRef<Path>>(root_dir: P) -> Result<()> {
|
||||
let root_dir = root_dir.as_ref();
|
||||
super::retry_io(|| {
|
||||
_force_stop_package(root_dir)
|
||||
})?;
|
||||
super::retry_io(|| _force_stop_package(root_dir))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -170,6 +171,72 @@ pub fn detect_current_manifest() -> Result<(PathBuf, Manifest)> {
|
||||
detect_manifest_from_update_path(&me)
|
||||
}
|
||||
|
||||
pub fn get_app_prefixed_folders<P: AsRef<Path>>(parent_path: P) -> Result<Vec<PathBuf>> {
|
||||
let parent_path = parent_path.as_ref();
|
||||
let re = Regex::new(r"(?i)^app-")?;
|
||||
let mut folders = Vec::new();
|
||||
// Squirrel.Windows and Clowd.Squirrel V2
|
||||
for entry in fs::read_dir(parent_path)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if re.is_match(name) {
|
||||
folders.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clowd.Squirrel V3
|
||||
let staging_dir = parent_path.join("staging");
|
||||
if staging_dir.exists() {
|
||||
for entry in fs::read_dir(&staging_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if re.is_match(name) {
|
||||
folders.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(folders)
|
||||
}
|
||||
|
||||
pub fn get_latest_app_version_folder<P: AsRef<Path>>(parent_path: P) -> Result<Option<(PathBuf, Version)>> {
|
||||
let mut latest_version: Option<Version> = None;
|
||||
let mut latest_folder: Option<PathBuf> = None;
|
||||
for entry in get_app_prefixed_folders(&parent_path)? {
|
||||
if let Some(name) = entry.file_name().and_then(|n| n.to_str()) {
|
||||
if let Some(version) = parse_version_from_folder_name(name) {
|
||||
if latest_version.is_none() || version > latest_version.clone().unwrap() {
|
||||
latest_version = Some(version);
|
||||
latest_folder = Some(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(latest_folder.zip(latest_version))
|
||||
}
|
||||
|
||||
pub fn has_app_prefixed_folder<P: AsRef<Path>>(parent_path: P) -> Result<bool> {
|
||||
Ok(!get_app_prefixed_folders(parent_path)?.is_empty())
|
||||
}
|
||||
|
||||
pub fn delete_app_prefixed_folders<P: AsRef<Path>>(parent_path: P) -> Result<()> {
|
||||
let folders = get_app_prefixed_folders(parent_path)?;
|
||||
for folder in folders {
|
||||
super::retry_io(|| remove_dir_all::remove_dir_all(&folder))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_version_from_folder_name(folder_name: &str) -> Option<Version> {
|
||||
folder_name.strip_prefix("app-").and_then(|v| Version::parse(v).ok())
|
||||
}
|
||||
|
||||
fn find_manifest_from_root_dir(root_path: &PathBuf) -> Result<Manifest> {
|
||||
// default to checking current/sq.version
|
||||
let cm = find_current_manifest(root_path);
|
||||
@@ -178,6 +245,7 @@ fn find_manifest_from_root_dir(root_path: &PathBuf) -> Result<Manifest> {
|
||||
}
|
||||
|
||||
// if that fails, check for latest full package
|
||||
warn!("Unable to find current manifest, checking for latest full package. (LEGACY MODE)");
|
||||
let latest = find_latest_full_package(root_path);
|
||||
if let Some(latest) = latest {
|
||||
let mani = latest.load_manifest()?;
|
||||
@@ -198,7 +266,7 @@ fn find_current_manifest(root_path: &PathBuf) -> Result<Manifest> {
|
||||
bail!("Unable to read nuspec file in current directory.")
|
||||
}
|
||||
|
||||
fn find_latest_full_package(root_path: &PathBuf) -> Option<EntryNameInfo> {
|
||||
pub fn find_latest_full_package(root_path: &PathBuf) -> Option<EntryNameInfo> {
|
||||
let packages = get_all_packages(root_path);
|
||||
let mut latest: Option<EntryNameInfo> = None;
|
||||
for pkg in packages {
|
||||
|
||||
@@ -31,6 +31,7 @@ fn root_command() -> Command {
|
||||
.arg(arg!(--nocolor "Disable colored output").hide(true).global(true))
|
||||
.arg(arg!(-s --silent "Don't show any prompts / dialogs").global(true))
|
||||
.arg(arg!(-l --log <PATH> "Override the default log file location").global(true).value_parser(value_parser!(PathBuf)))
|
||||
.arg(arg!(--forceLatest "Legacy / not used").hide(true))
|
||||
.disable_help_subcommand(true)
|
||||
.flatten_help(true);
|
||||
|
||||
|
||||
@@ -166,9 +166,13 @@ namespace Velopack
|
||||
VelopackHook defaultBlock = ((v) => { });
|
||||
var fastExitlookup = new[] {
|
||||
new { Key = "--veloapp-install", Value = _install ?? defaultBlock },
|
||||
new { Key = "--squirrel-install", Value = _install ?? defaultBlock },
|
||||
new { Key = "--veloapp-updated", Value = _update ?? defaultBlock },
|
||||
new { Key = "--squirrel-updated", Value = _update ?? defaultBlock },
|
||||
new { Key = "--veloapp-obsolete", Value = _obsolete ?? defaultBlock },
|
||||
new { Key = "--squirrel-obsolete", Value = _obsolete ?? defaultBlock },
|
||||
new { Key = "--veloapp-uninstall", Value = _uninstall ?? defaultBlock },
|
||||
new { Key = "--squirrel-uninstall", Value = _uninstall ?? defaultBlock },
|
||||
}.ToDictionary(k => k.Key, v => v.Value, StringComparer.OrdinalIgnoreCase);
|
||||
if (args.Length >= 2 && fastExitlookup.ContainsKey(args[0])) {
|
||||
try {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<IsTest>true</IsTest>
|
||||
<NoWarn>$(NoWarn);CS1998,xUnit2015,xUnit2017,xUnit2005,xUnit2009,xUnit2013,xUnit2004;CA2007;CS8002</NoWarn>
|
||||
<NoWarn>$(NoWarn);CS1998,xUnit2015,xUnit2017,xUnit2005,xUnit2009,xUnit2013,xUnit1013,xUnit2004;CA2007;CS8002</NoWarn>
|
||||
<SignAssembly>True</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\..\Velopack.snk</AssemblyOriginatorKeyFile>
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
@@ -372,7 +372,7 @@ public class WindowsPackTests
|
||||
|
||||
RunCoveredDotnet(appPath, new string[] { "--autoupdate" }, installDir, logger, exitCode: null);
|
||||
|
||||
Thread.Sleep(2000);
|
||||
Thread.Sleep(3000); // update.exe runs in separate process
|
||||
|
||||
var chk1version = RunCoveredDotnet(appPath, new string[] { "version" }, installDir, logger);
|
||||
Assert.EndsWith(Environment.NewLine + "2.0.0", chk1version);
|
||||
@@ -551,6 +551,53 @@ public class WindowsPackTests
|
||||
logger.Info("TEST: uninstalled / complete");
|
||||
}
|
||||
|
||||
[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")]
|
||||
public void LegacyAppCanSuccessfullyMigrate(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))
|
||||
Utility.DeleteFileOrDirectoryHard(rootDir);
|
||||
|
||||
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 updateExe = Path.Combine(rootDir, "Update.exe");
|
||||
Assert.True(File.Exists(appExe));
|
||||
Assert.True(File.Exists(updateExe));
|
||||
|
||||
using var _1 = Utility.GetTempDirectory(out var releaseDir);
|
||||
PackTestApp("LegacyTestApp", "2.0.0", "hello!", releaseDir, logger);
|
||||
|
||||
RunNoCoverage(appExe, new string[] { "download", releaseDir }, currentDir, logger, exitCode: 0);
|
||||
RunNoCoverage(appExe, new string[] { "apply", releaseDir }, currentDir, logger, exitCode: null);
|
||||
|
||||
Thread.Sleep(3000); // update.exe runs in a separate process here
|
||||
|
||||
if (origDirName != "current") {
|
||||
Assert.True(!Directory.Exists(currentDir));
|
||||
currentDir = Path.Combine(rootDir, "current");
|
||||
}
|
||||
|
||||
Assert.True(Directory.Exists(currentDir));
|
||||
appExe = Path.Combine(currentDir, "TestApp.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")));
|
||||
}
|
||||
|
||||
//private string RunCoveredRust(string binName, string[] args, string workingDir, ILogger logger, int? exitCode = 0)
|
||||
//{
|
||||
// var outputfile = GetPath($"coverage.runrust.{RandomString(8)}.xml");
|
||||
|
||||
Reference in New Issue
Block a user