Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

Commit

Permalink
Prompt to install missing dependencies in ProcessStart
Browse files Browse the repository at this point in the history
  • Loading branch information
caesay committed Apr 28, 2022
1 parent d98f936 commit 02b5b60
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 45 deletions.
2 changes: 1 addition & 1 deletion src/Squirrel/Internal/ReleasePackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public static Task ExtractZipForInstall(string zipFilePath, string outFolder, st
// extract .nuspec to app directory as 'current.version'
if (Path.GetExtension(reader.Entry.Key).Equals(NugetUtil.ManifestExtension, StringComparison.OrdinalIgnoreCase)) {
Utility.Retry(() => reader.WriteEntryToFile(Path.Combine(outFolder, "mysqver")));
Utility.Retry(() => reader.WriteEntryToFile(Path.Combine(outFolder, "current.version")));
continue;
}
Expand Down
19 changes: 17 additions & 2 deletions src/Squirrel/Internal/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -904,14 +904,29 @@ public static void KillProcessesInDirectory(string directoryToKill)

public record VersionDirInfo(IPackage Manifest, SemanticVersion Version, string DirectoryPath, bool IsCurrent, bool IsExecuting);

public static NuspecManifest ReadManifestFromVersionDir(string appVersionDir)
{
NuspecManifest manifest;

var nuspec = Path.Combine(appVersionDir, "mysqver");
if (File.Exists(nuspec) && NuspecManifest.TryParseFromFile(nuspec, out manifest))
return manifest;

nuspec = Path.Combine(appVersionDir, "current.version");
if (File.Exists(nuspec) && NuspecManifest.TryParseFromFile(nuspec, out manifest))
return manifest;

return null;
}

public static IEnumerable<VersionDirInfo> GetAppVersionDirectories(string rootAppDir)
{
foreach (var d in Directory.EnumerateDirectories(rootAppDir)) {
var directoryName = Path.GetFileName(d);
bool isExecuting = IsFileInDirectory(SquirrelRuntimeInfo.EntryExePath, d);
bool isCurrent = directoryName.Equals("current", StringComparison.InvariantCultureIgnoreCase);
var nuspec = Path.Combine(d, "mysqver");
if (File.Exists(nuspec) && NuspecManifest.TryParseFromFile(nuspec, out var manifest)) {
var manifest = ReadManifestFromVersionDir(d);
if (manifest != null) {
yield return new(manifest, manifest.Version, d, isCurrent, isExecuting);
} else if (directoryName.StartsWith("app-", StringComparison.InvariantCultureIgnoreCase) && NuGetVersion.TryParse(directoryName.Substring(4), out var ver)) {
yield return new(null, ver, d, isCurrent, isExecuting);
Expand Down
7 changes: 3 additions & 4 deletions src/Squirrel/UpdateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,9 @@ public SemanticVersion CurrentlyInstalledVersion(string executable = null)
return null;

// if a 'my version' file exists, use that instead.
var nuspec = Path.Combine(baseDir, "mysqver");
if (File.Exists(nuspec)) {
var package = NuspecManifest.ParseFromFile(nuspec);
return package.Version;
var manifest = Utility.ReadManifestFromVersionDir(baseDir);
if (manifest != null) {
return manifest.Version;
}

var exePathWithoutAppDir = executable.Substring(appDir.Length);
Expand Down
123 changes: 86 additions & 37 deletions src/Update/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,7 @@ static async Task Setup(string setupPath, bool silentInstall, bool checkInstall)
return;
}

var missingFrameworks = zp.RuntimeDependencies
.Select(f => Runtimes.GetRuntimeByName(f))
.Where(f => f != null)
.Where(f => !f.CheckIsInstalled().Result)
.ToArray();
var missingFrameworks = getMissingRuntimes(zp);

// prompt user to install missing dependencies
if (missingFrameworks.Any()) {
Expand All @@ -203,38 +199,8 @@ static async Task Setup(string setupPath, bool silentInstall, bool checkInstall)
return; // user cancelled install
}

bool rebootRequired = false;

// iterate through each missing dependency and download/run the installer.
foreach (var f in missingFrameworks) {
var localPath = Path.Combine(tempFolder, f.Id + ".exe");
splash.SetMessage($"Downloading {f.DisplayName}...");
await f.DownloadToFile(localPath, e => splash.SetProgress((ulong) e, 100));
splash.SetProgressIndeterminate();

// hide splash screen while the runtime installer is running so the user can see progress
splash.Hide();
var exitcode = await f.InvokeInstaller(localPath, silentInstall);
splash.Show();

if (exitcode == RestartRequired) {
rebootRequired = true;
continue;
} else if (exitcode != InstallSuccess) {
string rtmsg = exitcode switch {
UserCancelled => $"User cancelled install of {f.DisplayName}. Setup can not continue and will now exit.",
AnotherInstallInProgress => "Another installation is already in progress. Complete that installation before proceeding with this install.",
SystemDoesNotMeetRequirements => $"This computer does not meet the system requirements for {f.DisplayName}.",
_ => $"{f.DisplayName} installer exited with error code '{exitcode}'.",
};
splash.ShowErrorDialog($"Error installing {f.DisplayName}", rtmsg);
return;
}
}

if (rebootRequired) {
// TODO: automatic restart setup after reboot
splash.ShowInfoDialog("Restart required", $"A restart is required before Setup can continue.");
var success = await installMissingRuntimes(splash, missingFrameworks, silentInstall, tempFolder);
if (!success) {
return;
}
}
Expand Down Expand Up @@ -499,6 +465,29 @@ static void ProcessStart(string exeName, string arguments, bool shouldWait, bool
throw new ArgumentException();
}

// check that the app we're about to run has all of it's dependencies installed
var manifest = Utility.ReadManifestFromVersionDir(latestAppDir);
if (manifest != null && manifest.RuntimeDependencies?.Any() == true) {
var missingFrameworks = getMissingRuntimes(manifest);
if (missingFrameworks.Any()) {
Log.Info("App has missing dependencies: " + String.Join(", ", missingFrameworks.Select(s => s.DisplayName)));
var answer = Windows.User32MessageBox.Show(IntPtr.Zero, getMissingRuntimesMessage(manifest.ProductName, missingFrameworks),
"Cannot start " + manifest.ProductName, Windows.User32MessageBox.MessageBoxButtons.OKCancel);

if (answer != Windows.User32MessageBox.MessageBoxResult.OK) {
Log.Info("User did not want to install dependencies. Aborting.");
return;
}

using var splash = new ComposedWindow(manifest.ProductName, false, null, null);
splash.Show();
using var _ = Utility.WithTempDirectory(out var tempFolder);
var success = installMissingRuntimes(splash, missingFrameworks, false, tempFolder).Result;
if (!success) return;
}
}

// launch the EXE
try {
Log.Info("About to launch: '{0}': {1}", targetExe.FullName, arguments ?? "");
Process.Start(new ProcessStartInfo(targetExe.FullName, arguments ?? "") { WorkingDirectory = Path.GetDirectoryName(targetExe.FullName) });
Expand Down Expand Up @@ -533,6 +522,66 @@ static void waitForParentToExit()
}
}

static string getMissingRuntimesMessage(string appname, Runtimes.RuntimeInfo[] missingFrameworks)
{
return missingFrameworks.Length > 1
? $"{appname} is missing the following system components: {String.Join(", ", missingFrameworks.Select(s => s.DisplayName))}. " +
$"Would you like to install these now?"
: $"{appname} requires {missingFrameworks.First().DisplayName} installed to continue, would you like to install it now?";
}

static Runtimes.RuntimeInfo[] getMissingRuntimes(NuspecManifest manifest)
{
if (manifest.RuntimeDependencies == null || !manifest.RuntimeDependencies.Any())
return new Runtimes.RuntimeInfo[0];

return manifest.RuntimeDependencies
.Select(f => Runtimes.GetRuntimeByName(f))
.Where(f => f != null)
.Where(f => !f.CheckIsInstalled().Result)
.ToArray();
}

static async Task<bool> installMissingRuntimes(ISplashWindow splash, Runtimes.RuntimeInfo[] missingFrameworks, bool silentInstall, string tempFolder)
{
bool rebootRequired = false;

// iterate through each missing dependency and download/run the installer.
foreach (var f in missingFrameworks) {
var localPath = Path.Combine(tempFolder, f.Id + ".exe");
splash.SetMessage($"Downloading {f.DisplayName}...");
await f.DownloadToFile(localPath, e => splash.SetProgress((ulong) e, 100));
splash.SetProgressIndeterminate();

// hide splash screen while the runtime installer is running so the user can see progress
splash.Hide();
var exitcode = await f.InvokeInstaller(localPath, silentInstall);
splash.Show();

if (exitcode == RestartRequired) {
rebootRequired = true;
continue;
} else if (exitcode != InstallSuccess) {
string rtmsg = exitcode switch {
UserCancelled => $"User cancelled install of {f.DisplayName}. Setup can not continue and will now exit.",
AnotherInstallInProgress => "Another installation is already in progress. Complete that installation before proceeding with this install.",
SystemDoesNotMeetRequirements => $"This computer does not meet the system requirements for {f.DisplayName}.",
_ => $"{f.DisplayName} installer exited with error code '{exitcode}'.",
};
splash.ShowErrorDialog($"Error installing {f.DisplayName}", rtmsg);
return false;
}
}

if (rebootRequired) {
// TODO: automatic restart setup after reboot
splash.ShowInfoDialog("Restart required", $"A restart is required before Setup can continue.");
return false;
}

return true;
}


static string getAppNameFromDirectory(string path = null)
{
Expand Down
2 changes: 1 addition & 1 deletion test/UpdateManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public async Task InitialInstallSmokeTest()
Assert.True(File.Exists(Path.Combine(localAppDir, "current", "ReactiveUI.dll")));
Assert.True(File.Exists(Path.Combine(localAppDir, "current", "NSync.Core.dll")));

var manifest = NuspecManifest.ParseFromFile(Path.Combine(localAppDir, "current", "mysqver"));
var manifest = NuspecManifest.ParseFromFile(Path.Combine(localAppDir, "current", "current.version"));
Assert.Equal(new NuGetVersion(1, 0, 0, 0), manifest.Version);
}
}
Expand Down

0 comments on commit 02b5b60

Please sign in to comment.