Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Extensions System #262

Merged
merged 8 commits into from
Mar 7, 2022
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: 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