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
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ Imagine a scenario where sensor data streams in via MQTT, needs to be stored, an
First, we compose our MQTT client bridge in `ensamble.tw`:

```tinkwell
// ensamble.tw
compose service mqtt_bridge "Tinkwell.Bridge.MqttClient.dll" {
topic_filter: "sensor/+"
}
Expand All @@ -42,7 +41,6 @@ compose service mqtt_bridge "Tinkwell.Bridge.MqttClient.dll" {
Next, we define our measures and a signal in `measures.twm`. The MQTT bridge will automatically update `temperature_sensor_1` when data arrives on `sensor/temperature_sensor_1`.

```tinkwell
// measures.twm
measure temperature_sensor_1 {
type: "Temperature"
unit: "DegreeCelsius"
Expand All @@ -56,12 +54,11 @@ measure temperature_sensor_1 {
Finally, we define an action in `actions.twa` to log the alert:

```tinkwell
// actions.twa
when event high_temperature {
then {
mqtt_send {
topic: "home/ac/living_room/set"
payload: "{ \"power\": \"ON\" }"
payload: "make_json('power', 'ON')"
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions Source/Directory.Build.Props
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
<Description>Foundation runtime for edge-based IoT applications</Description>
<Copyright>(c) Adriano Repetti 2025</Copyright>

<InformationalVersion>1.0.0.0</InformationalVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Version>0.1.0.0</Version>
<InformationalVersion>0.1.0.0</InformationalVersion>
<AssemblyVersion>0.1.0.0</AssemblyVersion>
<FileVersion>0.1.0.0</FileVersion>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public async Task ConfigureBuilderAsync(IHostBuilder builder, CancellationToken
registrar.ConfigureServices(host);
}

_logger.LogInformation("{Name} loaded {Count} runner(s): {Runners}",
_logger.LogDebug("{Name} loaded {Count} runner(s): {Runners}",
Environment.GetEnvironmentVariable(WellKnownNames.RunnerNameEnvironmentVariable),
_dlls?.Count(),
string.Join(',', _dlls!.Select(x => Trim(x.Name))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ await ForEachRegistrarAsync(
(host, registrar) => registrar.ConfigureServices(host),
cancellationToken);

_logger.LogInformation("{Name} loaded {Count} runner(s): {Runners}",
_logger.LogDebug("{Name} loaded {Count} runner(s): {Runners}",
Environment.GetEnvironmentVariable(WellKnownNames.RunnerNameEnvironmentVariable),
_services?.Count(),
string.Join(',', _services!.Select(x => Trim(x.Name))));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json;
using NCalc.Handlers;
using System.Text.Json;

namespace Tinkwell.Bootstrapper.Expressions.CustomFunctions;

Expand Down Expand Up @@ -85,3 +86,18 @@ sealed class JsonGetBoolean : UnaryFunction<JsonElement>
protected override object? Call(JsonElement element)
=> element.GetBoolean();
}

sealed class MakeJson : NCalcCustomFunction
{
public override object? Call(FunctionArgs args)
{
if (args.Parameters.Length / 2 > 0)
throw new ArgumentException($"Function {Name}() requires an even number of arguments. You passed {args.Parameters.Length}.");

var parameters = args.EvaluateParameters();
var dictionary = new Dictionary<string, object?>();
for (int i = 0; i < parameters.Length - 1; i++)
dictionary.Add(ChangeType<string>(parameters[i]), parameters[i + 1]);
return JsonSerializer.Serialize(dictionary);
}
}
15 changes: 15 additions & 0 deletions Source/Tinkwell.Bootstrapper/Hosting/HostingInformation.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Configuration;
using Tinkwell.Bootstrapper.Ipc;
Expand All @@ -11,6 +12,20 @@ namespace Tinkwell.Bootstrapper.Hosting;
/// </summary>
public static class HostingInformation
{
/// <summary>
/// Gets the application version number.
/// </summary>
public static string ApplicationVersion
{
get
{
var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()!;
return assembly!.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version
?? assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? "0.0.0.0";
}
}

/// <summary>
/// Gets the default directory (at application level) where firmlets can store data.
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions Source/Tinkwell.Bootstrapper/Hosting/IApplicationInitializer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Tinkwell.Bootstrapper.Hosting;

/// <summary>
/// Generic interface implemented by plugins in charge of performing an initialization action.
/// </summary>
/// <remarks>
/// Usually this interface is used for actions that do not need the rest of the application: for
/// example setting up the environment and similar tasks.
/// </remarks>
public interface IApplicationInitializer
{
/// <summary>
/// Performs an initialization.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A <c>Task</c> representing the asynchronous operation.</returns>
Task InitializeAsync(CancellationToken cancellationToken);
}
11 changes: 7 additions & 4 deletions Source/Tinkwell.Cli/CommandAppExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ public static CommandApp AddCommandsViaReflection(this CommandApp app)
branch.SetDescription(branchCommand.Description);
foreach (var subCommand in allCommands.Where(x => x.Parent == branchCommand.Type))
{
branch
var configurator = branch
.AddCommand(subCommand.Name, subCommand.Type)
.WithDescription(subCommand.Description);

if (!string.IsNullOrWhiteSpace(subCommand.Alias))
configurator.WithAlias(subCommand.Alias);
}
});
}
Expand All @@ -35,7 +38,7 @@ public static CommandApp AddCommandsViaReflection(this CommandApp app)
return app;
}

private static IEnumerable<(Type Type, Type? Parent, string Name, string Description)> FindAllCommands()
private static IEnumerable<(Type Type, Type? Parent, string Name, string Description, string? Alias)> FindAllCommands()
{
// Some commands might have dependencies that cause errors at run-time, we load this platform-specific
// assemblies only if we know that they're going to work.
Expand All @@ -55,14 +58,14 @@ public static CommandApp AddCommandsViaReflection(this CommandApp app)
return inThisAssembly.Concat(extraCommands).ToArray();
}

private static IEnumerable<(Type Type, Type? Parent, string Name, string Description)> FindAllCommands(Assembly assembly)
private static IEnumerable<(Type Type, Type? Parent, string Name, string Description, string? Alias)> FindAllCommands(Assembly assembly)
{
var types = assembly.GetTypes().Where(IsCommand);
foreach (var type in types)
{
var attribute = type.GetCustomAttribute<CommandForAttribute>()!;
var description = type.GetCustomAttribute<DescriptionAttribute>()?.Description ?? "";
yield return (type, attribute.Parent, attribute.Name, description);
yield return (type, attribute.Parent, attribute.Name, description, attribute.Alias);
}
}

Expand Down
19 changes: 15 additions & 4 deletions Source/Tinkwell.Cli/CommandForAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,28 @@ namespace Tinkwell.Cli;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class CommandForAttribute : Attribute
{
public CommandForAttribute(string name) : this(name, null)
public CommandForAttribute(string name, string? alias, Type? parent)
{
Parent = parent;
Alias = alias;
Name = name;
}

public CommandForAttribute(string name, Type? parent)
public CommandForAttribute(string name) : this(name, null, null)
{
}

public CommandForAttribute(string name, string? alias) : this(name, alias, null)
{
}

public CommandForAttribute(string name, Type? parent) : this(name, null, parent)
{
Parent = parent;
Name = name;
}

public Type? Parent { get; }

public string? Alias { get; }

public string Name { get; }
}
2 changes: 1 addition & 1 deletion Source/Tinkwell.Cli/Commands/Actions/ActionsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Tinkwell.Cli.Commands.Actions;

[CommandFor("actions")]
[CommandFor("actions", alias: "executor")]
[Description("Inspect the actions configuration file.")]
public sealed class ActionsCommand : Command<ActionsCommand.Settings>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Tinkwell.Cli.Commands.Contracts;

[CommandFor("contracts")]
[CommandFor("contracts", alias: "services")]
[Description("Query registered services.")]
sealed class ContractsCommand : Command<ContractsCommand.Settings>
{
Expand Down
2 changes: 1 addition & 1 deletion Source/Tinkwell.Cli/Commands/Measures/MeasuresCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Tinkwell.Cli.Commands.Measures;

[CommandFor("measures")]
[CommandFor("measures", alias: "reducer")]
[Description("Manage and inspect measures and conditions.")]
sealed class MeasuresCommand : Command<MeasuresCommand.Settings>
{
Expand Down
45 changes: 45 additions & 0 deletions Source/Tinkwell.Cli/Commands/RootCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Spectre.Console;
using Spectre.Console.Cli;
using System.ComponentModel;
using System.Reflection;

namespace Tinkwell.Cli.Commands;

sealed class RootCommand : Command<RootCommand.Settings>
{
public class Settings : CommandSettings
{
[CommandOption("-v|--version")]
[Description("Prints version information")]
public bool ShowVersion { get; set; }

[CommandOption("--verbose")]
[Description("Verbose output")]
public bool Verbose { get; set; }
}

public override int Execute(CommandContext context, Settings settings)
{
if (settings.ShowVersion)
{
var assembly = Assembly.GetExecutingAssembly();
var productName = assembly.GetCustomAttribute<AssemblyProductAttribute>()?.Product ?? "";
var productVersion = assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "";

if (settings.Verbose)
{
var informationalVersion = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "";
AnsiConsole.WriteLine($"{productName} version {productVersion} ({informationalVersion})");
}
else
{
AnsiConsole.WriteLine($"{productName} version {productVersion}");
}

return ExitCode.Ok;
}

Console.WriteLine("Run with --help for usage.");
return ExitCode.Ok;
}
}
12 changes: 10 additions & 2 deletions Source/Tinkwell.Cli/Commands/Supervisor/SendCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,16 @@ private async Task<int> ExecuteForToolAsync(CommandContext context, Settings set
}
finally
{
if (client.IsConnected)
await client.SendCommandAsync("exit");
try
{
if (client.IsConnected)
await client.SendCommandAsync("exit");
}
catch
{
// Ignore this, the Supervisor might have closed the pipe
// already (for example because of a shutdown command).
}
}

return exitCode;
Expand Down
4 changes: 4 additions & 0 deletions Source/Tinkwell.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Spectre.Console;
using Spectre.Console.Cli;
using Tinkwell.Cli;
using Tinkwell.Cli.Commands;

var app = new CommandApp();
app
Expand All @@ -15,4 +16,7 @@
});
});


app.SetDefaultCommand<RootCommand>();

await app.RunAsync(args);
14 changes: 14 additions & 0 deletions Source/Tinkwell.Supervisor/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Tinkwell.Bootstrapper;
using Tinkwell.Bootstrapper.Hosting;
using Tinkwell.Supervisor;

Expand All @@ -8,6 +9,19 @@
Directory.CreateDirectory(HostingInformation.ApplicationDataDirectory);
Directory.CreateDirectory(HostingInformation.UserDataDirectory);

// External plugins have a chance to perform some initializations.
var initializers = StrategyAssemblyLoader
.LoadAssemblies(typeof(IRegistry).Namespace!, "Init")
.SelectMany(StrategyAssemblyLoader.FindTypesImplementing<IStrategyImplementationResolver>)
.Select(Activator.CreateInstance)
.Cast<IStrategyImplementationResolver>()
.Select(x => x.GetImplementationType(typeof(IApplicationInitializer)))
.Where(x => x is not null)
.Select(x => (IApplicationInitializer)Activator.CreateInstance(x!)!);

foreach (var initializer in initializers)
await initializer.InitializeAsync(CancellationToken.None);

var host = Host.CreateDefaultBuilder(args)
.AddWorker()
.ConfigureLogging(logging =>
Expand Down
4 changes: 2 additions & 2 deletions Source/Tinkwell.Supervisor/Sentinel/ChildProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public void Start(bool watch)
Interlocked.Exchange(ref _stopping, false);
_watching = watch;
_process = Process.Start(_startInfo);
_logger.LogInformation("Started process {Name} ({PID}) from {Path}.",
_process?.ProcessName, _process?.Id, _startInfo.FileName);
_logger.LogDebug("Started process {Name} ({PID}) from {Path} {Arguments}",
_process?.ProcessName, _process?.Id, _startInfo.FileName, _startInfo.Arguments);

if (_process is not null && watch)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ public SentinelProcessBuilder(IConfiguration configuration, ILogger<SentinelProc
{
_configuration = configuration;
_logger = logger;
_logger.LogInformation("Working directory: {Path}", HostingInformation.WorkingDirectory);
_logger.LogInformation("Current directory: {Path}", Environment.CurrentDirectory);
_logger.LogInformation("Executables directory: {Path}", StrategyAssemblyLoader.GetAppPath());
}

public IChildProcess Create(RunnerDefinition definition)
Expand Down
10 changes: 10 additions & 0 deletions Source/Tinkwell.Supervisor/Worker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
using Tinkwell.Bootstrapper;
using Tinkwell.Bootstrapper.Ensamble;
using Tinkwell.Bootstrapper.Expressions;
using Tinkwell.Bootstrapper.Hosting;
Expand All @@ -26,6 +28,14 @@ public Worker(IHost host, ILogger<Worker> logger, IConfiguration configuration,

public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Application version: {Version}", HostingInformation.ApplicationVersion);
_logger.LogDebug("OS: {OS} ({Architecture}). Process: {Process}, Runtime: {Runtime}",
RuntimeInformation.OSDescription, RuntimeInformation.OSArchitecture,
RuntimeInformation.ProcessArchitecture, RuntimeInformation.FrameworkDescription);
_logger.LogDebug("Working directory: {Path}", HostingInformation.WorkingDirectory);
_logger.LogDebug("Current directory: {Path}", Environment.CurrentDirectory);
_logger.LogDebug("Executables directory: {Path}", StrategyAssemblyLoader.GetAppPath());

if (!File.Exists(_ensambleFilePath))
{
await PanicAsync($"Ensamble file not found: '{_ensambleFilePath}'.");
Expand Down