mirror of
https://git.do.srb2.org/STJr/UltimateZoneBuilder.git
synced 2024-11-23 04:12:12 +00:00
6a55d0086f
- The updater can now update itself
590 lines
No EOL
16 KiB
C#
Executable file
590 lines
No EOL
16 KiB
C#
Executable file
#region ======================== Namespaces
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Security.AccessControl;
|
|
using System.Security.Principal;
|
|
using System.Threading;
|
|
using System.Windows.Forms;
|
|
using SharpCompress.Archives;
|
|
using SharpCompress.Readers;
|
|
using Microsoft.Win32;
|
|
|
|
#endregion
|
|
|
|
namespace mxd.GZDBUpdater
|
|
{
|
|
public partial class MainForm : Form
|
|
{
|
|
#region ======================== Variables
|
|
|
|
private string processToEnd = string.Empty;
|
|
private string downloadFile = string.Empty;
|
|
private string platform = string.Empty;
|
|
private const string revisionwildcard = "[REVNUM]";
|
|
private const string platformwildcard = "[PLATFORM]";
|
|
private string URL = string.Empty;
|
|
private readonly string updateFolder = Application.StartupPath + @"\_update\";
|
|
private string appFileName = string.Empty;
|
|
private static BackgroundWorker worker;
|
|
private static bool appclosing;
|
|
private static MainForm me;
|
|
private const string MESSAGEBOX_TITLE = "Ultimate Doom Builder Updater";
|
|
private bool useInstaller = false;
|
|
|
|
#endregion
|
|
|
|
#region ======================== Delegates
|
|
|
|
private delegate void SetLabelCallback(Label label, string text);
|
|
private delegate void UpdateProgressBarCallback(ByteArgs args, int step, int totalsteps);
|
|
private delegate void CloseDelegate();
|
|
|
|
#endregion
|
|
|
|
#region ======================== Properties
|
|
|
|
public static string ErrorDescription;
|
|
public static bool AppClosing { get { return appclosing; } }
|
|
public static Icon AppIcon { get { return me.Icon; } }
|
|
|
|
#endregion
|
|
|
|
#region ======================== Constructor
|
|
|
|
public MainForm()
|
|
{
|
|
// Check if we should use the installer or just unpack the update
|
|
string installLocation = (string)Registry.GetValue(@"HKEY_CURRENT_USER\SOFTWARE\Ultimate Doom Builder", "Location", null);
|
|
|
|
if (installLocation != null)
|
|
{
|
|
string ourpath = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName);
|
|
if (ourpath == installLocation)
|
|
useInstaller = true;
|
|
}
|
|
|
|
if (!CheckPremissions(Application.StartupPath))
|
|
{
|
|
ErrorDescription = "Update failed: your user account does not have write access to the destination folder \"" + Application.StartupPath + "\"\n\nMove the editor to a folder with write access,\nor run the updater as Administrator.";
|
|
InvokeClose();
|
|
}
|
|
else if(!File.Exists("Updater.ini"))
|
|
{
|
|
ErrorDescription = "Unable to locate 'Updater.ini'...";
|
|
InvokeClose();
|
|
}
|
|
else if(!LoadConfig("Updater.ini"))
|
|
{
|
|
InvokeClose();
|
|
}
|
|
else
|
|
{
|
|
me = this;
|
|
InitializeComponent();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ======================== Updater thread
|
|
|
|
private void BackgroundWorker(object sender, DoWorkEventArgs e)
|
|
{
|
|
UpdateLabel(label1, "1/6: Checking revisions...");
|
|
if(!UpdateRequired())
|
|
{
|
|
e.Cancel = true;
|
|
return;
|
|
}
|
|
PreDownload();
|
|
|
|
UpdateLabel(label1, "2/6: Downloading Update...");
|
|
Webdata.BytesDownloaded += WebdataOnBytesDownloaded;
|
|
if(!Webdata.SaveWebFile(URL, downloadFile, updateFolder))
|
|
{
|
|
e.Cancel = true;
|
|
Webdata.BytesDownloaded -= WebdataOnBytesDownloaded;
|
|
return;
|
|
}
|
|
|
|
// Check if the editor is running...
|
|
if(!EditorClosed())
|
|
{
|
|
// Error or user canceled
|
|
e.Cancel = true;
|
|
return;
|
|
}
|
|
|
|
// Rename the updater so that a new version can be written
|
|
UpdateLabel(label1, "4/6: Renaming updater...");
|
|
Thread.Sleep(500);
|
|
if(!RenameUpdater())
|
|
{
|
|
e.Cancel = true;
|
|
return;
|
|
}
|
|
|
|
// Install the update
|
|
if (useInstaller)
|
|
{
|
|
UpdateLabel(label1, "5/6: Running installer...");
|
|
Thread.Sleep(500);
|
|
if(!InstallUpdateWithInstaller())
|
|
{
|
|
e.Cancel = true;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UpdateLabel(label1, "5/6: Decompressing package...");
|
|
Thread.Sleep(500);
|
|
if (!Unpack(updateFolder + downloadFile, Application.StartupPath))
|
|
{
|
|
e.Cancel = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// This currently doesn't do anything, since the directory it tries to move the files
|
|
// from is empty (bar the update file)
|
|
/*
|
|
UpdateLabel(label1, "5/6: Moving files...");
|
|
Thread.Sleep(500);
|
|
MoveFiles();
|
|
*/
|
|
|
|
UpdateLabel(label1, "6/6: Wrapping up...");
|
|
Thread.Sleep(500);
|
|
PostDownload();
|
|
}
|
|
|
|
private bool InstallUpdateWithInstaller()
|
|
{
|
|
Process process;
|
|
|
|
try
|
|
{
|
|
// Run the installer in silent mode. This will skip any user interaction (if possible)
|
|
process = Process.Start(updateFolder + downloadFile, "/silent");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ErrorDescription = $"Failed to run installer: {ex.Message}";
|
|
return false;
|
|
}
|
|
|
|
process.WaitForExit();
|
|
|
|
// Check if the installer ran successfully. See https://jrsoftware.org/ishelp/index.php?topic=setupexitcodes
|
|
if (process.ExitCode == 2)
|
|
{
|
|
ErrorDescription = "Installer aborted by the user.";
|
|
return false;
|
|
}
|
|
else if (process.ExitCode > 0)
|
|
{
|
|
ErrorDescription = $"Installer failed with exit code {process.ExitCode}.";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool RenameUpdater()
|
|
{
|
|
string ourname = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName);
|
|
string movename = ourname + ".old";
|
|
|
|
try
|
|
{
|
|
if (File.Exists(movename))
|
|
File.Delete(movename);
|
|
File.Move(ourname, movename);
|
|
}
|
|
catch(Exception e)
|
|
{
|
|
ErrorDescription = $"Failed to rename updater from {ourname} to {movename}: {e.Message}";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool EditorClosed()
|
|
{
|
|
try
|
|
{
|
|
// Gather processes...
|
|
List<Process> toclose = GetProcesses(processToEnd);
|
|
|
|
// Ask the user how to proceed...
|
|
if(toclose.Count > 0)
|
|
{
|
|
TaskbarProgress.SetState(this.Handle, TaskbarProgress.TaskbarStates.Paused);
|
|
UpdateBlockedForm form = new UpdateBlockedForm();
|
|
switch(form.ShowDialog(this))
|
|
{
|
|
case DialogResult.Cancel: return false;
|
|
case DialogResult.OK:
|
|
UpdateLabel(label1, "3/6: Stopping " + processToEnd);
|
|
Thread.Sleep(50);
|
|
toclose = GetProcesses(processToEnd); // Re-gather processes
|
|
foreach(Process p in toclose) if(p != null) p.Kill();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
ErrorDescription = "Failed to stop the editor process...\n" + ex.Message;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static List<Process> GetProcesses(string processToEnd)
|
|
{
|
|
Process[] processes = Process.GetProcesses();
|
|
List<Process> toclose = new List<Process>();
|
|
|
|
// Gather all running editor processes...
|
|
foreach(Process process in processes)
|
|
{
|
|
if(process.ProcessName == processToEnd
|
|
&& Path.GetDirectoryName(process.MainModule.FileName) == Application.StartupPath)
|
|
toclose.Add(process);
|
|
}
|
|
|
|
return toclose;
|
|
}
|
|
|
|
private static void StopBackgroundWorker()
|
|
{
|
|
if(worker != null && !worker.CancellationPending)
|
|
{
|
|
me.UpdateLabel(me.label1, "Stopping Background Thread...");
|
|
worker.CancelAsync();
|
|
while(worker.IsBusy) Application.DoEvents();
|
|
}
|
|
}
|
|
|
|
private void WorkerOnRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
|
|
{
|
|
InvokeClose();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ======================== Methods
|
|
|
|
private void UpdateLabel(Label label, string text)
|
|
{
|
|
if(label.InvokeRequired)
|
|
{
|
|
SetLabelCallback d = UpdateLabel;
|
|
label.Invoke(d, new object[] { label, text });
|
|
}
|
|
else
|
|
{
|
|
label.Text = text;
|
|
label.Refresh();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
private void InvokeClose()
|
|
{
|
|
if(this.Disposing || this.IsDisposed) return;
|
|
if(this.InvokeRequired)
|
|
{
|
|
CloseDelegate d = Close;
|
|
this.Invoke(d);
|
|
}
|
|
else
|
|
{
|
|
if(!appclosing && !string.IsNullOrEmpty(ErrorDescription))
|
|
{
|
|
if(!string.IsNullOrEmpty(URL))
|
|
{
|
|
ErrorDescription += Environment.NewLine + Environment.NewLine + "Would you like to download the update manually?";
|
|
TaskbarProgress.SetState(this.Handle, TaskbarProgress.TaskbarStates.Error);
|
|
if(MessageBox.Show(this, ErrorDescription, MESSAGEBOX_TITLE, MessageBoxButtons.YesNo) == DialogResult.Yes)
|
|
Process.Start(URL);
|
|
}
|
|
else
|
|
{
|
|
MessageBox.Show(this, ErrorDescription, MESSAGEBOX_TITLE, MessageBoxButtons.OK);
|
|
}
|
|
}
|
|
|
|
WrapUp();
|
|
Close();
|
|
}
|
|
}
|
|
|
|
private bool UpdateRequired()
|
|
{
|
|
// Get local revision number
|
|
int localrev = -1;
|
|
if(File.Exists(appFileName))
|
|
{
|
|
var info = FileVersionInfo.GetVersionInfo(appFileName);
|
|
localrev = info.ProductPrivatePart;
|
|
}
|
|
|
|
// Get remote revision number
|
|
int remoterev;
|
|
using(MemoryStream stream = Webdata.DownloadWebFile(Path.Combine(URL, "Versions.txt")))
|
|
{
|
|
if(stream == null)
|
|
{
|
|
if(string.IsNullOrEmpty(ErrorDescription)) ErrorDescription = "Failed to retrieve remote revision info.";
|
|
return false;
|
|
}
|
|
|
|
string s;
|
|
using(StreamReader reader = new StreamReader(stream))
|
|
{
|
|
s = reader.ReadLine(); // First line should be editor revision
|
|
}
|
|
|
|
if(!int.TryParse(s, out remoterev))
|
|
{
|
|
ErrorDescription = "Failed to retrieve remote revision number.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Replace wildcard with remoterev
|
|
downloadFile = downloadFile.Replace(revisionwildcard, remoterev.ToString());
|
|
downloadFile = downloadFile.Replace(platformwildcard, platform.ToString());
|
|
|
|
if (remoterev > 0 && remoterev <= localrev)
|
|
{
|
|
URL = string.Empty;
|
|
ErrorDescription = "Your version is up to date!";
|
|
}
|
|
return remoterev > localrev;
|
|
}
|
|
|
|
private bool LoadConfig(string filename)
|
|
{
|
|
string[] lines = File.ReadAllLines(filename);
|
|
|
|
foreach (string line in lines)
|
|
{
|
|
if(line.StartsWith("URL"))
|
|
{
|
|
URL = line.Substring(3).Trim();
|
|
}
|
|
else if(line.StartsWith("FileName"))
|
|
{
|
|
appFileName = line.Substring(8).Trim();
|
|
processToEnd = Path.GetFileNameWithoutExtension(appFileName);
|
|
}
|
|
else if(!useInstaller && line.StartsWith("UpdateName"))
|
|
{
|
|
downloadFile = line.Substring(10).Trim();
|
|
}
|
|
else if(useInstaller && line.StartsWith("InstallerName"))
|
|
{
|
|
downloadFile = line.Substring(13).Trim();
|
|
}
|
|
else if(line.StartsWith("Platform"))
|
|
{
|
|
platform = line.Substring(9).Trim();
|
|
}
|
|
}
|
|
|
|
// Sanity cheks
|
|
if(string.IsNullOrEmpty(URL))
|
|
{
|
|
ErrorDescription = "URL is not specified in " + filename + "!";
|
|
return false;
|
|
}
|
|
|
|
if(string.IsNullOrEmpty(appFileName) || string.IsNullOrEmpty(processToEnd))
|
|
{
|
|
ErrorDescription = "FileName is not specified in " + filename + "!";
|
|
return false;
|
|
}
|
|
|
|
if(string.IsNullOrEmpty(downloadFile) || !downloadFile.Contains(revisionwildcard))
|
|
{
|
|
ErrorDescription = "UpdateName is invalid or not specified in " + filename + "!";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool Unpack(string file, string unZipTo)
|
|
{
|
|
try
|
|
{
|
|
using(IArchive arc = ArchiveFactory.Open(file))
|
|
{
|
|
if(!arc.IsComplete)
|
|
{
|
|
ErrorDescription = "Update failed: downloaded file is not complete...";
|
|
return false;
|
|
}
|
|
|
|
IReader reader = arc.ExtractAllEntries();
|
|
|
|
// Get number of files...
|
|
int curentry = 0;
|
|
int totalentries = arc.NumEntries;
|
|
|
|
string ourname = Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName);
|
|
|
|
// Unpack all
|
|
ExtractionOptions options = new ExtractionOptions {ExtractFullPath = true, Overwrite = true};
|
|
while(reader.MoveToNextEntry())
|
|
{
|
|
if(appclosing) break;
|
|
// if(reader.Entry.IsDirectory || Path.GetFileName(reader.Entry.Key) == ourname) continue; // Don't try to overrite ourselves...
|
|
reader.WriteEntryToDirectory(unZipTo, options);
|
|
UpdateProgressBar(new ByteArgs { Downloaded = curentry++, Total = totalentries }, 1, 2);
|
|
}
|
|
}
|
|
}
|
|
catch(Exception e)
|
|
{
|
|
ErrorDescription = "Update failed: failed to unpack the update...\n" + e.Message;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static bool CheckPremissions(string path)
|
|
{
|
|
try
|
|
{
|
|
DirectoryInfo di = new DirectoryInfo(path);
|
|
DirectorySecurity acl = di.GetAccessControl();
|
|
AuthorizationRuleCollection rules = acl.GetAccessRules(true, true, typeof(NTAccount));
|
|
|
|
WindowsIdentity currentUser = WindowsIdentity.GetCurrent();
|
|
WindowsPrincipal principal = new WindowsPrincipal(currentUser);
|
|
foreach(AuthorizationRule rule in rules)
|
|
{
|
|
FileSystemAccessRule fsAccessRule = rule as FileSystemAccessRule;
|
|
if(fsAccessRule == null) continue;
|
|
|
|
if((fsAccessRule.FileSystemRights & FileSystemRights.WriteData) > 0)
|
|
{
|
|
NTAccount ntAccount = rule.IdentityReference as NTAccount;
|
|
if(ntAccount == null) continue;
|
|
if(principal.IsInRole(ntAccount.Value)) return true;
|
|
}
|
|
}
|
|
}
|
|
catch(UnauthorizedAccessException) { }
|
|
|
|
return false;
|
|
}
|
|
|
|
private void PreDownload()
|
|
{
|
|
if(!Directory.Exists(updateFolder)) Directory.CreateDirectory(updateFolder);
|
|
}
|
|
|
|
private void PostDownload()
|
|
{
|
|
if(!File.Exists(appFileName))
|
|
{
|
|
ErrorDescription = "Unable to located updated executable ('" + appFileName + "')";
|
|
return;
|
|
}
|
|
|
|
if(appclosing) return;
|
|
Process.Start(new ProcessStartInfo { FileName = appFileName });
|
|
}
|
|
|
|
private void WrapUp()
|
|
{
|
|
if(Directory.Exists(updateFolder)) Directory.Delete(updateFolder, true);
|
|
}
|
|
|
|
private void MoveFiles()
|
|
{
|
|
DirectoryInfo di = new DirectoryInfo(updateFolder);
|
|
FileInfo[] files = di.GetFiles();
|
|
|
|
foreach(FileInfo fi in files)
|
|
{
|
|
if(fi.Name != downloadFile) File.Copy(updateFolder + fi.Name, Application.StartupPath + fi.Name, true);
|
|
}
|
|
}
|
|
|
|
private void UpdateProgressBar(ByteArgs e, int step, int totalsteps)
|
|
{
|
|
if(progressbar.InvokeRequired)
|
|
{
|
|
UpdateProgressBarCallback d = UpdateProgressBar;
|
|
progressbar.Invoke(d, new object[] { e, step, totalsteps });
|
|
}
|
|
else
|
|
{
|
|
int stepsize = (int)Math.Round((float)progressbar.Maximum / totalsteps);
|
|
float ratio = (float)e.Downloaded / e.Total;
|
|
int val = (int)Math.Floor(stepsize * step + stepsize * ratio);
|
|
|
|
if(val <= progressbar.Maximum)
|
|
{
|
|
progressbar.Value = val;
|
|
TaskbarProgress.SetValue(this.Handle, progressbar.Value, progressbar.Maximum);
|
|
}
|
|
progressbar.Refresh();
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ======================== Events
|
|
|
|
private void MainForm_Load(object sender, EventArgs e)
|
|
{
|
|
Version version = Assembly.GetEntryAssembly().GetName().Version;
|
|
this.Text += " v" + version.Major + "." + version.Revision.ToString("#00");
|
|
|
|
worker = new BackgroundWorker();
|
|
worker.DoWork += BackgroundWorker;
|
|
worker.RunWorkerCompleted += WorkerOnRunWorkerCompleted;
|
|
worker.WorkerSupportsCancellation = true;
|
|
worker.RunWorkerAsync();
|
|
}
|
|
|
|
private void cancel_Click(object sender, EventArgs e)
|
|
{
|
|
ErrorDescription = string.Empty;
|
|
appclosing = true;
|
|
StopBackgroundWorker();
|
|
InvokeClose();
|
|
}
|
|
|
|
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
|
|
{
|
|
appclosing = true;
|
|
StopBackgroundWorker();
|
|
}
|
|
|
|
private void WebdataOnBytesDownloaded(ByteArgs ba)
|
|
{
|
|
UpdateProgressBar(ba, 0, 2);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
} |