mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-26 06:27:19 +01:00
631 lines
17 KiB
C#
631 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using TGServiceInterface;
|
|
|
|
namespace TGServerService
|
|
{
|
|
partial class TGStationServer : ITGCompiler
|
|
{
|
|
#region Win32 Shit
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, SymbolicLink dwFlags);
|
|
enum SymbolicLink
|
|
{
|
|
File = 0,
|
|
Directory = 1
|
|
}
|
|
#endregion
|
|
|
|
const string StaticDirs = "Static";
|
|
const string StaticBackupDir = "Static_BACKUP";
|
|
|
|
const string LibMySQLFile = "/libmysql.dll";
|
|
|
|
const string GameDir = "Game";
|
|
const string GameDirA = GameDir + "/A";
|
|
const string GameDirB = GameDir + "/B";
|
|
const string GameDirLive = GameDir + "/Live";
|
|
|
|
const string LiveFile = "/TestLive.lk";
|
|
const string ADirTest = GameDirA + LiveFile;
|
|
const string BDirTest = GameDirB + LiveFile;
|
|
const string LiveDirTest = GameDirLive + LiveFile;
|
|
|
|
const string InterfaceDLLName = "TGServiceInterface.dll";
|
|
|
|
object CompilerLock = new object();
|
|
TGCompilerStatus compilerCurrentStatus;
|
|
string lastCompilerError;
|
|
|
|
Thread CompilerThread;
|
|
bool compilationCancellationRequestation = false;
|
|
bool canCancelCompilation = false;
|
|
bool silentCompile = false;
|
|
|
|
bool UpdateStaged = false;
|
|
|
|
//deletes leftovers and checks current status
|
|
void InitCompiler()
|
|
{
|
|
if(File.Exists(LiveDirTest))
|
|
File.Delete(LiveDirTest);
|
|
compilerCurrentStatus = IsInitialized();
|
|
}
|
|
|
|
//public api
|
|
public TGCompilerStatus GetStatus()
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
return compilerCurrentStatus;
|
|
}
|
|
}
|
|
|
|
//public api
|
|
public string CompileError()
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
var err = lastCompilerError;
|
|
lastCompilerError = null;
|
|
return err;
|
|
}
|
|
}
|
|
|
|
//kills the compiler if its running
|
|
void DisposeCompiler()
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
if (CompilerThread == null || !CompilerThread.IsAlive)
|
|
return;
|
|
CompilerThread.Abort(); //this will safely kill dm
|
|
InitCompiler(); //also cleanup
|
|
}
|
|
}
|
|
|
|
//translates the win32 api call into an exception if it fails
|
|
void CreateSymlink(string link, string target)
|
|
{
|
|
if (!CreateSymbolicLink(new DirectoryInfo(link).FullName, new DirectoryInfo(target).FullName, File.Exists(target) ? SymbolicLink.File : SymbolicLink.Directory))
|
|
throw new Exception(String.Format("Failed to create symlink from {0} to {1}! Error: {2}", target, link, Marshal.GetLastWin32Error()));
|
|
}
|
|
|
|
//requires CompilerLock to be locked
|
|
bool CompilerIdleNoLock()
|
|
{
|
|
return compilerCurrentStatus == TGCompilerStatus.Uninitialized || compilerCurrentStatus == TGCompilerStatus.Initialized;
|
|
}
|
|
|
|
//public api
|
|
public bool Initialize()
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
if (!CompilerIdleNoLock())
|
|
return false;
|
|
lastCompilerError = null;
|
|
compilerCurrentStatus = TGCompilerStatus.Initializing;
|
|
CompilerThread = new Thread(new ThreadStart(InitializeImpl));
|
|
CompilerThread.Start();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
//what is says on the tin
|
|
TGCompilerStatus IsInitialized()
|
|
{
|
|
if (File.Exists(GameDirLive + LibMySQLFile)) //its a good tell, jim
|
|
return TGCompilerStatus.Initialized;
|
|
return TGCompilerStatus.Uninitialized;
|
|
}
|
|
|
|
void CleanGameFolderList(string GameDir, IList<string> theList)
|
|
{
|
|
foreach (var I in theList)
|
|
{
|
|
var the_path = Path.Combine(GameDir, I);
|
|
if (Directory.Exists(the_path))
|
|
Directory.Delete(the_path);
|
|
}
|
|
}
|
|
|
|
//we need to remove symlinks before we can recursively delete
|
|
void CleanGameFolder()
|
|
{
|
|
if (Directory.Exists(Path.Combine(GameDirA, InterfaceDLLName)))
|
|
Directory.Delete(Path.Combine(GameDirA, InterfaceDLLName));
|
|
|
|
if (Directory.Exists(Path.Combine(GameDirB, InterfaceDLLName)))
|
|
Directory.Delete(Path.Combine(GameDirB, InterfaceDLLName));
|
|
|
|
if (Directory.Exists(GameDirLive))
|
|
Directory.Delete(GameDirLive);
|
|
}
|
|
|
|
//Initializing thread
|
|
public void InitializeImpl()
|
|
{
|
|
try
|
|
{
|
|
if (DaemonStatus() != TGDreamDaemonStatus.Offline)
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
lastCompilerError = "Dream daemon must not be running";
|
|
compilerCurrentStatus = IsInitialized();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!Exists()) //repo
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
lastCompilerError = "Repository is not setup!";
|
|
compilerCurrentStatus = IsInitialized();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!RepoConfigsMatch())
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
lastCompilerError = "Repository TGS3.json does not match cached version! Please update the config appropriately!";
|
|
compilerCurrentStatus = IsInitialized();
|
|
return;
|
|
}
|
|
}
|
|
try
|
|
{
|
|
SendMessage("DM: Setting up symlinks...", ChatMessageType.DeveloperInfo);
|
|
CleanGameFolder();
|
|
Program.DeleteDirectory(GameDir);
|
|
|
|
Directory.CreateDirectory(GameDirA);
|
|
Directory.CreateDirectory(GameDirB);
|
|
|
|
var Config = new RepoConfig(false);
|
|
|
|
if (Config != null) {
|
|
foreach (var I in Config.StaticDirectoryPaths)
|
|
CreateSymlink(Path.Combine(GameDirA, I), Path.Combine(StaticDirs, I));
|
|
foreach (var I in Config.DLLPaths)
|
|
CreateSymlink(Path.Combine(GameDirA, I), Path.Combine(StaticDirs, I));
|
|
}
|
|
|
|
CreateSymlink(Path.Combine(GameDirA, InterfaceDLLName), InterfaceDLLName);
|
|
CreateSymlink(Path.Combine(GameDirB, InterfaceDLLName), InterfaceDLLName);
|
|
|
|
CreateSymlink(GameDirLive, GameDirA);
|
|
|
|
lock (CompilerLock)
|
|
{
|
|
compilerCurrentStatus = TGCompilerStatus.Compiling;
|
|
silentCompile = true;
|
|
}
|
|
}
|
|
catch (ThreadAbortException)
|
|
{
|
|
return;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
SendMessage("DM: Setup failed!", ChatMessageType.DeveloperInfo);
|
|
lastCompilerError = e.ToString();
|
|
compilerCurrentStatus = TGCompilerStatus.Uninitialized;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
catch (ThreadAbortException)
|
|
{
|
|
return;
|
|
}
|
|
CompileImpl();
|
|
}
|
|
|
|
//Returns the A or B dir in which the game is NOT running
|
|
string GetStagingDir()
|
|
{
|
|
string TheDir;
|
|
if (!Directory.Exists(GameDirLive))
|
|
TheDir = GameDirA;
|
|
else
|
|
{
|
|
File.Create(LiveDirTest).Close();
|
|
try
|
|
{
|
|
if (File.Exists(ADirTest))
|
|
TheDir = GameDirA;
|
|
else if (File.Exists(BDirTest))
|
|
TheDir = GameDirB;
|
|
else
|
|
throw new Exception("Unable to determine current live directory!");
|
|
}
|
|
finally
|
|
{
|
|
File.Delete(LiveDirTest);
|
|
}
|
|
|
|
|
|
TheDir = InvertDirectory(TheDir);
|
|
|
|
}
|
|
//So TheDir is what the Live folder is NOT pointing to
|
|
//Now we need to check if DD is running that folder and swap it if necessary
|
|
|
|
var rsclock = TheDir + "/" + Properties.Settings.Default.ProjectName + ".rsc.lk";
|
|
if (File.Exists(rsclock))
|
|
{
|
|
try
|
|
{
|
|
File.Delete(rsclock);
|
|
}
|
|
catch //held open by byond
|
|
{
|
|
//This means there is a staged update waiting to be applied, we have to unstage it before we can work
|
|
Directory.Delete(GameDirLive);
|
|
CreateSymlink(GameDirLive, TheDir);
|
|
return InvertDirectory(TheDir);
|
|
}
|
|
}
|
|
return TheDir;
|
|
}
|
|
|
|
//I hope you can read this
|
|
string InvertDirectory(string gameDirectory)
|
|
{
|
|
if (gameDirectory == GameDirA)
|
|
return GameDirB;
|
|
else
|
|
return GameDirA;
|
|
}
|
|
|
|
//Compiler thread
|
|
void CompileImpl()
|
|
{
|
|
try
|
|
{
|
|
if (GetVersion(TGByondVersion.Installed) == null)
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
lastCompilerError = "BYOND not installed!";
|
|
compilerCurrentStatus = TGCompilerStatus.Initialized;
|
|
return;
|
|
}
|
|
}
|
|
if (!RepoConfigsMatch())
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
lastCompilerError = "Repository TGS3.json does not match cached version! Please update the config appropriately!";
|
|
compilerCurrentStatus = IsInitialized();
|
|
return;
|
|
}
|
|
}
|
|
string resurrectee;
|
|
bool repobusy_check = false;
|
|
if (!Monitor.TryEnter(RepoLock))
|
|
repobusy_check = true;
|
|
|
|
if (!repobusy_check)
|
|
{
|
|
if (RepoBusy)
|
|
repobusy_check = true;
|
|
else
|
|
RepoBusy = true;
|
|
Monitor.Exit(RepoLock);
|
|
}
|
|
|
|
if (repobusy_check)
|
|
{
|
|
SendMessage("DM: Copy aborted, repo locked!", ChatMessageType.DeveloperInfo);
|
|
lock (CompilerLock)
|
|
{
|
|
lastCompilerError = "The repo could not be locked for copying";
|
|
compilerCurrentStatus = TGCompilerStatus.Initialized; //still fairly valid
|
|
return;
|
|
}
|
|
}
|
|
try
|
|
{
|
|
bool silent;
|
|
lock (CompilerLock)
|
|
{
|
|
silent = silentCompile;
|
|
silentCompile = false;
|
|
}
|
|
|
|
if (!silent)
|
|
SendMessage("DM: Compiling...", ChatMessageType.DeveloperInfo);
|
|
|
|
resurrectee = GetStagingDir();
|
|
|
|
var Config = new RepoConfig(false);
|
|
var deleteExcludeList = new List<string>();
|
|
deleteExcludeList.AddRange(Config.StaticDirectoryPaths);
|
|
deleteExcludeList.AddRange(Config.DLLPaths);
|
|
Program.DeleteDirectory(resurrectee, true, deleteExcludeList);
|
|
|
|
|
|
Directory.CreateDirectory(resurrectee + "/.git/logs");
|
|
|
|
foreach (var I in Config.StaticDirectoryPaths)
|
|
{
|
|
var the_path = Path.Combine(resurrectee, I);
|
|
if (!Directory.Exists(the_path))
|
|
CreateSymlink(Path.Combine(resurrectee, I), Path.Combine(StaticDirs, I));
|
|
}
|
|
foreach (var I in Config.DLLPaths)
|
|
{
|
|
var the_path = Path.Combine(resurrectee, I);
|
|
if (!File.Exists(the_path))
|
|
CreateSymlink(the_path, Path.Combine(StaticDirs, I));
|
|
}
|
|
|
|
if (!File.Exists(Path.Combine(resurrectee, InterfaceDLLName)))
|
|
CreateSymlink(Path.Combine(resurrectee, InterfaceDLLName), InterfaceDLLName);
|
|
|
|
deleteExcludeList.Add(".git");
|
|
Program.CopyDirectory(RepoPath, resurrectee, deleteExcludeList);
|
|
//just the tip
|
|
const string GitLogsDir = "/.git/logs";
|
|
Program.CopyDirectory(RepoPath + GitLogsDir, resurrectee + GitLogsDir);
|
|
try
|
|
{
|
|
File.Copy(PRJobFile, resurrectee + Path.DirectorySeparatorChar + PRJobFile);
|
|
}
|
|
catch { }
|
|
}
|
|
finally
|
|
{
|
|
lock (RepoLock)
|
|
{
|
|
RepoBusy = false;
|
|
}
|
|
}
|
|
|
|
var res = CreateBackup();
|
|
if (res != null)
|
|
lock (CompilerLock)
|
|
{
|
|
lastCompilerError = res;
|
|
compilerCurrentStatus = TGCompilerStatus.Initialized;
|
|
return;
|
|
}
|
|
|
|
var dmeName = ProjectName() + ".dme";
|
|
var dmePath = resurrectee + "/" + dmeName;
|
|
if (!File.Exists(dmePath))
|
|
{
|
|
var errorMsg = String.Format("Could not find {0}!", dmeName);
|
|
SendMessage("DM: " + errorMsg, ChatMessageType.DeveloperInfo);
|
|
TGServerService.WriteError(errorMsg, TGServerService.EventID.DMCompileCrash);
|
|
lock (CompilerLock)
|
|
{
|
|
lastCompilerError = errorMsg;
|
|
compilerCurrentStatus = TGCompilerStatus.Initialized;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!PrecompileHook())
|
|
{
|
|
lastCompilerError = "The precompile hook failed";
|
|
compilerCurrentStatus = TGCompilerStatus.Initialized; //still fairly valid
|
|
TGServerService.WriteWarning("Precompile hook failed!", TGServerService.EventID.DMCompileError);
|
|
return;
|
|
}
|
|
|
|
using (var DM = new Process()) //will kill the process if the thread is terminated
|
|
{
|
|
DM.StartInfo.FileName = ByondDirectory + "/bin/dm.exe";
|
|
DM.StartInfo.Arguments = String.Format("-clean {0}", dmePath);
|
|
DM.StartInfo.RedirectStandardOutput = true;
|
|
DM.StartInfo.UseShellExecute = false;
|
|
var OutputList = new StringBuilder();
|
|
DM.OutputDataReceived += new DataReceivedEventHandler(
|
|
delegate (object sender, DataReceivedEventArgs e)
|
|
{
|
|
OutputList.Append(Environment.NewLine);
|
|
OutputList.Append(e.Data);
|
|
}
|
|
);
|
|
try
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
if (compilationCancellationRequestation)
|
|
return;
|
|
canCancelCompilation = true;
|
|
}
|
|
|
|
DM.Start();
|
|
DM.BeginOutputReadLine();
|
|
while (!DM.HasExited)
|
|
DM.WaitForExit(100);
|
|
DM.CancelOutputRead();
|
|
|
|
lock (CompilerLock)
|
|
{
|
|
canCancelCompilation = false;
|
|
compilationCancellationRequestation = false;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
if (!DM.HasExited)
|
|
{
|
|
DM.Kill();
|
|
DM.WaitForExit();
|
|
}
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
canCancelCompilation = false;
|
|
}
|
|
}
|
|
|
|
if (DM.ExitCode == 0)
|
|
{
|
|
lock (watchdogLock)
|
|
{
|
|
try
|
|
{
|
|
//gotta go fast
|
|
var online = currentStatus == TGDreamDaemonStatus.Online;
|
|
if (online)
|
|
Proc.Suspend();
|
|
try
|
|
{
|
|
if (Directory.Exists(GameDirLive))
|
|
//these two lines should be atomic but this is the best we can do
|
|
Directory.Delete(GameDirLive);
|
|
CreateSymlink(GameDirLive, resurrectee);
|
|
}
|
|
finally
|
|
{
|
|
if (online && !Proc.HasExited)
|
|
Proc.Resume();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (currentStatus == TGDreamDaemonStatus.Online)
|
|
{
|
|
try
|
|
{
|
|
Proc.PriorityClass = ProcessPriorityClass.Normal;
|
|
}
|
|
catch { }
|
|
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.Normal;
|
|
Thread.CurrentThread.Priority = ThreadPriority.Normal;
|
|
}
|
|
}
|
|
}
|
|
var staged = DaemonStatus() != TGDreamDaemonStatus.Offline;
|
|
if (!PostcompileHook())
|
|
{
|
|
lastCompilerError = "The postcompile hook failed";
|
|
compilerCurrentStatus = TGCompilerStatus.Initialized; //still fairly valid
|
|
TGServerService.WriteWarning("Postcompile hook failed!", TGServerService.EventID.DMCompileError);
|
|
return;
|
|
}
|
|
var msg = String.Format("Compile complete!{0}", !staged ? "" : " Server will update next round.");
|
|
SendMessage("DM: " + msg, ChatMessageType.DeveloperInfo);
|
|
TGServerService.WriteInfo(msg, TGServerService.EventID.DMCompileSuccess);
|
|
lock (CompilerLock)
|
|
{
|
|
if (staged)
|
|
UpdateStaged = true;
|
|
lastCompilerError = null;
|
|
compilerCurrentStatus = TGCompilerStatus.Initialized; //still fairly valid
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SendMessage("DM: Compile failed!", ChatMessageType.DeveloperInfo); //Also happens for warnings
|
|
TGServerService.WriteWarning("Compile error: " + OutputList.ToString(), TGServerService.EventID.DMCompileError);
|
|
lock (CompilerLock)
|
|
{
|
|
lastCompilerError = "DM compile failure";
|
|
compilerCurrentStatus = TGCompilerStatus.Initialized;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
catch (ThreadAbortException)
|
|
{
|
|
return;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
SendMessage("DM: Compiler thread crashed!", ChatMessageType.DeveloperInfo);
|
|
TGServerService.WriteError("Compile manager errror: " + e.ToString(), TGServerService.EventID.DMCompileCrash);
|
|
lock (CompilerLock)
|
|
{
|
|
lastCompilerError = e.ToString();
|
|
compilerCurrentStatus = TGCompilerStatus.Initialized; //still fairly valid
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
canCancelCompilation = false;
|
|
if (compilationCancellationRequestation)
|
|
{
|
|
compilerCurrentStatus = TGCompilerStatus.Initialized;
|
|
compilationCancellationRequestation = false;
|
|
SendMessage("DM: Compile cancelled!", ChatMessageType.DeveloperInfo);
|
|
TGServerService.WriteInfo("Compilation cancelled", TGServerService.EventID.DMCompileCancel);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//kicks off the compiler thread
|
|
//public api
|
|
public bool Compile(bool silent = false)
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
if (compilerCurrentStatus != TGCompilerStatus.Initialized)
|
|
return false;
|
|
silentCompile = silent;
|
|
lastCompilerError = null;
|
|
compilerCurrentStatus = TGCompilerStatus.Compiling;
|
|
CompilerThread = new Thread(new ThreadStart(CompileImpl));
|
|
CompilerThread.Start();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//public api
|
|
public string ProjectName()
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
return Properties.Settings.Default.ProjectName;
|
|
}
|
|
}
|
|
|
|
//public api
|
|
public void SetProjectName(string projectName)
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
Properties.Settings.Default.ProjectName = projectName;
|
|
}
|
|
}
|
|
|
|
public string Cancel()
|
|
{
|
|
lock (CompilerLock)
|
|
{
|
|
if (compilerCurrentStatus != TGCompilerStatus.Compiling)
|
|
return "Invalid state for cancellation!";
|
|
compilationCancellationRequestation = true;
|
|
if (canCancelCompilation)
|
|
CompilerThread.Abort();
|
|
else
|
|
return "Compilation will be cancelled when the repo copy is complete";
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|