Merge pull request #760 from Cyberboss/VariousMaintainiblityThings [TGSDeploy] [NugetDeploy]

Various maintainiblity things. 4.0.0.5 bump
This commit is contained in:
Jordan Brown
2018-10-02 14:36:49 -04:00
committed by GitHub
28 changed files with 349 additions and 45 deletions
+1 -1
View File
@@ -93,7 +93,7 @@ Note that the ratio of application installations to databases is 1:1. Do not att
For the Windows service version start the `tgstation-server-4` service. If it fails to start, check the Windows event log under Windows/Application for entries from tgstation-server-4 for errors.
For the console version run `dotnet Tgstation.Server.Host.Console.dll` in the installation directory. The `tgs.bat` and `tgs.sh` shell scripts are shortcuts for this. If on Windows and you wish to install byond versions >= 512.1427 you must do this as admin to give the server permission to install the required DirectX dependency
For the console version run `dotnet Tgstation.Server.Host.Console.dll` in the installation directory. The `tgs.bat` and `tgs.sh` shell scripts are shortcuts for this. If on Windows, you must do this as admin to give the server permission to install the required DirectX dependency for BYOND as well as create symlinks.
For the docker version run `docker start tgs`
@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<DebugType>Full</DebugType>
<Version>4.0.0.0</Version>
<Version>4.0.1</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Cyberboss</Authors>
<Company>/tg/station 13</Company>
@@ -5,6 +5,9 @@ using Tgstation.Server.Api.Models;
namespace Tgstation.Server.Client
{
/// <summary>
/// Occurs when a response is received that did not deserialize to one of the expected <see cref="Api.Models"/>
/// </summary>
sealed class UnrecognizedResponseException : ClientException
{
/// <summary>
@@ -4,8 +4,7 @@
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<DebugType>Full</DebugType>
<FileVersion>4.0.0.4</FileVersion>
<AssemblyVersion>4.0.0.4</AssemblyVersion>
<Version>4.0.0.5</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
@@ -7,5 +7,5 @@ using System.Runtime.InteropServices;
[assembly: Guid("29927416-3b78-49a7-a560-5ccaa638b6b4")]
[assembly: InternalsVisibleTo("Tgstation.Server.Host.Service.Tests")]
[assembly: AssemblyVersion("4.0.0.4")]
[assembly: AssemblyFileVersion("4.0.0.4")]
[assembly: AssemblyVersion("4.0.0.5")]
[assembly: AssemblyFileVersion("4.0.0.5")]
@@ -4,8 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<DebugType>Full</DebugType>
<AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
<AssemblyVersion>4.0.0.4</AssemblyVersion>
<FileVersion>4.0.0.4</FileVersion>
<Version>4.0.0.5</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
@@ -6,13 +6,26 @@ namespace Tgstation.Server.Host.Components.Byond
sealed class ByondExecutableLock : IByondExecutableLock
{
/// <inheritdoc />
public Version Version { get; set; }
public Version Version { get; }
/// <inheritdoc />
public string DreamDaemonPath { get; set; }
public string DreamDaemonPath { get; }
/// <inheritdoc />
public string DreamMakerPath { get; set; }
public string DreamMakerPath { get; }
/// <summary>
/// Construct a <see cref="ByondExecutableLock"/>
/// </summary>
/// <param name="version">The value of <see cref="Version"/></param>
/// <param name="dreamDaemonPath">The value of <see cref="DreamDaemonPath"/></param>
/// <param name="dreamMakerPath">The value of <see cref="DreamMakerPath"/></param>
public ByondExecutableLock(Version version, string dreamDaemonPath, string dreamMakerPath)
{
Version = version ?? throw new ArgumentNullException(nameof(version));
dreamDaemonPath = dreamDaemonPath ?? throw new ArgumentNullException(nameof(dreamDaemonPath));
dreamMakerPath = dreamMakerPath ?? throw new ArgumentNullException(nameof(dreamMakerPath));
}
//at one point in design, byond versions were to delete themselves if they werent the active version
//That changed at some point so these functions are intentioanlly left blank
@@ -197,12 +197,7 @@ namespace Tgstation.Server.Host.Components.Byond
var versionKey = VersionKey(versionToUse);
return new ByondExecutableLock
{
DreamDaemonPath = ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamDaemonName)),
DreamMakerPath = ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamMakerName)),
Version = versionToUse
};
return new ByondExecutableLock(versionToUse, ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamDaemonName)), ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamMakerName)));
}
/// <inheritdoc />
@@ -2,6 +2,7 @@
using LibGit2Sharp.Handlers;
using Microsoft.Extensions.Logging;
using System;
using Tgstation.Server.Host.Core;
namespace Tgstation.Server.Host.Components.Repository
{
@@ -8,6 +8,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Repository
@@ -1,8 +1,8 @@
using Microsoft.Extensions.Hosting;
using BetterWin32Errors;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
@@ -49,10 +49,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
{
var gcChildhandlesList = GCHandle.FromIntPtr(lParam);
if (gcChildhandlesList == null || gcChildhandlesList.Target == null)
if (gcChildhandlesList.Target == null)
return false;
var childHandles = (List <IntPtr>)gcChildhandlesList.Target;
var childHandles = (List<IntPtr>)gcChildhandlesList.Target;
childHandles.Add(hWnd);
return true;
@@ -61,12 +61,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
static List<IntPtr> GetAllChildHandles(IntPtr main)
{
var childHandles = new List<IntPtr>();
var gcChildhandlesList = GCHandle.Alloc(childHandles);
var pointerChildHandlesList = GCHandle.ToIntPtr(gcChildhandlesList);
try
{
var pointerChildHandlesList = GCHandle.ToIntPtr(gcChildhandlesList);
NativeMethods.EnumWindowProc childProc = new NativeMethods.EnumWindowProc(EnumWindow);
NativeMethods.EnumChildWindows(main, childProc, pointerChildHandlesList);
}
@@ -74,7 +72,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
{
gcChildhandlesList.Free();
}
return childHandles;
}
@@ -129,7 +126,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
if (NativeMethods.GetWindowText(I, stringBuilder, MaxLength) == 0)
{
logger.LogWarning("Error calling GetWindowText! Exception: {0}", new Win32Exception(Marshal.GetLastWin32Error()));
logger.LogWarning("Error calling GetWindowText! Exception: {0}", new Win32Exception());
continue;
}
@@ -152,7 +149,10 @@ namespace Tgstation.Server.Host.Components.Watchdog
logger.LogDebug("Unable to find \"Yes\" button for \"Network Accessibility\" window in owned process {0}!", processId);
}
}
catch (OperationCanceledException) { }
catch (OperationCanceledException)
{
logger.LogTrace("Cancelled!");
}
finally
{
logger.LogDebug("Exiting network prompt reaper...");
@@ -28,6 +28,9 @@ namespace Tgstation.Server.Host.Core
/// <inheritdoc />
public async Task UseContext(Func<IDatabaseContext, Task> operation)
{
if (operation == null)
throw new ArgumentNullException(nameof(operation));
using (var scope = scopeFactory.CreateScope())
await operation(scope.ServiceProvider.GetRequiredService<IDatabaseContext>()).ConfigureAwait(false);
}
@@ -1,6 +1,6 @@
using System;
namespace Tgstation.Server.Host
namespace Tgstation.Server.Host.Core
{
/// <summary>
/// Operation exceptions thrown from the context of a <see cref="Models.Job"/>
@@ -1,5 +1,4 @@
using System;
using Tgstation.Server.Host.Core;
namespace Tgstation.Server.Host.Core
{
@@ -1,4 +1,4 @@
namespace Tgstation.Server.Host.Core
namespace Tgstation.Server.Host.IO
{
interface IPostWriteHandler
{
@@ -1,7 +1,7 @@
using Mono.Unix;
using Mono.Unix.Native;
namespace Tgstation.Server.Host.Core
namespace Tgstation.Server.Host.IO
{
/// <summary>
/// <see cref="IPostWriteHandler"/> for POSIX systems
@@ -1,4 +1,5 @@
using Mono.Unix;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -13,6 +14,11 @@ namespace Tgstation.Server.Host.IO
/// <inheritdoc />
public Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
if (targetPath == null)
throw new ArgumentNullException(nameof(targetPath));
if (linkPath == null)
throw new ArgumentNullException(nameof(linkPath));
UnixFileSystemInfo fsInfo;
var isFile = File.Exists(targetPath);
cancellationToken.ThrowIfCancellationRequested();
@@ -1,4 +1,4 @@
namespace Tgstation.Server.Host.Core
namespace Tgstation.Server.Host.IO
{
/// <summary>
/// <see cref="IPostWriteHandler"/> for Windows systems
@@ -1,6 +1,6 @@
using System.ComponentModel;
using BetterWin32Errors;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -14,15 +14,27 @@ namespace Tgstation.Server.Host.IO
/// <inheritdoc />
public Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
{
if (targetPath == null)
throw new ArgumentNullException(nameof(targetPath));
if (linkPath == null)
throw new ArgumentNullException(nameof(linkPath));
//check if its not a file
var flags = File.Exists(targetPath) ? 0 : 1; //SYMBOLIC_LINK_FLAG_DIRECTORY
#if DEBUG
flags |= 2; //SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE on win10 1607+ developer mode
#endif
var flags = File.Exists(targetPath) ? NativeMethods.CreateSymbolicLinkFlags.None : NativeMethods.CreateSymbolicLinkFlags.Directory;
flags |= NativeMethods.CreateSymbolicLinkFlags.AllowUnprivilegedCreate;
cancellationToken.ThrowIfCancellationRequested();
if (!NativeMethods.CreateSymbolicLink(linkPath, targetPath, flags))
throw new Win32Exception(Marshal.GetLastWin32Error());
{
if (Win32Exception.GetLastWin32Error() == Win32Error.ERROR_INVALID_PARAMETER) //SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE isn't supported
{
flags &= ~NativeMethods.CreateSymbolicLinkFlags.AllowUnprivilegedCreate;
if (NativeMethods.CreateSymbolicLink(linkPath, targetPath, flags))
return;
}
throw new Win32Exception();
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
}
+13 -3
View File
@@ -9,6 +9,17 @@ namespace Tgstation.Server.Host
/// </summary>
static class NativeMethods
{
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-createsymboliclinka#parameters
/// </summary>
[Flags]
public enum CreateSymbolicLinkFlags : int
{
None = 0,
Directory = 1,
AllowUnprivilegedCreate = 2
}
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getwindowthreadprocessid
/// </summary>
@@ -35,8 +46,7 @@ namespace Tgstation.Server.Host
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-enumchildwindows
/// </summary>
[DllImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll")]
public static extern bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam);
/// <summary>
@@ -55,6 +65,6 @@ namespace Tgstation.Server.Host
/// See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-createsymboliclinkw
/// </summary>
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, int dwFlags);
public static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, CreateSymbolicLinkFlags dwFlags);
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore;
using System;
namespace Tgstation.Server.Host
{
@@ -6,6 +7,6 @@ namespace Tgstation.Server.Host
public sealed class ServerFactory : IServerFactory
{
/// <inheritdoc />
public IServer CreateServer(string[] args, string updatePath) => new Server(WebHost.CreateDefaultBuilder(args), updatePath);
public IServer CreateServer(string[] args, string updatePath) => new Server(WebHost.CreateDefaultBuilder(args ?? throw new ArgumentNullException(nameof(args))), updatePath);
}
}
@@ -3,8 +3,7 @@
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<DebugType>Full</DebugType>
<AssemblyVersion>4.0.0.4</AssemblyVersion>
<FileVersion>4.0.0.4</FileVersion>
<Version>4.0.0.5</Version>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
@@ -21,6 +20,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BetterWin32Errors" Version="0.2.0" />
<PackageReference Include="Byond.TopicSender" Version="1.1.4" />
<PackageReference Include="Cyberboss.AspNetCore.AsyncInitializer" Version="1.2.0" />
<PackageReference Include="Cyberboss.SmartIrc4net.Standard" Version="0.4.6" />
@@ -0,0 +1,56 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Core.Tests
{
[TestClass]
public sealed class TestDatabaseContextFactory
{
[TestMethod]
public void TestConstructionThrows()
{
Assert.ThrowsException<ArgumentNullException>(() => new DatabaseContextFactory(null));
var mockProvider = new Mock<IServiceProvider>();
mockProvider.Setup(x => x.GetService(typeof(IDatabaseContext))).Verifiable();
var mockScope = new Mock<IServiceScope>();
mockScope.SetupGet(x => x.ServiceProvider).Returns(mockProvider.Object).Verifiable();
var mockScopeFactory = new Mock<IServiceScopeFactory>();
mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object).Verifiable();
Assert.ThrowsException<InvalidOperationException>(() => new DatabaseContextFactory(mockScopeFactory.Object));
mockScopeFactory.VerifyAll();
mockScope.VerifyAll();
mockProvider.VerifyAll();
}
[TestMethod]
public async Task TestWorks()
{
var mockDatabase = new Mock<IDatabaseContext>();
var mockProvider = new Mock<IServiceProvider>();
var mockDbo = mockDatabase.Object;
mockProvider.Setup(x => x.GetService(typeof(IDatabaseContext))).Returns(mockDbo).Verifiable();
var mockScope = new Mock<IServiceScope>();
mockScope.SetupGet(x => x.ServiceProvider).Returns(mockProvider.Object).Verifiable();
var mockScopeFactory = new Mock<IServiceScopeFactory>();
mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object).Verifiable();
var factory = new DatabaseContextFactory(mockScopeFactory.Object);
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => factory.UseContext(null)).ConfigureAwait(false);
await factory.UseContext(context =>
{
Assert.AreSame(mockDbo, context);
return Task.CompletedTask;
});
mockScopeFactory.VerifyAll();
mockScope.VerifyAll();
mockProvider.VerifyAll();
}
}
}
@@ -0,0 +1,55 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Octokit;
using System;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Core.Tests
{
[TestClass]
public sealed class TestGitHubClientFactory
{
[TestMethod]
public void TestContruction() => Assert.ThrowsException<ArgumentNullException>(() => new GitHubClientFactory(null));
[TestMethod]
public async Task TestCreateBasicClient()
{
var mockApp = new Mock<IApplication>();
mockApp.SetupGet(x => x.Version).Returns(new Version()).Verifiable();
mockApp.SetupGet(x => x.VersionPrefix).Returns("TGSTests").Verifiable();
var factory = new GitHubClientFactory(mockApp.Object);
var client = factory.CreateClient();
Assert.IsNotNull(client);
var credentials = await client.Connection.CredentialStore.GetCredentials().ConfigureAwait(false);
Assert.AreEqual(AuthenticationType.Anonymous, credentials.AuthenticationType);
mockApp.VerifyAll();
}
[TestMethod]
public async Task TestCreateTokenClient()
{
var mockApp = new Mock<IApplication>();
mockApp.SetupGet(x => x.Version).Returns(new Version()).Verifiable();
mockApp.SetupGet(x => x.VersionPrefix).Returns("TGSTests").Verifiable();
var factory = new GitHubClientFactory(mockApp.Object);
Assert.ThrowsException<ArgumentNullException>(() => factory.CreateClient(null));
var client = factory.CreateClient("asdf");
Assert.IsNotNull(client);
var credentials = await client.Connection.CredentialStore.GetCredentials().ConfigureAwait(false);
Assert.AreEqual(AuthenticationType.Oauth, credentials.AuthenticationType);
mockApp.VerifyAll();
}
}
}
@@ -0,0 +1,17 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace Tgstation.Server.Host.Core.Tests
{
[TestClass]
public sealed class TestJobException
{
[TestMethod]
public void TestConstruction()
{
new JobException();
new JobException("Message");
new JobException("Message", new Exception());
}
}
}
@@ -0,0 +1,102 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.IO.Tests
{
[TestClass]
public sealed class TestSymlinkFactory
{
ISymlinkFactory symlinkFactory;
[TestInitialize]
public void SelectFactory()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
symlinkFactory = new WindowsSymlinkFactory();
else
symlinkFactory = new PosixSymlinkFactory();
}
[TestMethod]
public async Task TestFileWorks()
{
const string Text = "Hello world";
string f2 = null;
var f1 = Path.GetTempFileName();
try
{
f2 = f1 + ".linked";
File.WriteAllText(f1, Text);
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => symlinkFactory.CreateSymbolicLink(null, null, default)).ConfigureAwait(false);
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => symlinkFactory.CreateSymbolicLink(f1, null, default)).ConfigureAwait(false);
await symlinkFactory.CreateSymbolicLink(f1, f2, default).ConfigureAwait(false);
Assert.IsTrue(File.Exists(f2));
var f2Contents = File.ReadAllText(f2);
Assert.AreEqual(Text, f2Contents);
}
finally
{
File.Delete(f2);
File.Delete(f1);
}
}
[TestMethod]
public async Task TestDirectoryWorks()
{
const string FileName = "TestFile.txt";
const string Text = "Hello world";
string f2 = null;
var f1 = Path.GetTempFileName();
File.Delete(f1);
Directory.CreateDirectory(f1);
try
{
f2 = f1 + ".linked";
var p1 = Path.Combine(f1, FileName);
File.WriteAllText(p1, Text);
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => symlinkFactory.CreateSymbolicLink(null, null, default)).ConfigureAwait(false);
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => symlinkFactory.CreateSymbolicLink(f1, null, default)).ConfigureAwait(false);
await symlinkFactory.CreateSymbolicLink(f1, f2, default).ConfigureAwait(false);
var p2 = Path.Combine(f2, FileName);
Assert.IsTrue(File.Exists(p2));
var f2Contents = File.ReadAllText(p2);
Assert.AreEqual(Text, f2Contents);
File.Delete(p2);
Assert.IsFalse(File.Exists(p1));
}
finally
{
Directory.Delete(f2, true);
Directory.Delete(f1, true);
}
}
[TestMethod]
public async Task TestFailsProperly()
{
const string BadPath = "/../../?>O(UF+}P{{??>/////";
try
{
await symlinkFactory.CreateSymbolicLink(BadPath, BadPath, default).ConfigureAwait(false);
}
catch
{
return;
}
Assert.Fail("No exception thrown!");
}
}
}
@@ -0,0 +1,32 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace Tgstation.Server.Host.Tests
{
/// <summary>
/// Tests for <see cref="ServerFactory"/>
/// </summary>
[TestClass]
public sealed class TestServerFactory
{
[TestMethod]
public void TestWorksWithoutUpdatePath()
{
var factory = new ServerFactory();
Assert.ThrowsException<ArgumentNullException>(() => factory.CreateServer(null, null));
factory.CreateServer(Array.Empty<string>(), null);
}
[TestMethod]
public void TestWorksWithUpdatePath()
{
var factory = new ServerFactory();
const string Path = "/test";
Assert.ThrowsException<ArgumentNullException>(() => factory.CreateServer(null, null));
Assert.ThrowsException<ArgumentNullException>(() => factory.CreateServer(null, Path));
factory.CreateServer(Array.Empty<string>(), Path);
}
}
}