mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-25 14:06:41 +01:00
Fix issue with configuration API contention
This commit is contained in:
@@ -288,9 +288,8 @@ namespace Tgstation.Server.Api.Models
|
||||
/// <summary>
|
||||
/// Currently unused.
|
||||
/// </summary>
|
||||
[Obsolete("Unused", true)]
|
||||
[Description("Unknown error code.")]
|
||||
UnusedErrorCode3,
|
||||
[Description("IO operation could not start contended access to the instance's configuration directory!")]
|
||||
ConfigurationContendedAccess,
|
||||
|
||||
/// <summary>
|
||||
/// Attempted to add a chat bot when at or above the <see cref="Instance.ChatBotLimit"/> or it was set to something lower than the existing amount of chat bots.
|
||||
|
||||
@@ -237,35 +237,34 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
|
||||
void ListImpl()
|
||||
{
|
||||
try
|
||||
var enumerator = synchronousIOManager.GetDirectories(path, cancellationToken);
|
||||
result.AddRange(enumerator.Select(x => new ConfigurationFileResponse
|
||||
{
|
||||
var enumerator = synchronousIOManager.GetDirectories(path, cancellationToken);
|
||||
result.AddRange(enumerator.Select(x => new ConfigurationFileResponse
|
||||
{
|
||||
IsDirectory = true,
|
||||
Path = ioManager.ConcatPath(configurationRelativePath, x),
|
||||
}).OrderBy(file => file.Path));
|
||||
IsDirectory = true,
|
||||
Path = ioManager.ConcatPath(configurationRelativePath, x),
|
||||
}).OrderBy(file => file.Path));
|
||||
|
||||
enumerator = synchronousIOManager.GetFiles(path, cancellationToken);
|
||||
result.AddRange(enumerator.Select(x => new ConfigurationFileResponse
|
||||
{
|
||||
IsDirectory = false,
|
||||
Path = ioManager.ConcatPath(configurationRelativePath, x),
|
||||
}).OrderBy(file => file.Path));
|
||||
}
|
||||
catch (IOException ex)
|
||||
enumerator = synchronousIOManager.GetFiles(path, cancellationToken);
|
||||
result.AddRange(enumerator.Select(x => new ConfigurationFileResponse
|
||||
{
|
||||
logger.LogDebug(ex, "IOException while enumerating direcotry!");
|
||||
result = null;
|
||||
return;
|
||||
}
|
||||
IsDirectory = false,
|
||||
Path = ioManager.ConcatPath(configurationRelativePath, x),
|
||||
}).OrderBy(file => file.Path));
|
||||
}
|
||||
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
|
||||
using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
|
||||
{
|
||||
if (!locked)
|
||||
{
|
||||
logger.LogDebug("Contention when attempting to enumerate directory!");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (systemIdentity == null)
|
||||
ListImpl();
|
||||
else
|
||||
await systemIdentity.RunImpersonated(ListImpl, cancellationToken);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -280,90 +279,102 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
|
||||
void ReadImpl()
|
||||
{
|
||||
lock (semaphore)
|
||||
try
|
||||
{
|
||||
string GetFileSha()
|
||||
{
|
||||
var content = synchronousIOManager.ReadFile(path);
|
||||
using var sha1 = SHA1.Create();
|
||||
return String.Join(String.Empty, sha1.ComputeHash(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
var originalSha = GetFileSha();
|
||||
|
||||
var disposeToken = disposeCts.Token;
|
||||
var fileTicket = fileTransferService.CreateDownload(
|
||||
new FileDownloadProvider(
|
||||
() =>
|
||||
{
|
||||
if (disposeToken.IsCancellationRequested)
|
||||
return ErrorCode.InstanceOffline;
|
||||
|
||||
var newSha = GetFileSha();
|
||||
if (newSha != originalSha)
|
||||
return ErrorCode.ConfigurationFileUpdated;
|
||||
|
||||
return null;
|
||||
},
|
||||
async cancellationToken =>
|
||||
{
|
||||
FileStream result = null;
|
||||
void GetFileStream()
|
||||
{
|
||||
result = ioManager.GetFileStream(path, false);
|
||||
}
|
||||
|
||||
using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
|
||||
{
|
||||
if (!locked)
|
||||
return null;
|
||||
|
||||
if (systemIdentity == null)
|
||||
await Task.Factory.StartNew(GetFileStream, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
else
|
||||
await systemIdentity.RunImpersonated(GetFileStream, cancellationToken);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
path,
|
||||
false));
|
||||
|
||||
result = new ConfigurationFileResponse
|
||||
{
|
||||
FileTicket = fileTicket.FileTicket,
|
||||
IsDirectory = false,
|
||||
LastReadHash = originalSha,
|
||||
AccessDenied = false,
|
||||
Path = configurationRelativePath,
|
||||
};
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// this happens on windows, dunno about linux
|
||||
bool isDirectory;
|
||||
try
|
||||
{
|
||||
string GetFileSha()
|
||||
{
|
||||
var content = synchronousIOManager.ReadFile(path);
|
||||
using var sha1 = SHA1.Create();
|
||||
return String.Join(String.Empty, sha1.ComputeHash(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
|
||||
}
|
||||
|
||||
var originalSha = GetFileSha();
|
||||
|
||||
var disposeToken = disposeCts.Token;
|
||||
var fileTicket = fileTransferService.CreateDownload(
|
||||
new FileDownloadProvider(
|
||||
() =>
|
||||
{
|
||||
if (disposeToken.IsCancellationRequested)
|
||||
return ErrorCode.InstanceOffline;
|
||||
|
||||
var newSha = GetFileSha();
|
||||
if (newSha != originalSha)
|
||||
return ErrorCode.ConfigurationFileUpdated;
|
||||
|
||||
return null;
|
||||
},
|
||||
async cancellationToken =>
|
||||
{
|
||||
FileStream result = null;
|
||||
void GetFileStream()
|
||||
{
|
||||
result = ioManager.GetFileStream(path, false);
|
||||
}
|
||||
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
|
||||
if (systemIdentity == null)
|
||||
await Task.Factory.StartNew(GetFileStream, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
else
|
||||
await systemIdentity.RunImpersonated(GetFileStream, cancellationToken);
|
||||
|
||||
return result;
|
||||
},
|
||||
path,
|
||||
false));
|
||||
|
||||
result = new ConfigurationFileResponse
|
||||
{
|
||||
FileTicket = fileTicket.FileTicket,
|
||||
IsDirectory = false,
|
||||
LastReadHash = originalSha,
|
||||
AccessDenied = false,
|
||||
Path = configurationRelativePath,
|
||||
};
|
||||
isDirectory = synchronousIOManager.IsDirectory(path);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// this happens on windows, dunno about linux
|
||||
bool isDirectory;
|
||||
try
|
||||
{
|
||||
isDirectory = synchronousIOManager.IsDirectory(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogDebug(ex, "IsDirectory exception!");
|
||||
isDirectory = false;
|
||||
}
|
||||
|
||||
result = new ConfigurationFileResponse
|
||||
{
|
||||
Path = configurationRelativePath,
|
||||
};
|
||||
if (!isDirectory)
|
||||
result.AccessDenied = true;
|
||||
|
||||
result.IsDirectory = isDirectory;
|
||||
logger.LogDebug(ex, "IsDirectory exception!");
|
||||
isDirectory = false;
|
||||
}
|
||||
|
||||
result = new ConfigurationFileResponse
|
||||
{
|
||||
Path = configurationRelativePath,
|
||||
};
|
||||
if (!isDirectory)
|
||||
result.AccessDenied = true;
|
||||
|
||||
result.IsDirectory = isDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
|
||||
using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
|
||||
{
|
||||
if (!locked)
|
||||
{
|
||||
logger.LogDebug("Contention when attempting to read file!");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (systemIdentity == null)
|
||||
await Task.Factory.StartNew(ReadImpl, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
else
|
||||
await systemIdentity.RunImpersonated(ReadImpl, cancellationToken);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -448,93 +459,108 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
|
||||
void WriteImpl()
|
||||
{
|
||||
lock (semaphore)
|
||||
try
|
||||
try
|
||||
{
|
||||
var fileTicket = fileTransferService.CreateUpload(FileUploadStreamKind.ForSynchronousIO);
|
||||
var uploadCancellationToken = disposeCts.Token;
|
||||
async Task UploadHandler()
|
||||
{
|
||||
var fileTicket = fileTransferService.CreateUpload(FileUploadStreamKind.ForSynchronousIO);
|
||||
var uploadCancellationToken = disposeCts.Token;
|
||||
async Task UploadHandler()
|
||||
await using (fileTicket)
|
||||
{
|
||||
await using (fileTicket)
|
||||
var fileHash = previousHash;
|
||||
var uploadStream = await fileTicket.GetResult(uploadCancellationToken);
|
||||
if (uploadStream == null)
|
||||
return; // expired
|
||||
|
||||
bool success = false;
|
||||
void WriteCallback()
|
||||
{
|
||||
var fileHash = previousHash;
|
||||
var uploadStream = await fileTicket.GetResult(uploadCancellationToken);
|
||||
if (uploadStream == null)
|
||||
return; // expired
|
||||
success = synchronousIOManager.WriteFileChecked(path, uploadStream, ref fileHash, cancellationToken);
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
void WriteCallback()
|
||||
{
|
||||
success = synchronousIOManager.WriteFileChecked(path, uploadStream, ref fileHash, cancellationToken);
|
||||
}
|
||||
if (fileTicket == null)
|
||||
{
|
||||
logger.LogDebug("File upload ticket for {path} expired!", path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileTicket == null)
|
||||
using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
|
||||
{
|
||||
if (!locked)
|
||||
{
|
||||
logger.LogDebug("File upload ticket for {path} expired!", path);
|
||||
fileTicket.SetError(ErrorCode.ConfigurationContendedAccess, null);
|
||||
return;
|
||||
}
|
||||
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
|
||||
if (systemIdentity == null)
|
||||
await Task.Factory.StartNew(WriteCallback, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
else
|
||||
await systemIdentity.RunImpersonated(WriteCallback, cancellationToken);
|
||||
|
||||
if (!success)
|
||||
fileTicket.SetError(ErrorCode.ConfigurationFileUpdated, fileHash);
|
||||
else if (uploadStream.Length > 0)
|
||||
postWriteHandler.HandleWrite(path);
|
||||
if (systemIdentity == null)
|
||||
await Task.Factory.StartNew(WriteCallback, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
else
|
||||
await systemIdentity.RunImpersonated(WriteCallback, cancellationToken);
|
||||
}
|
||||
|
||||
if (!success)
|
||||
fileTicket.SetError(ErrorCode.ConfigurationFileUpdated, fileHash);
|
||||
else if (uploadStream.Length > 0)
|
||||
postWriteHandler.HandleWrite(path);
|
||||
}
|
||||
|
||||
result = new ConfigurationFileResponse
|
||||
{
|
||||
FileTicket = fileTicket.Ticket.FileTicket,
|
||||
LastReadHash = previousHash,
|
||||
IsDirectory = false,
|
||||
AccessDenied = false,
|
||||
Path = configurationRelativePath,
|
||||
};
|
||||
|
||||
lock (disposeCts)
|
||||
uploadTasks = Task.WhenAll(uploadTasks, UploadHandler());
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
|
||||
result = new ConfigurationFileResponse
|
||||
{
|
||||
// this happens on windows, dunno about linux
|
||||
bool isDirectory;
|
||||
try
|
||||
{
|
||||
isDirectory = synchronousIOManager.IsDirectory(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogDebug(ex, "IsDirectory exception!");
|
||||
isDirectory = false;
|
||||
}
|
||||
FileTicket = fileTicket.Ticket.FileTicket,
|
||||
LastReadHash = previousHash,
|
||||
IsDirectory = false,
|
||||
AccessDenied = false,
|
||||
Path = configurationRelativePath,
|
||||
};
|
||||
|
||||
result = new ConfigurationFileResponse
|
||||
{
|
||||
Path = configurationRelativePath,
|
||||
};
|
||||
if (!isDirectory)
|
||||
result.AccessDenied = true;
|
||||
|
||||
result.IsDirectory = isDirectory;
|
||||
lock (disposeCts)
|
||||
uploadTasks = Task.WhenAll(uploadTasks, UploadHandler());
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// this happens on windows, dunno about linux
|
||||
bool isDirectory;
|
||||
try
|
||||
{
|
||||
isDirectory = synchronousIOManager.IsDirectory(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogDebug(ex, "IsDirectory exception!");
|
||||
isDirectory = false;
|
||||
}
|
||||
|
||||
result = new ConfigurationFileResponse
|
||||
{
|
||||
Path = configurationRelativePath,
|
||||
};
|
||||
if (!isDirectory)
|
||||
result.AccessDenied = true;
|
||||
|
||||
result.IsDirectory = isDirectory;
|
||||
}
|
||||
}
|
||||
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
|
||||
using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
|
||||
{
|
||||
if (!locked)
|
||||
{
|
||||
logger.LogDebug("Contention when attempting to write file!");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (systemIdentity == null)
|
||||
await Task.Factory.StartNew(WriteImpl, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
else
|
||||
await systemIdentity.RunImpersonated(WriteImpl, cancellationToken);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
|
||||
public async Task<bool?> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureDirectories(cancellationToken);
|
||||
var path = ValidateConfigRelativePath(configurationRelativePath);
|
||||
@@ -542,11 +568,19 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
bool? result = null;
|
||||
void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
|
||||
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
|
||||
using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
|
||||
{
|
||||
if (!locked)
|
||||
{
|
||||
logger.LogDebug("Contention when attempting to create directory!");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (systemIdentity == null)
|
||||
await Task.Factory.StartNew(DoCreate, cancellationToken, DefaultIOManager.BlockingTaskCreationOptions, TaskScheduler.Current);
|
||||
else
|
||||
await systemIdentity.RunImpersonated(DoCreate, cancellationToken);
|
||||
}
|
||||
|
||||
return result.Value;
|
||||
}
|
||||
@@ -618,14 +652,20 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
|
||||
public async Task<bool?> DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
|
||||
{
|
||||
await EnsureDirectories(cancellationToken);
|
||||
var path = ValidateConfigRelativePath(configurationRelativePath);
|
||||
|
||||
var result = false;
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken))
|
||||
using (SemaphoreSlimContext.TryLock(semaphore, out var locked))
|
||||
{
|
||||
if (!locked)
|
||||
{
|
||||
logger.LogDebug("Contention when attempting to enumerate directory!");
|
||||
return null;
|
||||
}
|
||||
|
||||
void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
|
||||
|
||||
if (systemIdentity != null)
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
/// <param name="configurationRelativePath">The relative path in the Configuration directory.</param>
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> for the operation. If <see langword="null"/>, the operation will be performed as the user of the <see cref="Core.Application"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ConfigurationFileResponse"/>s for the items in the directory. <see cref="FileTicketResponse.FileTicket"/> and <see cref="IConfigurationFile.LastReadHash"/> will both be <see langword="null"/>.</returns>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ConfigurationFileResponse"/>s for the items in the directory. <see cref="FileTicketResponse.FileTicket"/> and <see cref="IConfigurationFile.LastReadHash"/> will both be <see langword="null"/>. <see langword="null"/> will be returned if the operation failed due to access contention.</returns>
|
||||
Task<IReadOnlyList<ConfigurationFileResponse>> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
@@ -47,7 +47,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
/// <param name="configurationRelativePath">The relative path in the Configuration directory.</param>
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> for the operation. If <see langword="null"/>, the operation will be performed as the user of the <see cref="Core.Application"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ConfigurationFileResponse"/> of the file.</returns>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ConfigurationFileResponse"/> of the file. <see langword="null"/> will be returned if the operation failed due to access contention.</returns>
|
||||
Task<ConfigurationFileResponse> Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
@@ -56,8 +56,8 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
/// <param name="configurationRelativePath">The relative path in the Configuration directory.</param>
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> for the operation. If <see langword="null"/>, the operation will be performed as the user of the <see cref="Core.Application"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation. Usage may result in partial writes.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the directory already existed, <see langword="false"/> otherwise.</returns>
|
||||
Task<bool> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the directory already existed, <see langword="false"/> otherwise. <see langword="null"/> will be returned if the operation failed due to access contention.</returns>
|
||||
Task<bool?> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to delete an empty directory at <paramref name="configurationRelativePath"/>.
|
||||
@@ -65,8 +65,8 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
/// <param name="configurationRelativePath">The path of the empty directory to delete.</param>
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> for the operation. If <see langword="null"/>, the operation will be performed as the user of the <see cref="Core.Application"/>.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns><see langword="true"/> if the directory was empty and deleted, <see langword="false"/> otherwise.</returns>
|
||||
Task<bool> DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
/// <returns><see langword="true"/> if the directory was empty and deleted, <see langword="false"/> otherwise. <see langword="null"/> will be returned if the operation failed due to access contention.</returns>
|
||||
Task<bool?> DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Writes to a given <paramref name="configurationRelativePath"/>.
|
||||
@@ -75,7 +75,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> for the operation. If <see langword="null"/>, the operation will be performed as the user of the <see cref="Core.Application"/>.</param>
|
||||
/// <param name="previousHash">The hash any existing file must match in order for the write to succeed.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation. Usage may result in partial writes.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the updated <see cref="ConfigurationFileResponse"/> or <see langword="null"/> if the write failed due to <see cref="IConfigurationFile.LastReadHash"/> conflicts.</returns>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the updated <see cref="ConfigurationFileResponse"/> and associated writing <see cref="FileTicketResponse"/>. <see langword="null"/> will be returned if the operation failed due to access contention.</returns>
|
||||
Task<ConfigurationFileResponse> Write(string configurationRelativePath, ISystemIdentity systemIdentity, string previousHash, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Api.Rights;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Database;
|
||||
using Tgstation.Server.Host.Extensions;
|
||||
using Tgstation.Server.Host.IO;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
@@ -86,6 +85,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
model.LastReadHash,
|
||||
cancellationToken);
|
||||
|
||||
if (newFile == null)
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.ConfigurationContendedAccess));
|
||||
|
||||
return model.LastReadHash == null ? Accepted(newFile) : Json(newFile);
|
||||
});
|
||||
}
|
||||
@@ -114,6 +116,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[HttpGet(Routes.File + "/{*filePath}")]
|
||||
[TgsAuthorize(ConfigurationRights.Read)]
|
||||
[ProducesResponseType(typeof(ConfigurationFileResponse), 200)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 409)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
|
||||
public async Task<IActionResult> File(string filePath, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -128,8 +131,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
var result = await instance
|
||||
.Configuration
|
||||
.Read(filePath, systemIdentity, cancellationToken);
|
||||
|
||||
if (result == null)
|
||||
return this.Gone();
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.ConfigurationContendedAccess));
|
||||
|
||||
return Json(result);
|
||||
});
|
||||
@@ -161,6 +165,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[HttpGet(Routes.List + "/{*directoryPath}")]
|
||||
[TgsAuthorize(ConfigurationRights.List)]
|
||||
[ProducesResponseType(typeof(PaginatedResponse<ConfigurationFileResponse>), 200)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 409)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
|
||||
public Task<IActionResult> Directory(
|
||||
string directoryPath,
|
||||
@@ -180,8 +185,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
var result = await instance
|
||||
.Configuration
|
||||
.ListDirectory(directoryPath, systemIdentity, cancellationToken);
|
||||
|
||||
if (result == null)
|
||||
return new PaginatableResult<ConfigurationFileResponse>(this.Gone());
|
||||
return new PaginatableResult<ConfigurationFileResponse>(
|
||||
Conflict(new ErrorMessageResponse(ErrorCode.ConfigurationContendedAccess)));
|
||||
|
||||
return new PaginatableResult<ConfigurationFileResponse>(
|
||||
result
|
||||
@@ -198,6 +205,15 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return new PaginatableResult<ConfigurationFileResponse>(
|
||||
Forbid());
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.LogInformation(ex, "IOException while enumerating directory!");
|
||||
return new PaginatableResult<ConfigurationFileResponse>(
|
||||
Conflict(new ErrorMessageResponse(ErrorCode.IOError)
|
||||
{
|
||||
AdditionalData = ex.Message,
|
||||
}));
|
||||
}
|
||||
},
|
||||
null,
|
||||
page,
|
||||
@@ -231,6 +247,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[TgsAuthorize(ConfigurationRights.Write)]
|
||||
[ProducesResponseType(typeof(ConfigurationFileResponse), 200)]
|
||||
[ProducesResponseType(typeof(ConfigurationFileResponse), 201)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 409)]
|
||||
public async Task<IActionResult> CreateDirectory([FromBody] ConfigurationFileRequest model, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(model);
|
||||
@@ -247,11 +264,19 @@ namespace Tgstation.Server.Host.Controllers
|
||||
};
|
||||
|
||||
return await WithComponentInstance(
|
||||
async instance => await instance
|
||||
.Configuration
|
||||
.CreateDirectory(model.Path, systemIdentity, cancellationToken)
|
||||
async instance =>
|
||||
{
|
||||
var result = await instance
|
||||
.Configuration
|
||||
.CreateDirectory(model.Path, systemIdentity, cancellationToken);
|
||||
|
||||
if (!result.HasValue)
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.ConfigurationContendedAccess));
|
||||
|
||||
return result.Value
|
||||
? Json(resultModel)
|
||||
: Created(resultModel));
|
||||
: Created(resultModel);
|
||||
});
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
@@ -281,6 +306,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[HttpDelete]
|
||||
[TgsAuthorize(ConfigurationRights.Delete)]
|
||||
[ProducesResponseType(204)]
|
||||
[ProducesResponseType(typeof(ErrorMessageResponse), 409)]
|
||||
public async Task<IActionResult> DeleteDirectory([FromBody] ConfigurationFileRequest directory, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(directory);
|
||||
@@ -294,11 +320,19 @@ namespace Tgstation.Server.Host.Controllers
|
||||
try
|
||||
{
|
||||
return await WithComponentInstance(
|
||||
async instance => await instance
|
||||
.Configuration
|
||||
.DeleteDirectory(directory.Path, systemIdentity, cancellationToken)
|
||||
? NoContent()
|
||||
: Conflict(new ErrorMessageResponse(ErrorCode.ConfigurationDirectoryNotEmpty)));
|
||||
async instance =>
|
||||
{
|
||||
var result = await instance
|
||||
.Configuration
|
||||
.DeleteDirectory(directory.Path, systemIdentity, cancellationToken);
|
||||
|
||||
if (!result.HasValue)
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.ConfigurationContendedAccess));
|
||||
|
||||
return result.Value
|
||||
? NoContent()
|
||||
: Conflict(new ErrorMessageResponse(ErrorCode.ConfigurationDirectoryNotEmpty));
|
||||
});
|
||||
}
|
||||
catch (NotImplementedException ex)
|
||||
{
|
||||
@@ -308,6 +342,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
Logger.LogInformation(ex, "IOException while deleting directory!");
|
||||
return Conflict(new ErrorMessageResponse(ErrorCode.IOError)
|
||||
{
|
||||
Message = ex.Message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Utils
|
||||
/// <summary>
|
||||
/// Async lock context helper.
|
||||
/// </summary>
|
||||
public sealed class SemaphoreSlimContext : IDisposable
|
||||
sealed class SemaphoreSlimContext : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Asyncronously locks a <paramref name="semaphore"/>.
|
||||
@@ -15,14 +15,28 @@ namespace Tgstation.Server.Host.Utils
|
||||
/// <param name="semaphore">The <see cref="SemaphoreSlim"/> to lock.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="SemaphoreSlimContext"/> for the lock.</returns>
|
||||
public static async Task<SemaphoreSlimContext> Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
|
||||
public static async ValueTask<SemaphoreSlimContext> Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(semaphore);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await semaphore.WaitAsync(cancellationToken);
|
||||
return new SemaphoreSlimContext(semaphore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asyncronously attempts to lock a <paramref name="semaphore"/>.
|
||||
/// </summary>
|
||||
/// <param name="semaphore">The <see cref="SemaphoreSlim"/> to lock.</param>
|
||||
/// <param name="locked">The <see cref="bool"/> result of the lock attempt.</param>
|
||||
/// <returns>A <see cref="SemaphoreSlimContext"/> for the lock on success, or <see langword="null"/> if it was not acquired.</returns>
|
||||
public static SemaphoreSlimContext TryLock(SemaphoreSlim semaphore, out bool locked)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(semaphore);
|
||||
locked = semaphore.Wait(TimeSpan.Zero);
|
||||
return locked
|
||||
? new SemaphoreSlimContext(semaphore)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The locked <see cref="SemaphoreSlim"/>.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user