add auto-updates from github

This commit is contained in:
zznty
2022-10-15 15:33:57 +07:00
parent 197d04a661
commit c5acf61f7c
9 changed files with 245 additions and 105 deletions

View File

@@ -32,7 +32,22 @@ namespace Torch
UGCServiceType UgcServiceType { get; set; } UGCServiceType UgcServiceType { get; set; }
bool EntityManagerEnabled { get; set; } bool EntityManagerEnabled { get; set; }
string LoginToken { get; set; } string LoginToken { get; set; }
UpdateSource UpdateSource { get; set; }
void Save(string path = null); void Save(string path = null);
} }
public class UpdateSource
{
public UpdateSourceType SourceType { get; set; }
public string Url { get; set; }
public string Repository { get; set; }
public string Branch { get; set; }
}
public enum UpdateSourceType
{
Github,
Jenkins
}
} }

View File

@@ -18,6 +18,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="JorgeSerrano.Json.JsonSnakeCaseNamingPolicy" Version="0.9.0" />
<PackageReference Include="NLog" Version="5.0.4" /> <PackageReference Include="NLog" Version="5.0.4" />
<PackageReference Include="SemanticVersioning" Version="2.0.2" /> <PackageReference Include="SemanticVersioning" Version="2.0.2" />
<PackageReference Include="SpaceEngineersDedicated.ReferenceAssemblies" Version="1.201.13"> <PackageReference Include="SpaceEngineersDedicated.ReferenceAssemblies" Version="1.201.13">

View File

@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using JorgeSerrano.Json;
using Version = SemanticVersioning.Version;
namespace Torch.API.WebAPI;
public class GithubQuery : IUpdateQuery
{
private readonly HttpClient _client;
public GithubQuery(string url)
{
if (url == null) throw new ArgumentNullException(nameof(url));
_client = new()
{
BaseAddress = new(url),
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
DefaultRequestHeaders =
{
{"User-Agent", "TorchAPI"}
}
};
}
public void Dispose()
{
_client?.Dispose();
}
public async Task<UpdateRelease> GetLatestReleaseAsync(string repository, string branch = null)
{
var response = await _client.GetFromJsonAsync<Release>($"/repos/{repository}/releases/latest", new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
PropertyNamingPolicy = new JsonSnakeCaseNamingPolicy()
});
if (response is null)
throw new($"Unable to get latest release for {repository}");
return new(Version.Parse(response.TagName), response.Assets.First(b => b.Name == "torch-server.zip").BrowserDownloadUrl);
}
private record Asset(
string Url,
int Id,
string NodeId,
string Name,
string Label,
Uploader Uploader,
string ContentType,
string State,
int Size,
int DownloadCount,
DateTime CreatedAt,
DateTime UpdatedAt,
string BrowserDownloadUrl
);
private record Author(
string Login,
int Id,
string NodeId,
string AvatarUrl,
string GravatarId,
string Url,
string HtmlUrl,
string FollowersUrl,
string FollowingUrl,
string GistsUrl,
string StarredUrl,
string SubscriptionsUrl,
string OrganizationsUrl,
string ReposUrl,
string EventsUrl,
string ReceivedEventsUrl,
string Type,
bool SiteAdmin
);
private record Release(
string Url,
string AssetsUrl,
string UploadUrl,
string HtmlUrl,
int Id,
Author Author,
string NodeId,
string TagName,
string TargetCommitish,
string Name,
bool Draft,
bool Prerelease,
DateTime CreatedAt,
DateTime PublishedAt,
IReadOnlyList<Asset> Assets,
string TarballUrl,
string ZipballUrl,
string Body
);
private record Uploader(
string Login,
int Id,
string NodeId,
string AvatarUrl,
string GravatarId,
string Url,
string HtmlUrl,
string FollowersUrl,
string FollowingUrl,
string GistsUrl,
string StarredUrl,
string SubscriptionsUrl,
string OrganizationsUrl,
string ReposUrl,
string EventsUrl,
string ReceivedEventsUrl,
string Type,
bool SiteAdmin
);
}

View File

@@ -0,0 +1,9 @@
using System;
using System.Threading.Tasks;
namespace Torch.API.WebAPI;
public interface IUpdateQuery : IDisposable
{
Task<UpdateRelease> GetLatestReleaseAsync(string repository, string branch = null);
}

View File

@@ -1,82 +1,52 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using NLog;
using Torch.API.Utils; using Torch.API.Utils;
using Version = SemanticVersioning.Version; using Version = SemanticVersioning.Version;
namespace Torch.API.WebAPI namespace Torch.API.WebAPI
{ {
public class JenkinsQuery public class JenkinsQuery : IUpdateQuery
{ {
private const string BRANCH_QUERY = "http://136.243.80.164:2690/job/Torch/job/{0}/" + API_PATH; private const string ApiPath = "api/json";
private const string ARTIFACT_PATH = "artifact/bin/torch-server.zip"; private readonly HttpClient _client;
private const string API_PATH = "api/json";
public JenkinsQuery(string url)
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
private static JenkinsQuery _instance;
public static JenkinsQuery Instance => _instance ??= new JenkinsQuery();
private HttpClient _client;
private JenkinsQuery()
{ {
_client = new HttpClient(); if (url == null) throw new ArgumentNullException(nameof(url));
}
public async Task<Job> GetLatestVersion(string branch)
{
var h = await _client.GetAsync(string.Format(BRANCH_QUERY, branch));
if (!h.IsSuccessStatusCode)
{
Log.Error($"'{branch}' 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;
}
var branchResponse = await h.Content.ReadFromJsonAsync<BranchResponse>();
if (branchResponse is null)
{
Log.Error("Error reading branch response");
return null;
}
h = await _client.GetAsync($"{branchResponse.LastStableBuild.Url}{API_PATH}"); _client = new()
if (h.IsSuccessStatusCode)
return await h.Content.ReadFromJsonAsync<Job>();
Log.Error($"Job query failed with code {h.StatusCode}");
return null;
}
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}"); BaseAddress = new(url)
return false; };
}
var s = await h.Content.ReadAsStreamAsync();
#if !NETFRAMEWORK
await using var fs = new FileStream(path, FileMode.Create);
#else
using var fs = new FileStream(path, FileMode.Create);
#endif
await s.CopyToAsync(fs);
return true;
} }
public async Task<UpdateRelease> GetLatestReleaseAsync(string repository, string branch = null)
{
branch ??= "master";
var response = await _client.GetFromJsonAsync<BranchResponse>($"/job/{repository}/job/{branch}/{ApiPath}");
if (response is null)
throw new($"Unable to get latest release for {repository}");
var job = await _client.GetFromJsonAsync<Job>(
$"/job/{repository}/job/{branch}/{response.LastBuild.Number}/{ApiPath}");
if (job is null)
throw new($"Unable to get latest release for job {repository}/{response.LastBuild.Number}");
return new(job.Version, job.Url + job.Artifacts.First(b => b.FileName == "torch-server.zip").RelativePath);
}
public void Dispose()
{
_client?.Dispose();
}
} }
public record BranchResponse(string Name, string Url, Build LastBuild, Build LastStableBuild); public record BranchResponse(string Name, string Url, Build LastBuild, Build LastStableBuild);
@@ -84,5 +54,12 @@ namespace Torch.API.WebAPI
public record Build(int Number, string Url); public record Build(int Number, string Url);
public record Job(int Number, bool Building, string Description, string Result, string Url, public record Job(int Number, bool Building, string Description, string Result, string Url,
[property: JsonConverter(typeof(SemanticVersionConverter))] Version Version); [property: JsonConverter(typeof(SemanticVersionConverter))] Version Version,
IReadOnlyList<Artifact> Artifacts);
public record Artifact(
string DisplayPath,
string FileName,
string RelativePath
);
} }

View File

@@ -0,0 +1,5 @@
using SemanticVersioning;
namespace Torch.API.WebAPI;
public record UpdateRelease(Version Version, string ArtifactUrl);

View File

@@ -111,7 +111,14 @@ public class TorchConfig : ViewModel, ITorchConfig
[Display(Name = "Login Token", Description = "Steam GSLT (can be used if you have dynamic ip)", GroupName = "Server")] [Display(Name = "Login Token", Description = "Steam GSLT (can be used if you have dynamic ip)", GroupName = "Server")]
public string LoginToken { get; set; } public string LoginToken { get; set; }
public UpdateSource UpdateSource { get; set; } = new()
{
Repository = "PveTeam/Torch",
Url = "https://api.github.com",
SourceType = UpdateSourceType.Github
};
// for backward compatibility // for backward compatibility
public void Save(string path = null) => Initializer.Instance?.ConfigPersistent?.Save(path); public void Save(string path = null) => Initializer.Instance?.ConfigPersistent?.Save(path);
} }

View File

@@ -5,6 +5,7 @@ using System.IO.Compression;
using System.IO.Packaging; using System.IO.Packaging;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
@@ -21,8 +22,9 @@ namespace Torch.Managers
public class UpdateManager : Manager public class UpdateManager : Manager
{ {
private readonly Timer _updatePollTimer; private readonly Timer _updatePollTimer;
private string _torchDir = new FileInfo(typeof(UpdateManager).Assembly.Location).DirectoryName; private readonly string _torchDir = ApplicationContext.Current.TorchDirectory.FullName;
private Logger _log = LogManager.GetCurrentClassLogger(); private readonly Logger _log = LogManager.GetCurrentClassLogger();
[Dependency] [Dependency]
private FilesystemManager _fsManager = null!; private FilesystemManager _fsManager = null!;
@@ -50,26 +52,22 @@ namespace Torch.Managers
try try
{ {
var job = await JenkinsQuery.Instance.GetLatestVersion(Torch.TorchVersion.Build); var updateSource = Torch.Config.UpdateSource;
if (job == null)
{
_log.Info("Failed to fetch latest version.");
return;
}
if (job.Version > Torch.TorchVersion) IUpdateQuery source = updateSource.SourceType switch
{ {
_log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {job.Version}"); UpdateSourceType.Github => new GithubQuery(updateSource.Url),
var updateName = Path.Combine(_fsManager.TempDirectory, "torchupdate.zip"); UpdateSourceType.Jenkins => new JenkinsQuery(updateSource.Url),
//new WebClient().DownloadFile(new Uri(releaseInfo.Item2), updateName); _ => throw new ArgumentOutOfRangeException()
if (!await JenkinsQuery.Instance.DownloadRelease(job, updateName)) };
{
_log.Warn("Failed to download new release!"); var release = await source.GetLatestReleaseAsync(updateSource.Repository, updateSource.Branch);
return;
} if (release.Version > Torch.TorchVersion)
UpdateFromZip(updateName, _torchDir); {
File.Delete(updateName); _log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {release.Version}");
_log.Warn($"Torch version {job.Version} has been installed, please restart Torch to finish the process."); await UpdateAsync(release, _torchDir);
_log.Warn($"Torch version {release.Version} has been installed, please restart Torch to finish the process.");
} }
else else
{ {
@@ -83,23 +81,29 @@ namespace Torch.Managers
} }
} }
private void UpdateFromZip(string zipFile, string extractPath) private async Task UpdateAsync(UpdateRelease release, string extractPath)
{ {
using (var zip = ZipFile.OpenRead(zipFile)) using var client = new HttpClient();
await using var stream = await client.GetStreamAsync(release.ArtifactUrl);
UpdateFromZip(stream, extractPath);
}
private void UpdateFromZip(Stream zipStream, string extractPath)
{
using var zip = new ZipArchive(zipStream, ZipArchiveMode.Read, true);
foreach (var file in zip.Entries)
{ {
foreach (var file in zip.Entries) if(file.Name == "NLog-user.config" && File.Exists(Path.Combine(extractPath, file.FullName)))
{ continue;
if(file.Name == "NLog-user.config" && File.Exists(Path.Combine(extractPath, file.FullName)))
continue;
_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(extractPath, file.FullName); _fsManager.SoftDelete(extractPath, file.FullName);
file.ExtractToFile(targetFile, true); file.ExtractToFile(targetFile, true);
}
//zip.ExtractToDirectory(extractPath); //throws exceptions sometimes?
} }
//zip.ExtractToDirectory(extractPath); //throws exceptions sometimes?
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -104,14 +104,8 @@ namespace Torch
#pragma warning restore CS0618 #pragma warning restore CS0618
Config = config; Config = config;
var versionString = GetType().Assembly var assemblyVersion = GetType().Assembly.GetName().Version ?? new Version();
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()! TorchVersion = new(assemblyVersion.Major, assemblyVersion.Minor, assemblyVersion.Build);
.InformationalVersion;
if (!SemanticVersioning.Version.TryParse(versionString, out var version))
throw new TypeLoadException("Unable to parse the Torch version from the assembly.");
TorchVersion = version;
RunArgs = Array.Empty<string>(); RunArgs = Array.Empty<string>();
@@ -129,7 +123,7 @@ namespace Torch
Managers.AddManager(sessionManager); Managers.AddManager(sessionManager);
Managers.AddManager(new PatchManager(this)); Managers.AddManager(new PatchManager(this));
Managers.AddManager(new FilesystemManager(this)); Managers.AddManager(new FilesystemManager(this));
// Managers.AddManager(new UpdateManager(this)); Managers.AddManager(new UpdateManager(this));
Managers.AddManager(new EventManager(this)); Managers.AddManager(new EventManager(this));
#pragma warning disable CS0618 #pragma warning disable CS0618
Managers.AddManager(Plugins); Managers.AddManager(Plugins);