Merge pull request #987 from tgstation/986-FixAutostart [DMDeploy][TGSDeploy]

Fix issue with compile job cleaning
This commit is contained in:
Jordan Brown
2020-05-14 23:50:26 -04:00
committed by GitHub
40 changed files with 416 additions and 206 deletions
+2 -2
View File
@@ -2,10 +2,10 @@
<PropertyGroup>
<!-- This is the authorative version list -->
<!-- Integration tests will ensure they match across the board -->
<TgsCoreVersion>4.2.0</TgsCoreVersion>
<TgsCoreVersion>4.2.1</TgsCoreVersion>
<TgsApiVersion>6.2.0</TgsApiVersion>
<TgsClientVersion>6.1.0</TgsClientVersion>
<TgsDmapiVersion>5.1.0</TgsDmapiVersion>
<TgsDmapiVersion>5.1.1</TgsDmapiVersion>
<TgsControlPanelVersion>0.4.0</TgsControlPanelVersion>
<TgsHostWatchdogVersion>1.1.0</TgsHostWatchdogVersion>
</PropertyGroup>
+4 -2
View File
@@ -1,5 +1,7 @@
$bf = $env:APPVEYOR_BUILD_FOLDER
[XML]$versionXML = Get-Content "$bf/build/Version.props"
$propsPath = "$bf/build/Version.props"
[XML]$versionXML = Get-Content $propsPath
$env:TGSVersion = $versionXML.Project.PropertyGroup.TgsCoreVersion
$env:APIVersion = $versionXML.Project.PropertyGroup.TgsApiVersion
$env:DMVersion = $versionXML.Project.PropertyGroup.TgsDmapiVersion
@@ -11,7 +13,7 @@ if (($env:CONFIGURATION -match "Release") -And ($env:APPVEYOR_REPO_BRANCH -match
$env:TGSDeploy = "Do it."
Write-Host "Generating release notes..."
dotnet run -p "$bf/tools/ReleaseNotes" $env:TGSVersion
dotnet run -p "$bf/tools/ReleaseNotes" $env:TGSVersion $propsPath
$env:TGSDraftNotes = !($?)
$releaseNotesPath = "$bf/release_notes.md"
Write-Host "Reading release notes from $releaseNotesPath..."
+1 -1
View File
@@ -1,6 +1,6 @@
//tgstation-server DMAPI
#define TGS_DMAPI_VERSION "5.1.0"
#define TGS_DMAPI_VERSION "5.1.1"
//All functions and datums outside this document are subject to change with any version and should not be relied on
+1 -1
View File
@@ -6,7 +6,7 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
/datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version)
. = ..()
src.event_handler = version
src.event_handler = event_handler
src.version = version
/datum/tgs_api/latest
+4 -4
View File
@@ -60,10 +60,10 @@
#define DMAPI5_TOPIC_COMMAND_CHANGE_PORT 2
#define DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE 3
#define DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED 4
#define DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE 4
#define DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE 5
#define DMAPI5_TOPIC_COMMAND_HEARTBEAT 6
#define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 7
#define DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE 5
#define DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE 6
#define DMAPI5_TOPIC_COMMAND_HEARTBEAT 7
#define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 8
#define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType"
#define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand"
+1 -1
View File
@@ -16,7 +16,7 @@
var/list/chat_channels
/datum/tgs_api/v5/ApiVersion()
return new /datum/tgs_version("5.1.0")
return new /datum/tgs_version("5.1.1")
/datum/tgs_api/v5/OnWorldNew(minimum_required_security_level)
server_port = world.params[DMAPI5_PARAM_SERVER_PORT]
+1 -1
View File
@@ -415,7 +415,7 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// Attempted to start the watchdog with a corrupted <see cref="CompileJob"/>.
/// </summary>
[Description("Cannot launch with active compile job as it is corrupted!")]
[Description("Cannot launch active compile job as it is missing or corrupted!")]
WatchdogCompileJobCorrupted,
/// <summary>
@@ -119,7 +119,10 @@ namespace Tgstation.Server.Host.Components.Byond
IProcess directXInstaller;
try
{
directXInstaller = processExecutor.LaunchProcess(IOManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true);
directXInstaller = processExecutor.LaunchProcess(
IOManager.ConcatPath(rbdx, "DXSETUP.exe"),
rbdx, "/silent",
noShellExecute: true);
}
catch (Exception e)
{
@@ -646,7 +646,8 @@ namespace Tgstation.Server.Host.Components.Chat
public async Task StopAsync(CancellationToken cancellationToken)
{
handlerCts.Cancel();
await chatHandler.ConfigureAwait(false);
if (chatHandler != null)
await chatHandler.ConfigureAwait(false);
await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
}
@@ -118,7 +118,7 @@ namespace Tgstation.Server.Host.Components.Deployment
if (!jobLockCounts.TryGetValue(job.Id, out var currentVal) || currentVal == 1)
{
jobLockCounts.Remove(job.Id);
logger.LogDebug("Cleaning compile job {0} => {1}", job.Id, job.DirectoryName);
logger.LogDebug("Cleaning lock-free compile job {0} => {1}", job.Id, job.DirectoryName);
cleanupTask = HandleCleanup();
}
else
@@ -247,7 +247,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <inheritdoc />
#pragma warning disable CA1506 // TODO: Decomplexify
public async Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken)
public async Task CleanUnusedCompileJobs(CancellationToken cancellationToken)
{
List<long> jobIdsToSkip;
@@ -260,12 +260,18 @@ namespace Tgstation.Server.Host.Components.Deployment
// find the uids of locked directories
await databaseContextFactory.UseContext(async db =>
{
jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
jobUidsToNotErase = (await db.CompileJobs.Where(
x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id))
.Select(x => x.DirectoryName.Value)
.ToListAsync(cancellationToken)
.ConfigureAwait(false))
.Select(x => x.ToString())
.ToList();
}).ConfigureAwait(false);
// add the other exemption
if (exceptThisOne != null)
jobUidsToNotErase.Add(exceptThisOne.DirectoryName.Value.ToString().ToUpperInvariant());
jobUidsToNotErase.Add(WindowsSwappableDmbProvider.LiveGameDirectory);
logger.LogTrace("We will not clean the following directories: {0}", String.Join(", ", jobUidsToNotErase));
// cleanup
var gameDirectory = ioManager.ResolvePath();
@@ -275,8 +281,9 @@ namespace Tgstation.Server.Host.Components.Deployment
var tasks = directories.Select(async x =>
{
var nameOnly = ioManager.GetFileName(x);
if (jobUidsToNotErase.Contains(nameOnly.ToUpperInvariant()))
if (jobUidsToNotErase.Contains(nameOnly))
return;
logger.LogDebug("Cleaning unused game folder: {0}...", nameOnly);
try
{
++deleting;
@@ -292,10 +299,7 @@ namespace Tgstation.Server.Host.Components.Deployment
}
}).ToList();
if (deleting > 0)
{
logger.LogDebug("Cleaning unused game folders: {0}...", String.Join(", ", directories));
await Task.WhenAll().ConfigureAwait(false);
}
await Task.WhenAll(tasks).ConfigureAwait(false);
}
#pragma warning restore CA1506
}
@@ -244,6 +244,7 @@ namespace Tgstation.Server.Host.Components.Deployment
ADirectoryName)),
$"-clean {job.DmeName}.{DmeExtension}",
true,
true,
true);
int exitCode;
using (cancellationToken.Register(() => dm.Terminate()))
@@ -381,7 +382,7 @@ namespace Tgstation.Server.Host.Components.Deployment
await chatManager.SendUpdateMessage(
String.Format(
CultureInfo.InvariantCulture,
"*Deployment Triggered*{0}Revision: {1}{2}{3}{0} BYOND Version: {4}.{5}",
"*Deployment Triggered*{0}Revision: {1}{2}{3}{0}BYOND Version: {4}.{5}",
Environment.NewLine,
commitInsert,
testmergeInsert,
@@ -37,11 +37,10 @@ namespace Tgstation.Server.Host.Components.Deployment
Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken);
/// <summary>
/// Deletes all compile jobs that are inactive in the Game folder <paramref name="exceptThisOne"/>
/// Deletes all compile jobs that are inactive in the Game folder.
/// </summary>
/// <param name="exceptThisOne">An optional compile job to not delete</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken);
Task CleanUnusedCompileJobs(CancellationToken cancellationToken);
}
}
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// The directory where the <see cref="baseProvider"/> is symlinked to.
/// </summary>
const string LiveGameDirectory = "Live";
public const string LiveGameDirectory = "Live";
/// <inheritdoc />
public string DmbName => baseProvider.DmbName;
@@ -30,14 +30,13 @@ namespace Tgstation.Server.Host.Components
}
/// <inheritdoc />
public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
{
if (watchdog == null)
throw new InvalidOperationException("EventConsumer used without watchdog set!");
if (!await configuration.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false))
return false;
return await watchdog.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false);
await configuration.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false);
await watchdog.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false);
}
/// <summary>
@@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Components
/// <param name="eventType">The <see cref="EventType"/></param>
/// <param name="parameters">The parameters for <paramref name="eventType"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if more <see cref="IEventConsumer"/> should run, <see langword="false"/> otherwise</returns>
Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken);
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken);
}
}
@@ -15,7 +15,6 @@ using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Components.Watchdog;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Models;
@@ -623,12 +622,7 @@ namespace Tgstation.Server.Host.Components
// dependent on so many things, its just safer this way
await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false);
CompileJob latestCompileJob = null;
await databaseContextFactory.UseContext(async db =>
{
latestCompileJob = await db.MostRecentCompletedCompileJobOrDefault(metadata, cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
await dmbFactory.CleanUnusedCompileJobs(latestCompileJob, cancellationToken).ConfigureAwait(false);
await dmbFactory.CleanUnusedCompileJobs(cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -235,6 +235,7 @@ namespace Tgstation.Server.Host.Components
bridgeRegistrar,
serverPortProvider,
loggerFactory,
loggerFactory.CreateLogger<SessionControllerFactory>(),
metadata.CloneMetadata());
var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger<DmbFactory>(), metadata.CloneMetadata());
@@ -33,7 +33,7 @@ namespace Tgstation.Server.Host.Components.Interop
/// <summary>
/// The DMAPI <see cref="Version"/> being used.
/// </summary>
public static readonly Version Version = new Version(5, 1, 0);
public static readonly Version Version = new Version(5, 1, 1);
/// <summary>
/// <see cref="JsonSerializerSettings"/> for use when communicating with the DMAPI.
@@ -163,8 +163,8 @@ namespace Tgstation.Server.Host.Components.Repository
if (remote.EndsWith(item, StringComparison.OrdinalIgnoreCase))
remote = remote.Substring(0, remote.LastIndexOf(item, StringComparison.OrdinalIgnoreCase));
var splits = remote.Split('/');
name = splits[splits.Length - 1];
owner = splits[splits.Length - 2].Split('.')[0];
name = splits.Last();
owner = splits[^2].Split('.').First();
logger.LogTrace("GetRepositoryOwnerName({0}) => {1} / {2}", remote, owner, name);
}
@@ -622,18 +622,14 @@ namespace Tgstation.Server.Host.Components.Repository
cancellationToken.ThrowIfCancellationRequested();
try
{
if (!await eventConsumer.HandleEvent(
await eventConsumer.HandleEvent(
EventType.RepoPreSynchronize,
new List<string>
{
ioMananger.ResolvePath()
},
cancellationToken)
.ConfigureAwait(false))
{
logger.LogDebug("Aborted synchronize due to event handler response!");
return false;
}
.ConfigureAwait(false);
}
finally
{
@@ -522,7 +522,6 @@ namespace Tgstation.Server.Host.Components.Session
var json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings);
logger.LogTrace("Topic request: {0}", json);
Exception caughtException;
try
{
var commandString = String.Format(CultureInfo.InvariantCulture,
@@ -558,19 +557,20 @@ namespace Tgstation.Server.Host.Components.Session
return new CombinedTopicResponse(topicResponse, interopResponse);
}
catch (OperationCanceledException e)
catch (OperationCanceledException)
{
logger.LogTrace(
"Topic request {0}!",
cancellationToken.IsCancellationRequested
? "aborted"
: "timed out");
cancellationToken.ThrowIfCancellationRequested();
caughtException = e;
}
catch (Exception e)
{
caughtException = e;
logger.LogWarning("Send command exception:{0}{1}", Environment.NewLine, e);
}
if (caughtException == null)
logger.LogWarning("Send command exception:{0}{1}", Environment.NewLine, caughtException.Message);
return null;
}
@@ -15,6 +15,7 @@ using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Components.Interop.Bridge;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
@@ -83,6 +84,11 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
readonly ILoggerFactory loggerFactory;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="SessionControllerFactory"/>
/// </summary>
readonly ILogger<SessionControllerFactory> logger;
/// <summary>
/// The <see cref="Api.Models.Instance"/> for the <see cref="SessionControllerFactory"/>
/// </summary>
@@ -120,6 +126,7 @@ namespace Tgstation.Server.Host.Components.Session
/// <param name="bridgeRegistrar">The value of <see cref="bridgeRegistrar"/>.</param>
/// <param name="serverPortProvider">The value of <see cref="serverPortProvider"/>.</param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/></param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public SessionControllerFactory(
IProcessExecutor processExecutor,
IByondManager byond,
@@ -133,6 +140,7 @@ namespace Tgstation.Server.Host.Components.Session
IBridgeRegistrar bridgeRegistrar,
IServerPortProvider serverPortProvider,
ILoggerFactory loggerFactory,
ILogger<SessionControllerFactory> logger,
Api.Models.Instance instance)
{
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
@@ -148,6 +156,7 @@ namespace Tgstation.Server.Host.Components.Session
this.bridgeRegistrar = bridgeRegistrar ?? throw new ArgumentNullException(nameof(bridgeRegistrar));
this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
@@ -192,6 +201,8 @@ namespace Tgstation.Server.Host.Components.Session
if (launchParameters.SecurityLevel == DreamDaemonSecurity.Trusted)
await byondLock.TrustDmbPath(ioManager.ConcatPath(basePath, dmbProvider.DmbName), cancellationToken).ConfigureAwait(false);
CheckPagerIsNotRunning();
var accessIdentifier = cryptographySuite.GetSecureString();
// set command line options
@@ -213,7 +224,24 @@ namespace Tgstation.Server.Host.Components.Session
var noShellExecute = !platformIdentifier.IsWindows;
// launch dd
var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments, noShellExecute: noShellExecute);
var process = processExecutor.LaunchProcess(
byondLock.DreamDaemonPath,
basePath,
arguments,
noShellExecute,
noShellExecute,
noShellExecute: noShellExecute);
if (noShellExecute)
{
// Log DD output
_ = process.Lifetime.ContinueWith(
x => logger.LogTrace(
"DreamDaemon Output:{0}{1}",
Environment.NewLine, process.GetCombinedOutput()),
TaskScheduler.Current);
}
try
{
networkPromptReaper.RegisterProcess(process);
@@ -371,5 +399,14 @@ namespace Tgstation.Server.Host.Components.Session
securityLevel,
apiValidateOnly);
}
/// <summary>
/// Make sure the BYOND pager is not running.
/// </summary>
void CheckPagerIsNotRunning()
{
if (platformIdentifier.IsWindows && processExecutor.IsProcessWithNameRunning("byond"))
throw new JobException("Cannot start DreamDaemon headless with the BYOND pager running!");
}
}
}
@@ -11,6 +11,7 @@ using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
@@ -442,12 +443,12 @@ namespace Tgstation.Server.Host.Components.StaticFiles
public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
/// <inheritdoc />
public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
{
await EnsureDirectories(cancellationToken).ConfigureAwait(false);
if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptName))
return true;
return;
// always execute in serial
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
@@ -456,17 +457,22 @@ namespace Tgstation.Server.Host.Components.StaticFiles
var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
foreach (var I in files.Select(x => ioManager.GetFileName(x)).Where(x => x.StartsWith(scriptName, StringComparison.Ordinal)))
using (var script = processExecutor.LaunchProcess(ioManager.ConcatPath(resolvedScriptsDir, I), resolvedScriptsDir, String.Join(' ', parameters), noShellExecute: true))
using (var script = processExecutor.LaunchProcess(
ioManager.ConcatPath(resolvedScriptsDir, I),
resolvedScriptsDir,
String.Join(' ', parameters),
true,
true,
true))
using (cancellationToken.Register(() => script.Terminate()))
{
var exitCode = await script.Lifetime.ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
var scriptOutput = script.GetCombinedOutput();
if (exitCode != 0)
return false;
throw new JobException($"Script {I} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
}
}
return true;
}
/// <inheritdoc />
@@ -727,19 +727,15 @@ namespace Tgstation.Server.Host.Components.Watchdog
}
/// <inheritdoc />
public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
{
if (!Running)
return true;
var notification = new EventNotification(eventType, parameters);
var activeServer = GetActiveController();
// Server may have ended
if (activeServer == null)
return true;
return;
var notification = new EventNotification(eventType, parameters);
var result = await activeServer.SendCommand(
new TopicParameters(notification),
cancellationToken)
@@ -762,8 +758,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
.Select(nullableChannelId => nullableChannelId.Value),
cancellationToken)))
.ConfigureAwait(false);
return true;
}
/// <inheritdoc />
@@ -1,8 +1,5 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Options;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Database.Design
{
@@ -11,31 +8,21 @@ namespace Tgstation.Server.Host.Database.Design
/// </summary>
static class DesignTimeDbContextFactoryHelpers
{
/// <summary>
/// Path to the json file to use for migrations configuration
/// </summary>
const string RootJson = "appsettings.json";
/// <summary>
/// Path to the development json file to use for migrations configuration
/// </summary>
const string DevJson = "appsettings.Development.json";
/// <summary>
/// Get the <see cref="IOptions{TOptions}"/> for the <see cref="DatabaseConfiguration"/>
/// </summary>
/// <param name="databaseType">The <see cref="DatabaseConfiguration.DatabaseType"/>.</param>
/// <param name="connectionString">The <see cref="DatabaseConfiguration.ConnectionString"/>.</param>
/// <returns>The <see cref="IOptions{TOptions}"/> for the <see cref="DatabaseConfiguration"/></returns>
public static IOptions<DatabaseConfiguration> GetDbContextOptions()
public static IOptions<DatabaseConfiguration> GetDbContextOptions(DatabaseType databaseType, string connectionString)
{
var builder = new ConfigurationBuilder();
var assemblyInfoProvider = new AssemblyInformationProvider();
var ioManager = new DefaultIOManager();
builder.SetBasePath(ioManager.GetDirectoryName(assemblyInfoProvider.Path));
builder.AddJsonFile(RootJson);
builder.AddJsonFile(DevJson);
var configuration = builder.Build();
var dbConfig = configuration.GetSection(DatabaseConfiguration.Section).Get<DatabaseConfiguration>();
dbConfig.DesignTime = true;
var dbConfig = new DatabaseConfiguration
{
DesignTime = true,
DatabaseType = databaseType,
ConnectionString = connectionString
};
return Options.Create(dbConfig);
}
}
@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
@@ -17,7 +18,9 @@ namespace Tgstation.Server.Host.Database.Design
using var loggerFactory = new LoggerFactory();
return new MySqlDatabaseContext(
new DbContextOptions<MySqlDatabaseContext>(),
DesignTimeDbContextFactoryHelpers.GetDbContextOptions(),
DesignTimeDbContextFactoryHelpers.GetDbContextOptions(
DatabaseType.MariaDB,
"Server=127.0.0.1;User Id=root;Password=fake;Database=TGS_Design"),
new DatabaseSeeder(
new CryptographySuite(
new PasswordHasher<User>()),
@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
@@ -17,7 +18,9 @@ namespace Tgstation.Server.Host.Database.Design
using var loggerFactory = new LoggerFactory();
return new SqlServerDatabaseContext(
new DbContextOptions<SqlServerDatabaseContext>(),
DesignTimeDbContextFactoryHelpers.GetDbContextOptions(),
DesignTimeDbContextFactoryHelpers.GetDbContextOptions(
DatabaseType.SqlServer,
"Data Source=fake;Initial Catalog=TGS_Design;Integrated Security=True;Application Name=tgstation-server"),
new DatabaseSeeder(
new CryptographySuite(
new PasswordHasher<User>()),
@@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
using Tgstation.Server.Host.System;
@@ -17,7 +18,9 @@ namespace Tgstation.Server.Host.Database.Design
using var loggerFactory = new LoggerFactory();
return new SqliteDatabaseContext(
new DbContextOptions<SqliteDatabaseContext>(),
DesignTimeDbContextFactoryHelpers.GetDbContextOptions(),
DesignTimeDbContextFactoryHelpers.GetDbContextOptions(
DatabaseType.Sqlite,
"Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate"),
new DatabaseSeeder(
new CryptographySuite(
new PasswordHasher<User>()),
@@ -23,5 +23,12 @@
/// <param name="id">The <see cref="IProcess.Id"/></param>
/// <returns>The <see cref="IProcess"/> represented by <paramref name="id"/> on success, <see langword="null"/> on failure</returns>
IProcess GetProcess(int id);
/// <summary>
/// Check if a <see cref="IProcess"/> with a given <paramref name="name"/> is running.
/// </summary>
/// <param name="name">The name of the process without the extension.</param>
/// <returns><see langword="true"/> if the process is running, <see langword="false"/> otherwise.</returns>
bool IsProcessWithNameRunning(string name);
}
}
+1 -1
View File
@@ -117,7 +117,7 @@ namespace Tgstation.Server.Host.System
{
if (outputStringBuilder == null)
throw new InvalidOperationException("Output reading was not enabled!");
return errorStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
return outputStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
}
/// <inheritdoc />
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -102,9 +103,30 @@ namespace Tgstation.Server.Host.System
}
/// <inheritdoc />
public IProcess LaunchProcess(string fileName, string workingDirectory, string arguments, bool readOutput, bool readError, bool noShellExecute)
public IProcess LaunchProcess(
string fileName,
string workingDirectory,
string arguments,
bool readOutput,
bool readError,
bool noShellExecute)
{
logger.LogDebug("Launching process in {0}: {1} {2}", workingDirectory, fileName, arguments);
if (fileName == null)
throw new ArgumentNullException(nameof(fileName));
if (workingDirectory == null)
throw new ArgumentNullException(nameof(workingDirectory));
if (arguments == null)
throw new ArgumentNullException(nameof(arguments));
if (!noShellExecute && (readOutput || readError))
throw new InvalidOperationException("Requesting output/error reading requires noShellExecute to be true!");
logger.LogDebug(
"{0}aunching process in {1}: {2} {3}",
noShellExecute ? "L" : "Shell l",
workingDirectory,
fileName,
arguments);
var handle = new global::System.Diagnostics.Process();
try
{
@@ -112,9 +134,12 @@ namespace Tgstation.Server.Host.System
handle.StartInfo.Arguments = arguments;
handle.StartInfo.WorkingDirectory = workingDirectory;
handle.StartInfo.UseShellExecute = !(noShellExecute || readOutput || readError);
handle.StartInfo.UseShellExecute = !noShellExecute;
StringBuilder outputStringBuilder = null, errorStringBuilder = null, combinedStringBuilder = null;
TaskCompletionSource<object> outputReadTcs = null;
TaskCompletionSource<object> errorReadTcs = null;
if (readOutput || readError)
{
combinedStringBuilder = new StringBuilder();
@@ -122,8 +147,15 @@ namespace Tgstation.Server.Host.System
{
outputStringBuilder = new StringBuilder();
handle.StartInfo.RedirectStandardOutput = true;
outputReadTcs = new TaskCompletionSource<object>();
handle.OutputDataReceived += (sender, e) =>
{
if (e.Data == null)
{
outputReadTcs.SetResult(null);
return;
}
combinedStringBuilder.Append(Environment.NewLine);
combinedStringBuilder.Append(e.Data);
outputStringBuilder.Append(Environment.NewLine);
@@ -135,8 +167,15 @@ namespace Tgstation.Server.Host.System
{
errorStringBuilder = new StringBuilder();
handle.StartInfo.RedirectStandardError = true;
errorReadTcs = new TaskCompletionSource<object>();
handle.ErrorDataReceived += (sender, e) =>
{
if (e.Data == null)
{
errorReadTcs.SetResult(null);
return;
}
combinedStringBuilder.Append(Environment.NewLine);
combinedStringBuilder.Append(e.Data);
errorStringBuilder.Append(Environment.NewLine);
@@ -148,16 +187,30 @@ namespace Tgstation.Server.Host.System
var lifetimeTask = AttachExitHandler(handle);
handle.Start();
static async Task<int> AddToLifetimeTask(Task<int> originalTask, TaskCompletionSource<object> tcs)
{
var exitCode = await originalTask.ConfigureAwait(false);
await tcs.Task.ConfigureAwait(false);
return exitCode;
}
try
{
if (readOutput)
{
handle.BeginOutputReadLine();
lifetimeTask = AddToLifetimeTask(lifetimeTask, outputReadTcs);
}
}
catch (InvalidOperationException) { }
try
{
if (readError)
{
handle.BeginErrorReadLine();
lifetimeTask = AddToLifetimeTask(lifetimeTask, errorReadTcs);
}
}
catch (InvalidOperationException) { }
@@ -176,5 +229,15 @@ namespace Tgstation.Server.Host.System
throw;
}
}
/// <inheritdoc />
public bool IsProcessWithNameRunning(string name)
{
var procs = global::System.Diagnostics.Process.GetProcessesByName(name);
foreach (var proc in procs)
proc.Dispose();
return procs.Any();
}
}
}
@@ -48,7 +48,7 @@
<ItemGroup>
<PackageReference Include="BetterWin32Errors" Version="0.2.0" />
<PackageReference Include="Byond.TopicSender" Version="4.0.0" />
<PackageReference Include="Byond.TopicSender" Version="5.0.0" />
<PackageReference Include="Cyberboss.AspNetCore.AsyncInitializer" Version="1.2.0" />
<PackageReference Include="Cyberboss.SmartIrc4net.Standard" Version="0.4.6" />
<PackageReference Include="Discord.Net.WebSocket" Version="2.2.0" />
@@ -112,19 +112,10 @@
<ClientApp Include="ClientApp\src\**\*;ClientApp\public\**\*;ClientApp\tsconfig.json;ClientApp\package.json;ClientApp\package-lock.json" />
<AdditionalFiles Include="../../build/stylecop.json" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.Development.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.Development.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+5
View File
@@ -26,6 +26,11 @@
world.log << "You really shouldn't be able to read this"
/world/Topic(T, Addr, Master, Keys)
world.log << "Topic: [T]"
. = HandleTopic(T)
world.log << "Response: [.]"
/world/proc/HandleTopic(T)
TGS_TOPIC
/world/Reboot(reason)
+5
View File
@@ -12,6 +12,11 @@
world.TgsInitializationComplete()
/world/Topic(T, Addr, Master, Keys)
world.log << "Topic: [T]"
. = HandleTopic(T)
world.log << "Response: [.]"
/world/proc/HandleTopic(T)
TGS_TOPIC
world.sleep_offline = FALSE
@@ -25,7 +25,7 @@ namespace Tgstation.Server.Tests.Instance
public async Task Run(CancellationToken cancellationToken)
{
await TestNoVersion(cancellationToken).ConfigureAwait(false);
await TestInstall511(cancellationToken).ConfigureAwait(false);
await TestInstallStable(cancellationToken).ConfigureAwait(false);
await TestInstallFakeVersion(cancellationToken).ConfigureAwait(false);
}
@@ -40,11 +40,11 @@ namespace Tgstation.Server.Tests.Instance
await WaitForJob(test.InstallJob, 60, true, cancellationToken).ConfigureAwait(false);
}
async Task TestInstall511(CancellationToken cancellationToken)
async Task TestInstallStable(CancellationToken cancellationToken)
{
var newModel = new Api.Models.Byond
{
Version = new Version(511, 1385)
Version = new Version(513, 1514)
};
var test = await byondClient.SetActiveVersion(newModel, cancellationToken).ConfigureAwait(false);
Assert.IsNotNull(test.InstallJob);
@@ -57,7 +57,7 @@ namespace Tgstation.Server.Tests.Instance
if (new PlatformIdentifier().IsWindows)
dreamMaker += ".exe";
var dreamMakerDir = Path.Combine(metadata.Path, "Byond", "511.1385", "byond", "bin");
var dreamMakerDir = Path.Combine(metadata.Path, "Byond", newModel.Version.ToString(), "byond", "bin");
Assert.IsTrue(Directory.Exists(dreamMakerDir), $"Directory {dreamMakerDir} does not exist!");
Assert.IsTrue(File.Exists(Path.Combine(dreamMakerDir, dreamMaker)), $"Missing DreamMaker executable! Dir contents: {String.Join(", ", Directory.GetFileSystemEntries(dreamMakerDir))}");
@@ -42,11 +42,15 @@ namespace Tgstation.Server.Tests.Instance
}, cancellationToken), ErrorCode.DreamDaemonDoubleSoft);
await RunBasicTest(cancellationToken);
await RunHeartbeatTest(cancellationToken);
// await RunLongRunningTestThenUpdate(cancellationToken);
// await RunLongRunningTestThenUpdateWithByondVersionSwitch(cancellationToken);
// Remove this deploy when the above tests are reenabled
await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, cancellationToken);
await RunHeartbeatTest(cancellationToken);
await StartAndLeaveRunning(cancellationToken);
}
@@ -89,21 +93,29 @@ namespace Tgstation.Server.Tests.Instance
await WaitForJob(startJob, 10, false, cancellationToken);
await instanceClient.DreamDaemon.Update(new DreamDaemon
{
SoftShutdown = true
}, cancellationToken);
// lock on to DD and pause it so it can't heartbeat
var ddProcs = System.Diagnostics.Process.GetProcessesByName("DreamDaemon").ToList();
if (ddProcs.Count != 1)
Assert.Inconclusive($"Incorrect number of DD processes: {ddProcs.Count}");
var pid = ddProcs.Single().Id;
using var ddProc = ddProcs.Single();
using var ourProcessHandler = new ProcessExecutor(
new PlatformIdentifier().IsWindows ? (IProcessSuspender)new WindowsProcessSuspender(Mock.Of<ILogger<WindowsProcessSuspender>>()) : new PosixProcessSuspender(Mock.Of<ILogger<PosixProcessSuspender>>()),
new PlatformIdentifier().IsWindows
? (IProcessSuspender)new WindowsProcessSuspender(Mock.Of<ILogger<WindowsProcessSuspender>>())
: new PosixProcessSuspender(Mock.Of<ILogger<PosixProcessSuspender>>()),
Mock.Of<ILogger<ProcessExecutor>>(),
LoggerFactory.Create(x => { })).GetProcess(pid);
LoggerFactory.Create(x => { }))
.GetProcess(ddProc.Id);
// Ensure it's responding to heartbeats
await Task.WhenAny(Task.Delay(20000), ourProcessHandler.Lifetime);
Assert.IsFalse(ddProc.HasExited);
await instanceClient.DreamDaemon.Update(new DreamDaemon
{
SoftShutdown = true
}, cancellationToken);
ourProcessHandler.Suspend();
await Task.WhenAny(ourProcessHandler.Lifetime, Task.Delay(TimeSpan.FromSeconds(20)));
@@ -121,6 +133,12 @@ namespace Tgstation.Server.Tests.Instance
Assert.Fail("DreamDaemon didn't shutdown within the timeout!");
}
while (timeout > 0);
// disable heartbeats
await instanceClient.DreamDaemon.Update(new DreamDaemon
{
HeartbeatSeconds = 0,
}, cancellationToken);
}
async Task RunLongRunningTestThenUpdate(CancellationToken cancellationToken)
+69 -17
View File
@@ -1,4 +1,3 @@
using Discord.WebSocket;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@@ -16,7 +15,8 @@ using Tgstation.Server.Api;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Client;
using Tgstation.Server.Host;
using Tgstation.Server.Host.Components.Chat.Providers;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.System;
using Tgstation.Server.Tests.Instance;
namespace Tgstation.Server.Tests
@@ -28,13 +28,9 @@ namespace Tgstation.Server.Tests
readonly IServerClientFactory clientFactory = new ServerClientFactory(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
[TestMethod]
public async Task TestServerUpdate()
public async Task TestUpdateProtocol()
{
using var server = new TestingServer();
if (server.DatabaseType == "Sqlite")
Assert.Inconclusive("Cannot run this test on SQLite yet!");
using var serverCts = new CancellationTokenSource();
var cancellationToken = serverCts.Token;
var serverTask = server.Run(cancellationToken);
@@ -101,14 +97,22 @@ namespace Tgstation.Server.Tests
static void TerminateAllDDs()
{
foreach (var proc in Process.GetProcessesByName("DreamDaemon"))
foreach (var proc in System.Diagnostics.Process.GetProcessesByName("DreamDaemon"))
using (proc)
proc.Kill();
}
[TestMethod]
public async Task TestFullStandardOperation()
public async Task TestServer()
{
var procs = System.Diagnostics.Process.GetProcessesByName("byond");
if(procs.Any())
{
foreach (var proc in procs)
proc.Dispose();
Assert.Inconclusive("Cannot run server test because DreamDaemon will not start headless while the BYOND pager is running!");
}
using var server = new TestingServer();
using var serverCts = new CancellationTokenSource();
var cancellationToken = serverCts.Token;
@@ -191,20 +195,35 @@ namespace Tgstation.Server.Tests
await Task.WhenAny(serverTask, Task.Delay(30000, cancellationToken));
Assert.IsTrue(serverTask.IsCompleted);
var preStartupTime = DateTimeOffset.Now;
serverTask = server.Run(cancellationToken);
using (var adminClient = await CreateAdminClient())
{
var instanceClient = adminClient.Instances.CreateClient(instance);
// reattach job
var jobs = await instanceClient.Jobs.ListActive(cancellationToken);
if (jobs.Any())
if (!jobs.Any())
{
Assert.AreEqual(1, jobs.Count);
var entities = await instanceClient.Jobs.List(cancellationToken);
var getTasks = entities
.Select(e => instanceClient.Jobs.GetId(e, cancellationToken))
.ToList();
await new JobsRequiredTest(instanceClient.Jobs).WaitForJob(jobs.Single(), 40, false, cancellationToken);
await Task.WhenAll(getTasks);
jobs = getTasks
.Select(x => x.Result)
.Where(x => x.StartedAt.Value > preStartupTime)
.ToList();
}
Assert.AreEqual(1, jobs.Count);
var reattachJob = jobs.Single();
Assert.IsTrue(reattachJob.StartedAt.Value >= preStartupTime);
await new JobsRequiredTest(instanceClient.Jobs).WaitForJob(reattachJob, 40, false, cancellationToken);
var dd = await instanceClient.DreamDaemon.Read(cancellationToken);
Assert.IsTrue(dd.Running.Value);
@@ -220,20 +239,34 @@ namespace Tgstation.Server.Tests
await Task.WhenAny(serverTask, Task.Delay(30000, cancellationToken));
Assert.IsTrue(serverTask.IsCompleted);
preStartupTime = DateTimeOffset.Now;
serverTask = server.Run(cancellationToken);
using (var adminClient = await CreateAdminClient())
{
var instanceClient = adminClient.Instances.CreateClient(instance);
// launch job
var jobs = await instanceClient.Jobs.ListActive(cancellationToken);
if (jobs.Any())
if (!jobs.Any())
{
Assert.AreEqual(1, jobs.Count);
var entities = await instanceClient.Jobs.List(cancellationToken);
var getTasks = entities
.Select(e => instanceClient.Jobs.GetId(e, cancellationToken))
.ToList();
await new JobsRequiredTest(instanceClient.Jobs).WaitForJob(jobs.Single(), 40, false, cancellationToken);
await Task.WhenAll(getTasks);
jobs = getTasks
.Select(x => x.Result)
.Where(x => x.StartedAt.Value > preStartupTime)
.ToList();
}
Assert.AreEqual(1, jobs.Count);
var launchJob = jobs.Single();
Assert.IsTrue(launchJob.StartedAt.Value >= preStartupTime);
await new JobsRequiredTest(instanceClient.Jobs).WaitForJob(launchJob, 40, false, cancellationToken);
var dd = await instanceClient.DreamDaemon.Read(cancellationToken);
Assert.IsTrue(dd.Running.Value);
@@ -262,5 +295,24 @@ namespace Tgstation.Server.Tests
TerminateAllDDs();
}
}
[TestMethod]
public async Task TestScriptExecution()
{
var platformIdentifier = new PlatformIdentifier();
var processExecutor = new ProcessExecutor(
Mock.Of<IProcessSuspender>(),
Mock.Of<ILogger<ProcessExecutor>>(),
LoggerFactory.Create(x => { }));
using var process = processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", String.Empty, true, true, true);
using var cts = new CancellationTokenSource();
cts.CancelAfter(3000);
var exitCode = await process.Lifetime.WithToken(cts.Token);
Assert.AreEqual(0, exitCode);
Assert.AreEqual(String.Empty, process.GetErrorOutput().Trim());
Assert.AreEqual("Hello World!", process.GetStandardOutput().Trim());
}
}
}
@@ -26,4 +26,13 @@
</Reference>
</ItemGroup>
<ItemGroup>
<None Update="test.bat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="test.sh">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+3
View File
@@ -0,0 +1,3 @@
@echo off
echo Hello World!
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
echo Hello World!
+79 -58
View File
@@ -1,7 +1,6 @@
using Octokit;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
@@ -33,7 +32,11 @@ namespace ReleaseNotes
return 2;
}
var doNotCloseMilestone = args.Length >= 2 && args[1].ToUpperInvariant() == "--NO-CLOSE";
var doNotCloseMilestone = args.Length > 1 && args[1].ToUpperInvariant() == "--NO-CLOSE";
string propsPath = "../../../../../build/Version.props";
if (args.Length > 1 && !doNotCloseMilestone)
propsPath = args[1];
const string ReleaseNotesEnvVar = "TGS4_RELEASE_NOTES_TOKEN";
var githubToken = Environment.GetEnvironmentVariable(ReleaseNotesEnvVar);
@@ -73,7 +76,7 @@ namespace ReleaseNotes
Task<Milestone> milestoneTask = null;
var milestoneTaskLock = new object();
var releaseDictionary = new Dictionary<int, List<string>>();
var releaseDictionary = new Dictionary<string, List<Tuple<string, int>>>(StringComparer.OrdinalIgnoreCase);
var authorizedUsers = new Dictionary<long, Task<bool>>();
bool postControlPanelMessage = false;
@@ -94,74 +97,84 @@ namespace ReleaseNotes
if (milestoneTask == null)
milestoneTask = GetMilestone();
if (!fullPR.Merged)
return;
// if (!fullPR.Merged)
//return;
async Task BuildNotesFromComment(string comment, User user)
{
async Task CommitNotes(string component, List<string> notes)
{
Task<bool> authTask;
TaskCompletionSource<bool> ourTcs = null;
lock (authorizedUsers)
{
if (!authorizedUsers.TryGetValue(user.Id, out authTask))
{
ourTcs = new TaskCompletionSource<bool>();
authTask = ourTcs.Task;
authorizedUsers.Add(user.Id, authTask);
}
}
if (ourTcs != null)
try
{
//check if the user has access
var perm = String.IsNullOrWhiteSpace(githubToken)
? PermissionLevel.Write
: (await client.Repository.Collaborator.ReviewPermission(RepoOwner, RepoName, user.Login).ConfigureAwait(false)).Permission;
ourTcs.SetResult(perm == PermissionLevel.Write || perm == PermissionLevel.Admin);
}
catch
{
ourTcs.SetResult(false);
throw;
}
var authorized = await authTask.ConfigureAwait(false);
if (!authorized)
return;
lock (releaseDictionary)
{
foreach (var I in notes)
Console.WriteLine(component + " #" + fullPR.Number + " - " + I + " (@" + user.Login + ")");
var tupleSelector = notes.Select(note => Tuple.Create(note, fullPR.Number));
if (releaseDictionary.TryGetValue(component, out var currentValues))
currentValues.AddRange(tupleSelector);
else
releaseDictionary.Add(component, tupleSelector.ToList());
}
}
var commentSplits = comment.Split('\n');
var notesOpen = false;
string targetComponent = null;
var notes = new List<string>();
foreach (var line in commentSplits)
{
var trimmedLine = line.Trim();
if (!notesOpen)
if (targetComponent == null)
{
notesOpen = trimmedLine.StartsWith(":cl:", StringComparison.Ordinal);
if (trimmedLine.StartsWith(":cl:", StringComparison.Ordinal))
{
targetComponent = trimmedLine.Substring(4).Trim();
if (targetComponent.Length == 0)
targetComponent = "Core";
}
continue;
}
if (trimmedLine.StartsWith("/:cl:", StringComparison.Ordinal))
{
notesOpen = false;
await CommitNotes(targetComponent, notes);
targetComponent = null;
notes.Clear();
continue;
}
if (trimmedLine.Length == 0)
continue;
notes.Add(trimmedLine);
}
if (notesOpen || notes.Count == 0)
return;
Task<bool> authTask;
TaskCompletionSource<bool> ourTcs = null;
lock (authorizedUsers)
{
if (!authorizedUsers.TryGetValue(user.Id, out authTask))
{
ourTcs = new TaskCompletionSource<bool>();
authTask = ourTcs.Task;
authorizedUsers.Add(user.Id, authTask);
}
}
if (ourTcs != null)
try
{
//check if the user has access
var perm = String.IsNullOrWhiteSpace(githubToken)
? PermissionLevel.Write
: (await client.Repository.Collaborator.ReviewPermission(RepoOwner, RepoName, user.Login).ConfigureAwait(false)).Permission;
ourTcs.SetResult(perm == PermissionLevel.Write || perm == PermissionLevel.Admin);
}
catch
{
ourTcs.SetResult(false);
throw;
}
var authorized = await authTask.ConfigureAwait(false);
if (!authorized)
return;
lock (releaseDictionary)
{
foreach (var I in notes)
Console.WriteLine("#" + fullPR.Number + " - " + I + " (@" + user.Login + ")");
if (releaseDictionary.TryGetValue(fullPR.Number, out var currentValues))
currentValues.AddRange(notes);
else
releaseDictionary.Add(fullPR.Number, notes);
}
}
var comments = await client.Issue.Comment.GetAllForIssue(RepoOwner, RepoName, fullPR.Number).ConfigureAwait(false);
@@ -205,7 +218,7 @@ namespace ReleaseNotes
//trim away all the lines that don't start with #
string keepThisRelease;
if (version.Build == 0)
if (version.Build <= 1)
keepThisRelease = "# ";
else
keepThisRelease = "## ";
@@ -223,7 +236,7 @@ namespace ReleaseNotes
switch (releasingSuite)
{
case 4:
var doc = XDocument.Load("../../../../../build/Version.props");
var doc = XDocument.Load(propsPath);
var project = doc.Root;
var xmlNamespace = project.GetDefaultNamespace();
var versionsPropertyGroup = project.Elements().First();
@@ -294,17 +307,25 @@ namespace ReleaseNotes
}
foreach (var I in releaseDictionary.OrderBy(kvp => kvp.Key))
foreach (var note in I.Value)
{
newNotes.Append(Environment.NewLine);
newNotes.Append("#### ");
newNotes.Append(I.Key);
foreach (var noteTuple in I.Value)
{
newNotes.Append(Environment.NewLine);
newNotes.Append("- ");
newNotes.Append(note);
newNotes.Append(noteTuple.Item1);
newNotes.Append(" (#");
newNotes.Append(I.Key);
newNotes.Append(noteTuple.Item2);
newNotes.Append(')');
}
newNotes.Append(Environment.NewLine);
newNotes.Append(Environment.NewLine);
}
newNotes.Append(Environment.NewLine);
if (version != new Version(4, 1, 0))