From 34616607a83282304de74e9a8b8945d8a8148cb3 Mon Sep 17 00:00:00 2001 From: Brant Martin Date: Sun, 3 Mar 2019 17:42:02 -0500 Subject: [PATCH] Implement Torch auto-update. Sadly does not work for Patron branch. --- Jenkinsfile | 2 +- Torch.API/InformationalVersion.cs | 24 +++-- Torch.API/Torch.API.csproj | 1 + Torch.API/WebAPI/JenkinsQuery.cs | 133 ++++++++++++++++++++++++++++ Torch/Managers/FilesystemManager.cs | 12 +-- Torch/Managers/UpdateManager.cs | 61 +++++-------- Torch/Plugins/PluginManager.cs | 97 ++++++++++---------- Torch/Torch.csproj | 7 +- Torch/packages.config | 1 - 9 files changed, 234 insertions(+), 104 deletions(-) create mode 100644 Torch.API/WebAPI/JenkinsQuery.cs diff --git a/Jenkinsfile b/Jenkinsfile index e554cf4..98d7c36 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -31,7 +31,7 @@ node { stage('Build') { currentBuild.description = bat(returnStdout: true, script: '@powershell -File Versioning/version.ps1').trim() - if (env.BRANCH_NAME == "master") { + if (env.BRANCH_NAME == "master" || enb.BRANCH_NAME == "Patron") { buildMode = "Release" } else { buildMode = "Debug" diff --git a/Torch.API/InformationalVersion.cs b/Torch.API/InformationalVersion.cs index 5310234..ef80213 100644 --- a/Torch.API/InformationalVersion.cs +++ b/Torch.API/InformationalVersion.cs @@ -7,36 +7,36 @@ using System.Threading.Tasks; namespace Torch.API { /// - /// Version in the form v#.#.#.#-info + /// Version in the form v#.#.#.#-branch /// public class InformationalVersion { public Version Version { get; set; } - public string[] Information { get; set; } + public string Branch { get; set; } public static bool TryParse(string input, out InformationalVersion version) { version = default(InformationalVersion); var trim = input.TrimStart('v'); - var info = trim.Split('-'); + var info = trim.Split(new[]{'-'}, 2); if (!Version.TryParse(info[0], out Version result)) return false; version = new InformationalVersion { Version = result }; if (info.Length > 1) - version.Information = info.Skip(1).ToArray(); - + version.Branch = info[1]; + return true; } /// public override string ToString() { - if (Information == null || Information.Length == 0) + if (Branch == null) return $"v{Version}"; - return $"v{Version}-{string.Join("-", Information)}"; + return $"v{Version}-{string.Join("-", Branch)}"; } public static explicit operator InformationalVersion(Version v) @@ -48,5 +48,15 @@ namespace Torch.API { return v.Version; } + + public static bool operator >(InformationalVersion lhs, InformationalVersion rhs) + { + return lhs.Version > rhs.Version; + } + + public static bool operator <(InformationalVersion lhs, InformationalVersion rhs) + { + return lhs.Version < rhs.Version; + } } } diff --git a/Torch.API/Torch.API.csproj b/Torch.API/Torch.API.csproj index 987b979..aa0dea8 100644 --- a/Torch.API/Torch.API.csproj +++ b/Torch.API/Torch.API.csproj @@ -193,6 +193,7 @@ + diff --git a/Torch.API/WebAPI/JenkinsQuery.cs b/Torch.API/WebAPI/JenkinsQuery.cs new file mode 100644 index 0000000..5c9ea1c --- /dev/null +++ b/Torch.API/WebAPI/JenkinsQuery.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using NLog; + +namespace Torch.API.WebAPI +{ + public class JenkinsQuery + { + private const string BRANCH_QUERY = "https://build.torchapi.net/job/Torch/job/Torch/job/{0}/" + API_PATH; + private const string ARTIFACT_PATH = "artifact/bin/torch-server.zip"; + private const string API_PATH = "api/json"; + + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + + private static JenkinsQuery _instance; + public static JenkinsQuery Instance => _instance ?? (_instance = new JenkinsQuery()); + private HttpClient _client; + + private JenkinsQuery() + { + _client = new HttpClient(); + } + + public async Task GetLatestVersion(string branch) + { +#if DEBUG + branch = "master"; +#endif + var h = await _client.GetAsync(string.Format(BRANCH_QUERY, branch)); + if (!h.IsSuccessStatusCode) + { + Log.Error($"Branch query failed with code {h.StatusCode}"); + if(h.StatusCode == HttpStatusCode.NotFound) + Log.Error("This likely means you're trying to update a branch that is not public on Jenkins. Sorry :("); + return null; + } + + string r = await h.Content.ReadAsStringAsync(); + + BranchResponse response; + try + { + response = JsonConvert.DeserializeObject(r); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to deserialize branch response!"); + return null; + } + + h = await _client.GetAsync($"{response.LastStableBuild.URL}{API_PATH}"); + if (!h.IsSuccessStatusCode) + { + Log.Error($"Job query failed with code {h.StatusCode}"); + return null; + } + + r = await h.Content.ReadAsStringAsync(); + + Job job; + try + { + job = JsonConvert.DeserializeObject(r); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to deserialize job response!"); + return null; + } + return job; + } + + public async Task DownloadRelease(Job job, string path) + { + var h = await _client.GetAsync(job.URL + ARTIFACT_PATH); + if (!h.IsSuccessStatusCode) + { + Log.Error($"Job download failed with code {h.StatusCode}"); + return false; + } + var s = await h.Content.ReadAsStreamAsync(); + using (var fs = new FileStream(path, FileMode.Create)) + { + await s.CopyToAsync(fs); + await fs.FlushAsync(); + } + return true; + } + + } + + public class BranchResponse + { + public string Name; + public string URL; + public Build LastBuild; + public Build LastStableBuild; + } + + public class Build + { + public int Number; + public string URL; + } + + public class Job + { + public int Number; + public bool Building; + public string Description; + public string Result; + public string URL; + private InformationalVersion _version; + + public InformationalVersion Version + { + get + { + if (_version == null) + InformationalVersion.TryParse(Description, out _version); + + return _version; + } + } + } +} diff --git a/Torch/Managers/FilesystemManager.cs b/Torch/Managers/FilesystemManager.cs index b0fdcb1..254c9a0 100644 --- a/Torch/Managers/FilesystemManager.cs +++ b/Torch/Managers/FilesystemManager.cs @@ -22,9 +22,8 @@ namespace Torch.Managers public FilesystemManager(ITorchBase torchInstance) : base(torchInstance) { - var temp = Path.Combine(Path.GetTempPath(), "Torch"); - TempDirectory = Directory.CreateDirectory(temp).FullName; var torch = new FileInfo(typeof(FilesystemManager).Assembly.Location).Directory.FullName; + TempDirectory = Directory.CreateDirectory(Path.Combine(torch, "tmp")).FullName; TorchDirectory = torch; ClearTemp(); @@ -39,13 +38,16 @@ namespace Torch.Managers /// /// Move the given file (if it exists) to a temporary directory that will be cleared the next time the application starts. /// - public void SoftDelete(string file) + public void SoftDelete(string path, string file) { - if (!File.Exists(file)) + string source = Path.Combine(path, file); + if (!File.Exists(source)) return; var rand = Path.GetRandomFileName(); var dest = Path.Combine(TempDirectory, rand); - File.Move(file, dest); + File.Move(source, rand); + string rsource = Path.Combine(path, rand); + File.Move(rsource, dest); } } } diff --git a/Torch/Managers/UpdateManager.cs b/Torch/Managers/UpdateManager.cs index bdaaf32..de2e73c 100644 --- a/Torch/Managers/UpdateManager.cs +++ b/Torch/Managers/UpdateManager.cs @@ -10,8 +10,8 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using NLog; -using Octokit; using Torch.API; +using Torch.API.WebAPI; namespace Torch.Managers { @@ -21,12 +21,11 @@ namespace Torch.Managers public class UpdateManager : Manager { private Timer _updatePollTimer; - private GitHubClient _gitClient = new GitHubClient(new ProductHeaderValue("Torch")); private string _torchDir = new FileInfo(typeof(UpdateManager).Assembly.Location).DirectoryName; private Logger _log = LogManager.GetCurrentClassLogger(); [Dependency] private FilesystemManager _fsManager; - + public UpdateManager(ITorchBase torchInstance) : base(torchInstance) { //_updatePollTimer = new Timer(TimerElapsed, this, TimeSpan.Zero, TimeSpan.FromMinutes(5)); @@ -42,49 +41,36 @@ namespace Torch.Managers { CheckAndUpdateTorch(); } - - private async Task> TryGetLatestArchiveUrl(string owner, string name) - { - try - { - var latest = await _gitClient.Repository.Release.GetLatest(owner, name).ConfigureAwait(false); - if (latest == null) - return new Tuple(new Version(), null); - - var zip = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip")); - if (zip == null) - _log.Error($"Latest release of {owner}/{name} does not contain a zip archive."); - if (!latest.TagName.TryExtractVersion(out Version version)) - _log.Error($"Unable to parse version tag for {owner}/{name}"); - return new Tuple(version, zip?.BrowserDownloadUrl); - } - catch (Exception e) - { - _log.Error($"An error occurred getting release information for '{owner}/{name}'"); - _log.Error(e); - return default(Tuple); - } - } - + private async void CheckAndUpdateTorch() { - // Doesn't work properly or reliably, TODO update when Jenkins is fully configured - return; - if (!Torch.Config.GetTorchUpdates) return; try { - var releaseInfo = await TryGetLatestArchiveUrl("TorchAPI", "Torch").ConfigureAwait(false); - if (releaseInfo.Item1 > Torch.TorchVersion) + var job = await JenkinsQuery.Instance.GetLatestVersion(Torch.TorchVersion.Branch); + if (job == null) { - _log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {releaseInfo.Item1}"); + _log.Info("Failed to fetch latest version."); + return; + } + + _log.Info($"Clearing tmp directory at {_fsManager.TempDirectory}"); + + if (job.Version > Torch.TorchVersion) + { + _log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {job.Version}"); var updateName = Path.Combine(_fsManager.TempDirectory, "torchupdate.zip"); - new WebClient().DownloadFile(new Uri(releaseInfo.Item2), updateName); + //new WebClient().DownloadFile(new Uri(releaseInfo.Item2), updateName); + if (!await JenkinsQuery.Instance.DownloadRelease(job, updateName)) + { + _log.Warn("Failed to download new release!"); + return; + } UpdateFromZip(updateName, _torchDir); File.Delete(updateName); - _log.Warn($"Torch version {releaseInfo.Item1} has been installed, please restart Torch to finish the process."); + _log.Warn($"Torch version {job.Version} has been installed, please restart Torch to finish the process."); } else { @@ -106,10 +92,11 @@ namespace Torch.Managers { _log.Debug($"Unzipping {file.FullName}"); var targetFile = Path.Combine(extractPath, file.FullName); - _fsManager.SoftDelete(targetFile); + _fsManager.SoftDelete(extractPath, file.FullName); + file.ExtractToFile(targetFile, true); } - zip.ExtractToDirectory(extractPath); + //zip.ExtractToDirectory(extractPath); //throws exceptions sometimes? } } diff --git a/Torch/Plugins/PluginManager.cs b/Torch/Plugins/PluginManager.cs index df10ef2..85d00e9 100644 --- a/Torch/Plugins/PluginManager.cs +++ b/Torch/Plugins/PluginManager.cs @@ -10,7 +10,6 @@ using System.Reflection; using System.Threading.Tasks; using System.Xml.Serialization; using NLog; -using Octokit; using Torch.API; using Torch.API.Managers; using Torch.API.Plugins; @@ -24,7 +23,6 @@ namespace Torch.Managers /// public class PluginManager : Manager, IPluginManager { - private GitHubClient _gitClient = new GitHubClient(new ProductHeaderValue("Torch")); private static Logger _log = LogManager.GetCurrentClassLogger(); private const string MANIFEST_NAME = "manifest.xml"; public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins"); @@ -145,45 +143,45 @@ namespace Torch.Managers Parallel.ForEach(pluginItems, async item => { PluginManifest manifest = null; - try - { - var path = Path.Combine(PluginDir, item); - var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase); - manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path); - if (manifest == null) - { - _log.Warn($"Item '{item}' is missing a manifest, skipping update check."); - return; - } + //try + //{ + // var path = Path.Combine(PluginDir, item); + // var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase); + // manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path); + // if (manifest == null) + // { + // _log.Warn($"Item '{item}' is missing a manifest, skipping update check."); + // return; + // } - manifest.Version.TryExtractVersion(out Version currentVersion); - var latest = await GetLatestArchiveAsync(manifest.Repository).ConfigureAwait(false); + // manifest.Version.TryExtractVersion(out Version currentVersion); + // var latest = await GetLatestArchiveAsync(manifest.Repository).ConfigureAwait(false); - if (currentVersion == null || latest.Item1 == null) - { - _log.Error($"Error parsing version from manifest or GitHub for plugin '{manifest.Name}.'"); - return; - } + // if (currentVersion == null || latest.Item1 == null) + // { + // _log.Error($"Error parsing version from manifest or GitHub for plugin '{manifest.Name}.'"); + // return; + // } - if (latest.Item1 <= currentVersion) - { - _log.Debug($"{manifest.Name} {manifest.Version} is up to date."); - return; - } + // if (latest.Item1 <= currentVersion) + // { + // _log.Debug($"{manifest.Name} {manifest.Version} is up to date."); + // return; + // } - _log.Info($"Updating plugin '{manifest.Name}' from {currentVersion} to {latest.Item1}."); - await UpdatePluginAsync(path, latest.Item2).ConfigureAwait(false); - count++; - } - catch (NotFoundException) - { - _log.Warn($"GitHub repository not found for {manifest.Name}"); - } - catch (Exception e) - { - _log.Warn($"An error occurred updating the plugin {manifest.Name}."); - _log.Warn(e); - } + // _log.Info($"Updating plugin '{manifest.Name}' from {currentVersion} to {latest.Item1}."); + // await UpdatePluginAsync(path, latest.Item2).ConfigureAwait(false); + // count++; + //} + //catch (NotFoundException) + //{ + // _log.Warn($"GitHub repository not found for {manifest.Name}"); + //} + //catch (Exception e) + //{ + // _log.Warn($"An error occurred updating the plugin {manifest.Name}."); + // _log.Warn(e); + //} }); _log.Info($"Updated {count} plugins."); @@ -193,20 +191,21 @@ namespace Torch.Managers { try { - var split = repository.Split('/'); - var latest = await _gitClient.Repository.Release.GetLatest(split[0], split[1]).ConfigureAwait(false); - if (!latest.TagName.TryExtractVersion(out Version latestVersion)) - { - _log.Error($"Unable to parse version tag for the latest release of '{repository}.'"); - } + //var split = repository.Split('/'); + //var latest = await _gitClient.Repository.Release.GetLatest(split[0], split[1]).ConfigureAwait(false); + //if (!latest.TagName.TryExtractVersion(out Version latestVersion)) + //{ + // _log.Error($"Unable to parse version tag for the latest release of '{repository}.'"); + //} - var zipAsset = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip", StringComparison.CurrentCultureIgnoreCase)); - if (zipAsset == null) - { - _log.Error($"Unable to find archive for the latest release of '{repository}.'"); - } + //var zipAsset = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip", StringComparison.CurrentCultureIgnoreCase)); + //if (zipAsset == null) + //{ + // _log.Error($"Unable to find archive for the latest release of '{repository}.'"); + //} - return new Tuple(latestVersion, zipAsset?.BrowserDownloadUrl); + //return new Tuple(latestVersion, zipAsset?.BrowserDownloadUrl); + return null; } catch (Exception e) { diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj index 6254956..928ddce 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -36,7 +36,9 @@ 1591 - + + + ..\packages\ControlzEx.3.0.2.4\lib\net45\ControlzEx.dll @@ -61,9 +63,6 @@ ..\packages\NLog.4.4.12\lib\net45\NLog.dll True - - ..\packages\Octokit.0.24.0\lib\net45\Octokit.dll - diff --git a/Torch/packages.config b/Torch/packages.config index 3bc7aff..e77c03c 100644 --- a/Torch/packages.config +++ b/Torch/packages.config @@ -5,7 +5,6 @@ -