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') { stage('Build') {
currentBuild.description = bat(returnStdout: true, script: '@powershell -File Versioning/version.ps1').trim() 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" buildMode = "Release"
} else { } else {
buildMode = "Debug" buildMode = "Debug"

View File

@@ -7,36 +7,36 @@ using System.Threading.Tasks;
namespace Torch.API namespace Torch.API
{ {
/// <summary> /// <summary>
/// Version in the form v#.#.#.#-info /// Version in the form v#.#.#.#-branch
/// </summary> /// </summary>
public class InformationalVersion public class InformationalVersion
{ {
public Version Version { get; set; } public Version Version { get; set; }
public string[] Information { get; set; } public string Branch { get; set; }
public static bool TryParse(string input, out InformationalVersion version) public static bool TryParse(string input, out InformationalVersion version)
{ {
version = default(InformationalVersion); version = default(InformationalVersion);
var trim = input.TrimStart('v'); var trim = input.TrimStart('v');
var info = trim.Split('-'); var info = trim.Split(new[]{'-'}, 2);
if (!Version.TryParse(info[0], out Version result)) if (!Version.TryParse(info[0], out Version result))
return false; return false;
version = new InformationalVersion { Version = result }; version = new InformationalVersion { Version = result };
if (info.Length > 1) if (info.Length > 1)
version.Information = info.Skip(1).ToArray(); version.Branch = info[1];
return true; return true;
} }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {
if (Information == null || Information.Length == 0) if (Branch == null)
return $"v{Version}"; return $"v{Version}";
return $"v{Version}-{string.Join("-", Information)}"; return $"v{Version}-{string.Join("-", Branch)}";
} }
public static explicit operator InformationalVersion(Version v) public static explicit operator InformationalVersion(Version v)
@@ -48,5 +48,15 @@ namespace Torch.API
{ {
return v.Version; 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\ITorchSessionManager.cs" />
<Compile Include="Session\TorchSessionState.cs" /> <Compile Include="Session\TorchSessionState.cs" />
<Compile Include="TorchGameState.cs" /> <Compile Include="TorchGameState.cs" />
<Compile Include="WebAPI\JenkinsQuery.cs" />
<Compile Include="WebAPI\PluginQuery.cs" /> <Compile Include="WebAPI\PluginQuery.cs" />
</ItemGroup> </ItemGroup>
<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) 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; var torch = new FileInfo(typeof(FilesystemManager).Assembly.Location).Directory.FullName;
TempDirectory = Directory.CreateDirectory(Path.Combine(torch, "tmp")).FullName;
TorchDirectory = torch; TorchDirectory = torch;
ClearTemp(); ClearTemp();
@@ -39,13 +38,16 @@ namespace Torch.Managers
/// <summary> /// <summary>
/// Move the given file (if it exists) to a temporary directory that will be cleared the next time the application starts. /// Move the given file (if it exists) to a temporary directory that will be cleared the next time the application starts.
/// </summary> /// </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; return;
var rand = Path.GetRandomFileName(); var rand = Path.GetRandomFileName();
var dest = Path.Combine(TempDirectory, rand); 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;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog; using NLog;
using Octokit;
using Torch.API; using Torch.API;
using Torch.API.WebAPI;
namespace Torch.Managers namespace Torch.Managers
{ {
@@ -21,12 +21,11 @@ namespace Torch.Managers
public class UpdateManager : Manager public class UpdateManager : Manager
{ {
private Timer _updatePollTimer; private Timer _updatePollTimer;
private GitHubClient _gitClient = new GitHubClient(new ProductHeaderValue("Torch"));
private string _torchDir = new FileInfo(typeof(UpdateManager).Assembly.Location).DirectoryName; private string _torchDir = new FileInfo(typeof(UpdateManager).Assembly.Location).DirectoryName;
private Logger _log = LogManager.GetCurrentClassLogger(); private Logger _log = LogManager.GetCurrentClassLogger();
[Dependency] [Dependency]
private FilesystemManager _fsManager; private FilesystemManager _fsManager;
public UpdateManager(ITorchBase torchInstance) : base(torchInstance) public UpdateManager(ITorchBase torchInstance) : base(torchInstance)
{ {
//_updatePollTimer = new Timer(TimerElapsed, this, TimeSpan.Zero, TimeSpan.FromMinutes(5)); //_updatePollTimer = new Timer(TimerElapsed, this, TimeSpan.Zero, TimeSpan.FromMinutes(5));
@@ -42,49 +41,36 @@ namespace Torch.Managers
{ {
CheckAndUpdateTorch(); 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() private async void CheckAndUpdateTorch()
{ {
// Doesn't work properly or reliably, TODO update when Jenkins is fully configured
return;
if (!Torch.Config.GetTorchUpdates) if (!Torch.Config.GetTorchUpdates)
return; return;
try try
{ {
var releaseInfo = await TryGetLatestArchiveUrl("TorchAPI", "Torch").ConfigureAwait(false); var job = await JenkinsQuery.Instance.GetLatestVersion(Torch.TorchVersion.Branch);
if (releaseInfo.Item1 > Torch.TorchVersion) 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"); 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); UpdateFromZip(updateName, _torchDir);
File.Delete(updateName); 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 else
{ {
@@ -106,10 +92,11 @@ namespace Torch.Managers
{ {
_log.Debug($"Unzipping {file.FullName}"); _log.Debug($"Unzipping {file.FullName}");
var targetFile = Path.Combine(extractPath, 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.Threading.Tasks;
using System.Xml.Serialization; using System.Xml.Serialization;
using NLog; using NLog;
using Octokit;
using Torch.API; using Torch.API;
using Torch.API.Managers; using Torch.API.Managers;
using Torch.API.Plugins; using Torch.API.Plugins;
@@ -24,7 +23,6 @@ namespace Torch.Managers
/// <inheritdoc /> /// <inheritdoc />
public class PluginManager : Manager, IPluginManager public class PluginManager : Manager, IPluginManager
{ {
private GitHubClient _gitClient = new GitHubClient(new ProductHeaderValue("Torch"));
private static Logger _log = LogManager.GetCurrentClassLogger(); private static Logger _log = LogManager.GetCurrentClassLogger();
private const string MANIFEST_NAME = "manifest.xml"; private const string MANIFEST_NAME = "manifest.xml";
public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins"); public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
@@ -145,45 +143,45 @@ namespace Torch.Managers
Parallel.ForEach(pluginItems, async item => Parallel.ForEach(pluginItems, async item =>
{ {
PluginManifest manifest = null; PluginManifest manifest = null;
try //try
{ //{
var path = Path.Combine(PluginDir, item); // var path = Path.Combine(PluginDir, item);
var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase); // var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path); // manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
if (manifest == null) // if (manifest == null)
{ // {
_log.Warn($"Item '{item}' is missing a manifest, skipping update check."); // _log.Warn($"Item '{item}' is missing a manifest, skipping update check.");
return; // return;
} // }
manifest.Version.TryExtractVersion(out Version currentVersion); // manifest.Version.TryExtractVersion(out Version currentVersion);
var latest = await GetLatestArchiveAsync(manifest.Repository).ConfigureAwait(false); // var latest = await GetLatestArchiveAsync(manifest.Repository).ConfigureAwait(false);
if (currentVersion == null || latest.Item1 == null) // if (currentVersion == null || latest.Item1 == null)
{ // {
_log.Error($"Error parsing version from manifest or GitHub for plugin '{manifest.Name}.'"); // _log.Error($"Error parsing version from manifest or GitHub for plugin '{manifest.Name}.'");
return; // return;
} // }
if (latest.Item1 <= currentVersion) // if (latest.Item1 <= currentVersion)
{ // {
_log.Debug($"{manifest.Name} {manifest.Version} is up to date."); // _log.Debug($"{manifest.Name} {manifest.Version} is up to date.");
return; // return;
} // }
_log.Info($"Updating plugin '{manifest.Name}' from {currentVersion} to {latest.Item1}."); // _log.Info($"Updating plugin '{manifest.Name}' from {currentVersion} to {latest.Item1}.");
await UpdatePluginAsync(path, latest.Item2).ConfigureAwait(false); // await UpdatePluginAsync(path, latest.Item2).ConfigureAwait(false);
count++; // count++;
} //}
catch (NotFoundException) //catch (NotFoundException)
{ //{
_log.Warn($"GitHub repository not found for {manifest.Name}"); // _log.Warn($"GitHub repository not found for {manifest.Name}");
} //}
catch (Exception e) //catch (Exception e)
{ //{
_log.Warn($"An error occurred updating the plugin {manifest.Name}."); // _log.Warn($"An error occurred updating the plugin {manifest.Name}.");
_log.Warn(e); // _log.Warn(e);
} //}
}); });
_log.Info($"Updated {count} plugins."); _log.Info($"Updated {count} plugins.");
@@ -193,20 +191,21 @@ namespace Torch.Managers
{ {
try try
{ {
var split = repository.Split('/'); //var split = repository.Split('/');
var latest = await _gitClient.Repository.Release.GetLatest(split[0], split[1]).ConfigureAwait(false); //var latest = await _gitClient.Repository.Release.GetLatest(split[0], split[1]).ConfigureAwait(false);
if (!latest.TagName.TryExtractVersion(out Version latestVersion)) //if (!latest.TagName.TryExtractVersion(out Version latestVersion))
{ //{
_log.Error($"Unable to parse version tag for the latest release of '{repository}.'"); // _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)); //var zipAsset = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip", StringComparison.CurrentCultureIgnoreCase));
if (zipAsset == null) //if (zipAsset == null)
{ //{
_log.Error($"Unable to find archive for the latest release of '{repository}.'"); // _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) catch (Exception e)
{ {

View File

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

View File

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