Compare commits

...

17 Commits

22 changed files with 463 additions and 317 deletions

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<!--package info--> <!--package info-->
<PackageId>DTLib.Logging.Microsoft</PackageId> <PackageId>DTLib.Logging.Microsoft</PackageId>
<Version>1.1.1</Version> <Version>1.1.3</Version>
<Authors>Timerix</Authors> <Authors>Timerix</Authors>
<Description>DTLib logger wrapper with dependency injection</Description> <Description>DTLib logger wrapper with dependency injection</Description>
<RepositoryType>GIT</RepositoryType> <RepositoryType>GIT</RepositoryType>
@@ -11,7 +11,7 @@
<Configuration>Release</Configuration> <Configuration>Release</Configuration>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<!--compilation properties--> <!--compilation properties-->
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFramework>netstandard2.0</TargetFramework>
<!--language features--> <!--language features-->
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
@@ -20,7 +20,7 @@
<!--external dependencies--> <!--external dependencies-->
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.5" />
</ItemGroup> </ItemGroup>
<!--DTLib dependencies--> <!--DTLib dependencies-->
@@ -28,6 +28,6 @@
<ProjectReference Include="..\DTLib\DTLib.csproj" /> <ProjectReference Include="..\DTLib\DTLib.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(Configuration)' != 'Debug' "> <ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
<PackageReference Include="DTLib" Version="1.6.*" /> <PackageReference Include="DTLib" Version="1.7.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<!--package info--> <!--package info-->
<PackageId>DTLib.Web</PackageId> <PackageId>DTLib.Web</PackageId>
<Version>1.0.0</Version> <Version>1.4.0</Version>
<Authors>Timerix</Authors> <Authors>Timerix</Authors>
<Description>HTTP Server with simple routing</Description> <Description>HTTP Server with simple routing</Description>
<RepositoryType>GIT</RepositoryType> <RepositoryType>GIT</RepositoryType>
@@ -11,7 +11,7 @@
<Configuration>Release</Configuration> <Configuration>Release</Configuration>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<!--compilation properties--> <!--compilation properties-->
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFramework>netstandard2.0</TargetFramework>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
<!--language features--> <!--language features-->
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
@@ -25,6 +25,6 @@
<ProjectReference Include="..\DTLib\DTLib.csproj" /> <ProjectReference Include="..\DTLib\DTLib.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(Configuration)' != 'Debug' "> <ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
<PackageReference Include="DTLib" Version="1.6.*" /> <PackageReference Include="DTLib" Version="1.7.4" />
</ItemGroup> </ItemGroup>
</Project> </Project>

18
DTLib.Web/HttpMethod.cs Normal file
View File

@@ -0,0 +1,18 @@
namespace DTLib.Web;
/// <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods"/>
[Flags]
public enum HttpMethod : ushort
{
NONE = 0,
GET = 1,
POST = 2,
PUT = 4,
DELETE = 8,
PATCH = 16,
HEAD = 32,
OPTIONS = 64,
TRACE = 128,
CONNECT = 256,
ANY = 65535
}

View File

@@ -1,6 +0,0 @@
namespace DTLib.Web.Routes;
public class DelegateRoute(Func<HttpListenerContext, Task<HttpStatusCode>> routeHandler) : Route
{
public override Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx) => routeHandler(ctx);
}

View File

@@ -0,0 +1,9 @@
namespace DTLib.Web.Routes;
public class DelegateRouteHandler(
Func<HttpListenerContext, ContextLogger, Task<HttpStatusCode>> routeHandler
) : IRouteHandler
{
public Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx, ContextLogger requestLogger)
=> routeHandler(ctx, requestLogger);
}

View File

@@ -0,0 +1,6 @@
namespace DTLib.Web.Routes;
public interface IRouteHandler
{
Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx, ContextLogger requestLogger);
}

View File

@@ -0,0 +1,6 @@
namespace DTLib.Web.Routes;
public interface IRouter
{
Task Resolve(HttpListenerContext ctx, ContextLogger requestLogger);
}

View File

@@ -1,6 +0,0 @@
namespace DTLib.Web.Routes;
public abstract class Route
{
public abstract Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx);
}

View File

@@ -1,33 +0,0 @@
namespace DTLib.Web.Routes;
public class ServeFilesRoute(IOPath _publicDir, string _homePageUrl = "index.html") : Route
{
public override async Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx)
{
if (ctx.Request.HttpMethod != "GET")
return HttpStatusCode.BadRequest;
string requestPath = ctx.Request.Url?.AbsolutePath ?? "/";
if (requestPath == "/")
requestPath = _homePageUrl;
string ext = Path.Extension(requestPath).Str;
IOPath filePath = Path.Concat(_publicDir, requestPath);
if (!File.Exists(filePath))
return HttpStatusCode.NotFound;
string contentType = ext switch
{
"html" => "text/html",
"css" => "text/css",
"js" or "jsx" or "ts" or "tsx" or "map" => "text/javascript",
_ => "binary/octet-stream"
};
ctx.Response.Headers.Set("Content-Type", contentType);
ctx.Response.Headers.Set("Content-Disposition", "attachment filename=" + filePath.LastName());
var fileStream = File.OpenRead(filePath);
ctx.Response.ContentLength64 = fileStream.Length;
await fileStream.CopyToAsync(ctx.Response.OutputStream);
return HttpStatusCode.OK;
}
}

View File

@@ -0,0 +1,40 @@
namespace DTLib.Web.Routes;
public class ServeFilesRouteHandler(IOPath _publicDir, string _homePageUrl = "index.html") : IRouteHandler
{
public async Task<HttpStatusCode> HandleRequest(HttpListenerContext ctx, ContextLogger requestLogger)
{
if (ctx.Request.HttpMethod != "GET")
return HttpStatusCode.BadRequest;
string requestPath = ctx.Request.Url?.AbsolutePath!;
if (string.IsNullOrEmpty(requestPath) || requestPath == "/")
{
requestPath = _homePageUrl;
}
string ext = Path.Extension(requestPath).Str;
IOPath filePath = Path.Concat(_publicDir, requestPath);
if (!File.Exists(filePath))
return HttpStatusCode.NotFound;
List<(string key, string val)> headers = ext switch
{
"html" => [("Content-Type", "text/html")],
"css" => [("Content-Type", "text/css")],
"js" or "jsx" or "ts" or "tsx" or "map" => [("Content-Type", "text/javascript")],
_ =>
[
("Content-Type", "binary/octet-stream"),
("Content-Disposition", $"attachment filename={filePath.LastName()}")
]
};
foreach (var header in headers)
ctx.Response.Headers.Set(header.key, header.val);
var fileStream = File.OpenRead(filePath);
ctx.Response.ContentLength64 = fileStream.Length;
await fileStream.CopyToAsync(ctx.Response.OutputStream);
return HttpStatusCode.OK;
}
}

View File

@@ -0,0 +1,65 @@
namespace DTLib.Web.Routes;
public class SimpleRouter : IRouter
{
/// route for any url that doesn't have its own handler
public record RouteWithMethod(HttpMethod method, IRouteHandler routeHandler)
{
public bool CheckMethod(HttpMethod requestMethod) => (requestMethod & method) != 0;
public bool CheckMethod(string requestMethodStr)
=> Enum.TryParse<HttpMethod>(requestMethodStr, out var requestMethod)
&& CheckMethod(requestMethod);
}
private readonly Dictionary<string, RouteWithMethod> _routes = new();
private readonly ILogger _logger;
public RouteWithMethod? DefaultRoute { get; set; }
public SimpleRouter(ILogger logger)
{
_logger = new ContextLogger(nameof(SimpleRouter), logger);
}
public void MapRoute(string url, HttpMethod method, IRouteHandler route)
=> _routes.Add(url, new RouteWithMethod(method, route));
public void MapRoute(string url, HttpMethod method,
Func<HttpListenerContext, ContextLogger, Task<HttpStatusCode>> route)
=> MapRoute(url, method, new DelegateRouteHandler(route));
public async Task Resolve(HttpListenerContext ctx, ContextLogger requestLogger)
{
HttpStatusCode status = HttpStatusCode.InternalServerError;
try
{
string? requestPath = ctx.Request.Url?.AbsolutePath;
if (string.IsNullOrEmpty(requestPath))
requestPath = "/";
if(!_routes.TryGetValue(requestPath!, out var routeWithMethod))
routeWithMethod = DefaultRoute;
if (routeWithMethod is null)
{
_logger.LogWarn(nameof(SimpleRouter),
$"couldn't resolve request path {ctx.Request.HttpMethod} {requestPath}");
status = HttpStatusCode.NotFound;
}
else if (!routeWithMethod.CheckMethod(ctx.Request.HttpMethod))
{
_logger.LogWarn(nameof(SimpleRouter),
$"received request with invalid method {ctx.Request.HttpMethod} {requestPath}");
status = HttpStatusCode.MethodNotAllowed;
}
else status = await routeWithMethod.routeHandler.HandleRequest(ctx, requestLogger);
}
finally
{
ctx.Response.StatusCode = (int)status;
await ctx.Response.OutputStream.FlushAsync();
ctx.Response.OutputStream.Close();
}
}
}

View File

@@ -3,97 +3,71 @@ global using System.Collections.Generic;
global using System.Text; global using System.Text;
global using System.Threading; global using System.Threading;
global using System.Threading.Tasks; global using System.Threading.Tasks;
global using DTLib;
global using DTLib.Demystifier;
global using DTLib.Filesystem; global using DTLib.Filesystem;
global using DTLib.Logging; global using DTLib.Logging;
global using System.Net; global using System.Net;
using System.Diagnostics;
using DTLib.Extensions;
using DTLib.Web.Routes; using DTLib.Web.Routes;
namespace DTLib.Web; namespace DTLib.Web;
internal class WebApp public class WebApp
{ {
/// route for base url private readonly string _baseUrl;
public Route? HomePageRoute = null; private readonly ContextLogger _logger;
/// route for any url that doesn't have its own handler private readonly IRouter _router;
public Route? DefaultRoute = null; private readonly CancellationToken _stopToken;
private ContextLogger _logger;
private string _baseUrl;
private CancellationToken _stopToken;
private Dictionary<string, Route> routes = new();
public WebApp(ILogger logger, string baseUrl, CancellationToken stopToken) public WebApp(string baseUrl, ILogger logger, IRouter router, CancellationToken stopToken = default)
{ {
_logger = new ContextLogger(nameof(WebApp), logger); _logger = new ContextLogger(nameof(WebApp), logger);
_baseUrl = baseUrl; _baseUrl = baseUrl;
_stopToken = stopToken; _stopToken = stopToken;
_router = router;
} }
public async Task Run() public async Task Run()
{ {
_logger.LogInfo($"starting webserver at {_baseUrl} ..."); _logger.LogInfo($"starting server at '{_baseUrl}'...");
HttpListener server = new HttpListener(); HttpListener server = new HttpListener();
server.Prefixes.Add(_baseUrl); server.Prefixes.Add(_baseUrl);
server.Start(); server.Start();
_logger.LogInfo("server started"); _logger.LogInfo("server started");
long requestId = 0; long requestId = 1;
while (!_stopToken.IsCancellationRequested)
try
{ {
var ctx = await server.GetContextAsync(); while (!_stopToken.IsCancellationRequested)
HandleRequestAsync(ctx, requestId); {
requestId++; var ctx = await server.GetContextAsync().AsCancellable(_stopToken);
HandleRequestAsync(ctx, new ContextLogger($"Request-{requestId++}", _logger));
}
} }
catch (OperationCanceledException)
// stop {}
server.Stop(); server.Stop();
_logger.LogInfo("server stopped"); _logger.LogInfo("server stopped");
} }
// ReSharper disable once AsyncVoidMethod private async void HandleRequestAsync(HttpListenerContext ctx, ContextLogger requestLogger)
private async void HandleRequestAsync(HttpListenerContext ctx, long requestId)
{ {
string logContext = $"Request {requestId}";
try try
{ {
_logger.LogInfo(logContext, $"[{ctx.Request.HttpMethod}] {ctx.Request.RawUrl} from {ctx.Request.RemoteEndPoint} ..."); requestLogger.LogInfo($"{ctx.Request.HttpMethod} {ctx.Request.RawUrl} from {ctx.Request.RemoteEndPoint}...");
var status = await Resolve(ctx); var stopwatch = new Stopwatch();
_logger.LogInfo(logContext, status); stopwatch.Start();
await _router.Resolve(ctx, requestLogger);
stopwatch.Stop();
requestLogger.LogInfo($"responded {ctx.Response.StatusCode}" +
$" ({(HttpStatusCode)ctx.Response.StatusCode})" +
$" in {stopwatch.ElapsedMilliseconds}ms");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarn(logContext, ex); requestLogger.LogWarn(ex);
} }
} }
public void MapRoute(string url, Func<HttpListenerContext, Task<HttpStatusCode>> route)
=> MapRoute(url, new DelegateRoute(route));
public void MapRoute(string url, Route route) => routes.Add(url, route);
public async Task<HttpStatusCode> Resolve(HttpListenerContext ctx)
{
string requestPath = ctx.Request.Url?.AbsolutePath ?? "/";
Route? route;
if(HomePageRoute != null && requestPath == "/")
route = HomePageRoute;
else if (routes.TryGetValue(requestPath, out var routeDelegate))
route = routeDelegate;
else route = DefaultRoute;
HttpStatusCode status;
if (route == null)
{
_logger.LogWarn("couldn't resolve request path {requestPath}");
status = HttpStatusCode.NotFound;
}
else status = await route.HandleRequest(ctx);
ctx.Response.StatusCode = (int)status;
await ctx.Response.OutputStream.FlushAsync(_stopToken);
ctx.Response.OutputStream.Close();
return status;
}
} }

View File

@@ -14,6 +14,9 @@ public static class ColoredConsole
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ResetColor() => System.Console.ResetColor(); public static void ResetColor() => System.Console.ResetColor();
/// <summary>
/// Clears console buffer and resets color
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clear() public static void Clear()
{ {
@@ -21,26 +24,15 @@ public static class ColoredConsole
System.Console.Clear(); System.Console.Clear();
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Write(char c) => System.Console.Write(c);
public static void Write(string msg, ConsoleColor? fg = null, ConsoleColor? bg = null) public static void Write(string msg, ConsoleColor? fg = null, ConsoleColor? bg = null)
{ {
if(fg != null) Fg(fg.Value); if(fg != null) Fg(fg.Value);
if(bg != null) Bg(bg.Value); if(bg != null) Bg(bg.Value);
#if NETSTANDARD2_0 System.Console.Write(msg);
var chars = msg.ToCharArray();
#else
var chars = msg.AsSpan();
#endif
for (int i = 0; i < chars.Length; ++i)
{
Write(chars[i]);
}
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteLine() => Write('\n'); public static void WriteLine() => System.Console.Write('\n');
public static void WriteLine(string msg, ConsoleColor? fg = null, ConsoleColor? bg = null) public static void WriteLine(string msg, ConsoleColor? fg = null, ConsoleColor? bg = null)
{ {
@@ -48,19 +40,16 @@ public static class ColoredConsole
WriteLine(); WriteLine();
} }
public static string? ReadLine(string query, ConsoleColor? fg = null, ConsoleColor? bg = null) public static string ReadLine(ConsoleColor? fg = null, ConsoleColor? bg = null)
{ {
Write(query, fg, bg); if(fg != null) Fg(fg.Value);
Write(':'); if(bg != null) Bg(bg.Value);
Write(' '); return System.Console.ReadLine() ?? string.Empty;
return System.Console.ReadLine();
} }
public static void WriteHLine(char c, ConsoleColor? fg = null, ConsoleColor? bg = null) public static void WriteHLine(char c, ConsoleColor? fg = null, ConsoleColor? bg = null) =>
{
WriteLine(c.Multiply(Width - 1), fg, bg); WriteLine(c.Multiply(Width - 1), fg, bg);
}
public static void WriteTitle(string title, char spacing = '-', public static void WriteTitle(string title, char spacing = '-',
string left_framing = "[", string right_framing = "]", string left_framing = "[", string right_framing = "]",
ConsoleColor? fg = null, ConsoleColor? bg = null) ConsoleColor? fg = null, ConsoleColor? bg = null)
@@ -82,8 +71,6 @@ public static class ColoredConsole
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteCentred(string title, ConsoleColor? fg = null, ConsoleColor? bg = null) public static void WriteCentred(string title, ConsoleColor? fg = null, ConsoleColor? bg = null)
{ => WriteTitle(title, ' ', "", "", fg, bg);
WriteTitle(title, ' ', "", "", fg, bg);
}
} }

View File

@@ -1,64 +1,96 @@
namespace DTLib.Console; namespace DTLib.Console;
#nullable enable public record LaunchArgument
public class LaunchArgument
{ {
public string[] Aliases; public struct Param
public string Description; {
protected string? ParamName1; public readonly string Name;
protected string? ParamName2; public string? Value { get; internal set; } = null;
public Action? Handler;
public Action<string>? HandlerWithArg1; public Param(string name)
public Action<string, string>? HandlerWithArg2; {
public int RequiredArgsCount; Name = name;
public int Priority; }
}
private LaunchArgument(string[] aliases, string description, int priority) public readonly string[] Aliases;
public readonly string Description;
public readonly int Priority;
public readonly Param[] Params;
private readonly Action? Handler;
private readonly Action<string>? Handler1Param;
private readonly Action<string, string>? Handler2Params;
public LaunchArgument(string[] aliases, string description,
Action handler, int priority = 0)
{ {
Aliases = aliases; Aliases = aliases;
Description = description; Description = description;
Priority = priority; Priority = priority;
}
public LaunchArgument(string[] aliases, string description,
Action handler, int priority = 0)
: this(aliases, description, priority)
{
Handler = handler; Handler = handler;
RequiredArgsCount = 0; Params = [];
} }
public LaunchArgument(string[] aliases, string description, public LaunchArgument(string[] aliases, string description,
Action<string> handler, string paramName1, int priority=0) Action<string> handler, string paramName1, int priority=0)
: this(aliases, description, priority)
{ {
HandlerWithArg1 = handler; Aliases = aliases;
ParamName1 = paramName1; Description = description;
RequiredArgsCount = 1; Priority = priority;
Handler1Param = handler;
Params = [
new Param(paramName1)
];
} }
public LaunchArgument(string[] aliases, string description, public LaunchArgument(string[] aliases, string description,
Action<string, string> handler, string paramName1, string paramName2, int priority=0) Action<string, string> handler, string paramName1, string paramName2, int priority=0)
: this(aliases, description, priority)
{ {
HandlerWithArg2 = handler; Aliases = aliases;
ParamName1 = paramName1; Description = description;
ParamName2 = paramName2; Priority = priority;
RequiredArgsCount = 2;
Handler2Params = handler;
Params = [
new Param(paramName1),
new Param(paramName2),
];
} }
public StringBuilder AppendHelpInfo(StringBuilder b) internal StringBuilder AppendHelpInfo(StringBuilder b)
{ {
b.Append(Aliases[0]); b.Append(Aliases[0]);
for (int i = 1; i < Aliases.Length; i++) for (int i = 1; i < Aliases.Length; i++)
b.Append(", ").Append(Aliases[i]); b.Append(", ").Append(Aliases[i]);
if (!string.IsNullOrEmpty(ParamName1)) foreach (var param in Params)
b.Append(" [").Append(ParamName1).Append("] "); b.Append(" [").Append(param.Name).Append("]");
if (!string.IsNullOrEmpty(ParamName2)) b.Append(" - ").Append(Description);
b.Append(" [").Append(ParamName2).Append("] ");
b.Append("- ").Append(Description);
return b; return b;
} }
public override string ToString() => internal void Handle()
$"{{{{{Aliases.MergeToString(", ")}}}, Handler: {Handler is null}, HandlerWithArg: {HandlerWithArg1 is null}}}"; {
switch (Params.Length)
{
default:
throw new ArgumentOutOfRangeException(Params.Length.ToString());
case 0:
Handler!.Invoke();
break;
case 1:
if (Params[0].Value is null)
throw new NullReferenceException($"Argument '{Aliases[0]}' hasnt got Param[0] value");
Handler1Param!.Invoke(Params[0].Value!);
break;
case 2:
if (Params[0].Value is null)
throw new NullReferenceException($"Argument '{Aliases[0]}' hasnt got Param[0] value");
if (Params[1].Value is null)
throw new NullReferenceException($"Argument '{Aliases[0]}' hasnt got Param[1] value");
Handler2Params!.Invoke(Params[0].Value!, Params[1].Value!);
break;
}
}
} }

View File

@@ -2,31 +2,135 @@ namespace DTLib.Console;
public class LaunchArgumentParser public class LaunchArgumentParser
{ {
private Dictionary<string, LaunchArgument> argDict = new(); public string HelpMessageHeader = "USAGE:";
private List<LaunchArgument> argList = new(); public bool AllowedNoArguments;
public bool ExitIfNoArgs = true; public bool AllowedUnknownArguments;
// ReSharper disable once CollectionNeverQueried.Global
public readonly List<string> UnknownArguments = new();
private readonly Dictionary<string, LaunchArgument> argDict = new();
private readonly List<LaunchArgument> argList = new();
public class ExitAfterHelpException : Exception public LaunchArgumentParser()
{ {
internal ExitAfterHelpException() : base("your program can use this exception to exit after displaying help message") var help = new LaunchArgument(new[] { "h", "help" },
{ } "shows help message", HelpHandler);
Add(help);
var helpArg = new LaunchArgument(new[] { "ha", "helparg" },
"shows help message for specific argument",
HelpArgHandler, "argument");
Add(helpArg);
} }
public LaunchArgumentParser(ICollection<LaunchArgument> arguments) : this() => WithArgs(arguments);
public LaunchArgumentParser(params LaunchArgument[] arguments) : this() => WithArgs(arguments);
public LaunchArgumentParser WithArgs(IEnumerable<LaunchArgument> args)
{
foreach (var arg in args)
Add(arg);
return this;
}
public LaunchArgumentParser WithArgs(params LaunchArgument[] args)
{
foreach (var arg in args)
Add(arg);
return this;
}
public LaunchArgumentParser WithHelpMessageHeader(string header)
{
HelpMessageHeader = header;
return this;
}
public LaunchArgumentParser AllowNoArguments()
{
AllowedNoArguments = true;
return this;
}
public LaunchArgumentParser AllowUnknownArguments()
{
AllowedUnknownArguments = true;
return this;
}
public string CreateHelpMessage() public string CreateHelpMessage()
{ {
StringBuilder b = new(); StringBuilder b = new(HelpMessageHeader);
foreach (var arg in argList) foreach (var arg in argList)
arg.AppendHelpInfo(b).Append('\n'); {
b.Remove(b.Length-1, 1); b.Append('\n');
arg.AppendHelpInfo(b);
}
return b.ToString(); return b.ToString();
} }
public string CreateHelpArgMessage(string argAlias) public string CreateHelpArgMessage(string argAlias)
{ {
StringBuilder b = new(); StringBuilder b = new();
var arg = Parse(argAlias); if(!TryParseArg(argAlias, out var arg))
throw new Exception($"unknown argument '{argAlias}'");
arg.AppendHelpInfo(b); arg.AppendHelpInfo(b);
return b.ToString(); return b.ToString();
} }
public void Add(LaunchArgument arg)
{
argList.Add(arg);
foreach (string alias in arg.Aliases)
argDict.Add(alias, arg);
}
public bool TryParseArg(string argAlias, out LaunchArgument arg)
{
// different argument providing patterns
arg = null!;
return argAlias.StartsWith("--") && argDict.TryGetValue(argAlias.Substring(2), out arg) || // --arg
argAlias.StartsWith('-') && argDict.TryGetValue(argAlias.Substring(1), out arg) || // -arg
argAlias.StartsWith('/') && argDict.TryGetValue(argAlias.Substring(1), out arg); // /arg
}
/// <param name="args">program launch args</param>
/// <exception cref="Exception">argument {args[i]} should have a parameter after it</exception>
/// <exception cref="NullReferenceException">argument hasn't got any handlers</exception>
/// <exception cref="ExitAfterHelpException">happens after help message is displayed</exception>
public void ParseAndHandle(string[] args)
{
// show help message and throw ExitAfterHelpException
if (args.Length == 0 && !AllowedNoArguments)
HelpHandler();
List<LaunchArgument> execQueue = new();
for (int i = 0; i < args.Length; i++)
{
if (!TryParseArg(args[i], out var arg))
{
if (!AllowedUnknownArguments)
throw new Exception($"unknown argument '{args[i]}'");
UnknownArguments.Add(args[i]);
}
for (int j = 0; j < arg.Params.Length; j++)
{
if (++i >= args.Length)
throw new Exception(
$"argument '{arg.Aliases[0]}' should have parameter '{arg.Params[j]}' after it");
arg.Params[j].Value = args[i];
}
execQueue.Add(arg);
}
// ascending sort by priority
execQueue.Sort((a0, a1) => a0.Priority - a1.Priority);
// finally executing handlers
foreach (var a in execQueue)
a.Handle();
}
private void HelpHandler() private void HelpHandler()
{ {
System.Console.WriteLine(CreateHelpMessage()); System.Console.WriteLine(CreateHelpMessage());
@@ -39,104 +143,11 @@ public class LaunchArgumentParser
throw new ExitAfterHelpException(); throw new ExitAfterHelpException();
} }
public class ExitAfterHelpException : Exception
public LaunchArgumentParser()
{ {
var help = new LaunchArgument(new[] { "h", "help" }, public ExitAfterHelpException()
"shows help message", HelpHandler); : base("your program can use this exception to exit after displaying help message")
Add(help);
var helpArg = new LaunchArgument( new[]{ "ha", "helparg" },
"shows help message for particular argument",
HelpArgHandler, "argAlias");
Add(helpArg);
}
public LaunchArgumentParser WithNoExit()
{
ExitIfNoArgs = false;
return this;
}
public LaunchArgumentParser(ICollection<LaunchArgument> arguments) : this()
{
foreach (var arg in arguments)
Add(arg);
}
public LaunchArgumentParser(params LaunchArgument[] arguments) : this()
{
for (var i = 0; i < arguments.Length; i++)
Add(arguments[i]);
}
public void Add(LaunchArgument arg)
{
argList.Add(arg);
for(int a=0; a<arg.Aliases.Length; a++)
argDict.Add(arg.Aliases[a], arg);
}
public LaunchArgument Parse(string argAlias)
{
// different argument providing patterns
if (!argDict.TryGetValue(argAlias, out var arg) && // arg
!(argAlias.StartsWith("--") && argDict.TryGetValue(argAlias.Substring(2), out arg)) && // --arg
!(argAlias.StartsWith('-') && argDict.TryGetValue(argAlias.Substring(1), out arg)) && // -arg
!(argAlias.StartsWith('/') && argDict.TryGetValue(argAlias.Substring(1), out arg))) // /arg
throw new Exception($"invalid argument: {argAlias}\n{CreateHelpMessage()}");
return arg;
}
/// <param name="args">program launch args</param>
/// <exception cref="Exception">argument {args[i]} should have a parameter after it</exception>
/// <exception cref="NullReferenceException">argument hasn't got any handlers</exception>
/// <exception cref="ExitAfterHelpException">happens after help message is displayed</exception>
public void ParseAndHandle(string[] args)
{
// show help and throw
if (args.Length == 0 && ExitIfNoArgs)
HelpHandler();
List<LaunchArgument> execQueue = new();
for (int i = 0; i < args.Length; i++)
{ {
LaunchArgument arg = Parse(args[i]);
switch (arg.RequiredArgsCount)
{
case 0:
if (arg.Handler is null)
throw new NullReferenceException($"argument <{args[i]}> hasn't got any handlers");
break;
case 1:
{
if (arg.HandlerWithArg1 is null)
throw new NullReferenceException($"argument <{args[i]}> hasn't got any handlers");
if (i + 1 >= args.Length)
throw new Exception($"argument <{args[i]}> should have a parameter after it");
string arg1 = args[++i];
arg.Handler = () => arg.HandlerWithArg1(arg1);
break;
}
case 2:
{
if (arg.HandlerWithArg2 is null)
throw new NullReferenceException($"argument <{args[i]}> hasn't got any handlers");
if (i + 2 >= args.Length)
throw new Exception($"argument <{args[i]}> should have two params after it");
string arg1 = args[++i], arg2 = args[++i];
arg.Handler = () => arg.HandlerWithArg2(arg1, arg2);
break;
}
}
execQueue.Add(arg);
} }
// ascending sort by priority
execQueue.Sort((a0, a1) => a0.Priority-a1.Priority);
// finally executing handlers
foreach (var a in execQueue)
a.Handler!();
} }
} }

View File

@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<!--package info--> <!--package info-->
<PackageId>DTLib</PackageId> <PackageId>DTLib</PackageId>
<Version>1.6.3</Version> <Version>1.7.4</Version>
<Authors>Timerix</Authors> <Authors>Timerix</Authors>
<Description>Library for all my C# projects</Description> <Description>Library for all my C# projects</Description>
<RepositoryType>GIT</RepositoryType> <RepositoryType>GIT</RepositoryType>
@@ -31,6 +31,6 @@
<ProjectReference Include="..\DTLib.Demystifier\DTLib.Demystifier.csproj" /> <ProjectReference Include="..\DTLib.Demystifier\DTLib.Demystifier.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(Configuration)' != 'Debug' "> <ItemGroup Condition=" '$(Configuration)' != 'Debug' ">
<PackageReference Include="DTLib.Demystifier" Version="1.1.0" /> <PackageReference Include="DTLib.Demystifier" Version="1.1.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,37 @@
namespace DTLib.Extensions;
public static class TaskExtensions
{
// https://stackoverflow.com/a/69861689
public static Task<T> AsCancellable<T>(this Task<T> task, CancellationToken token)
{
if (!token.CanBeCanceled)
{
return task;
}
var tcs = new TaskCompletionSource<T>();
// This cancels the returned task:
// 1. If the token has been canceled, it cancels the TCS straightaway
// 2. Otherwise, it attempts to cancel the TCS whenever
// the token indicates cancelled
token.Register(() => tcs.TrySetCanceled(token),
useSynchronizationContext: false);
task.ContinueWith(t =>
{
// Complete the TCS per task status
// If the TCS has been cancelled, this continuation does nothing
if (task.IsCanceled)
tcs.TrySetCanceled();
else if (task.IsFaulted)
tcs.TrySetException(t.Exception!);
else tcs.TrySetResult(t.Result);
},
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
return tcs.Task;
}
}

View File

@@ -15,29 +15,40 @@ public readonly struct IOPath
public IOPath(char[] path, bool separatorsFixed=false) public IOPath(char[] path, bool separatorsFixed=false)
{ {
if (path.Length == 0)
throw new Exception("path is null or empty");
Str = separatorsFixed ? new string(path) : FixSeparators(path); Str = separatorsFixed ? new string(path) : FixSeparators(path);
} }
public IOPath(string path, bool separatorsFixed=false) public IOPath(string path, bool separatorsFixed=false) : this(path.ToCharArray(), separatorsFixed)
{ {
if (path.IsNullOrEmpty())
throw new Exception("path is null or empty");
Str = separatorsFixed ? path : FixSeparators(path.ToCharArray());
} }
static string FixSeparators(char[] path) static string FixSeparators(char[] path)
{ {
int length = path.Length; int length = path.Length;
if (path[length-1] == Path.Sep || path[length-1] == Path.NotSep) char lastChar = path[path.Length - 1];
if (lastChar == Path.Sep || lastChar == Path.NotSep)
length--; // removing trailing sep length--; // removing trailing sep
char[] fixed_path = new char[length]; StringBuilder sb = new StringBuilder();
bool prevWasSeparator = false;
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
{ {
if (path[i] == Path.NotSep) if (path[i] == Path.NotSep)
fixed_path[i] = Path.Sep; {
else fixed_path[i] = path[i]; // prevent double separators like this "a//b"
if(!prevWasSeparator)
sb.Append(Path.Sep);
prevWasSeparator = true;
}
else
{
sb.Append(path[i]);
prevWasSeparator = false;
}
} }
return new string(fixed_path);
return sb.ToString();
} }
public static IOPath[] ArrayCast(string[] a, bool separatorsFixed=false) public static IOPath[] ArrayCast(string[] a, bool separatorsFixed=false)

View File

@@ -96,10 +96,13 @@ public static class Path
/// returns just dir name or file name with extension /// returns just dir name or file name with extension
public static IOPath LastName(this IOPath path) public static IOPath LastName(this IOPath path)
{ {
if(path.Length < 2)
return path;
int i = path.LastIndexOf(Sep); int i = path.LastIndexOf(Sep);
if (i == path.Length - 1) // ends with separator if (i == path.Length - 1) // ends with separator
i = path.LastIndexOf(Sep, i - 1); i = path.LastIndexOf(Sep, i - 1);
if (i == -1) return path; if (i == -1)
return path;
return path.Substring(i + 1); return path.Substring(i + 1);
} }

View File

@@ -11,8 +11,8 @@ public class CompositeLogger : ILogger
set set
{ {
_debugLogEnabled = value; _debugLogEnabled = value;
for (int i = 0; i < _loggers.Length; i++) foreach (var childLogger in ChildLoggers)
_loggers[i].DebugLogEnabled = value; childLogger.DebugLogEnabled = value;
} }
} }
@@ -22,8 +22,8 @@ public class CompositeLogger : ILogger
set set
{ {
_infoLogEnabled = true; _infoLogEnabled = true;
for (int i = 0; i < _loggers.Length; i++) foreach (var childLogger in ChildLoggers)
_loggers[i].InfoLogEnabled = value; childLogger.InfoLogEnabled = value;
} }
} }
@@ -33,8 +33,8 @@ public class CompositeLogger : ILogger
set set
{ {
_warnLogEnabled = value; _warnLogEnabled = value;
for (int i = 0; i < _loggers.Length; i++) foreach (var childLogger in ChildLoggers)
_loggers[i].WarnLogEnabled = value; childLogger.WarnLogEnabled = value;
} }
} }
@@ -44,46 +44,38 @@ public class CompositeLogger : ILogger
set set
{ {
_errorLogenabled = value; _errorLogenabled = value;
for (int i = 0; i < _loggers.Length; i++) foreach (var childLogger in ChildLoggers)
_loggers[i].ErrorLogEnabled = value; childLogger.ErrorLogEnabled = value;
} }
} }
public ILogFormat Format { get; set; } public ILogFormat Format { get; set; }
public readonly List<ILogger> ChildLoggers;
protected ILogger[] _loggers; private bool _debugLogEnabled;
private bool _debugLogEnabled =
#if DEBUG
true;
#else
false;
#endif
private bool _infoLogEnabled = true; private bool _infoLogEnabled = true;
private bool _warnLogEnabled = true; private bool _warnLogEnabled = true;
private bool _errorLogenabled = true; private bool _errorLogenabled = true;
public CompositeLogger(ILogFormat format, params ILogger[] loggers) public CompositeLogger(ILogFormat format, params ILogger[] childLoggers)
{ {
Format = format; Format = format;
_loggers = loggers; ChildLoggers = new List<ILogger>(childLoggers);
} }
public CompositeLogger(params ILogger[] loggers) : this(new DefaultLogFormat(), loggers) public CompositeLogger(params ILogger[] childLoggers) : this(new DefaultLogFormat(), childLoggers)
{} {}
public void Log(string context, LogSeverity severity, object message, ILogFormat format) public void Log(string context, LogSeverity severity, object message, ILogFormat format)
{ {
if(!this.CheckSeverity(severity)) foreach (var childLogger in ChildLoggers)
return; childLogger.Log(context, severity, message, format);
for (int i = 0; i < _loggers.Length; i++)
_loggers[i].Log(context, severity, message, format);
} }
public void Dispose() public void Dispose()
{ {
for (int i = 0; i < _loggers.Length; i++) foreach (var childLogger in ChildLoggers)
_loggers[i].Dispose(); childLogger.Dispose();
} }
} }