diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6ac8dbf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine as BUILD + +RUN apk add -U bash build-base clang icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib-dev +WORKDIR /build +COPY . . + +RUN dotnet publish src/Alma/Alma.csproj -c Release -r linux-musl-x64 -p:PublishAot=true -o /app + +FROM alpine:edge as FINAL + +RUN mkdir /data +WORKDIR /data + +ENV ALMA_APP_DATA_FALLBACK=/appdata +ENV ALMA_CONFIG_FALLBACK=/appconfig + +RUN mkdir /appdata +RUN mkdir /appconfig + +RUN apk add -U icu-libs libstdc++ + +COPY --from=BUILD /app/Alma /alma + +ENTRYPOINT ["/alma"] +CMD ["help"] \ No newline at end of file diff --git a/src/Alma.App/Command/Diag/DiagCommand.cs b/src/Alma.App/Command/Diag/DiagCommand.cs new file mode 100644 index 0000000..e7631d1 --- /dev/null +++ b/src/Alma.App/Command/Diag/DiagCommand.cs @@ -0,0 +1,116 @@ +using System.Reflection; +using Alma.Logging; + +namespace Alma.Command.Diag; + +public class DiagCommand : ICommand +{ + private readonly ILogger _logger; + public string CommandString => "diag"; + private readonly Lazy> _diagnosticHelpersLazy; + + public DiagCommand(ILogger logger) + { + _logger = logger; + _diagnosticHelpersLazy = new Lazy>(FindDiagnosticHelpers); + } + + public Task Run(List parameters) + { + if (parameters.Count < 1) + { + _logger.LogInformation("No diagnostic helper specified."); + return Task.CompletedTask; + } + + var command = parameters[0]; + + if (command == "list") + { + ListDiagnosticHelpers(); + return Task.CompletedTask; + } + + var diagnosticHelpers = _diagnosticHelpersLazy.Value; + + var helper = diagnosticHelpers.FirstOrDefault( + h => GetDiagnosticHelper(h) is { } attr && attr.Command == command + ); + + if (helper is null) + { + _logger.LogInformation($"Diagnostic helper {command} is not found."); + return Task.CompletedTask; + } + + if (!helper.IsStatic) + { + _logger.LogInformation($"Diagnostic helper {helper.Name} is not static."); + return Task.CompletedTask; + } + + HandleHelper(helper, parameters.Skip(1)); + + return Task.CompletedTask; + } + + private void ListDiagnosticHelpers() + { + var diagnosticHelpers = _diagnosticHelpersLazy.Value; + var commands = diagnosticHelpers + .Select(h => GetDiagnosticHelper(h)?.Command) + .OfType() + .ToList(); + + _logger.LogInformation("Available diagnostic helpers:"); + foreach (var command in commands) + { + _logger.LogInformation(command); + } + } + + private void HandleHelper(MethodInfo helper, IEnumerable parameters) + { + var helperParameters = helper.GetParameters(); + var helperArguments = new object[helperParameters.Length]; + for (var i = 0; i < helperParameters.Length; i++) + { + var parameterType = helperParameters[i].ParameterType; + if (parameterType == typeof(IEnumerable)) + { + helperArguments[i] = parameters; + } + else if (parameterType == typeof(ILogger)) + { + helperArguments[i] = _logger; + } + else + { + _logger.LogInformation($"Diagnostic helper {helper.Name} has wrong parameter type, could not resolve '{parameterType}'."); + return; + } + } + + helper.Invoke(null, helperArguments); + } + + private IReadOnlyList FindDiagnosticHelpers() + { + var assemblies = new[] + { + Assembly.GetExecutingAssembly(), + }; + + return assemblies.SelectMany( + a => + a + .GetTypes() + .SelectMany(t => t.GetMethods()) + .Where(t => t.GetCustomAttributes(typeof(DiagnosticHelper)).Any())) + .ToList() + .AsReadOnly(); + } + + private DiagnosticHelper? GetDiagnosticHelper(MethodInfo o) + => o.GetCustomAttributes(typeof(DiagnosticHelper)).FirstOrDefault() as DiagnosticHelper; +} \ No newline at end of file diff --git a/src/Alma.App/Command/Diag/DiagnosticHelper.cs b/src/Alma.App/Command/Diag/DiagnosticHelper.cs new file mode 100644 index 0000000..19af6c7 --- /dev/null +++ b/src/Alma.App/Command/Diag/DiagnosticHelper.cs @@ -0,0 +1,12 @@ +namespace Alma.Command.Diag; + +[AttributeUsage(AttributeTargets.Method)] +public class DiagnosticHelper : Attribute +{ + public string Command { get; } + + public DiagnosticHelper(string command) + { + Command = command; + } +} \ No newline at end of file diff --git a/src/Alma.App/Services/FolderService.cs b/src/Alma.App/Services/FolderService.cs index be09233..289e0b4 100644 --- a/src/Alma.App/Services/FolderService.cs +++ b/src/Alma.App/Services/FolderService.cs @@ -1,47 +1,70 @@ +using System.Diagnostics.CodeAnalysis; +using Alma.Command.Diag; +using Alma.Logging; + namespace Alma.Services; public class FolderService : IFolderService { + private readonly Dictionary> _configHomeProviders; + private readonly Dictionary> _appDataProviders; public string? ConfigRoot { get; } public string AppData { get; } public string ApplicationSubfolderName => "alma"; + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FolderService))] public FolderService() { + _configHomeProviders = ConfigHomeProviders(); + _appDataProviders = AppDataProviders(); + ConfigRoot = GetConfigHomePath(); AppData = GetAppDataPath(); if (!Directory.Exists(AppData)) Directory.CreateDirectory(AppData); } - public string GetPreferredConfigurationFolder() + private static Dictionary> ConfigHomeProviders() + { + return new Dictionary> + { + {"ALMA_CONFIG", () => Environment.GetEnvironmentVariable("ALMA_CONFIG")}, + {"XDG_CONFIG_HOME", () => Environment.GetEnvironmentVariable("XDG_CONFIG_HOME")}, + {"UserProfile", GetPreferredConfigurationFolder}, + {"ALMA_CONFIG_FALLBACK", () => Environment.GetEnvironmentVariable("ALMA_CONFIG_FALLBACK")}, + }; + } + + private static Dictionary> AppDataProviders() + { + return new Dictionary> + { + {"ALMA_APP_DATA", () => Environment.GetEnvironmentVariable("ALMA_APP_DATA")}, + {"LocalApplicationData", () => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)}, + {"ALMA_APP_DATA_FALLBACK", () => Environment.GetEnvironmentVariable("ALMA_APP_DATA_FALLBACK")}, + }; + } + + string IFolderService.GetPreferredConfigurationFolder() + => GetPreferredConfigurationFolder(); + + public static string GetPreferredConfigurationFolder() => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); private string? GetConfigHomePath() { - var configHomeProviders = new List> - { - () => Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"), - () => GetPreferredConfigurationFolder() - }; - - var configHome = EnumerateProviders(configHomeProviders); + var configHome = EnumerateProviders(_configHomeProviders.Values); return configHome == null ? null : Path.Combine(configHome, ApplicationSubfolderName); } private string GetAppDataPath() { - var appDataProviders = new List> - { - () => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) - }; - - var appData = EnumerateProviders(appDataProviders) ?? Environment.CurrentDirectory; + var appData = EnumerateProviders(_appDataProviders.Values) ?? Environment.CurrentDirectory; return Path.Combine(appData, ApplicationSubfolderName); } - private static string? EnumerateProviders(List> providers) + private static string? EnumerateProviders(IEnumerable> providers) { string? result = null; @@ -53,4 +76,26 @@ public class FolderService : IFolderService return result; } + + [DiagnosticHelper("config-home-providers")] + public static void ConfigHomeProviderDiag(ILogger logger) + { + var configHomeProviders = ConfigHomeProviders(); + logger.LogInformation($"There are {configHomeProviders.Count} config home providers:"); + foreach (var configHome in configHomeProviders) + { + logger.LogInformation($"{configHome.Key} => {configHome.Value() ?? ""}"); + } + } + + [DiagnosticHelper("app-data-providers")] + public static void AppDataProviderDiag(ILogger logger) + { + var appDataProviders = AppDataProviders(); + logger.LogInformation($"There are {appDataProviders.Count} app data providers:"); + foreach (var appData in appDataProviders) + { + logger.LogInformation($"{appData.Key} => {appData.Value() ?? ""}"); + } + } } \ No newline at end of file diff --git a/src/Alma.App/Services/PathHelperService.cs b/src/Alma.App/Services/PathHelperService.cs index f2d3617..dd5e18d 100644 --- a/src/Alma.App/Services/PathHelperService.cs +++ b/src/Alma.App/Services/PathHelperService.cs @@ -1,4 +1,7 @@ -using Alma.Data; +using System.Diagnostics.CodeAnalysis; +using Alma.Command.Diag; +using Alma.Data; +using Alma.Logging; namespace Alma.Services; @@ -10,6 +13,7 @@ public class PathHelperService : IPathHelperService new("%DOCUMENTS%", () => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)), }; + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(PathHelperService))] public string ResolvePath(string path, string? currentDirectory = null) { var skipCombiningCurrentDirectory = false; @@ -30,4 +34,14 @@ public class PathHelperService : IPathHelperService ? path : Path.Combine(currentDirectory, path); } + + [DiagnosticHelper("special-path-resolver")] + public static void SpecialPathResolverDiag(ILogger logger) + { + logger.LogInformation($"There are {_specialPathResolvers.Count} special path resolvers:"); + foreach (var specialPathResolver in _specialPathResolvers) + { + logger.LogInformation($"{specialPathResolver.PathName} => {specialPathResolver.Resolver()}"); + } + } } \ No newline at end of file diff --git a/src/Alma.Logging/ILogger.cs b/src/Alma.Logging/ILogger.cs index 33677e8..875c810 100644 --- a/src/Alma.Logging/ILogger.cs +++ b/src/Alma.Logging/ILogger.cs @@ -1,6 +1,6 @@ namespace Alma.Logging; -public interface ILogger +public interface ILogger { LogLevel DefaultLogLevel { get; } void LogInformation(string logMessage); @@ -9,4 +9,9 @@ public interface ILogger void Log(string logMessage, LogLevel logLevel); void LogError(string logMessage); void LogCritical(string logMessage); +} + +public interface ILogger : ILogger +{ + } \ No newline at end of file diff --git a/src/Alma.Logging/ILoggerFactory.cs b/src/Alma.Logging/ILoggerFactory.cs index 47fd757..7536221 100644 --- a/src/Alma.Logging/ILoggerFactory.cs +++ b/src/Alma.Logging/ILoggerFactory.cs @@ -3,5 +3,6 @@ public interface ILoggerFactory { ILogger CreateLogger(); + ILogger CreateLogger(Type t); LogLevel DefaultLogLevel { get; } } \ No newline at end of file diff --git a/src/Alma.Logging/Logger.cs b/src/Alma.Logging/Logger.cs index cb71945..e280f3e 100644 --- a/src/Alma.Logging/Logger.cs +++ b/src/Alma.Logging/Logger.cs @@ -1,10 +1,11 @@ namespace Alma.Logging; -public class Logger : ILogger + +public class Logger : ILogger { public LogLevel DefaultLogLevel { get; } - public Logger(LogLevel defaultLogLevel) + public Logger(LogLevel defaultLogLevel, string topicName) { DefaultLogLevel = defaultLogLevel; } @@ -32,4 +33,12 @@ public class Logger : ILogger { Log(logMessage, LogLevel.Critical); } +} + +public class Logger : Logger, ILogger +{ + public Logger(LogLevel defaultLogLevel) : base(defaultLogLevel, typeof(T).Name) + { + + } } \ No newline at end of file diff --git a/src/Alma.Logging/LoggerFactory.cs b/src/Alma.Logging/LoggerFactory.cs index cc7d425..df57cb8 100644 --- a/src/Alma.Logging/LoggerFactory.cs +++ b/src/Alma.Logging/LoggerFactory.cs @@ -13,4 +13,9 @@ public class LoggerFactory : ILoggerFactory { return new Logger(DefaultLogLevel); } + + public ILogger CreateLogger(Type t) + { + return new Logger(DefaultLogLevel, t.Name); + } } \ No newline at end of file diff --git a/src/Alma/Program.cs b/src/Alma/Program.cs index 2cf724a..8546f1d 100644 --- a/src/Alma/Program.cs +++ b/src/Alma/Program.cs @@ -1,5 +1,8 @@ -using Alma.Command; +using System.IO; +using System; +using Alma.Command; using Alma.Command.Configure; +using Alma.Command.Diag; using Alma.Command.Help; using Alma.Command.Info; using Alma.Command.Install; @@ -19,6 +22,14 @@ public static class Program { InitLogging(); + var logger = AlmaLoggerFactory.CreateLogger(typeof(Program)); + + var workdir = GetWorkdir(logger); + if (workdir != null) + { + Environment.CurrentDirectory = workdir; + } + var services = new AlmaServiceProvider(); var repositoryConfiguration = services.GetService(); @@ -28,6 +39,33 @@ public static class Program await application.Run(args); } + private static string? GetWorkdir(ILogger logger) + { + var workdirProviders = new Dictionary> + { + {"ALMA_WORKDIR", () => Environment.GetEnvironmentVariable("ALMA_WORKDIR")}, + {"WORKDIR", () => Environment.GetEnvironmentVariable("WORKDIR")}, + }; + + foreach (var workdirProvider in workdirProviders) + { + var workdir = workdirProvider.Value(); + if (workdir != null) + { + if (Directory.Exists(workdir)) + { + return workdir; + } + else + { + logger.LogInformation($"{workdirProvider.Key} is set to {workdir} but this directory does not exist."); + } + } + } + + return null; + } + private static void InitLogging() { AlmaLoggerFactory = new LoggerFactory(); @@ -49,6 +87,7 @@ public static class Program [Singleton(typeof(ICommand), typeof(InstallCommand))] [Singleton(typeof(ICommand), typeof(HelpCommand))] [Singleton(typeof(ICommand), typeof(ConfigureCommand))] +[Singleton(typeof(ICommand), typeof(DiagCommand))] [Singleton(typeof(IModuleConfigurationResolver), typeof(ModuleConfigurationResolver))] [Singleton(typeof(IMetadataHandler), typeof(MetadataHandler))] [Singleton(typeof(IShellService), typeof(ShellService))]