Implement Torch auto-update. Sadly does not work for Patron branch.

This commit is contained in:
Brant Martin
2019-03-03 17:42:02 -05:00
parent 796feb05e6
commit 34616607a8
9 changed files with 234 additions and 104 deletions

2
Jenkinsfile vendored
View File

@@ -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"

View File

@@ -7,36 +7,36 @@ using System.Threading.Tasks;
namespace Torch.API
{
/// <summary>
/// Version in the form v#.#.#.#-info
/// Version in the form v#.#.#.#-branch
/// </summary>
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;
}
/// <inheritdoc />
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;
}
}
}

View File

@@ -193,6 +193,7 @@
<Compile Include="Session\ITorchSessionManager.cs" />
<Compile Include="Session\TorchSessionState.cs" />
<Compile Include="TorchGameState.cs" />
<Compile Include="WebAPI\JenkinsQuery.cs" />
<Compile Include="WebAPI\PluginQuery.cs" />
</ItemGroup>
<ItemGroup>

View File

@@ -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<Job> 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<BranchResponse>(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<Job>(r);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to deserialize job response!");
return null;
}
return job;
}
public async Task<bool> 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;
}
}
}
}

View File

@@ -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
/// <summary>
/// Move the given file (if it exists) to a temporary directory that will be cleared the next time the application starts.
/// </summary>
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);
}
}
}

View File

@@ -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<Tuple<Version, string>> TryGetLatestArchiveUrl(string owner, string name)
{
try
{
var latest = await _gitClient.Repository.Release.GetLatest(owner, name).ConfigureAwait(false);
if (latest == null)
return new Tuple<Version, string>(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, string>(version, zip?.BrowserDownloadUrl);
}
catch (Exception e)
{
_log.Error($"An error occurred getting release information for '{owner}/{name}'");
_log.Error(e);
return default(Tuple<Version, string>);
}
}
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?
}
}

View File

@@ -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
/// <inheritdoc />
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<Version, string>(latestVersion, zipAsset?.BrowserDownloadUrl);
//return new Tuple<Version, string>(latestVersion, zipAsset?.BrowserDownloadUrl);
return null;
}
catch (Exception e)
{

View File

@@ -36,7 +36,9 @@
<NoWarn>1591</NoWarn>
</PropertyGroup>
<!-- I don't know why this needs to exist -->
<ItemGroup> <Reference Include="netstandard" /> </ItemGroup>
<ItemGroup>
<Reference Include="netstandard" />
</ItemGroup>
<ItemGroup>
<Reference Include="ControlzEx, Version=3.0.2.4, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\ControlzEx.3.0.2.4\lib\net45\ControlzEx.dll</HintPath>
@@ -61,9 +63,6 @@
<HintPath>..\packages\NLog.4.4.12\lib\net45\NLog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Octokit, Version=0.24.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Octokit.0.24.0\lib\net45\Octokit.dll</HintPath>
</Reference>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="Sandbox.Common">

View File

@@ -5,7 +5,6 @@
<package id="Microsoft.Win32.Registry" version="4.4.0" targetFramework="net461" />
<package id="Mono.TextTransform" version="1.0.0" targetFramework="net461" />
<package id="NLog" version="4.4.12" targetFramework="net461" />
<package id="Octokit" version="0.24.0" targetFramework="net461" />
<package id="protobuf-net" version="2.1.0" targetFramework="net461" />
<package id="SteamKit2" version="2.1.0" targetFramework="net461" />
<package id="System.Security.AccessControl" version="4.4.0" targetFramework="net461" />