Skip to content

Commit

Permalink
Implement Extensions System (#262)
Browse files Browse the repository at this point in the history
* create base implementation

* ensure proper deserialization

* ensure calling activate/deactivate doesn't crash

* ensure extension loading/unloading

* major refactoring
- removed `Newtonsoft.Json` in favor of `System.Text.Json`
- refactored `VendorExtension` to allow for testing
- refactored file-backed vendor extensions
- renamed `BuiltIn` to `Host`
- implement basic vendor APIs
- add tests

* further refactor and add tests

* fix failing test

Co-authored-by: Ayane Satomi <[email protected]>
  • Loading branch information
LeNitrous and sr229 authored Mar 7, 2022
1 parent 2577598 commit 796ff07
Show file tree
Hide file tree
Showing 27 changed files with 1,149 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Report
uses: dorny/[email protected]
with:
artifact: TestResults-${{ inputs.name }}
artifact: TestResults-Vignette.Core.Tests
name: Test Results
path: "*.trx"
reporter: dotnet-trx
1 change: 1 addition & 0 deletions Vignette.Universal.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"projects": [
"src\\desktop\\Vignette.Desktop\\Vignette.Desktop.csproj",
"src\\core\\Vignette.Core\\Vignette.Core.csproj",
"tests\\Vignette.Core.Tests\\Vignette.Core.Tests.csproj"
]
}
}
1 change: 1 addition & 0 deletions Vignette.Windows.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"projects": [
"src\\desktop\\Vignette.Desktop.Windows\\Vignette.Desktop.Windows.csproj",
"src\\core\\Vignette.Core\\Vignette.Core.csproj",
"tests\\Vignette.Core.Tests\\Vignette.Core.Tests.csproj"
]
}
}
19 changes: 18 additions & 1 deletion Vignette.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 16
VisualStudioVersion = 16.0.0.0
Expand All @@ -15,6 +15,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vignette.Desktop", "src\des
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vignette.Desktop.Windows", "src\desktop\Vignette.Desktop.Windows\Vignette.Desktop.Windows.csproj", "{F2EC1766-D8F0-4AAF-8E61-E58DA667F273}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9382184D-E916-4D97-B545-4885E19F362B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vignette.Core.Tests", "tests\Vignette.Core.Tests\Vignette.Core.Tests.csproj", "{22C61C8D-5399-4F4F-AAA4-A822CA058A60}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -61,12 +65,25 @@ Global
{F2EC1766-D8F0-4AAF-8E61-E58DA667F273}.Release|x64.Build.0 = Release|Any CPU
{F2EC1766-D8F0-4AAF-8E61-E58DA667F273}.Release|x86.ActiveCfg = Release|Any CPU
{F2EC1766-D8F0-4AAF-8E61-E58DA667F273}.Release|x86.Build.0 = Release|Any CPU
{22C61C8D-5399-4F4F-AAA4-A822CA058A60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{22C61C8D-5399-4F4F-AAA4-A822CA058A60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{22C61C8D-5399-4F4F-AAA4-A822CA058A60}.Debug|x64.ActiveCfg = Debug|Any CPU
{22C61C8D-5399-4F4F-AAA4-A822CA058A60}.Debug|x64.Build.0 = Debug|Any CPU
{22C61C8D-5399-4F4F-AAA4-A822CA058A60}.Debug|x86.ActiveCfg = Debug|Any CPU
{22C61C8D-5399-4F4F-AAA4-A822CA058A60}.Debug|x86.Build.0 = Debug|Any CPU
{22C61C8D-5399-4F4F-AAA4-A822CA058A60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{22C61C8D-5399-4F4F-AAA4-A822CA058A60}.Release|Any CPU.Build.0 = Release|Any CPU
{22C61C8D-5399-4F4F-AAA4-A822CA058A60}.Release|x64.ActiveCfg = Release|Any CPU
{22C61C8D-5399-4F4F-AAA4-A822CA058A60}.Release|x64.Build.0 = Release|Any CPU
{22C61C8D-5399-4F4F-AAA4-A822CA058A60}.Release|x86.ActiveCfg = Release|Any CPU
{22C61C8D-5399-4F4F-AAA4-A822CA058A60}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{AB5C2DA7-E113-4FBB-A15E-F861839975CC} = {074EAA67-8F73-4FE6-A559-3BDE0730F5CA}
{4DFC1ACE-88EF-43F1-B2C2-4EF3D97983D3} = {AB5C2DA7-E113-4FBB-A15E-F861839975CC}
{6DA85738-CC0C-4032-B4E0-A5C7B0359569} = {074EAA67-8F73-4FE6-A559-3BDE0730F5CA}
{F9E6FD1F-ECEB-4D6B-AF7E-0D956F162CFF} = {6DA85738-CC0C-4032-B4E0-A5C7B0359569}
{F2EC1766-D8F0-4AAF-8E61-E58DA667F273} = {6DA85738-CC0C-4032-B4E0-A5C7B0359569}
{22C61C8D-5399-4F4F-AAA4-A822CA058A60} = {9382184D-E916-4D97-B545-4885E19F362B}
EndGlobalSection
EndGlobal
117 changes: 117 additions & 0 deletions src/core/Vignette.Core/Extensions/Extension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) The Vignette Authors
// Licensed under GPL-3.0 (With SDK Exception). See LICENSE for details.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Vignette.Core.Extensions
{
public abstract class Extension : IExtension
{
public abstract string Name { get; }
public abstract string Author { get; }
public abstract string Description { get; }
public abstract string Identifier { get; }
public abstract Version Version { get; }
public bool Activated { get; private set; }
protected ExtensionSystem ExtensionSystem { get; private set; }
protected IReadOnlyDictionary<string, object> Channels => channels;
private readonly Dictionary<string, object> channels = new Dictionary<string, object>();

public void Activate(ExtensionSystem extensionSystem)
{
if (Activated)
return;

ExtensionSystem = extensionSystem ?? throw new ArgumentNullException(nameof(extensionSystem));

Initialize();

Activated = true;
}

public void Deactivate()
{
if (!Activated)
return;

Destroy();

ExtensionSystem = null;
Activated = false;
}

public virtual Task<bool> TryDispatchAsync(IExtension actor, string channel, out object value, CancellationToken token = default, params object[] args)
{
if (!channels.TryGetValue(channel, out var item))
{
value = new ChannelNotFoundException();
return Task.FromResult(false);
}

try
{
value = Invoke(item, args);
return Task.FromResult(true);
}
catch (Exception e)
{
value = e;
return Task.FromResult(false);
}
}

public bool TryDispatch(IExtension actor, string channel, out object value, params object[] args)
=> TryDispatchAsync(actor, channel, out value, default, args).GetAwaiter().GetResult();

public async Task<object> DispatchAsync(IExtension actor, string channel, params object[] args)
{
await TryDispatchAsync(actor, channel, out var value, default, args);

if (value is Exception e)
throw e;

return value;
}

public object Dispatch(IExtension actor, string channel, params object[] args)
=> DispatchAsync(actor, channel, args).GetAwaiter().GetResult();

public async Task<T> DispatchAsync<T>(IExtension actor, string channel, params object[] args)
=> (T)await DispatchAsync(actor, channel, args);

public T Dispatch<T>(IExtension actor, string channel, params object[] args)
=> (T)Dispatch(actor, channel, args);

protected virtual void Initialize()
{
}

protected virtual void Destroy()
{
}

protected bool Register(string channel, object action) => channels.TryAdd(channel, action);
protected void Unregister(string channel) => channels.Remove(channel);
protected abstract object Invoke(object method, params object[] args);

public bool Equals(IExtension other)
=> Name.Equals(other.Name) && Author.Equals(other.Author)
&& Description.Equals(other.Description) && Identifier.Equals(other.Identifier)
&& Version.Equals(other.Version);

public override bool Equals(object obj)
=> Equals(obj as IExtension);

public override int GetHashCode()
=> HashCode.Combine(Name, Author, Description, Identifier, Version);

public override string ToString() => $@"{Identifier} ({Version.ToString(3)})";
}

public class ChannelNotFoundException : Exception
{
}
}
14 changes: 14 additions & 0 deletions src/core/Vignette.Core/Extensions/ExtensionIntents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) The Vignette Authors
// Licensed under GPL-3.0 (With SDK Exception). See LICENSE for details.

using System;

namespace Vignette.Core.Extensions
{
[Flags]
public enum ExtensionIntents
{
None,
Files,
}
}
11 changes: 11 additions & 0 deletions src/core/Vignette.Core/Extensions/ExtensionMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) The Vignette Authors
// Licensed under GPL-3.0 (With SDK Exception). See LICENSE for details.

namespace Vignette.Core.Extensions.Vendor
{
public enum ExtensionMode
{
Production,
Development,
}
}
92 changes: 92 additions & 0 deletions src/core/Vignette.Core/Extensions/ExtensionSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) The Vignette Authors
// Licensed under GPL-3.0 (With SDK Exception). See LICENSE for details.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.ClearScript.V8;
using Stride.Core;
using Stride.Core.Annotations;
using Stride.Games;
using Vignette.Core.Extensions.Host;
using Vignette.Core.Extensions.Vendor;

namespace Vignette.Core.Extensions
{
public class ExtensionSystem : GameSystemBase
{
public readonly V8Runtime Runtime = new V8Runtime();
public IReadOnlyCollection<IExtension> Loaded => extensions;
private readonly HashSet<Extension> extensions = new HashSet<Extension>();

public ExtensionSystem([NotNull] IServiceRegistry registry)
: base(registry)
{
}

public void Load(Extension extension)
{
if (extension == null)
throw new ArgumentNullException(nameof(extension));

if (Loaded.Contains(extension, EqualityComparer<IExtension>.Default))
throw new ExtensionLoadException(@"Extension is already loaded.");

if (extension is VendorExtension vendored)
{
var missing = vendored.Dependencies?.Where(dep => !Loaded.Any(ext => ext.Identifier == dep.Identifier && ext.Version >= dep.Version));

if (missing.Any())
throw new ExtensionLoadException(@"Failed to load extension as one or more dependencies have not been met.");
}

if (extensions.Add(extension))
extension.Activate(this);
}

public void Unload(Extension extension)
{
if (extension == null)
throw new ArgumentNullException(nameof(extension));

if (!Loaded.Contains(extension, EqualityComparer<IExtension>.Default))
throw new ExtensionUnloadException(@"Extension is not loaded.");

if (extension is VendorExtension vendored)
{
var allDependencies = Loaded.OfType<VendorExtension>().SelectMany(ext => ext.Dependencies).Distinct(EqualityComparer<VendorExtensionDependency>.Default);
if (allDependencies.Any(dep => extension.Identifier == dep.Identifier && extension.Version >= dep.Version))
throw new ExtensionUnloadException(@"Failed to unload extension as one or more extensions depend on it.");
}

if (extensions.Remove(extension) && extension is not HostExtension)
extension.Deactivate();
}

protected override void Destroy()
{
base.Destroy();

foreach (var ext in Loaded.OfType<VendorExtension>())
ext.Dispose();

Runtime.Dispose();
}
}

public class ExtensionLoadException : Exception
{
public ExtensionLoadException(string message)
: base(message)
{
}
}

public class ExtensionUnloadException : Exception
{
public ExtensionUnloadException(string message)
: base(message)
{
}
}
}
Loading

0 comments on commit 796ff07

Please sign in to comment.