From 250522b46937e12fe820bb097d1f072cd78e5688 Mon Sep 17 00:00:00 2001 From: Eirik Wilhelmsen Date: Mon, 25 Jul 2022 14:33:48 +0200 Subject: [PATCH 01/11] Copied Windows support from minorai --- .gitignore | 1 + Command/Command.csproj | 2 +- sttz.InstallUnity/Installer/Configuration.cs | 3 + sttz.InstallUnity/Installer/Helpers.cs | 6 +- .../Installer/IInstallerPlatform.cs | 2 +- .../Installer/Platforms/WIndowsPlatform.cs | 282 ++++++++++++++++++ sttz.InstallUnity/Installer/Scraper.cs | 4 +- sttz.InstallUnity/Installer/UnityInstaller.cs | 8 +- sttz.InstallUnity/sttz.InstallUnity.csproj | 2 +- 9 files changed, 301 insertions(+), 9 deletions(-) create mode 100644 sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs diff --git a/.gitignore b/.gitignore index c68b193..953f377 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ bin obj .vscode Releases +/.vs diff --git a/Command/Command.csproj b/Command/Command.csproj index 80aeb48..4002f13 100644 --- a/Command/Command.csproj +++ b/Command/Command.csproj @@ -2,7 +2,7 @@ Exe - net6.0;net472 + net6.0 7.1 true true diff --git a/sttz.InstallUnity/Installer/Configuration.cs b/sttz.InstallUnity/Installer/Configuration.cs index aeb2a7a..7a502bf 100644 --- a/sttz.InstallUnity/Installer/Configuration.cs +++ b/sttz.InstallUnity/Installer/Configuration.cs @@ -61,6 +61,9 @@ public class Configuration "/Applications/Unity {major}.{minor};" + "/Applications/Unity {major}.{minor}.{patch}{type}{build};" + "/Applications/Unity {major}.{minor}.{patch}{type}{build} ({hash})"; + + [Description("Windwos installation paths, separted by ; (first non-existing will be used, variables: {major} {minor} {patch} {type} {build} {hash}).")] + public string installPathWindows = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "\\Unity\\Hub\\Editor\\{major}.{minor}.{patch}{type}{build};"; // -------- Serialization -------- diff --git a/sttz.InstallUnity/Installer/Helpers.cs b/sttz.InstallUnity/Installer/Helpers.cs index 14ce899..0e93a80 100644 --- a/sttz.InstallUnity/Installer/Helpers.cs +++ b/sttz.InstallUnity/Installer/Helpers.cs @@ -13,7 +13,7 @@ namespace sttz.InstallUnity public static class Helpers { static readonly string[] SizeNames = new string[] { - "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" + "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; /// @@ -24,8 +24,8 @@ public static class Helpers /// Size formatted with appropriate size suffix (B, KB, MB, etc) public static string FormatSize(long bytes, string format = "{0:0.00} {1}") { - if (bytes < 0) return "? B"; - else if (bytes < 1024) return bytes + " B"; + if (bytes < 0) return "? KB"; + else if (bytes < 1024) return bytes + " KB"; var size = bytes / 1024.0; var index = Math.Min((int)Math.Log(size, 1024), SizeNames.Length - 1); diff --git a/sttz.InstallUnity/Installer/IInstallerPlatform.cs b/sttz.InstallUnity/Installer/IInstallerPlatform.cs index 19f1acd..fb9818e 100644 --- a/sttz.InstallUnity/Installer/IInstallerPlatform.cs +++ b/sttz.InstallUnity/Installer/IInstallerPlatform.cs @@ -103,7 +103,7 @@ public interface IInstallerPlatform /// /// Uninstall a Unity installation. /// - Task Uninstall(Installation instalation, CancellationToken cancellation = default); + Task Uninstall(Installation installation, CancellationToken cancellation = default); /// /// Run a Unity installation with the given arguments. diff --git a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs new file mode 100644 index 0000000..2af0dcd --- /dev/null +++ b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs @@ -0,0 +1,282 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; + +namespace sttz.InstallUnity +{ + public class WIndowsPlatform : IInstallerPlatform + { + + private string INSTALL_PATH => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "Hub", "Editor"); + + string GetUserApplicationSupportDirectory() + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + UnityInstaller.PRODUCT_NAME); + } + + public Task GetCurrentPlatform() + { + return Task.FromResult(CachePlatform.Windows); + } + + public Task> GetInstallablePlatforms() + { + IEnumerable platforms = new CachePlatform[] { CachePlatform.Windows }; + return Task.FromResult(platforms); + } + + public string GetCacheDirectory() + { + return GetUserApplicationSupportDirectory(); + } + + public string GetConfigurationDirectory() + { + return GetUserApplicationSupportDirectory(); + } + + public string GetDownloadDirectory() + { + return Path.Combine(Path.GetTempPath(), UnityInstaller.PRODUCT_NAME); + } + + public async Task IsAdmin(CancellationToken cancellation = default) + { + return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + } + + public async Task CompleteInstall(bool aborted, CancellationToken cancellation = default) + { + if (!installing.version.IsValid) + throw new InvalidOperationException("Not installing any version to complete"); + + if (!aborted) + { + var executable = Path.Combine(installationPaths, "Editor", "Unity.exe"); + if (executable == null) return default; + + var installation = new Installation() + { + version = installing.version, + executable = executable, + path = installationPaths + }; + + installing = default; + + return installation; + } + else + { + return default; + } + } + + public async Task> FindInstallations(CancellationToken cancellation = default) + { + var hubInstallations = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "Hub", "Editor"); + var defaultUnityPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "Editor"); + var installUnityPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "install-unity"); + var unityCandidates = new List(); + if (Directory.Exists(hubInstallations)) + unityCandidates.AddRange(Directory.GetDirectories(hubInstallations)); + if (Directory.Exists(defaultUnityPath)) + unityCandidates.Add(defaultUnityPath); + if (Directory.Exists(installUnityPath)) + unityCandidates.AddRange(Directory.GetDirectories(installUnityPath)); + var unityInstallations = new List(); + foreach (var unityCandidate in unityCandidates) + { + var modulesJsonPath = Path.Combine(unityCandidate, "Editor", "Unity.exe"); + if (!File.Exists(modulesJsonPath)) + { + Logger.LogDebug($"No Unity.exe in {unityCandidate}\\Editor"); + continue; + } + var versionInfo = FileVersionInfo.GetVersionInfo(modulesJsonPath); + Logger.LogDebug($"Found version {versionInfo.ProductVersion}"); + unityInstallations.Add(new Installation { + executable = modulesJsonPath, + path = unityCandidate, + version = new UnityVersion(versionInfo.ProductVersion.Substring(0, versionInfo.ProductVersion.LastIndexOf("."))) + }); + } + return unityInstallations; + } + + public async Task Install(UnityInstaller.Queue queue, UnityInstaller.QueueItem item, CancellationToken cancellation = default) + { + if (item.package.name != PackageMetadata.EDITOR_PACKAGE_NAME && !installedEditor) + { + throw new InvalidOperationException("Cannot install package without installing editor first."); + } + + var installPath = GetInstallationPath(installing.version, installationPaths); + // TODO: start info runas + var result = await RunAsAdmin(item.filePath, $"/S /D={installPath}"); + if (result.exitCode != 0) + { + throw new Exception($"Failed to install {item.filePath} output: {result.output} / {result.error}"); + } + + if (item.package.name == PackageMetadata.EDITOR_PACKAGE_NAME) + { + installedEditor = true; + } + } + + public async Task MoveInstallation(Installation installation, string newPath, CancellationToken cancellation = default) + { + // do nothing + } + + public async Task PrepareInstall(UnityInstaller.Queue queue, string installationPaths, CancellationToken cancellation = default) + { + if (installing.version.IsValid) + throw new InvalidOperationException($"Already installing another version: {installing.version}"); + + installing = queue.metadata; + this.installationPaths = installationPaths; + installedEditor = false; + + // Check for upgrading installation + if (!queue.items.Any(i => i.package.name == PackageMetadata.EDITOR_PACKAGE_NAME)) + { + var installs = await FindInstallations(cancellation); + var existingInstall = installs.Where(i => i.version == queue.metadata.version).FirstOrDefault(); + if (existingInstall == null) + { + throw new InvalidOperationException($"Not installing editor but version {queue.metadata.version} not already installed."); + } + + installedEditor = true; + } + } + + public async Task PromptForPasswordIfNecessary(CancellationToken cancellation = default) + { + // Don't care about password. The system will ask for elevated priviliges automatically + return true; + } + + public async Task Uninstall(Installation installation, CancellationToken cancellation = default) + { + var result = await RunAsAdmin(Path.Combine(installation.path, "Editor", "Uninstall.exe"), "/AllUsers /Q /S"); + if (result.exitCode != 0) + { + throw new Exception($"Could not uninstall Unity. output: {result.output}, error: {result.error}"); + } + } + + // -------- Helpers -------- + + ILogger Logger = UnityInstaller.CreateLogger(); + + VersionMetadata installing; + string installationPaths; + bool installedEditor; + + async Task<(int exitCode, string output, string error)> RunAsAdmin(string filename, string arguments) + { + var startInfo = new ProcessStartInfo(); + startInfo.FileName = filename; + startInfo.Arguments = arguments; + startInfo.CreateNoWindow = true; + startInfo.WindowStyle = ProcessWindowStyle.Hidden; + startInfo.RedirectStandardError = true; + startInfo.RedirectStandardOutput = true; + startInfo.UseShellExecute = false; + startInfo.WorkingDirectory = Environment.CurrentDirectory; + startInfo.Verb = "runas"; + try + { + var p = Process.Start(startInfo); + p.WaitForExit(); + return (p.ExitCode, p.StandardOutput.ReadToEnd(), p.StandardError.ReadToEnd()); + } catch (Exception) + { + Logger.LogError($"Execution of {filename} with {arguments} failed!"); + throw; + } + } + + string GetInstallationPath(UnityVersion version, string installationPaths) + { + string expanded = null; + if (!string.IsNullOrEmpty(installationPaths)) + { + var comparison = StringComparison.OrdinalIgnoreCase; + var paths = installationPaths.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var path in paths) + { + expanded = path.Trim(); + expanded = Helpers.Replace(expanded, "{major}", version.major.ToString(), comparison); + expanded = Helpers.Replace(expanded, "{minor}", version.minor.ToString(), comparison); + expanded = Helpers.Replace(expanded, "{patch}", version.patch.ToString(), comparison); + expanded = Helpers.Replace(expanded, "{type}", ((char)version.type).ToString(), comparison); + expanded = Helpers.Replace(expanded, "{build}", version.build.ToString(), comparison); + expanded = Helpers.Replace(expanded, "{hash}", version.hash, comparison); + + return expanded; + } + } + + if (expanded != null) + { + return Helpers.GenerateUniqueFileName(expanded); + } + else + { + return Helpers.GenerateUniqueFileName(INSTALL_PATH); + } + } + + public async Task Run(Installation installation, IEnumerable arguments, bool child) + { + // child argument is ignored. We are always a child + if (!arguments.Contains("-logFile")) + { + arguments = arguments.Append("-logFile").Append("-"); + } + + var cmd = new System.Diagnostics.Process(); + cmd.StartInfo.FileName = installation.executable; + cmd.StartInfo.Arguments = string.Join(" ", arguments); + cmd.StartInfo.UseShellExecute = false; + + cmd.StartInfo.RedirectStandardOutput = true; + cmd.StartInfo.RedirectStandardError = true; + cmd.EnableRaisingEvents = true; + + cmd.OutputDataReceived += (s, a) => { + if (a.Data == null) return; + Logger.LogInformation(a.Data); + }; + cmd.ErrorDataReceived += (s, a) => { + if (a.Data == null) return; + Logger.LogError(a.Data); + }; + + cmd.Start(); + cmd.BeginOutputReadLine(); + cmd.BeginErrorReadLine(); + + while (!cmd.HasExited) + { + await Task.Delay(100); + } + + cmd.WaitForExit(); // Let stdout and stderr flush + Logger.LogInformation($"Unity exited with code {cmd.ExitCode}"); + Environment.Exit(cmd.ExitCode); + } + } +} diff --git a/sttz.InstallUnity/Installer/Scraper.cs b/sttz.InstallUnity/Installer/Scraper.cs index 683e789..86addb3 100644 --- a/sttz.InstallUnity/Installer/Scraper.cs +++ b/sttz.InstallUnity/Installer/Scraper.cs @@ -184,8 +184,8 @@ void ParseVersions(CachePlatform cachePlatform, HubUnityVersion[] versions, List url = version.downloadUrl, install = true, mandatory = false, - size = long.Parse(version.downloadSize), - installedsize = long.Parse(version.installedSize), + size = long.Parse(version.downloadSize) * 1024, + installedsize = long.Parse(version.installedSize) * 1024, version = version.version, md5 = version.checksum }; diff --git a/sttz.InstallUnity/Installer/UnityInstaller.cs b/sttz.InstallUnity/Installer/UnityInstaller.cs index 9aa00e8..1283a42 100644 --- a/sttz.InstallUnity/Installer/UnityInstaller.cs +++ b/sttz.InstallUnity/Installer/UnityInstaller.cs @@ -215,6 +215,9 @@ public UnityInstaller(Configuration config = null, string dataPath = null, ILogg if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { Logger.LogDebug("Loading platform integration for macOS"); Platform = new MacPlatform(); + } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + Logger.LogDebug("Loading platform integration for WIndows"); + Platform = new WIndowsPlatform(); } else { throw new NotImplementedException("Installer does not currently support the platform: " + RuntimeInformation.OSDescription); } @@ -527,6 +530,8 @@ public async Task Process(InstallStep steps, Queue queue, bool ski string installationPaths = null; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { installationPaths = Configuration.installPathMac; + } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + installationPaths = Configuration.installPathWindows; } else { throw new NotImplementedException("Installer does not currently support the platform: " + RuntimeInformation.OSDescription); } @@ -654,7 +659,8 @@ public void CleanUpDownloads(VersionMetadata metadata, string downloadPath, IEnu continue; if (!packageFileNames.Contains(fileName)) { - throw new Exception("Unexpected file in downloads folder: " + path); + Logger.LogWarning("Unexpected file in downloads folder: " + path); + //throw new Exception("Unexpected file in downloads folder: " + path); } } diff --git a/sttz.InstallUnity/sttz.InstallUnity.csproj b/sttz.InstallUnity/sttz.InstallUnity.csproj index 094f8c9..99e843b 100644 --- a/sttz.InstallUnity/sttz.InstallUnity.csproj +++ b/sttz.InstallUnity/sttz.InstallUnity.csproj @@ -1,7 +1,7 @@ - net6.0;net472 + net6.0 7.1 sttz.InstallUnity From 6c64a2a57959c8a6b071a6affe9d891a228ae8f1 Mon Sep 17 00:00:00 2001 From: Eirik Wilhelmsen Date: Mon, 25 Jul 2022 15:11:48 +0200 Subject: [PATCH 02/11] Added Runtime Identifies to be able to build with .Net 6.0 --- Command/Command.csproj | 1 + Tests/Tests.csproj | 1 + sttz.InstallUnity/sttz.InstallUnity.csproj | 1 + 3 files changed, 3 insertions(+) diff --git a/Command/Command.csproj b/Command/Command.csproj index 4002f13..15a24f2 100644 --- a/Command/Command.csproj +++ b/Command/Command.csproj @@ -3,6 +3,7 @@ Exe net6.0 + win-x64 7.1 true true diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 63039f8..717a7ba 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -2,6 +2,7 @@ net6.0 + win-x64 7.1 false diff --git a/sttz.InstallUnity/sttz.InstallUnity.csproj b/sttz.InstallUnity/sttz.InstallUnity.csproj index 99e843b..a35bece 100644 --- a/sttz.InstallUnity/sttz.InstallUnity.csproj +++ b/sttz.InstallUnity/sttz.InstallUnity.csproj @@ -2,6 +2,7 @@ net6.0 + win-x64 7.1 sttz.InstallUnity From 5704ad146a4b2022fa71792c4bbe5fe5252ddfcd Mon Sep 17 00:00:00 2001 From: Eirik Wilhelmsen Date: Mon, 25 Jul 2022 18:45:30 +0200 Subject: [PATCH 03/11] Fixed FindInstallations on Windows --- BUILD.md | 5 +++++ sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 BUILD.md diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..c09d0cc --- /dev/null +++ b/BUILD.md @@ -0,0 +1,5 @@ +# How to build + +```shell +dotnet publish -r win-x64 -c Release --self-contained --framework net6.0 +``` \ No newline at end of file diff --git a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs index 2af0dcd..d0c9e9d 100644 --- a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs +++ b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs @@ -106,7 +106,7 @@ public async Task> FindInstallations(CancellationToken unityInstallations.Add(new Installation { executable = modulesJsonPath, path = unityCandidate, - version = new UnityVersion(versionInfo.ProductVersion.Substring(0, versionInfo.ProductVersion.LastIndexOf("."))) + version = new UnityVersion(versionInfo.ProductVersion.Substring(0, versionInfo.ProductVersion.LastIndexOf("_"))) // Versions are on format 2020.3.34f1_9a4c9c70452b }); } return unityInstallations; From 02e5c9475d1ee250f4bbdd75443fb9393d266e58 Mon Sep 17 00:00:00 2001 From: Eirik Wilhelmsen Date: Mon, 25 Jul 2022 19:25:08 +0200 Subject: [PATCH 04/11] Delete unity folder after uninstalling --- sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs index d0c9e9d..4252239 100644 --- a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs +++ b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs @@ -174,6 +174,9 @@ public async Task Uninstall(Installation installation, CancellationToken cancell { throw new Exception($"Could not uninstall Unity. output: {result.output}, error: {result.error}"); } + + // TODO: Should folder be deleted even when uninstall command returns with exitcode != 0? + Directory.Delete(installation.path, true); } // -------- Helpers -------- From a5058c5b22e17d269b6141ba8f8ccb418547b84a Mon Sep 17 00:00:00 2001 From: Eirik Wilhelmsen Date: Tue, 26 Jul 2022 08:09:23 +0200 Subject: [PATCH 05/11] Revert kb change --- sttz.InstallUnity/Installer/Helpers.cs | 6 +++--- sttz.InstallUnity/Installer/Scraper.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sttz.InstallUnity/Installer/Helpers.cs b/sttz.InstallUnity/Installer/Helpers.cs index 0e93a80..14ce899 100644 --- a/sttz.InstallUnity/Installer/Helpers.cs +++ b/sttz.InstallUnity/Installer/Helpers.cs @@ -13,7 +13,7 @@ namespace sttz.InstallUnity public static class Helpers { static readonly string[] SizeNames = new string[] { - "MB", "GB", "TB", "PB", "EB", "ZB", "YB" + "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; /// @@ -24,8 +24,8 @@ public static class Helpers /// Size formatted with appropriate size suffix (B, KB, MB, etc) public static string FormatSize(long bytes, string format = "{0:0.00} {1}") { - if (bytes < 0) return "? KB"; - else if (bytes < 1024) return bytes + " KB"; + if (bytes < 0) return "? B"; + else if (bytes < 1024) return bytes + " B"; var size = bytes / 1024.0; var index = Math.Min((int)Math.Log(size, 1024), SizeNames.Length - 1); diff --git a/sttz.InstallUnity/Installer/Scraper.cs b/sttz.InstallUnity/Installer/Scraper.cs index 86addb3..683e789 100644 --- a/sttz.InstallUnity/Installer/Scraper.cs +++ b/sttz.InstallUnity/Installer/Scraper.cs @@ -184,8 +184,8 @@ void ParseVersions(CachePlatform cachePlatform, HubUnityVersion[] versions, List url = version.downloadUrl, install = true, mandatory = false, - size = long.Parse(version.downloadSize) * 1024, - installedsize = long.Parse(version.installedSize) * 1024, + size = long.Parse(version.downloadSize), + installedsize = long.Parse(version.installedSize), version = version.version, md5 = version.checksum }; From 22b7ab448f6100327aff4088ccdcf798892176d5 Mon Sep 17 00:00:00 2001 From: Eirik Wilhelmsen Date: Tue, 26 Jul 2022 08:22:29 +0200 Subject: [PATCH 06/11] Fix productversion split on older Unity versions --- sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs index 4252239..d836952 100644 --- a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs +++ b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs @@ -102,11 +102,12 @@ public async Task> FindInstallations(CancellationToken continue; } var versionInfo = FileVersionInfo.GetVersionInfo(modulesJsonPath); + var splitCharacter = versionInfo.ProductVersion.Contains("_") ? '_' : '.'; // Versions are on format 2020.3.34f1_xxxx or 2020.3.34f1.xxxx Logger.LogDebug($"Found version {versionInfo.ProductVersion}"); unityInstallations.Add(new Installation { executable = modulesJsonPath, path = unityCandidate, - version = new UnityVersion(versionInfo.ProductVersion.Substring(0, versionInfo.ProductVersion.LastIndexOf("_"))) // Versions are on format 2020.3.34f1_9a4c9c70452b + version = new UnityVersion(versionInfo.ProductVersion.Substring(0, versionInfo.ProductVersion.LastIndexOf(splitCharacter))) }); } return unityInstallations; From 4c6c43d389f8fece1fefd9d5734b3ca61c4de86e Mon Sep 17 00:00:00 2001 From: Eirik Wilhelmsen Date: Tue, 26 Jul 2022 09:00:13 +0200 Subject: [PATCH 07/11] Added try-catch to directory delete --- .../Installer/Platforms/WIndowsPlatform.cs | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs index d836952..5d77370 100644 --- a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs +++ b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs @@ -176,8 +176,38 @@ public async Task Uninstall(Installation installation, CancellationToken cancell throw new Exception($"Could not uninstall Unity. output: {result.output}, error: {result.error}"); } - // TODO: Should folder be deleted even when uninstall command returns with exitcode != 0? - Directory.Delete(installation.path, true); + Logger.LogDebug($"Unity {installation.version} uninstalled successfully"); + + try + { + // TODO: Should folder be deleted even when uninstall command returns with exitcode != 0? + Logger.LogInformation($"Deleting folder path {installation.path} recursively"); + await Task.Delay(1000); // Wait for uninstallation + Directory.Delete(installation.path, true); + + Logger.LogDebug($"Folder path {installation.path} deleted"); + } + catch (UnauthorizedAccessException _) + { + try + { + // Sometimes access to folders and files are still in use by Unity uninstall, so we wait some more + await Task.Delay(3000); + Directory.Delete(installation.path, true); + + Logger.LogDebug($"Folder path {installation.path} deleted at second attempt"); + } + catch (Exception e) + { + Logger.LogError(e, $"Failed to delete folder path {installation.path} at second attempt"); + // Continue even though errors occur deleting file path + } + } + catch (Exception e) + { + Logger.LogError(e, $"Failed to delete folder path {installation.path}"); + // Continue even though errors occur deleting file path + } } // -------- Helpers -------- From ff7f6ede1d3dbfa1579e0baf028be1e6ba608f88 Mon Sep 17 00:00:00 2001 From: Eirik Wilhelmsen Date: Mon, 8 Aug 2022 12:48:23 +0200 Subject: [PATCH 08/11] Fix PR comments and cleanup code --- BUILD.md | 5 -- Readme.md | 8 ++- sttz.InstallUnity/Installer/Configuration.cs | 16 +++-- .../Installer/Platforms/WIndowsPlatform.cs | 60 ++++++++++--------- sttz.InstallUnity/Installer/Scraper.cs | 2 +- sttz.InstallUnity/Installer/UnityInstaller.cs | 10 +++- 6 files changed, 56 insertions(+), 45 deletions(-) delete mode 100644 BUILD.md diff --git a/BUILD.md b/BUILD.md deleted file mode 100644 index c09d0cc..0000000 --- a/BUILD.md +++ /dev/null @@ -1,5 +0,0 @@ -# How to build - -```shell -dotnet publish -r win-x64 -c Release --self-contained --framework net6.0 -``` \ No newline at end of file diff --git a/Readme.md b/Readme.md index 1fd88b0..fe48f6f 100644 --- a/Readme.md +++ b/Readme.md @@ -2,7 +2,7 @@ A command-line utility to install any recent version of Unity. -Currently only supports macOS (Intel & Apple Silicon) but support for Windows/Linux is possible, PRs welcome. +Currently only supports macOS (Intel & Apple Silicon) and Windows, but support for Linux is possible, PRs welcome. ## Table of Contents @@ -25,6 +25,12 @@ Installing the latest release version of Unity is as simple as: install-unity install f +# How to build plugin on Windows + +```shell +dotnet publish -r win-x64 -c Release --self-contained --framework net6.0 +``` + ## Versions Most commands take a version as input, either to select the version to install or to filter the output. diff --git a/sttz.InstallUnity/Installer/Configuration.cs b/sttz.InstallUnity/Installer/Configuration.cs index 7a502bf..0241ab6 100644 --- a/sttz.InstallUnity/Installer/Configuration.cs +++ b/sttz.InstallUnity/Installer/Configuration.cs @@ -61,15 +61,19 @@ public class Configuration "/Applications/Unity {major}.{minor};" + "/Applications/Unity {major}.{minor}.{patch}{type}{build};" + "/Applications/Unity {major}.{minor}.{patch}{type}{build} ({hash})"; - - [Description("Windwos installation paths, separted by ; (first non-existing will be used, variables: {major} {minor} {patch} {type} {build} {hash}).")] - public string installPathWindows = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "\\Unity\\Hub\\Editor\\{major}.{minor}.{patch}{type}{build};"; + + + [Description("Windows installation paths, separted by ; (first non-existing will be used, variables: {major} {minor} {patch} {type} {build} {hash}).")] + public string installPathWindows = + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "\\Unity\\Hub\\Editor\\{major}.{minor}.{patch}{type}{build};" + + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "\\Unity\\Editor\\{major}.{minor}.{patch}{type}{build};" + + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "\\Unity\\{major}.{minor}.{patch}{type}{build};"; // -------- Serialization -------- - /// - /// Save the configuration as JSON to the given path. - /// + /// + /// Save the configuration as JSON to the given path. + /// public bool Save(string path) { try { diff --git a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs index 5d77370..728988a 100644 --- a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs +++ b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs @@ -10,9 +10,8 @@ namespace sttz.InstallUnity { - public class WIndowsPlatform : IInstallerPlatform + public class WindowsPlatform : IInstallerPlatform { - private string INSTALL_PATH => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "Hub", "Editor"); @@ -50,7 +49,9 @@ public string GetDownloadDirectory() public async Task IsAdmin(CancellationToken cancellation = default) { - return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); +#pragma warning disable CA1416 // Validate platform compatibility + return await Task.FromResult(new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)); +#pragma warning restore CA1416 // Validate platform compatibility } public async Task CompleteInstall(bool aborted, CancellationToken cancellation = default) @@ -72,7 +73,7 @@ public async Task CompleteInstall(bool aborted, CancellationToken installing = default; - return installation; + return await Task.FromResult(installation); } else { @@ -110,7 +111,7 @@ public async Task> FindInstallations(CancellationToken version = new UnityVersion(versionInfo.ProductVersion.Substring(0, versionInfo.ProductVersion.LastIndexOf(splitCharacter))) }); } - return unityInstallations; + return await Task.FromResult(unityInstallations); } public async Task Install(UnityInstaller.Queue queue, UnityInstaller.QueueItem item, CancellationToken cancellation = default) @@ -121,7 +122,6 @@ public async Task Install(UnityInstaller.Queue queue, UnityInstaller.QueueItem i } var installPath = GetInstallationPath(installing.version, installationPaths); - // TODO: start info runas var result = await RunAsAdmin(item.filePath, $"/S /D={installPath}"); if (result.exitCode != 0) { @@ -134,9 +134,10 @@ public async Task Install(UnityInstaller.Queue queue, UnityInstaller.QueueItem i } } - public async Task MoveInstallation(Installation installation, string newPath, CancellationToken cancellation = default) + public Task MoveInstallation(Installation installation, string newPath, CancellationToken cancellation = default) { - // do nothing + // Don't need to move installation on Windows, Unity is installed in the correct location automatically. + return Task.CompletedTask; } public async Task PrepareInstall(UnityInstaller.Queue queue, string installationPaths, CancellationToken cancellation = default) @@ -165,7 +166,7 @@ public async Task PrepareInstall(UnityInstaller.Queue queue, string installation public async Task PromptForPasswordIfNecessary(CancellationToken cancellation = default) { // Don't care about password. The system will ask for elevated priviliges automatically - return true; + return await Task.FromResult(true); } public async Task Uninstall(Installation installation, CancellationToken cancellation = default) @@ -173,46 +174,52 @@ public async Task Uninstall(Installation installation, CancellationToken cancell var result = await RunAsAdmin(Path.Combine(installation.path, "Editor", "Uninstall.exe"), "/AllUsers /Q /S"); if (result.exitCode != 0) { - throw new Exception($"Could not uninstall Unity. output: {result.output}, error: {result.error}"); + throw new Exception($"Could not uninstall Unity. output: {result.output}, error: {result.error}."); } - Logger.LogDebug($"Unity {installation.version} uninstalled successfully"); + // Uninstall.exe captures the files within the folder and retains sole access to them for some time even after returning a result + // We wait for a period of time and then make sure that the folder and contents are deleted + const int msDelay = 5000; + bool deletedFolder = false; try { - // TODO: Should folder be deleted even when uninstall command returns with exitcode != 0? - Logger.LogInformation($"Deleting folder path {installation.path} recursively"); - await Task.Delay(1000); // Wait for uninstallation + Logger.LogDebug($"Deleting folder path {installation.path} recursively in {msDelay}ms."); + await Task.Delay(msDelay); // Wait for uninstallation to let go of files in folder Directory.Delete(installation.path, true); - Logger.LogDebug($"Folder path {installation.path} deleted"); + Logger.LogDebug($"Folder path {installation.path} deleted."); + deletedFolder = true; } - catch (UnauthorizedAccessException _) + catch (UnauthorizedAccessException) { try { - // Sometimes access to folders and files are still in use by Unity uninstall, so we wait some more - await Task.Delay(3000); + // Sometimes access to folders and files are still in use by Uninstall.exe, so we wait some more + await Task.Delay(msDelay); Directory.Delete(installation.path, true); - Logger.LogDebug($"Folder path {installation.path} deleted at second attempt"); + Logger.LogDebug($"Folder path {installation.path} deleted at second attempt."); + deletedFolder = true; } catch (Exception e) { - Logger.LogError(e, $"Failed to delete folder path {installation.path} at second attempt"); + Logger.LogError(e, $"Failed to delete folder path {installation.path} at second attempt. Ignoring excess files."); // Continue even though errors occur deleting file path } } catch (Exception e) { - Logger.LogError(e, $"Failed to delete folder path {installation.path}"); + Logger.LogError(e, $"Failed to delete folder path {installation.path}."); // Continue even though errors occur deleting file path } + + Logger.LogInformation($"Unity {installation.version} uninstalled successfully {(deletedFolder ? "and folder was deleted" : "but folder was not deleted")}."); } // -------- Helpers -------- - ILogger Logger = UnityInstaller.CreateLogger(); + ILogger Logger = UnityInstaller.CreateLogger(); VersionMetadata installing; string installationPaths; @@ -233,7 +240,7 @@ public async Task Uninstall(Installation installation, CancellationToken cancell try { var p = Process.Start(startInfo); - p.WaitForExit(); + await p.WaitForExitAsync(); return (p.ExitCode, p.StandardOutput.ReadToEnd(), p.StandardError.ReadToEnd()); } catch (Exception) { @@ -302,13 +309,8 @@ public async Task Run(Installation installation, IEnumerable arguments, cmd.Start(); cmd.BeginOutputReadLine(); cmd.BeginErrorReadLine(); + await cmd.WaitForExitAsync(); // Let stdout and stderr flush - while (!cmd.HasExited) - { - await Task.Delay(100); - } - - cmd.WaitForExit(); // Let stdout and stderr flush Logger.LogInformation($"Unity exited with code {cmd.ExitCode}"); Environment.Exit(cmd.ExitCode); } diff --git a/sttz.InstallUnity/Installer/Scraper.cs b/sttz.InstallUnity/Installer/Scraper.cs index 683e789..e7e3ca2 100644 --- a/sttz.InstallUnity/Installer/Scraper.cs +++ b/sttz.InstallUnity/Installer/Scraper.cs @@ -149,7 +149,7 @@ public async Task> LoadLatest(CachePlatform cachePl response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); - Logger.LogDebug("Received response: {json}"); + Logger.LogDebug($"Received response: {json}"); var data = JsonConvert.DeserializeObject>(json); var result = new List(); diff --git a/sttz.InstallUnity/Installer/UnityInstaller.cs b/sttz.InstallUnity/Installer/UnityInstaller.cs index 1283a42..caf2639 100644 --- a/sttz.InstallUnity/Installer/UnityInstaller.cs +++ b/sttz.InstallUnity/Installer/UnityInstaller.cs @@ -217,7 +217,7 @@ public UnityInstaller(Configuration config = null, string dataPath = null, ILogg Platform = new MacPlatform(); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Logger.LogDebug("Loading platform integration for WIndows"); - Platform = new WIndowsPlatform(); + Platform = new WindowsPlatform(); } else { throw new NotImplementedException("Installer does not currently support the platform: " + RuntimeInformation.OSDescription); } @@ -659,8 +659,12 @@ public void CleanUpDownloads(VersionMetadata metadata, string downloadPath, IEnu continue; if (!packageFileNames.Contains(fileName)) { - Logger.LogWarning("Unexpected file in downloads folder: " + path); - //throw new Exception("Unexpected file in downloads folder: " + path); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + // Don't throw on unexcpeted files in Windows Download folder + Logger.LogWarning("Unexpected file in downloads folder: " + path); + } else { + throw new Exception("Unexpected file in downloads folder: " + path); + } } } From a2ac40e54035946baaea78bc438aea939445fc90 Mon Sep 17 00:00:00 2001 From: Eirik Wilhelmsen Date: Mon, 8 Aug 2022 13:40:44 +0200 Subject: [PATCH 09/11] Fixed misc --- Command/Command.csproj | 2 +- Tests/Tests.csproj | 4 +- sttz.InstallUnity/Installer/Configuration.cs | 6 +- .../Installer/Platforms/MacPlatform.cs | 13 +- .../Installer/Platforms/WIndowsPlatform.cs | 483 +++++++++--------- sttz.InstallUnity/Installer/Scraper.cs | 12 +- sttz.InstallUnity/sttz.InstallUnity.csproj | 2 +- 7 files changed, 260 insertions(+), 262 deletions(-) diff --git a/Command/Command.csproj b/Command/Command.csproj index 15a24f2..56762f1 100644 --- a/Command/Command.csproj +++ b/Command/Command.csproj @@ -3,7 +3,7 @@ Exe net6.0 - win-x64 + win-x64;osx-x64 7.1 true true diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 717a7ba..28777aa 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,8 +1,8 @@ - net6.0 - win-x64 + net6.0 + win-x64;osx-x64 7.1 false diff --git a/sttz.InstallUnity/Installer/Configuration.cs b/sttz.InstallUnity/Installer/Configuration.cs index 0241ab6..9849801 100644 --- a/sttz.InstallUnity/Installer/Configuration.cs +++ b/sttz.InstallUnity/Installer/Configuration.cs @@ -71,9 +71,9 @@ public class Configuration // -------- Serialization -------- - /// - /// Save the configuration as JSON to the given path. - /// + /// + /// Save the configuration as JSON to the given path. + /// public bool Save(string path) { try { diff --git a/sttz.InstallUnity/Installer/Platforms/MacPlatform.cs b/sttz.InstallUnity/Installer/Platforms/MacPlatform.cs index 45af46f..b16c660 100644 --- a/sttz.InstallUnity/Installer/Platforms/MacPlatform.cs +++ b/sttz.InstallUnity/Installer/Platforms/MacPlatform.cs @@ -318,10 +318,7 @@ public async Task Run(Installation installation, IEnumerable arguments, Logger.LogInformation($"$ {cmd.StartInfo.FileName} {cmd.StartInfo.Arguments}"); cmd.Start(); - - while (!cmd.HasExited) { - await Task.Delay(100); - } + await cmd.WaitForExitAsync(); } else { if (!arguments.Contains("-logFile")) { @@ -349,12 +346,7 @@ public async Task Run(Installation installation, IEnumerable arguments, cmd.Start(); cmd.BeginOutputReadLine(); cmd.BeginErrorReadLine(); - - while (!cmd.HasExited) { - await Task.Delay(100); - } - - cmd.WaitForExit(); // Let stdout and stderr flush + await cmd.WaitForExitAsync(); // Let stdout and stderr flush Logger.LogInformation($"Unity exited with code {cmd.ExitCode}"); Environment.Exit(cmd.ExitCode); } @@ -697,5 +689,4 @@ async Task CheckIsRoot(bool withSudo, CancellationToken cancellation) } } } - } diff --git a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs index 728988a..2871af1 100644 --- a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs +++ b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs @@ -10,309 +10,316 @@ namespace sttz.InstallUnity { - public class WindowsPlatform : IInstallerPlatform - { - private string INSTALL_PATH => Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "Hub", "Editor"); - string GetUserApplicationSupportDirectory() - { - return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - UnityInstaller.PRODUCT_NAME); - } +/// +/// Platform-specific installer code for Windows. +/// +public class WindowsPlatform : IInstallerPlatform +{ + private string INSTALL_PATH => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "Hub", "Editor"); - public Task GetCurrentPlatform() - { - return Task.FromResult(CachePlatform.Windows); - } + string GetUserApplicationSupportDirectory() + { + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + UnityInstaller.PRODUCT_NAME); + } - public Task> GetInstallablePlatforms() - { - IEnumerable platforms = new CachePlatform[] { CachePlatform.Windows }; - return Task.FromResult(platforms); - } + public Task GetCurrentPlatform() + { + return Task.FromResult(CachePlatform.Windows); + } - public string GetCacheDirectory() - { - return GetUserApplicationSupportDirectory(); - } + public Task> GetInstallablePlatforms() + { + IEnumerable platforms = new CachePlatform[] { CachePlatform.Windows }; + return Task.FromResult(platforms); + } - public string GetConfigurationDirectory() - { - return GetUserApplicationSupportDirectory(); - } + public string GetCacheDirectory() + { + return GetUserApplicationSupportDirectory(); + } - public string GetDownloadDirectory() - { - return Path.Combine(Path.GetTempPath(), UnityInstaller.PRODUCT_NAME); - } + public string GetConfigurationDirectory() + { + return GetUserApplicationSupportDirectory(); + } - public async Task IsAdmin(CancellationToken cancellation = default) - { + public string GetDownloadDirectory() + { + return Path.Combine(Path.GetTempPath(), UnityInstaller.PRODUCT_NAME); + } + + public Task IsAdmin(CancellationToken cancellation = default) + { #pragma warning disable CA1416 // Validate platform compatibility - return await Task.FromResult(new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)); + return Task.FromResult(new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator)); #pragma warning restore CA1416 // Validate platform compatibility - } + } - public async Task CompleteInstall(bool aborted, CancellationToken cancellation = default) + public Task CompleteInstall(bool aborted, CancellationToken cancellation = default) + { + if (!installing.version.IsValid) + throw new InvalidOperationException("Not installing any version to complete"); + + if (!aborted) { - if (!installing.version.IsValid) - throw new InvalidOperationException("Not installing any version to complete"); + var executable = Path.Combine(installationPaths, "Editor", "Unity.exe"); + if (executable == null) return default; - if (!aborted) + var installation = new Installation() { - var executable = Path.Combine(installationPaths, "Editor", "Unity.exe"); - if (executable == null) return default; - - var installation = new Installation() - { - version = installing.version, - executable = executable, - path = installationPaths - }; + version = installing.version, + executable = executable, + path = installationPaths + }; - installing = default; + installing = default; - return await Task.FromResult(installation); - } - else - { - return default; - } + return Task.FromResult(installation); } - - public async Task> FindInstallations(CancellationToken cancellation = default) + else { - var hubInstallations = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "Hub", "Editor"); - var defaultUnityPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "Editor"); - var installUnityPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "install-unity"); - var unityCandidates = new List(); - if (Directory.Exists(hubInstallations)) - unityCandidates.AddRange(Directory.GetDirectories(hubInstallations)); - if (Directory.Exists(defaultUnityPath)) - unityCandidates.Add(defaultUnityPath); - if (Directory.Exists(installUnityPath)) - unityCandidates.AddRange(Directory.GetDirectories(installUnityPath)); - var unityInstallations = new List(); - foreach (var unityCandidate in unityCandidates) - { - var modulesJsonPath = Path.Combine(unityCandidate, "Editor", "Unity.exe"); - if (!File.Exists(modulesJsonPath)) - { - Logger.LogDebug($"No Unity.exe in {unityCandidate}\\Editor"); - continue; - } - var versionInfo = FileVersionInfo.GetVersionInfo(modulesJsonPath); - var splitCharacter = versionInfo.ProductVersion.Contains("_") ? '_' : '.'; // Versions are on format 2020.3.34f1_xxxx or 2020.3.34f1.xxxx - Logger.LogDebug($"Found version {versionInfo.ProductVersion}"); - unityInstallations.Add(new Installation { - executable = modulesJsonPath, - path = unityCandidate, - version = new UnityVersion(versionInfo.ProductVersion.Substring(0, versionInfo.ProductVersion.LastIndexOf(splitCharacter))) - }); - } - return await Task.FromResult(unityInstallations); + return default; } + } - public async Task Install(UnityInstaller.Queue queue, UnityInstaller.QueueItem item, CancellationToken cancellation = default) + public async Task> FindInstallations(CancellationToken cancellation = default) + { + var hubInstallations = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "Hub", "Editor"); + var defaultUnityPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "Editor"); + var installUnityPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Unity", "install-unity"); + + var unityCandidates = new List(); + if (Directory.Exists(hubInstallations)) + unityCandidates.AddRange(Directory.GetDirectories(hubInstallations)); + if (Directory.Exists(defaultUnityPath)) + unityCandidates.Add(defaultUnityPath); + if (Directory.Exists(installUnityPath)) + unityCandidates.AddRange(Directory.GetDirectories(installUnityPath)); + + var unityInstallations = new List(); + foreach (var unityCandidate in unityCandidates) { - if (item.package.name != PackageMetadata.EDITOR_PACKAGE_NAME && !installedEditor) + var modulesJsonPath = Path.Combine(unityCandidate, "Editor", "Unity.exe"); + if (!File.Exists(modulesJsonPath)) { - throw new InvalidOperationException("Cannot install package without installing editor first."); - } - - var installPath = GetInstallationPath(installing.version, installationPaths); - var result = await RunAsAdmin(item.filePath, $"/S /D={installPath}"); - if (result.exitCode != 0) - { - throw new Exception($"Failed to install {item.filePath} output: {result.output} / {result.error}"); + Logger.LogDebug($"No Unity.exe in {unityCandidate}\\Editor"); + continue; } + var versionInfo = FileVersionInfo.GetVersionInfo(modulesJsonPath); + var splitCharacter = versionInfo.ProductVersion.Contains("_") ? '_' : '.'; // Versions are on format 2020.3.34f1_xxxx or 2020.3.34f1.xxxx + + Logger.LogDebug($"Found version {versionInfo.ProductVersion}"); + unityInstallations.Add(new Installation { + executable = modulesJsonPath, + path = unityCandidate, + version = new UnityVersion(versionInfo.ProductVersion.Substring(0, versionInfo.ProductVersion.LastIndexOf(splitCharacter))) + }); + } + return await Task.FromResult(unityInstallations); + } - if (item.package.name == PackageMetadata.EDITOR_PACKAGE_NAME) - { - installedEditor = true; - } + public async Task Install(UnityInstaller.Queue queue, UnityInstaller.QueueItem item, CancellationToken cancellation = default) + { + if (item.package.name != PackageMetadata.EDITOR_PACKAGE_NAME && !installedEditor) + { + throw new InvalidOperationException("Cannot install package without installing editor first."); } - public Task MoveInstallation(Installation installation, string newPath, CancellationToken cancellation = default) + var installPath = GetInstallationPath(installing.version, installationPaths); + var result = await RunAsAdmin(item.filePath, $"/S /D={installPath}"); + if (result.exitCode != 0) { - // Don't need to move installation on Windows, Unity is installed in the correct location automatically. - return Task.CompletedTask; + throw new Exception($"Failed to install {item.filePath} output: {result.output} / {result.error}"); } - public async Task PrepareInstall(UnityInstaller.Queue queue, string installationPaths, CancellationToken cancellation = default) + if (item.package.name == PackageMetadata.EDITOR_PACKAGE_NAME) { - if (installing.version.IsValid) - throw new InvalidOperationException($"Already installing another version: {installing.version}"); + installedEditor = true; + } + } - installing = queue.metadata; - this.installationPaths = installationPaths; - installedEditor = false; + public Task MoveInstallation(Installation installation, string newPath, CancellationToken cancellation = default) + { + // Don't need to move installation on Windows, Unity is installed in the correct location automatically. + return Task.CompletedTask; + } + + public async Task PrepareInstall(UnityInstaller.Queue queue, string installationPaths, CancellationToken cancellation = default) + { + if (installing.version.IsValid) + throw new InvalidOperationException($"Already installing another version: {installing.version}"); - // Check for upgrading installation - if (!queue.items.Any(i => i.package.name == PackageMetadata.EDITOR_PACKAGE_NAME)) + installing = queue.metadata; + this.installationPaths = installationPaths; + installedEditor = false; + + // Check for upgrading installation + if (!queue.items.Any(i => i.package.name == PackageMetadata.EDITOR_PACKAGE_NAME)) + { + var installs = await FindInstallations(cancellation); + var existingInstall = installs.Where(i => i.version == queue.metadata.version).FirstOrDefault(); + if (existingInstall == null) { - var installs = await FindInstallations(cancellation); - var existingInstall = installs.Where(i => i.version == queue.metadata.version).FirstOrDefault(); - if (existingInstall == null) - { - throw new InvalidOperationException($"Not installing editor but version {queue.metadata.version} not already installed."); - } - - installedEditor = true; + throw new InvalidOperationException($"Not installing editor but version {queue.metadata.version} not already installed."); } + + installedEditor = true; } + } + + public Task PromptForPasswordIfNecessary(CancellationToken cancellation = default) + { + // Don't care about password. The system will ask for elevated priviliges automatically + return Task.FromResult(true); + } - public async Task PromptForPasswordIfNecessary(CancellationToken cancellation = default) + public async Task Uninstall(Installation installation, CancellationToken cancellation = default) + { + var result = await RunAsAdmin(Path.Combine(installation.path, "Editor", "Uninstall.exe"), "/AllUsers /Q /S"); + if (result.exitCode != 0) { - // Don't care about password. The system will ask for elevated priviliges automatically - return await Task.FromResult(true); + throw new Exception($"Could not uninstall Unity. output: {result.output}, error: {result.error}."); } - public async Task Uninstall(Installation installation, CancellationToken cancellation = default) - { - var result = await RunAsAdmin(Path.Combine(installation.path, "Editor", "Uninstall.exe"), "/AllUsers /Q /S"); - if (result.exitCode != 0) - { - throw new Exception($"Could not uninstall Unity. output: {result.output}, error: {result.error}."); - } + // Uninstall.exe captures the files within the folder and retains sole access to them for some time even after returning a result + // We wait for a period of time and then make sure that the folder and contents are deleted + const int msDelay = 5000; + bool deletedFolder = false; - // Uninstall.exe captures the files within the folder and retains sole access to them for some time even after returning a result - // We wait for a period of time and then make sure that the folder and contents are deleted - const int msDelay = 5000; - bool deletedFolder = false; + try + { + Logger.LogDebug($"Deleting folder path {installation.path} recursively in {msDelay}ms."); + await Task.Delay(msDelay); // Wait for uninstallation to let go of files in folder + Directory.Delete(installation.path, true); + Logger.LogDebug($"Folder path {installation.path} deleted."); + deletedFolder = true; + } + catch (UnauthorizedAccessException) + { try { - Logger.LogDebug($"Deleting folder path {installation.path} recursively in {msDelay}ms."); - await Task.Delay(msDelay); // Wait for uninstallation to let go of files in folder + // Sometimes access to folders and files are still in use by Uninstall.exe, so we wait some more + await Task.Delay(msDelay); Directory.Delete(installation.path, true); - Logger.LogDebug($"Folder path {installation.path} deleted."); + Logger.LogDebug($"Folder path {installation.path} deleted at second attempt."); deletedFolder = true; } - catch (UnauthorizedAccessException) - { - try - { - // Sometimes access to folders and files are still in use by Uninstall.exe, so we wait some more - await Task.Delay(msDelay); - Directory.Delete(installation.path, true); - - Logger.LogDebug($"Folder path {installation.path} deleted at second attempt."); - deletedFolder = true; - } - catch (Exception e) - { - Logger.LogError(e, $"Failed to delete folder path {installation.path} at second attempt. Ignoring excess files."); - // Continue even though errors occur deleting file path - } - } catch (Exception e) { - Logger.LogError(e, $"Failed to delete folder path {installation.path}."); + Logger.LogError(e, $"Failed to delete folder path {installation.path} at second attempt. Ignoring excess files."); // Continue even though errors occur deleting file path } - - Logger.LogInformation($"Unity {installation.version} uninstalled successfully {(deletedFolder ? "and folder was deleted" : "but folder was not deleted")}."); } + catch (Exception e) + { + Logger.LogError(e, $"Failed to delete folder path {installation.path}."); + // Continue even though errors occur deleting file path + } + + Logger.LogInformation($"Unity {installation.version} uninstalled successfully {(deletedFolder ? "and folder was deleted" : "but folder was not deleted")}."); + } - // -------- Helpers -------- + // -------- Helpers -------- - ILogger Logger = UnityInstaller.CreateLogger(); + ILogger Logger = UnityInstaller.CreateLogger(); - VersionMetadata installing; - string installationPaths; - bool installedEditor; + VersionMetadata installing; + string installationPaths; + bool installedEditor; - async Task<(int exitCode, string output, string error)> RunAsAdmin(string filename, string arguments) + async Task<(int exitCode, string output, string error)> RunAsAdmin(string filename, string arguments) + { + var startInfo = new ProcessStartInfo(); + startInfo.FileName = filename; + startInfo.Arguments = arguments; + startInfo.CreateNoWindow = true; + startInfo.WindowStyle = ProcessWindowStyle.Hidden; + startInfo.RedirectStandardError = true; + startInfo.RedirectStandardOutput = true; + startInfo.UseShellExecute = false; + startInfo.WorkingDirectory = Environment.CurrentDirectory; + startInfo.Verb = "runas"; + try { - var startInfo = new ProcessStartInfo(); - startInfo.FileName = filename; - startInfo.Arguments = arguments; - startInfo.CreateNoWindow = true; - startInfo.WindowStyle = ProcessWindowStyle.Hidden; - startInfo.RedirectStandardError = true; - startInfo.RedirectStandardOutput = true; - startInfo.UseShellExecute = false; - startInfo.WorkingDirectory = Environment.CurrentDirectory; - startInfo.Verb = "runas"; - try - { - var p = Process.Start(startInfo); - await p.WaitForExitAsync(); - return (p.ExitCode, p.StandardOutput.ReadToEnd(), p.StandardError.ReadToEnd()); - } catch (Exception) - { - Logger.LogError($"Execution of {filename} with {arguments} failed!"); - throw; - } + var p = Process.Start(startInfo); + await p.WaitForExitAsync(); + return (p.ExitCode, p.StandardOutput.ReadToEnd(), p.StandardError.ReadToEnd()); + } catch (Exception) + { + Logger.LogError($"Execution of {filename} with {arguments} failed!"); + throw; } + } - string GetInstallationPath(UnityVersion version, string installationPaths) + string GetInstallationPath(UnityVersion version, string installationPaths) + { + string expanded = null; + if (!string.IsNullOrEmpty(installationPaths)) { - string expanded = null; - if (!string.IsNullOrEmpty(installationPaths)) - { - var comparison = StringComparison.OrdinalIgnoreCase; - var paths = installationPaths.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var path in paths) - { - expanded = path.Trim(); - expanded = Helpers.Replace(expanded, "{major}", version.major.ToString(), comparison); - expanded = Helpers.Replace(expanded, "{minor}", version.minor.ToString(), comparison); - expanded = Helpers.Replace(expanded, "{patch}", version.patch.ToString(), comparison); - expanded = Helpers.Replace(expanded, "{type}", ((char)version.type).ToString(), comparison); - expanded = Helpers.Replace(expanded, "{build}", version.build.ToString(), comparison); - expanded = Helpers.Replace(expanded, "{hash}", version.hash, comparison); - - return expanded; - } - } - - if (expanded != null) - { - return Helpers.GenerateUniqueFileName(expanded); - } - else + var comparison = StringComparison.OrdinalIgnoreCase; + var paths = installationPaths.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var path in paths) { - return Helpers.GenerateUniqueFileName(INSTALL_PATH); + expanded = path.Trim(); + expanded = Helpers.Replace(expanded, "{major}", version.major.ToString(), comparison); + expanded = Helpers.Replace(expanded, "{minor}", version.minor.ToString(), comparison); + expanded = Helpers.Replace(expanded, "{patch}", version.patch.ToString(), comparison); + expanded = Helpers.Replace(expanded, "{type}", ((char)version.type).ToString(), comparison); + expanded = Helpers.Replace(expanded, "{build}", version.build.ToString(), comparison); + expanded = Helpers.Replace(expanded, "{hash}", version.hash, comparison); + + return expanded; } } - public async Task Run(Installation installation, IEnumerable arguments, bool child) + if (expanded != null) { - // child argument is ignored. We are always a child - if (!arguments.Contains("-logFile")) - { - arguments = arguments.Append("-logFile").Append("-"); - } - - var cmd = new System.Diagnostics.Process(); - cmd.StartInfo.FileName = installation.executable; - cmd.StartInfo.Arguments = string.Join(" ", arguments); - cmd.StartInfo.UseShellExecute = false; - - cmd.StartInfo.RedirectStandardOutput = true; - cmd.StartInfo.RedirectStandardError = true; - cmd.EnableRaisingEvents = true; - - cmd.OutputDataReceived += (s, a) => { - if (a.Data == null) return; - Logger.LogInformation(a.Data); - }; - cmd.ErrorDataReceived += (s, a) => { - if (a.Data == null) return; - Logger.LogError(a.Data); - }; - - cmd.Start(); - cmd.BeginOutputReadLine(); - cmd.BeginErrorReadLine(); - await cmd.WaitForExitAsync(); // Let stdout and stderr flush + return Helpers.GenerateUniqueFileName(expanded); + } + else + { + return Helpers.GenerateUniqueFileName(INSTALL_PATH); + } + } - Logger.LogInformation($"Unity exited with code {cmd.ExitCode}"); - Environment.Exit(cmd.ExitCode); + public async Task Run(Installation installation, IEnumerable arguments, bool child) + { + // child argument is ignored. We are always a child + if (!arguments.Contains("-logFile")) + { + arguments = arguments.Append("-logFile").Append("-"); } + + var cmd = new System.Diagnostics.Process(); + cmd.StartInfo.FileName = installation.executable; + cmd.StartInfo.Arguments = string.Join(" ", arguments); + cmd.StartInfo.UseShellExecute = false; + + cmd.StartInfo.RedirectStandardOutput = true; + cmd.StartInfo.RedirectStandardError = true; + cmd.EnableRaisingEvents = true; + + cmd.OutputDataReceived += (s, a) => { + if (a.Data == null) return; + Logger.LogInformation(a.Data); + }; + cmd.ErrorDataReceived += (s, a) => { + if (a.Data == null) return; + Logger.LogError(a.Data); + }; + + cmd.Start(); + cmd.BeginOutputReadLine(); + cmd.BeginErrorReadLine(); + await cmd.WaitForExitAsync(); // Let stdout and stderr flush + + Logger.LogInformation($"Unity exited with code {cmd.ExitCode}"); + Environment.Exit(cmd.ExitCode); } } +} diff --git a/sttz.InstallUnity/Installer/Scraper.cs b/sttz.InstallUnity/Installer/Scraper.cs index e7e3ca2..20c113a 100644 --- a/sttz.InstallUnity/Installer/Scraper.cs +++ b/sttz.InstallUnity/Installer/Scraper.cs @@ -184,8 +184,8 @@ void ParseVersions(CachePlatform cachePlatform, HubUnityVersion[] versions, List url = version.downloadUrl, install = true, mandatory = false, - size = long.Parse(version.downloadSize), - installedsize = long.Parse(version.installedSize), + size = long.Parse(version.downloadSize) * 1024, + installedsize = long.Parse(version.installedSize) * 1024, version = version.version, md5 = version.checksum }; @@ -199,8 +199,8 @@ void ParseVersions(CachePlatform cachePlatform, HubUnityVersion[] versions, List url = module.downloadUrl, install = module.selected, mandatory = false, - size = long.Parse(module.downloadSize), - installedsize = long.Parse(module.installedSize), + size = long.Parse(module.downloadSize) * 1024, + installedsize = long.Parse(module.installedSize) * 1024, version = version.version, md5 = module.checksum }; @@ -496,10 +496,10 @@ public async Task LoadPackages(VersionMetadata metadata, CacheP meta.mandatory = bool.Parse(pair.Value); break; case "size": - meta.size = long.Parse(pair.Value); + meta.size = long.Parse(pair.Value) * 1024; break; case "installedsize": - meta.installedsize = long.Parse(pair.Value); + meta.installedsize = long.Parse(pair.Value) * 1024; break; case "version": meta.version = pair.Value; diff --git a/sttz.InstallUnity/sttz.InstallUnity.csproj b/sttz.InstallUnity/sttz.InstallUnity.csproj index a35bece..b679c16 100644 --- a/sttz.InstallUnity/sttz.InstallUnity.csproj +++ b/sttz.InstallUnity/sttz.InstallUnity.csproj @@ -2,7 +2,7 @@ net6.0 - win-x64 + win-x64;osx-x64 7.1 sttz.InstallUnity From 4e67d52acc2458b6896edcd0a7adeebddb92300e Mon Sep 17 00:00:00 2001 From: Eirik Wilhelmsen Date: Mon, 8 Aug 2022 14:45:40 +0200 Subject: [PATCH 10/11] Fix windows path and error handling --- sttz.InstallUnity/Installer/Configuration.cs | 5 +---- .../Installer/Platforms/WIndowsPlatform.cs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/sttz.InstallUnity/Installer/Configuration.cs b/sttz.InstallUnity/Installer/Configuration.cs index 9849801..0ee4af4 100644 --- a/sttz.InstallUnity/Installer/Configuration.cs +++ b/sttz.InstallUnity/Installer/Configuration.cs @@ -64,10 +64,7 @@ public class Configuration [Description("Windows installation paths, separted by ; (first non-existing will be used, variables: {major} {minor} {patch} {type} {build} {hash}).")] - public string installPathWindows = - Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "\\Unity\\Hub\\Editor\\{major}.{minor}.{patch}{type}{build};" - + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "\\Unity\\Editor\\{major}.{minor}.{patch}{type}{build};" - + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "\\Unity\\{major}.{minor}.{patch}{type}{build};"; + public string installPathWindows = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "\\Unity\\Hub\\Editor\\{major}.{minor}.{patch}{type}{build};"; // -------- Serialization -------- diff --git a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs index 2871af1..8f38aaa 100644 --- a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs +++ b/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs @@ -209,12 +209,20 @@ public async Task Uninstall(Installation installation, CancellationToken cancell Logger.LogDebug($"Folder path {installation.path} deleted at second attempt."); deletedFolder = true; } + catch (DirectoryNotFoundException) + { + // Ignore, path already deleted + } catch (Exception e) { Logger.LogError(e, $"Failed to delete folder path {installation.path} at second attempt. Ignoring excess files."); // Continue even though errors occur deleting file path } } + catch (DirectoryNotFoundException) + { + // Ignore, path already deleted + } catch (Exception e) { Logger.LogError(e, $"Failed to delete folder path {installation.path}."); @@ -249,9 +257,9 @@ public async Task Uninstall(Installation installation, CancellationToken cancell var p = Process.Start(startInfo); await p.WaitForExitAsync(); return (p.ExitCode, p.StandardOutput.ReadToEnd(), p.StandardError.ReadToEnd()); - } catch (Exception) + } catch (Exception e) { - Logger.LogError($"Execution of {filename} with {arguments} failed!"); + Logger.LogError(e, $"Execution of {filename} with {arguments} failed!"); throw; } } From ed9495707b978de90ea5ca2f8c0dd21bff33d05c Mon Sep 17 00:00:00 2001 From: Eirik Wilhelmsen Date: Mon, 8 Aug 2022 14:52:40 +0200 Subject: [PATCH 11/11] Fixed case in filename --- .../Platforms/{WIndowsPlatform.cs => WindowsPlatform.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sttz.InstallUnity/Installer/Platforms/{WIndowsPlatform.cs => WindowsPlatform.cs} (100%) diff --git a/sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs b/sttz.InstallUnity/Installer/Platforms/WindowsPlatform.cs similarity index 100% rename from sttz.InstallUnity/Installer/Platforms/WIndowsPlatform.cs rename to sttz.InstallUnity/Installer/Platforms/WindowsPlatform.cs