Skip to content
Open
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 .github/workflows/publish-studio-linux.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ jobs:
dotnet-version: 10.0.x
- name: Publish
run: dotnet publish ./studio/OneWare.Studio.Desktop/OneWare.Studio.Desktop.csproj -c Release -r ${{ matrix.arch }} -o ./out
- name: Mark launchers executable
run: chmod +x ./out/oneware ./out/oneware-studio
- name: Compress
uses: a7ul/tar-action@v1.2.0
id: compress
Expand Down
1 change: 1 addition & 0 deletions OneWare.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
</Folder>
<Folder Name="/Studio/">
<Project Path="studio/OneWare.Studio.Browser/OneWare.Studio.Browser.csproj" />
<Project Path="studio/OneWare.Studio.Cli/OneWare.Studio.Cli.csproj" />
<Project Path="studio/OneWare.Studio.Desktop/OneWare.Studio.Desktop.csproj" />
<Project Path="studio/OneWare.Studio/OneWare.Studio.csproj" />
</Folder>
Expand Down
69 changes: 61 additions & 8 deletions docs/PluginDevelopment.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ and how UniversalFpgaProject support is wired so you can extend it safely.
## Quick start

1) Create a class library that references `OneWare.Essentials`.
2) Implement a module by deriving from `OneWare.Essentials.Services.OneWareModuleBase`.
3) Add a `compatibility.txt` file next to your plugin assemblies.
4) Package the plugin as a folder (assemblies + `compatibility.txt`) and install it via the plugin manager.
2) Implement a GUI module by deriving from `OneWare.Essentials.Services.OneWareModuleBase`.
3) Optionally implement a CLI module by deriving from `OneWare.Essentials.Services.OneWareCliModuleBase`.
4) Add a `compatibility.txt` file next to your plugin assemblies.
5) Package the plugin as a folder (assemblies + `compatibility.txt`) and install it via the plugin manager.

Minimal module example:

Expand Down Expand Up @@ -38,6 +39,33 @@ public sealed class MyPluginModule : OneWareModuleBase
}
```

Minimal CLI module example:

```csharp
using System.CommandLine;
using OneWare.Essentials.Services;

namespace MyCompany.MyPlugin;

public sealed class MyPluginCliModule : OneWareCliModuleBase
{
public override IReadOnlyList<Command> RegisterCommands(IServiceProvider serviceProvider)
{
var helloCommand = new Command("myplugin", "Commands for MyPlugin");
var pingCommand = new Command("ping", "Simple health check");

pingCommand.SetAction((_, _) =>
{
Console.WriteLine("pong");
return Task.FromResult(0);
});

helloCommand.Subcommands.Add(pingCommand);
return [helloCommand];
}
}
```

Minimal `compatibility.txt` (one dependency per line):

```
Expand Down Expand Up @@ -92,18 +120,33 @@ You can generate `compatibility.txt` automatically during build by marking depen
- A plugin must include a `compatibility.txt` file at its root. It lists assembly dependencies and
versions using `AssemblyName : Version` lines. The check is enforced by
`OneWare.Essentials.PackageManager.Compatibility.PluginCompatibilityChecker`.
- Modules are discovered by scanning assemblies for `IOneWareModule` implementations.
- If `IOneWareModule.RegisterServices` adds services, they are injected into the main container
before module initialization. If the app is already running, modules are initialized immediately.
- GUI modules are discovered by scanning assemblies for `IOneWareModule` implementations.
- CLI modules are discovered by scanning assemblies for `IOneWareCliModule` implementations.
- `IOneWareModule.RegisterServices` contributes services to the desktop app container before
`Initialize(IServiceProvider)` runs.
- `IOneWareCliModule.RegisterServices` contributes services to the CLI container before
`RegisterCommands(IServiceProvider)` runs.
- A single assembly may contain both GUI and CLI modules when the feature supports both surfaces.

## Module lifecycle and dependency injection

`IOneWareModule` is the entry point for plugins:
`IOneWareModule` is the entry point for desktop/UI plugins:

- `RegisterServices(IServiceCollection services)` lets you register your own services.
- `Initialize(IServiceProvider serviceProvider)` runs after services are registered.
- `Dependencies` allows declaring other module IDs to load before yours.

`IOneWareCliModule` is the entry point for CLI plugins:

- `RegisterServices(IServiceCollection services)` lets you register services needed by CLI commands.
- `RegisterCommands(IServiceProvider serviceProvider)` returns the commands your module contributes.
- `Dependencies` allows declaring other CLI module IDs that must load first.
- CLI module types must be public, non-abstract, and expose a parameterless constructor so the CLI loader can discover and instantiate them.

CLI modules are intentionally headless: they should not depend on Avalonia UI state, windows, or
desktop-only lifecycles. Prefer shared services for reusable logic and keep CLI-specific behavior in
the command layer.

Use `serviceProvider.Resolve<T>()` or `ContainerLocator.Current` to resolve OneWare services.

## OneWare.Essentials interfaces
Expand Down Expand Up @@ -142,6 +185,14 @@ The following sections document the core services and their key functions.
- `RegisterServices(IServiceCollection)`: add services to the DI container.
- `Initialize(IServiceProvider)`: run after services are registered.

#### `IOneWareCliModule` (src/OneWare.Essentials/Services/IOneWareCliModule.cs)

- `Id`: module identifier (defaults to class name in `OneWareCliModuleBase`).
- `Dependencies`: other CLI module IDs that must load before this one.
- `RegisterServices(IServiceCollection)`: add services to the CLI DI container.
- `RegisterCommands(IServiceProvider)`: return top-level CLI commands for registration.
- Module type: must be public, non-abstract, and expose a parameterless constructor for CLI discovery.

#### `IPluginService` (src/OneWare.Essentials/Services/IPluginService.cs)

- `InstalledPlugins`: current plugin list.
Expand Down Expand Up @@ -565,7 +616,9 @@ Use `IWindowService.RegisterUiExtension` to add UI to these extension points:
## Suggested validation and troubleshooting

- Ensure `compatibility.txt` matches the core dependency versions.
- Verify your module class is public and implements `IOneWareModule`.
- Verify your module class is public and implements `IOneWareModule` and/or `IOneWareCliModule`.
- If CLI commands do not appear under `oneware --help`, verify the assembly is copied with the
plugin and the command name does not collide with an existing top-level command or alias.
- If your UI does not appear, confirm you used the correct UI extension key.
- For FPGA tooling, confirm your toolchain ID matches the project `toolchain` property.

Expand Down
9 changes: 9 additions & 0 deletions snap/snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ icon: ./studio/OneWare.Studio/Assets/com.one_ware.OneWare.svg

apps:
oneware:
command: ./OneWareCLI
environment:
PATH: "$SNAP/usr/bin:$SNAP/bin:$PATH"
oneware-studio:
command: ./OneWareStudio
common-id: com.one_ware.OneWare
desktop: ./com.one_ware.OneWare.desktop
Expand Down Expand Up @@ -73,12 +77,17 @@ parts:
dotnet publish -r "$DOTNET_RID" -c Release -o "$SNAPCRAFT_PART_INSTALL"

chmod +x "$SNAPCRAFT_PART_INSTALL/OneWareStudio"
chmod +x "$SNAPCRAFT_PART_INSTALL/OneWareCLI"
chmod +x "$SNAPCRAFT_PART_INSTALL/AsmichiChildProcessHelper"
chmod +x "$SNAPCRAFT_PART_INSTALL/oneware"
chmod +x "$SNAPCRAFT_PART_INSTALL/oneware-studio"

file "$SNAPCRAFT_PART_INSTALL/OneWareStudio"
file "$SNAPCRAFT_PART_INSTALL/AsmichiChildProcessHelper"

sed -i 's|^Icon=.*$|Icon=${SNAP}/meta/gui/com.one_ware.OneWare.svg|' "$SNAPCRAFT_PART_INSTALL/com.one_ware.OneWare.desktop"
sed -i 's|^TryExec=oneware-studio$|TryExec=oneware.oneware-studio|' "$SNAPCRAFT_PART_INSTALL/com.one_ware.OneWare.desktop"
sed -i 's|^Exec=oneware-studio|Exec=oneware.oneware-studio|' "$SNAPCRAFT_PART_INSTALL/com.one_ware.OneWare.desktop"
stage-packages:
- libgtk-3-0
- libx11-6
Expand Down
130 changes: 130 additions & 0 deletions src/OneWare.CloudIntegration/OneWareCloudCliModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System.CommandLine;
using System.IO.Pipes;
using Microsoft.Extensions.DependencyInjection;
using OneWare.CloudIntegration.Services;
using OneWare.Essentials.Services;

namespace OneWare.CloudIntegration;

public sealed class OneWareCloudCliModule : OneWareCliModuleBase
{
public override void RegisterServices(IServiceCollection services)
{
services.AddSingleton<OneWareCloudLoginService>();
}

public override IReadOnlyList<Command> RegisterCommands(IServiceProvider serviceProvider)
{
EnsureSettingsInitialized(serviceProvider);

var cloudCommand = new Command("cloud", "OneWare Cloud commands");

var loginCommand = new Command("login", "Log in to OneWare Cloud");
loginCommand.SetAction((_, cancellationToken) => LoginAsync(serviceProvider, cancellationToken));

var logoutCommand = new Command("logout", "Log out of OneWare Cloud");
logoutCommand.SetAction((_, cancellationToken) => LogoutAsync(serviceProvider, cancellationToken));

cloudCommand.Subcommands.Add(loginCommand);
cloudCommand.Subcommands.Add(logoutCommand);
return [cloudCommand];
}

private static async Task<int> LoginAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var loginService = serviceProvider.GetRequiredService<OneWareCloudLoginService>();

Console.WriteLine("Opening browser for OneWare Cloud login...");

var success = await loginService.LoginAsync(cancellationToken);
if (!success)
{
Console.Error.WriteLine("OneWare Cloud login failed.");
return 1;
}

var userId = settingsService.GetSettingValue<string>(OneWareCloudIntegrationModule.OneWareAccountUserIdKey);
if (string.IsNullOrWhiteSpace(userId))
{
Console.Error.WriteLine("OneWare Cloud login did not produce a stored account.");
return 1;
}

Console.WriteLine("Logged in to OneWare Cloud.");
return 0;
}

private static async Task<int> LogoutAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var paths = serviceProvider.GetRequiredService<IPaths>();
var userId = settingsService.GetSettingValue<string>(OneWareCloudIntegrationModule.OneWareAccountUserIdKey);

if (string.IsNullOrWhiteSpace(userId))
{
Console.WriteLine("OneWare Cloud is already logged out.");
return 0;
}

try
{
serviceProvider.GetRequiredService<OneWareCloudLoginService>().Logout(userId);
settingsService.SaveValues(paths.SettingsPath,
new Dictionary<string, object?>
{
[OneWareCloudIntegrationModule.OneWareAccountUserIdKey] =
settingsService.GetSettingValue<string>(OneWareCloudIntegrationModule.OneWareAccountUserIdKey)
}, autoSave: false);

await NotifyRunningStudioAsync(cancellationToken);
Console.WriteLine("Logged out of OneWare Cloud.");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Failed to persist OneWare Cloud logout: {ex.Message}");
return 1;
}
}

private static void EnsureSettingsInitialized(IServiceProvider serviceProvider)
{
var settingsService = serviceProvider.GetRequiredService<ISettingsService>();
var paths = serviceProvider.GetRequiredService<IPaths>();

if (!settingsService.HasSetting(OneWareCloudIntegrationModule.OneWareCloudHostKey))
settingsService.Register(OneWareCloudIntegrationModule.OneWareCloudHostKey,
OneWareCloudIntegrationModule.OfficialHost);

if (!settingsService.HasSetting(OneWareCloudIntegrationModule.OneWareAccountUserIdKey))
settingsService.Register(OneWareCloudIntegrationModule.OneWareAccountUserIdKey, string.Empty);

settingsService.Load(paths.SettingsPath);
}

private static async Task NotifyRunningStudioAsync(CancellationToken cancellationToken)
{
try
{
await using var client = new NamedPipeClientStream(".", "oneware-studio-ipc", PipeDirection.Out);
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(TimeSpan.FromSeconds(2));
await client.ConnectAsync(timeout.Token);

await using var writer = new StreamWriter(client);
await writer.WriteAsync(OneWareCloudIntegrationModule.LogoutIpcMessage.AsMemory(), cancellationToken);
await writer.FlushAsync(cancellationToken);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
// Studio is not running or is not ready to receive IPC messages.
}
catch (IOException)
{
// Studio is not running or is not ready to receive IPC messages.
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class OneWareCloudIntegrationModule : OneWareModuleBase
public const string OneWareCloudHostKey = "General_OneWareCloud_Host";
public const string OneWareAccountUserIdKey = "General_OneWareCloud_AccountUserId";
public const string CredentialStore = OfficialHost;
public const string LogoutIpcMessage = "cloud-logout";

public override void RegisterServices(IServiceCollection services)
{
Expand Down
Loading
Loading