using System.Diagnostics; using System.Reflection; using System.Text; using System.Xml.Serialization; using PluginLoader.Compiler; using PluginLoader.GUI; using Sandbox.Graphics.GUI; using VRage; namespace PluginLoader.Data; public class LocalFolderPlugin : PluginData { private const string XmlDataType = "Xml files (*.xml)|*.xml|All files (*.*)|*.*"; private const int GitTimeout = 10000; private string[] sourceDirectories; public LocalFolderPlugin(Config settings) { Id = settings.Folder; FriendlyName = Path.GetFileName(Id); Status = PluginStatus.None; FolderSettings = settings; DeserializeFile(settings.DataFile); } private LocalFolderPlugin(string folder) { Id = folder; Status = PluginStatus.None; FolderSettings = new() { Folder = folder }; } public override string Source => MyTexts.GetString(MyCommonTexts.Local); public Config FolderSettings { get; } public override Assembly GetAssembly() { if (Directory.Exists(Id)) { var compiler = new RoslynCompiler(FolderSettings.DebugBuild); var hasFile = false; var sb = new StringBuilder(); sb.Append("Compiling files from ").Append(Id).Append(":").AppendLine(); foreach (var file in GetProjectFiles(Id)) using (var fileStream = File.OpenRead(file)) { hasFile = true; var name = file.Substring(Id.Length + 1, file.Length - (Id.Length + 1)); sb.Append(name).Append(", "); compiler.Load(fileStream, file); } if (hasFile) { sb.Length -= 2; LogFile.WriteLine(sb.ToString()); } else { throw new IOException("No files were found in the directory specified."); } var data = compiler.Compile(FriendlyName + '_' + Path.GetRandomFileName(), out var symbols); var a = Assembly.Load(data, symbols); Version = a.GetName().Version; return a; } throw new DirectoryNotFoundException("Unable to find directory '" + Id + "'"); } private IEnumerable GetProjectFiles(string folder) { string gitError = null; try { var p = new Process(); // Redirect the output stream of the child process. p.StartInfo.UseShellExecute = false; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.RedirectStandardError = true; p.StartInfo.FileName = "git"; p.StartInfo.Arguments = "ls-files --cached --others --exclude-standard"; p.StartInfo.WorkingDirectory = folder; p.Start(); // Do not wait for the child process to exit before // reading to the end of its redirected stream. // Read the output stream first and then wait. var gitOutput = p.StandardOutput.ReadToEnd(); gitError = p.StandardError.ReadToEnd(); if (!p.WaitForExit(GitTimeout)) { p.Kill(); throw new TimeoutException("Git operation timed out."); } if (p.ExitCode == 0) { var files = gitOutput.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); return files.Where(x => x.EndsWith(".cs") && IsValidProjectFile(x)) .Select(x => Path.Combine(folder, x.Trim().Replace('/', Path.DirectorySeparatorChar))) .Where(x => File.Exists(x)); } var sb = new StringBuilder("An error occurred while checking git for project files.").AppendLine(); if (!string.IsNullOrWhiteSpace(gitError)) { sb.AppendLine("Git output: "); sb.Append(gitError).AppendLine(); } LogFile.WriteLine(sb.ToString()); } catch (Exception e) { var sb = new StringBuilder("An error occurred while checking git for project files.").AppendLine(); if (!string.IsNullOrWhiteSpace(gitError)) { sb.AppendLine(" Git output: "); sb.Append(gitError).AppendLine(); } sb.AppendLine("Exception: "); sb.Append(e).AppendLine(); LogFile.WriteLine(sb.ToString()); } var sep = Path.DirectorySeparatorChar; return Directory.EnumerateFiles(folder, "*.cs", SearchOption.AllDirectories) .Where(x => !x.Contains(sep + "bin" + sep) && !x.Contains(sep + "obj" + sep) && IsValidProjectFile(x)); } private bool IsValidProjectFile(string file) { if (sourceDirectories == null || sourceDirectories.Length == 0) return true; file = file.Replace('\\', '/'); foreach (var dir in sourceDirectories) if (file.StartsWith(dir)) return true; return false; } public override string ToString() { return Id; } public override void Show() { var folder = Path.GetFullPath(Id); if (Directory.Exists(folder)) Process.Start("explorer.exe", $"\"{folder}\""); } public override bool OpenContextMenu(MyGuiControlContextMenu menu) { menu.Clear(); menu.AddItem(new("Remove")); menu.AddItem(new("Load data file")); if (FolderSettings.DebugBuild) menu.AddItem(new("Switch to release build")); else menu.AddItem(new("Switch to debug build")); return true; } public override void ContextMenuClicked(MyGuiScreenPluginConfig screen, MyGuiControlContextMenu.EventArgs args) { switch (args.ItemIndex) { case 0: Main.Instance.Config.PluginFolders.Remove(Id); screen.RemovePlugin(this); screen.RequireRestart(); break; case 1: LoaderTools.OpenFileDialog("Open an xml data file", Path.GetDirectoryName(FolderSettings.DataFile), XmlDataType, file => DeserializeFile(file, screen)); break; case 2: FolderSettings.DebugBuild = !FolderSettings.DebugBuild; screen.RequireRestart(); break; } } // Deserializes a file and refreshes the plugin screen private void DeserializeFile(string file, MyGuiScreenPluginConfig screen = null) { if (!File.Exists(file)) return; try { var xml = new XmlSerializer(typeof(PluginData)); using (var reader = File.OpenText(file)) { var resultObj = xml.Deserialize(reader); if (resultObj.GetType() != typeof(GitHubPlugin)) throw new("Xml file is not of type GitHubPlugin!"); var github = (GitHubPlugin)resultObj; github.Init(LoaderTools.PluginsDir); FriendlyName = github.FriendlyName; Tooltip = github.Tooltip; Author = github.Author; Description = github.Description; sourceDirectories = github.SourceDirectories; FolderSettings.DataFile = file; if (screen != null && screen.Visible && screen.IsOpened) screen.RefreshSidePanel(); } } catch (Exception e) { LogFile.WriteLine("Error while reading the xml file: " + e); } } public static void CreateNew(Action onComplete) { LoaderTools.OpenFolderDialog("Open the root of your project", LoaderTools.PluginsDir, folder => { if (Main.Instance.List.Contains(folder)) { MyGuiSandbox.CreateMessageBox(MyMessageBoxStyleEnum.Error, messageText: new("That folder already exists in the list!")); return; } var plugin = new LocalFolderPlugin(folder); LoaderTools.OpenFileDialog("Open the xml data file", folder, XmlDataType, file => { plugin.DeserializeFile(file); onComplete(plugin); }); }); } public class Config { public Config() { } public Config(string folder, string dataFile) { Folder = folder; DataFile = dataFile; } public string Folder { get; set; } public string DataFile { get; set; } public bool DebugBuild { get; set; } = true; public bool Valid => Directory.Exists(Folder) && File.Exists(DataFile); } }