Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions SquidStd.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<Project Path="src/SquidStd.Caching.Abstractions/SquidStd.Caching.Abstractions.csproj"/>
<Project Path="src/SquidStd.Caching.Redis/SquidStd.Caching.Redis.csproj"/>
<Project Path="src/SquidStd.Caching/SquidStd.Caching.csproj"/>
<Project Path="src/SquidStd.ConsoleCommands/SquidStd.ConsoleCommands.csproj"/>
<Project Path="src/SquidStd.Core/SquidStd.Core.csproj"/>
<Project Path="src/SquidStd.Database.Abstractions/SquidStd.Database.Abstractions.csproj"/>
<Project Path="src/SquidStd.Database/SquidStd.Database.csproj"/>
Expand Down Expand Up @@ -55,6 +56,7 @@
<Project Path="samples/SquidStd.Samples.Networking/SquidStd.Samples.Networking.csproj"/>
<Project Path="samples/SquidStd.Samples.Persistence/SquidStd.Samples.Persistence.csproj"/>
<Project Path="samples/SquidStd.Samples.Commands/SquidStd.Samples.Commands.csproj"/>
<Project Path="samples/SquidStd.Samples.ConsoleCommands/SquidStd.Samples.ConsoleCommands.csproj"/>
<Project Path="samples/SquidStd.Samples.Email/SquidStd.Samples.Email.csproj"/>
<Project Path="samples/SquidStd.Samples.Database/SquidStd.Samples.Database.csproj"/>
<Project Path="samples/SquidStd.Samples.AspNetCore/SquidStd.Samples.AspNetCore.csproj"/>
Expand Down
11 changes: 11 additions & 0 deletions docs/articles/concepts/bootstrap-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ Services implementing `ISquidStdService` participate in the lifecycle. On `Start

The bootstrap logs its whole lifecycle: a startup banner with the application name and version (set `SquidStdOptions.AppName`; it defaults to the entry assembly name and is attached to every event as the `Application` / `ApplicationVersion` properties), a registration summary (per-registration detail at Debug), one line per service started with its duration, and the shutdown sequence. A service that fails to stop is logged as a warning and the remaining services are still stopped. Extra Serilog sinks can be plugged by registering `ILogEventSink` instances in the container before start.

When an event bus is registered, the bootstrap publishes `EngineStartingEvent`, `EngineStartedEvent` and `EngineStoppedEvent` on it during the lifecycle.
`EngineStartingEvent` is only visible to subscriptions made before start - auto-registered listeners start during the service loop.

```mermaid
sequenceDiagram
participant App
Expand All @@ -93,3 +96,11 @@ sequenceDiagram
## RunAsync for long-running hosts

For long-running hosts, call `RunAsync`. It starts every service and then blocks until cancellation, stopping services cleanly on shutdown. Resolve dependencies anywhere with `bootstrap.Resolve<T>()`. See the [architecture](architecture.md) overview for how the host fits the layers.

`RunAsync` also completes when a shutdown is requested through the shared lifetime - this is
what the `exit` command of SquidStd.ConsoleCommands does - and Ctrl+C performs an orderly
shutdown instead of killing the process:

```csharp
bootstrap.Container.Resolve<ISquidStdLifetime>().RequestShutdown();
```
1 change: 1 addition & 0 deletions docs/articles/consolecommands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[!include[](../../src/SquidStd.ConsoleCommands/README.md)]
2 changes: 2 additions & 0 deletions docs/articles/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@
href: aws-abstractions.md
- name: SquidStd.Tui
href: tui.md
- name: SquidStd.ConsoleCommands
href: consolecommands.md
- name: SquidStd.Crypto
href: crypto.md
- name: SquidStd.Secrets.Aws
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/scripting-lua.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ dotnet run --project samples/SquidStd.Samples.ScriptingLua
Prints:

```
lua modules = 1
lua modules = 3
3 + 4 = 7
result = hello from C# and lua
```
Expand Down
70 changes: 70 additions & 0 deletions samples/SquidStd.Samples.ConsoleCommands/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using SquidStd.ConsoleCommands.Attributes;
using SquidStd.ConsoleCommands.Data;
using SquidStd.ConsoleCommands.Extensions;
using SquidStd.ConsoleCommands.Interfaces;
using SquidStd.Core.Data.Bootstrap;
using SquidStd.Generators.ConsoleCommands;
using SquidStd.Services.Core.Extensions;
using SquidStd.Services.Core.Services.Bootstrap;

var bootstrap = SquidStdBootstrap.Create(
new SquidStdOptions
{
ConfigName = "squidstd",
RootDirectory = AppContext.BaseDirectory,
AppName = "ConsoleDemo"
}
);

bootstrap.ConfigureServices(
container =>
{
container.RegisterCoreServices();
container.AddConsoleCommands();
container.RegisterGeneratedConsoleCommands();

return container;
}
);

// The prompt sink renders the log lines: disable the standard console sink to avoid duplicates.
bootstrap.OnConfigLoaded<SquidStdLoggerOptions>(o => o.EnableConsole = false);

await bootstrap.StartAsync();

var commands = bootstrap.Resolve<ICommandSystemService>();
commands.RegisterCommand(
"echo",
ctx =>
{
ctx.WriteLine(string.Join(' ', ctx.Arguments));

return Task.CompletedTask;
},
"Echoes the arguments back."
);

foreach (var line in await commands.ExecuteCommandWithOutputAsync("help"))
{
Console.WriteLine(line);
}

foreach (var line in await commands.ExecuteCommandWithOutputAsync("ping"))
{
Console.WriteLine(line);
}

await bootstrap.StopAsync();

[RegisterConsoleCommand("ping|p", "Replies pong.")]
internal sealed class PingCommand : IConsoleCommandExecutor
{
public string Description => "Replies pong.";

public Task ExecuteAsync(ConsoleCommandContext context)
{
context.WriteLine("pong");

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\SquidStd.ConsoleCommands\SquidStd.ConsoleCommands.csproj"/>
<ProjectReference Include="..\..\src\SquidStd.Services.Core\SquidStd.Services.Core.csproj"/>
<ProjectReference Include="..\..\src\SquidStd.Generators\SquidStd.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>

</Project>
12 changes: 12 additions & 0 deletions samples/SquidStd.Samples.ScriptingLua/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using SquidStd.Scripting.Lua.Attributes;
using SquidStd.Scripting.Lua.Attributes.Scripts;
using SquidStd.Scripting.Lua.Data.Config;
using SquidStd.Scripting.Lua.Extensions.Scripts;
using SquidStd.Scripting.Lua.Interfaces.Scripts;
using SquidStd.Scripting.Lua.Modules;
using SquidStd.Scripting.Lua.Services;
using SquidStd.Core.Data.Bootstrap;
using SquidStd.Services.Core.Extensions;
Expand Down Expand Up @@ -38,6 +40,8 @@
container.RegisterInstance(engineConfig);
container.RegisterStdService<IScriptEngineService, LuaScriptEngineService>();
container.RegisterGeneratedScriptModules();
container.RegisterScriptModule<LogModule>();
container.RegisterLuaEvents();

return container;
}
Expand All @@ -64,6 +68,14 @@

#endregion

engine.ExecuteScript(
"""
events.subscribe("engine_stopped", function(e)
log.info("lua saw engine_stopped for " .. e.application)
end)
"""
);

await bootstrap.StopAsync();

#region step-3
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace SquidStd.ConsoleCommands.Attributes;

/// <summary>
/// Marks an <see cref="Interfaces.IConsoleCommandExecutor" /> class for source-generated registration.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class RegisterConsoleCommandAttribute : Attribute
{
/// <summary>Primary command name or aliases separated by <c>|</c>.</summary>
public string CommandName { get; }

/// <summary>Help description.</summary>
public string Description { get; }

/// <summary>Initializes the attribute.</summary>
/// <param name="commandName">Primary command name or aliases separated by <c>|</c>.</param>
/// <param name="description">Help description.</param>
public RegisterConsoleCommandAttribute(string commandName, string description = "")
{
CommandName = commandName;
Description = description;
}
}
16 changes: 16 additions & 0 deletions src/SquidStd.ConsoleCommands/Data/Config/ConsoleCommandsConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace SquidStd.ConsoleCommands.Data.Config;

/// <summary>
/// Configuration for the interactive console command prompt.
/// </summary>
public sealed class ConsoleCommandsConfig
{
/// <summary>Prompt prefix shown on the input row.</summary>
public string Prompt { get; set; } = "squid> ";

/// <summary>Whether the prompt starts locked.</summary>
public bool StartLocked { get; set; } = true;

/// <summary>Character that unlocks the prompt when locked.</summary>
public char UnlockCharacter { get; set; } = '*';
}
32 changes: 32 additions & 0 deletions src/SquidStd.ConsoleCommands/Data/ConsoleCommandContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace SquidStd.ConsoleCommands.Data;

/// <summary>
/// Execution context handed to a console command handler.
/// </summary>
public sealed class ConsoleCommandContext
{
private readonly Action<string> _writeLine;

/// <summary>The raw command line as typed.</summary>
public string RawText { get; }

/// <summary>Parsed arguments (command name excluded).</summary>
public IReadOnlyList<string> Arguments { get; }

/// <summary>Initializes the context.</summary>
/// <param name="rawText">Raw command line.</param>
/// <param name="arguments">Parsed arguments.</param>
/// <param name="writeLine">Writer for command output lines.</param>
public ConsoleCommandContext(string rawText, IReadOnlyList<string> arguments, Action<string> writeLine)
{
ArgumentNullException.ThrowIfNull(writeLine);

RawText = rawText;
Arguments = arguments;
_writeLine = writeLine;
}

/// <summary>Writes one output line.</summary>
public void WriteLine(string line)
=> _writeLine(line);
}
9 changes: 9 additions & 0 deletions src/SquidStd.ConsoleCommands/Data/ConsoleCommandDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace SquidStd.ConsoleCommands.Data;

/// <summary>
/// Describes a registered console command.
/// </summary>
/// <param name="Name">Primary command name.</param>
/// <param name="Aliases">Additional aliases.</param>
/// <param name="Description">Help description.</param>
public sealed record ConsoleCommandDefinition(string Name, IReadOnlyList<string> Aliases, string Description);
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using DryIoc;
using Serilog.Core;
using SquidStd.Abstractions.Extensions.Config;
using SquidStd.Abstractions.Extensions.Services;
using SquidStd.ConsoleCommands.Data.Config;
using SquidStd.ConsoleCommands.Interfaces;
using SquidStd.ConsoleCommands.Internal;
using SquidStd.ConsoleCommands.Internal.Logging;
using SquidStd.ConsoleCommands.Services;
using SquidStd.Core.Interfaces.Bootstrap;
using SquidStd.Core.Interfaces.Lifecycle;

namespace SquidStd.ConsoleCommands.Extensions;

/// <summary>
/// Registration helpers for the interactive console command stack.
/// </summary>
public static class ConsoleCommandsRegistrationExtensions
{
/// <param name="container">Container that receives the registrations.</param>
extension(IContainer container)
{
/// <summary>
/// Registers the console command stack: the prompt UI, the command system with the
/// built-in help/clear/exit commands, and the Serilog sink that renders log lines above
/// the prompt. Set <c>logger.EnableConsole: false</c> so the sink replaces the standard
/// console sink.
/// </summary>
/// <returns>The same container for chaining.</returns>
public IContainer AddConsoleCommands()
{
container.RegisterConfigSection("consoleCommands", static () => new ConsoleCommandsConfig(), -50);
container.Register<IConsoleUiService, ConsoleUiService>(Reuse.Singleton);
container.RegisterDelegate<ICommandSystemService>(
resolver =>
{
var ui = resolver.Resolve<IConsoleUiService>();
var system = new CommandSystemService(ui.WriteLine);
var lifetime = resolver.Resolve<ISquidStdLifetime>(IfUnresolved.ReturnDefault);
var bootstrap = resolver.Resolve<ISquidStdBootstrap>(IfUnresolved.ReturnDefault);
BuiltinConsoleCommands.Register(
system,
lifetime,
bootstrap,
static () =>
{
if (!Console.IsOutputRedirected)
{
Console.Clear();
}
}
);

return system;
},
Reuse.Singleton
);
container.RegisterDelegate<ILogEventSink>(
resolver => new ConsolePromptLogSink(resolver.Resolve<IConsoleUiService>()),
Reuse.Singleton
);
container.RegisterStdService<ConsoleInputLoopService, ConsoleInputLoopService>(500);

return container;
}
}
}
36 changes: 36 additions & 0 deletions src/SquidStd.ConsoleCommands/Interfaces/ICommandSystemService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using SquidStd.ConsoleCommands.Data;

namespace SquidStd.ConsoleCommands.Interfaces;

/// <summary>
/// Registers and dispatches interactive console commands.
/// </summary>
public interface ICommandSystemService
{
/// <summary>Registers one command or multiple aliases separated by <c>|</c>.</summary>
/// <param name="commandName">Primary name or alias list, e.g. <c>"gc|collect"</c>.</param>
/// <param name="handler">Handler invoked with the parsed context.</param>
/// <param name="description">Help description.</param>
/// <param name="autocompleteProvider">Optional argument suggestions for the current line.</param>
void RegisterCommand(
string commandName,
Func<ConsoleCommandContext, Task> handler,
string description = "",
Func<string, IReadOnlyList<string>>? autocompleteProvider = null
);

/// <summary>Executes a raw command line; failures are reported, never thrown.</summary>
Task ExecuteCommandAsync(string commandWithArgs, CancellationToken cancellationToken = default);

/// <summary>Executes a raw command line collecting the lines it writes.</summary>
Task<IReadOnlyList<string>> ExecuteCommandWithOutputAsync(
string commandWithArgs,
CancellationToken cancellationToken = default
);

/// <summary>Gets suggestions for the current input line.</summary>
IReadOnlyList<string> GetAutocompleteSuggestions(string commandWithArgs);

/// <summary>Gets the registered command definitions.</summary>
IReadOnlyList<ConsoleCommandDefinition> GetRegisteredCommands();
}
15 changes: 15 additions & 0 deletions src/SquidStd.ConsoleCommands/Interfaces/IConsoleCommandExecutor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using SquidStd.ConsoleCommands.Data;

namespace SquidStd.ConsoleCommands.Interfaces;

/// <summary>
/// Contract for attribute-registered console command classes.
/// </summary>
public interface IConsoleCommandExecutor
{
/// <summary>Help description of the command.</summary>
string Description { get; }

/// <summary>Executes the command.</summary>
Task ExecuteAsync(ConsoleCommandContext context);
}
Loading
Loading