// Update installer for Craft++ using top-level statements (.NET 6+) using System; using System.Diagnostics; using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Security.Cryptography; using System.Security.Principal; using System.Text.Json; using System.Windows.Forms; using Microsoft.Win32; // Log path for the update installer string LogPath = Path.Combine(Path.GetTempPath(), "CraftPlusPlus-update-install.log"); try { // Command-line argument processing (for --source, --destination, --executable, --parent-pid, --temp) string? argsSource = null; string? argsDestination = null; string? argsExecutable = null; int? argsParentPid = null; string? argsTemp = null; string? argsRestartArgument = null; for (int i = 0; i < args.Length; i++) { string arg = args[i]; if (arg.StartsWith("-") || arg.StartsWith("/")) { string key = arg.TrimStart('-', '/'); if (i + 1 < args.Length) { string value = args[i + 1]; switch (key) { case "source": argsSource = value; i++; break; case "destination": argsDestination = value; i++; break; case "executable": argsExecutable = value; i++; break; case "parent-pid": if (int.TryParse(value, out int pid)) argsParentPid = pid; i++; break; case "temp": argsTemp = value; i++; break; case "restart-argument": argsRestartArgument = value; i++; break; } } } } // If running with --source arguments, perform the full installation if (argsSource != null) { // Validate the payload source. Multi-file payloads also copy their runtime DLLs. string[] required = { "codelanguage.exe" }; string[] missing = required.Where(file => !File.Exists(Path.Combine(argsSource, file))).ToArray(); if (missing.Length > 0) { TryLog(LogPath, $"The update payload is missing the main executable: {string.Join(", ", missing)}"); Environment.Exit(1); return; } // Wait for the parent IDE process to exit (use value or 0 if null) int parentPidValue = argsParentPid ?? 0; if (parentPidValue > 0) WaitForParentExit(parentPidValue); // Create destination directory, back up the current build, then replace it. string destination = argsDestination!; Directory.CreateDirectory(destination); string backupDirectory = Path.Combine(Path.GetTempPath(), "CraftPlusPlusUpdateBackup-" + Guid.NewGuid().ToString("N")); bool hasBackup = Directory.EnumerateFileSystemEntries(destination).Any(); if (hasBackup) { CopyDirectory(destination, backupDirectory); TryLog(LogPath, $"Backed up the current installation to {backupDirectory}."); } int copied = 0; try { if (hasBackup) { foreach (string entry in Directory.GetFileSystemEntries(destination)) { if (Directory.Exists(entry)) Directory.Delete(entry, recursive: true); else File.Delete(entry); } } if (Directory.Exists(argsSource)) { foreach (string sourceFile in Directory.GetFiles(argsSource, "*", SearchOption.AllDirectories)) { string relative = Path.GetRelativePath(argsSource, sourceFile); string destinationFile = Path.Combine(destination, relative); Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!); File.Copy(sourceFile, destinationFile, overwrite: true); copied++; } } // Preserve the per-install game TLS certificate across updates. if (hasBackup) { foreach (string preservedName in new[] { "game-cert.key", "game-cert.pem", "game-cert.pfx" }) { string backedUp = Path.Combine(backupDirectory, preservedName); if (File.Exists(backedUp)) File.Copy(backedUp, Path.Combine(destination, preservedName), overwrite: true); } } } catch { if (hasBackup) { RestoreDirectory(backupDirectory, destination); TryLog(LogPath, $"Copy failed; restored the previous installation from {backupDirectory}."); } throw; } // Verify the executable was installed if (argsExecutable == null || !File.Exists(argsExecutable)) { TryLog(LogPath, $"The updated application executable was not installed: {argsExecutable}"); Environment.Exit(1); return; } RegisterCraftProtocol(argsExecutable); // Verify assembly hash match using fixed-time comparison // Only perform this if the DLL exists (non-single-file mode). string sourceAssembly = Path.Combine(argsSource!, "codelanguage.dll"); string installedAssembly = Path.Combine(argsDestination!, "codelanguage.dll"); if (File.Exists(sourceAssembly) && File.Exists(installedAssembly)) { if (!CryptographicOperations.FixedTimeEquals(HashFile(sourceAssembly), HashFile(installedAssembly))) { TryLog(LogPath, "The installed application assembly does not match the downloaded build."); Environment.Exit(1); return; } } // Restart the IDE TryLog(LogPath, $"Copied and verified the complete build ({copied} files). Restarting the IDE."); var restartInfo = new ProcessStartInfo(argsExecutable!) { WorkingDirectory = argsDestination!, UseShellExecute = true }; if (!string.IsNullOrWhiteSpace(argsRestartArgument)) restartInfo.ArgumentList.Add(argsRestartArgument); Process? restarted = Process.Start(restartInfo); if (restarted == null) throw new InvalidOperationException("Windows did not start the updated IDE."); Thread.Sleep(TimeSpan.FromSeconds(5)); restarted.Refresh(); if (restarted.HasExited) { TryLog(LogPath, $"The updated IDE exited during startup (exit code {restarted.ExitCode})."); if (hasBackup) { RestoreDirectory(backupDirectory, destination); TryLog(LogPath, $"Restored the previous installation from {backupDirectory}."); var rollbackInfo = new ProcessStartInfo(argsExecutable!) { WorkingDirectory = destination, UseShellExecute = true }; if (!string.IsNullOrWhiteSpace(argsRestartArgument)) rollbackInfo.ArgumentList.Add(argsRestartArgument); Process.Start(rollbackInfo); } Environment.Exit(1); } TryLog(LogPath, $"IDE restarted successfully (PID {restarted.Id})."); if (hasBackup) { try { Directory.Delete(backupDirectory, recursive: true); } catch { } } ScheduleCleanup(LogPath, argsTemp!); Environment.Exit(0); return; } // If running without --source arguments, the update checker UI would show here Environment.Exit(0); } catch (Exception ex) { TryLog(LogPath, $"CRITICAL INSTALLER ERROR: {ex}"); Environment.Exit(1); } // Helper methods as local functions (valid in top-level statements .NET 6+) static void TryLog(string logPath, string message) { try { File.AppendAllText(logPath, $"[{DateTime.Now:O}] {message}{Environment.NewLine}"); } catch { } } static void CopyDirectory(string source, string destination) { Directory.CreateDirectory(destination); foreach (string sourceFile in Directory.GetFiles(source, "*", SearchOption.AllDirectories)) { string relative = Path.GetRelativePath(source, sourceFile); string destinationFile = Path.Combine(destination, relative); Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!); File.Copy(sourceFile, destinationFile, overwrite: true); } } static void RestoreDirectory(string backup, string destination) { if (!Directory.Exists(backup)) return; if (Directory.Exists(destination)) { foreach (string entry in Directory.GetFileSystemEntries(destination)) { if (Directory.Exists(entry)) Directory.Delete(entry, recursive: true); else File.Delete(entry); } } CopyDirectory(backup, destination); } static void RegisterCraftProtocol(string executablePath) { using var protocol = Registry.CurrentUser.CreateSubKey(@"Software\Classes\craft", writable: true) ?? throw new InvalidOperationException("Could not register the craft:// protocol."); protocol.SetValue("", "URL:Craft++ Game Protocol"); protocol.SetValue("URL Protocol", ""); using var icon = protocol.CreateSubKey("DefaultIcon", writable: true); icon?.SetValue("", $"\"{executablePath}\",0"); using var command = protocol.CreateSubKey(@"shell\open\command", writable: true); command?.SetValue("", $"\"{executablePath}\" \"%1\""); } static void ScheduleCleanup(string logPath, string path) { try { string cmd = $"/c ping 127.0.0.1 -n 4 > nul & rmdir /s /q \"{path}\""; Process.Start(new ProcessStartInfo("cmd.exe", cmd) { CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden }); } catch { } } static void WaitForParentExit(int processId) { try { using Process parent = Process.GetProcessById(processId); if (!parent.WaitForExit((int)TimeSpan.FromMinutes(3).TotalMilliseconds)) throw new TimeoutException("The IDE did not close, so its files could not be replaced."); } catch (ArgumentException) { } } static byte[] HashFile(string path) { using FileStream stream = File.OpenRead(path); return SHA256.HashData(stream); } record RunningIde(int ProcessId, string ExecutablePath, DateTime StartTime);