From b4d01386a644a50e18f6b5d7eb2386a016cea3a0 Mon Sep 17 00:00:00 2001 From: "Jamie.Rees" Date: Mon, 23 Oct 2017 11:35:02 +0100 Subject: [PATCH] Added the ability to run a user defined update script #1460 --- src/Ombi.DependencyInjection/IocExtensions.cs | 3 + src/Ombi.Helpers/IProcessProvider.cs | 21 ++ src/Ombi.Helpers/ProcessProvider.cs | 236 ++++++++++++++++++ .../Jobs/Ombi/OmbiAutomaticUpdater.cs | 52 +++- .../Settings/Models/UpdateSettings.cs | 3 + src/Ombi.Tests/IdentityControllerTests.cs | 2 +- src/Ombi.Updater/IProcessProvider.cs | 19 ++ src/Ombi.Updater/ProcessProvider.cs | 7 +- src/Ombi.Updater/Program.cs | 3 +- .../ClientApp/app/interfaces/ISettings.ts | 2 + .../app/settings/update/update.component.html | 47 ++-- .../app/settings/update/update.component.ts | 10 + 12 files changed, 378 insertions(+), 27 deletions(-) create mode 100644 src/Ombi.Helpers/IProcessProvider.cs create mode 100644 src/Ombi.Helpers/ProcessProvider.cs create mode 100644 src/Ombi.Updater/IProcessProvider.cs diff --git a/src/Ombi.DependencyInjection/IocExtensions.cs b/src/Ombi.DependencyInjection/IocExtensions.cs index 0f2e49ad4..5420e0960 100644 --- a/src/Ombi.DependencyInjection/IocExtensions.cs +++ b/src/Ombi.DependencyInjection/IocExtensions.cs @@ -38,6 +38,7 @@ using Ombi.Api.Service; using Ombi.Api.Slack; using Ombi.Core.Rule.Interfaces; using Ombi.Core.Senders; +using Ombi.Helpers; using Ombi.Schedule.Jobs.Couchpotato; using Ombi.Schedule.Jobs.Emby; using Ombi.Schedule.Jobs.Ombi; @@ -45,6 +46,7 @@ using Ombi.Schedule.Jobs.Plex; using Ombi.Schedule.Jobs.Sonarr; using Ombi.Store.Entities; using Ombi.Store.Repository.Requests; +using Ombi.Updater; using PlexContentCacher = Ombi.Schedule.Jobs.Plex.PlexContentCacher; namespace Ombi.DependencyInjection @@ -152,6 +154,7 @@ namespace Ombi.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } } } diff --git a/src/Ombi.Helpers/IProcessProvider.cs b/src/Ombi.Helpers/IProcessProvider.cs new file mode 100644 index 000000000..a7937fc92 --- /dev/null +++ b/src/Ombi.Helpers/IProcessProvider.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Diagnostics; +using Ombi.Helpers; + +namespace Ombi.Updater +{ + public interface IProcessProvider + { + bool Exists(int processId); + bool Exists(string processName); + List FindProcessByName(string name); + ProcessInfo GetCurrentProcess(); + int GetCurrentProcessId(); + ProcessInfo GetProcessById(int id); + void Kill(int processId); + void KillAll(string processName); + void SetPriority(int processId, ProcessPriorityClass priority); + Process Start(string path, string args = null); + void WaitForExit(Process process); + } +} \ No newline at end of file diff --git a/src/Ombi.Helpers/ProcessProvider.cs b/src/Ombi.Helpers/ProcessProvider.cs new file mode 100644 index 000000000..97ac13c50 --- /dev/null +++ b/src/Ombi.Helpers/ProcessProvider.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Extensions.Logging; +using Ombi.Updater; + +namespace Ombi.Helpers +{ + public class ProcessProvider : IProcessProvider + { + public ProcessProvider(ILogger log) + { + _log = log; + } + + private readonly ILogger _log; + + public int GetCurrentProcessId() + { + return Process.GetCurrentProcess().Id; + } + + public ProcessInfo GetCurrentProcess() + { + return ConvertToProcessInfo(Process.GetCurrentProcess()); + } + public bool Exists(int processId) + { + return GetProcessById(processId) != null; + } + + public bool Exists(string processName) + { + return GetProcessesByName(processName).Any(); + } + public ProcessInfo GetProcessById(int id) + { + _log.LogInformation("Finding process with Id:{0}", id); + + var processInfo = ConvertToProcessInfo(Process.GetProcesses().FirstOrDefault(p => p.Id == id)); + + if (processInfo == null) + { + _log.LogInformation("Unable to find process with ID {0}", id); + } + else + { + _log.LogInformation("Found process {0}", processInfo.ToString()); + } + + return processInfo; + } + + public List FindProcessByName(string name) + { + return GetProcessesByName(name).Select(ConvertToProcessInfo).Where(c => c != null).ToList(); + } + + + public void WaitForExit(Process process) + { + _log.LogInformation("Waiting for process {0} to exit.", process.ProcessName); + + process.WaitForExit(); + } + + public void SetPriority(int processId, ProcessPriorityClass priority) + { + var process = Process.GetProcessById(processId); + + _log.LogInformation("Updating [{0}] process priority from {1} to {2}", + process.ProcessName, + process.PriorityClass, + priority); + + process.PriorityClass = priority; + } + + public void Kill(int processId) + { + var process = Process.GetProcesses().FirstOrDefault(p => p.Id == processId); + + if (process == null) + { + _log.LogInformation("Cannot find process with id: {0}", processId); + return; + } + + process.Refresh(); + + if (process.Id != Process.GetCurrentProcess().Id && process.HasExited) + { + _log.LogInformation("Process has already exited"); + return; + } + + _log.LogInformation("[{0}]: Killing process", process.Id); + process.Kill(); + _log.LogInformation("[{0}]: Waiting for exit", process.Id); + process.WaitForExit(); + _log.LogInformation("[{0}]: Process terminated successfully", process.Id); + } + + public void KillAll(string processName) + { + var processes = GetProcessesByName(processName); + + _log.LogInformation("Found {0} processes to kill", processes.Count); + + foreach (var processInfo in processes) + { + if (processInfo.Id == Process.GetCurrentProcess().Id) + { + _log.LogInformation("Tried killing own process, skipping: {0} [{1}]", processInfo.Id, processInfo.ProcessName); + continue; + } + + _log.LogInformation("Killing process: {0} [{1}]", processInfo.Id, processInfo.ProcessName); + Kill(processInfo.Id); + } + } + + + private ProcessInfo ConvertToProcessInfo(Process process) + { + if (process == null) return null; + + process.Refresh(); + + ProcessInfo processInfo = null; + + try + { + if (process.Id <= 0) return null; + + processInfo = new ProcessInfo + { + Id = process.Id, + Name = process.ProcessName, + StartPath = GetExeFileName(process) + }; + + if (process.Id != Process.GetCurrentProcess().Id && process.HasExited) + { + processInfo = null; + } + } + catch (Exception e) + { + _log.LogInformation(e.Message); + } + + return processInfo; + + } + + public Process Start(string path, string args = null) + { + var startInfo = new ProcessStartInfo(path, args) + { + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + RedirectStandardInput = true + }; + + _log.LogDebug("Starting {0} {1}", path, args); + + var process = new Process + { + StartInfo = startInfo + }; + + process.OutputDataReceived += (sender, eventArgs) => + { + if (string.IsNullOrWhiteSpace(eventArgs.Data)) return; + + _log.LogDebug(eventArgs.Data); + }; + + process.ErrorDataReceived += (sender, eventArgs) => + { + if (string.IsNullOrWhiteSpace(eventArgs.Data)) return; + + _log.LogDebug(eventArgs.Data); + }; + + process.Start(); + + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + + return process; + } + + private static string GetExeFileName(Process process) + { + return process.MainModule.FileName; + } + + private List GetProcessesByName(string name) + { + var processes = Process.GetProcessesByName(name).ToList(); + + _log.LogInformation("Found {0} processes with the name: {1}", processes.Count, name); + + try + { + foreach (var process in processes) + { + _log.LogInformation(" - [{0}] {1}", process.Id, process.ProcessName); + } + } + catch + { + // Don't crash on gettings some log data. + } + + return processes; + } + } + + public class ProcessInfo + { + public int Id { get; set; } + public string Name { get; set; } + public string StartPath { get; set; } + + public override string ToString() + { + return string.Format("{0}:{1} [{2}]", Id, Name ?? "Unknown", StartPath ?? "Unknown"); + } + } +} diff --git a/src/Ombi.Schedule/Jobs/Ombi/OmbiAutomaticUpdater.cs b/src/Ombi.Schedule/Jobs/Ombi/OmbiAutomaticUpdater.cs index 2c107f3c1..7f4e96975 100644 --- a/src/Ombi.Schedule/Jobs/Ombi/OmbiAutomaticUpdater.cs +++ b/src/Ombi.Schedule/Jobs/Ombi/OmbiAutomaticUpdater.cs @@ -19,6 +19,7 @@ using Ombi.Api.Service.Models; using Ombi.Core.Settings; using Ombi.Helpers; using Ombi.Settings.Settings.Models; +using Ombi.Updater; using SharpCompress.Readers; using SharpCompress.Readers.Tar; @@ -27,16 +28,18 @@ namespace Ombi.Schedule.Jobs.Ombi public class OmbiAutomaticUpdater : IOmbiAutomaticUpdater { public OmbiAutomaticUpdater(ILogger log, IOmbiService service, - ISettingsService s) + ISettingsService s, IProcessProvider proc) { Logger = log; OmbiService = service; Settings = s; + _processProvider = proc; } private ILogger Logger { get; } private IOmbiService OmbiService { get; } private ISettingsService Settings { get; } + private readonly IProcessProvider _processProvider; private static PerformContext Ctx { get; set; } public string[] GetVersion() @@ -126,6 +129,22 @@ namespace Ombi.Schedule.Jobs.Ombi Ctx.WriteLine("Found the download! {0}", download.Name); Ctx.WriteLine("URL {0}", download.Url); + Ctx.WriteLine("Clearing out Temp Path"); + var tempPath = Path.Combine(currentLocation, "TempUpdate"); + if (Directory.Exists(tempPath)) + { + Directory.Delete(tempPath, true); + } + + // Temp Path + Directory.CreateDirectory(tempPath); + + if (settings.UseScript) + { + RunScript(settings, download.Url); + return; + } + // Download it Logger.LogInformation(LoggingEvents.Updater, "Downloading the file {0} from {1}", download.Name, download.Url); var extension = download.Name.Split('.').Last(); @@ -149,12 +168,7 @@ namespace Ombi.Schedule.Jobs.Ombi Logger.LogError(LoggingEvents.Updater, e, "Error when downloading the zip"); throw; } - Ctx.WriteLine("Clearing out Temp Path"); - var tempPath = Path.Combine(currentLocation, "TempUpdate"); - if (Directory.Exists(tempPath)) - { - Directory.Delete(tempPath, true); - } + // Extract it Ctx.WriteLine("Extracting ZIP"); Extract(zipDir, tempPath); @@ -199,14 +213,34 @@ namespace Ombi.Schedule.Jobs.Ombi } } + private void RunScript(UpdateSettings settings, string downloadUrl) + { + var scriptToRun = settings.ScriptLocation; + if (scriptToRun.IsNullOrEmpty()) + { + Logger.LogError("Use Script is enabled but there is no script to run"); + return; + } + + if (!File.Exists(scriptToRun)) + { + Logger.LogError("Cannot find the file {0}", scriptToRun); + return; + } + + var ombiProcess = _processProvider.FindProcessByName(settings.ProcessName).FirstOrDefault(); + var currentInstallLocation = Assembly.GetEntryAssembly().Location; + _processProvider.Start(scriptToRun, string.Join(" ", downloadUrl, ombiProcess.Id, currentInstallLocation)); + + Logger.LogInformation("Script started"); + } + private void Extract(string zipDir, string tempPath) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { using (var files = ZipFile.OpenRead(zipDir)) { - // Temp Path - Directory.CreateDirectory(tempPath); foreach (var entry in files.Entries) { if (entry.FullName.Contains("/")) diff --git a/src/Ombi.Settings/Settings/Models/UpdateSettings.cs b/src/Ombi.Settings/Settings/Models/UpdateSettings.cs index 3021aa4f0..1655592f4 100644 --- a/src/Ombi.Settings/Settings/Models/UpdateSettings.cs +++ b/src/Ombi.Settings/Settings/Models/UpdateSettings.cs @@ -6,5 +6,8 @@ public string Username { get; set; } public string Password { get; set; } public string ProcessName { get; set; } + + public bool UseScript { get; set; } + public string ScriptLocation { get; set; } } } \ No newline at end of file diff --git a/src/Ombi.Tests/IdentityControllerTests.cs b/src/Ombi.Tests/IdentityControllerTests.cs index fb0314362..eaa7334ac 100644 --- a/src/Ombi.Tests/IdentityControllerTests.cs +++ b/src/Ombi.Tests/IdentityControllerTests.cs @@ -82,7 +82,7 @@ namespace Ombi.Tests _userManager = _serviceProvider.GetRequiredService(); Controller = new IdentityController(_userManager, _mapper.Object, _serviceProvider.GetService>(), _emailProvider.Object, - _emailSettings.Object, _customizationSettings.Object,_welcomeEmail.Object); + _emailSettings.Object, _customizationSettings.Object,_welcomeEmail.Object, null, null, null); } private OmbiUserManager _userManager; diff --git a/src/Ombi.Updater/IProcessProvider.cs b/src/Ombi.Updater/IProcessProvider.cs new file mode 100644 index 000000000..be1bf3e17 --- /dev/null +++ b/src/Ombi.Updater/IProcessProvider.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Ombi.Updater +{ + public interface IProcessProvider + { + bool Exists(int processId); + bool Exists(string processName); + List FindProcessByName(string name); + ProcessInfo GetCurrentProcess(); + int GetCurrentProcessId(); + ProcessInfo GetProcessById(int id); + void Kill(int processId); + void KillAll(string processName); + void SetPriority(int processId, ProcessPriorityClass priority); + void WaitForExit(Process process); + } +} \ No newline at end of file diff --git a/src/Ombi.Updater/ProcessProvider.cs b/src/Ombi.Updater/ProcessProvider.cs index e060aba89..97cfa2078 100644 --- a/src/Ombi.Updater/ProcessProvider.cs +++ b/src/Ombi.Updater/ProcessProvider.cs @@ -5,9 +5,12 @@ using System.Linq; namespace Ombi.Updater { - public class ProcessProvider + public class ProcessProvider : IProcessProvider { - public const string OmbiProcessName = "Ombi"; + public ProcessProvider() + { + + } public int GetCurrentProcessId() { diff --git a/src/Ombi.Updater/Program.cs b/src/Ombi.Updater/Program.cs index 93ce79fce..3f09c62cd 100644 --- a/src/Ombi.Updater/Program.cs +++ b/src/Ombi.Updater/Program.cs @@ -57,7 +57,8 @@ namespace Ombi.Updater //// Add services serviceCollection.AddTransient(); - } + serviceCollection.AddTransient(); + } private static StartupOptions CheckArgs(string[] args) { diff --git a/src/Ombi/ClientApp/app/interfaces/ISettings.ts b/src/Ombi/ClientApp/app/interfaces/ISettings.ts index fd13a4290..7af21c1a4 100644 --- a/src/Ombi/ClientApp/app/interfaces/ISettings.ts +++ b/src/Ombi/ClientApp/app/interfaces/ISettings.ts @@ -20,6 +20,8 @@ export interface IUpdateSettings extends ISettings { username: string; password: string; processName: string; + useScript: boolean; + scriptLocation: string; } export interface IEmbySettings extends ISettings { diff --git a/src/Ombi/ClientApp/app/settings/update/update.component.html b/src/Ombi/ClientApp/app/settings/update/update.component.html index 0bdd9213b..1d2786c43 100644 --- a/src/Ombi/ClientApp/app/settings/update/update.component.html +++ b/src/Ombi/ClientApp/app/settings/update/update.component.html @@ -1,11 +1,12 @@  +
Update Settings
- +
@@ -20,23 +21,28 @@
- - If you are getting any permissions issues, you can specify a user for the update process to run under. -
- - +
+ + +
- -
- - + +
+ For information how to use this, please press the wiki button at the top of the page +
+ + +
+ - By default the process name is Ombi, but this could be different for your system. We need to know the process name so we can kill that process to update the files. -
- - +
+ By default the process name is Ombi, but this could be different for your system. We need to know the process name so we can kill that process to update the files. +
+ + +
@@ -45,6 +51,19 @@
+
+ If you are getting any permissions issues, you can specify a user for the update process to run under (Only supported on Windows). + +
+ + +
+ +
+ + +
+
diff --git a/src/Ombi/ClientApp/app/settings/update/update.component.ts b/src/Ombi/ClientApp/app/settings/update/update.component.ts index 90130a130..6a3bd3891 100644 --- a/src/Ombi/ClientApp/app/settings/update/update.component.ts +++ b/src/Ombi/ClientApp/app/settings/update/update.component.ts @@ -11,6 +11,12 @@ export class UpdateComponent implements OnInit { public form: FormGroup; public updateAvailable = false; + public enableUpdateButton = false; + public get useScript() { + const control = this.form.get("useScript"); + console.log(control); + return control!.value!; + } constructor(private settingsService: SettingsService, private notificationService: NotificationService, @@ -25,7 +31,10 @@ export class UpdateComponent implements OnInit { username: [x.username], password: [x.password], processName: [x.processName], + useScript: [x.useScript], + scriptLocation: [x.scriptLocation], }); + this.enableUpdateButton = x.autoUpdateEnabled; }); } @@ -50,6 +59,7 @@ export class UpdateComponent implements OnInit { this.notificationService.error("Validation", "Please check your entered values"); return; } + this.enableUpdateButton = form.value.autoUpdateEnabled; this.settingsService.saveUpdateSettings(form.value) .subscribe(x => { if (x) {