using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Net; using System.Text.RegularExpressions; using System.Threading; using TGServiceInterface; using TGServiceInterface.Components; namespace TGServerService { sealed partial class ServerInstance : ITGByond { /// /// The instance directory to store the BYOND installation /// const string ByondDirectory = "BYOND"; /// /// The instance directory to use when updating the BYOND installation /// const string StagingDirectory = "BYOND_staged"; /// /// Path to the actual BYOND installation within the /// const string StagingDirectoryInner = StagingDirectory + "/byond"; /// /// The path in the instance directory to store the downloaded BYOND revision /// const string RevisionDownloadPath = "BYONDRevision.zip"; /// /// The location of the BYOND version data of an installation /// const string VersionFile = "/byond_version.dat"; /// /// The URL format string for getting BYOND version {0}.{1} zipfile /// const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip"; /// /// The URL for getting the latest BYOND version zipfile /// const string ByondLatestURL = "https://secure.byond.com/download/build/LATEST/"; /// /// The instance directory to modify the BYOND cfg before installation /// const string ByondConfigDir = StagingDirectory + "/BYOND/cfg"; /// /// BYOND's DreamDaemon config file in the cfg modification directory /// const string ByondDDConfig = ByondConfigDir + "/daemon.txt"; /// /// Setting to add to to suppress an invisible user prompt for running a trusted mode .dmb /// const string ByondNoPromptTrustedMode = "trusted-check 0"; /// /// The status of the BYOND updater /// ByondStatus updateStat = ByondStatus.Idle; /// /// Used for multithreading safety /// object ByondLock = new object(); /// /// The last error the BYOND updater encountered /// string lastError; /// /// Thread used for staging BYOND revisions /// Thread RevisionStaging; /// /// Called when the is setup. Prepares the BYOND updater /// void InitByond() { CleanByondStaging(); } /// /// Cleans the BYOND staging directory /// void CleanByondStaging() { var rrdp = RelativePath(RevisionDownloadPath); //linger not if (File.Exists(rrdp)) File.Delete(rrdp); Program.DeleteDirectory(RelativePath(StagingDirectory)); } /// /// Called when the is shutdown /// void DisposeByond() { lock (ByondLock) { if (RevisionStaging != null) RevisionStaging.Abort(); CleanByondStaging(); } } /// /// Checks if the updater is considered busy /// /// if the updater is considered busy, otherwise bool BusyCheck() { lock (ByondLock) switch (updateStat) { default: case ByondStatus.Starting: case ByondStatus.Downloading: case ByondStatus.Staging: case ByondStatus.Updating: return true; case ByondStatus.Idle: case ByondStatus.Staged: return false; } } /// public ByondStatus CurrentStatus() { lock (ByondLock) { return updateStat; } } /// public string GetError() { lock (ByondLock) { var error = lastError; lastError = null; return error; } } /// public string GetVersion(ByondVersion type) { try { lock (ByondLock) { if (type == ByondVersion.Latest) { //get the latest version from the website HttpWebRequest request = (HttpWebRequest)WebRequest.Create(ByondLatestURL); var results = new List(); using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { using (StreamReader reader = new StreamReader(response.GetResponseStream())) { string html = reader.ReadToEnd(); Regex regex = new Regex("\\\"([^\"]*)\\\""); MatchCollection matches = regex.Matches(html); foreach (Match match in matches) if (match.Success && match.Value.Contains("_byond.exe")) results.Add(match.Value.Replace("\"", "").Replace("_byond.exe", "")); } } results.Sort(); results.Reverse(); return results.Count > 0 ? results[0] : null; } else { string DirToUse = RelativePath(type == ByondVersion.Staged ? StagingDirectoryInner : ByondDirectory); if (Directory.Exists(DirToUse)) { string file = DirToUse + VersionFile; if (File.Exists(file)) return File.ReadAllText(file); } } return null; } } catch (Exception e) { return "Error: " + e.ToString(); } } /// /// Downloads and unzips a BYOND revision. Calls afterwards if the isn't running, otherwise, calls . Sets on failure /// /// Stringified BYOND revision public void UpdateToVersionImpl(object param) { lock (ByondLock) { if (updateStat != ByondStatus.Starting) return; updateStat = ByondStatus.Downloading; } try { CleanByondStaging(); var vi = ((string)param).Split('.'); var major = Convert.ToInt32(vi[0]); var minor = Convert.ToInt32(vi[1]); var rrdp = RelativePath(RevisionDownloadPath); using (var client = new WebClient()) { SendMessage(String.Format("BYOND: Updating to version {0}.{1}...", major, minor), MessageType.DeveloperInfo); //DOWNLOADING try { client.DownloadFile(String.Format(ByondRevisionsURL, major, minor), rrdp); } catch { SendMessage("BYOND: Update download failed. Does the specified version exist?", MessageType.DeveloperInfo); lastError = String.Format("Download of BYOND version {0}.{1} failed! Does it exist?", major, minor); WriteWarning(String.Format("Failed to update BYOND to version {0}.{1}!", major, minor), EventID.BYONDUpdateFail); lock (ByondLock) { updateStat = ByondStatus.Idle; } return; } } lock (ByondLock) { updateStat = ByondStatus.Staging; } //STAGING ZipFile.ExtractToDirectory(rrdp, RelativePath(StagingDirectory)); lock (ByondLock) { File.WriteAllText(RelativePath(StagingDirectoryInner + VersionFile), String.Format("{0}.{1}", major, minor)); //IMPORTANT: SET THE BYOND CONFIG TO NOT PROMPT FOR TRUSTED MODE REEE Directory.CreateDirectory(RelativePath(ByondConfigDir)); File.WriteAllText(RelativePath(ByondDDConfig), ByondNoPromptTrustedMode); } File.Delete(RevisionDownloadPath); lock (ByondLock) { updateStat = ByondStatus.Staged; } switch (DaemonStatus()) { case DreamDaemonStatus.Offline: if(ApplyStagedUpdate()) lastError = null; else lastError = "Failed to apply update!"; break; default: RequestRestart(); lastError = "Update staged. Awaiting server restart..."; SendMessage(String.Format("BYOND: Staging complete. Awaiting server restart...", major, minor), MessageType.DeveloperInfo); WriteInfo(String.Format("BYOND update {0}.{1} staged", major, minor), EventID.BYONDUpdateStaged); break; } } catch (ThreadAbortException) { return; } catch (Exception e) { WriteError("Revision staging errror: " + e.ToString(), EventID.BYONDUpdateFail); lock (ByondLock) { updateStat = ByondStatus.Idle; lastError = e.ToString(); RevisionStaging = null; } } } /// public bool UpdateToVersion(int major, int minor) { lock (ByondLock) { if (!BusyCheck()) { updateStat = ByondStatus.Starting; RevisionStaging = new Thread(new ParameterizedThreadStart(UpdateToVersionImpl)) { IsBackground = true //don't slow me down }; RevisionStaging.Start(String.Format("{0}.{1}", major, minor)); return true; } return false; } } /// /// Attempts to move the staged update from to . Sets on failure /// /// on success, on failure bool ApplyStagedUpdate() { lock (CompilerLock) { if (compilerCurrentStatus == CompilerStatus.Compiling) return false; lock (ByondLock) { if (updateStat != ByondStatus.Staged) return false; updateStat = ByondStatus.Updating; } try { var rbd = RelativePath(ByondDirectory); Program.DeleteDirectory(rbd); Directory.Move(RelativePath(StagingDirectoryInner), rbd); Program.DeleteDirectory(RelativePath(StagingDirectory)); lastError = null; SendMessage("BYOND: Update completed!", MessageType.DeveloperInfo); WriteInfo(String.Format("BYOND update {0} completed!", GetVersion(ByondVersion.Installed)), EventID.BYONDUpdateComplete); return true; } catch (Exception e) { lastError = e.ToString(); SendMessage("BYOND: Update failed!", MessageType.DeveloperInfo); WriteError("BYOND update failed! Error: " + e.ToString(), EventID.BYONDUpdateFail); return false; } finally { lock(ByondLock) { updateStat = ByondStatus.Idle; } } } } } }