Skip to content

Commit 2e0e4ac

Browse files
committed
Module config stuffs
1 parent b1eee63 commit 2e0e4ac

12 files changed

+255
-60
lines changed

Desktop/Config/ModuleConfig.cs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using OpenShock.Desktop.ModuleBase.Config;
4+
using OpenShock.Desktop.Utils;
5+
using OpenShock.SDK.CSharp.Hub.Utils;
6+
7+
namespace OpenShock.Desktop.Config;
8+
9+
/// <summary>
10+
/// Module config implementation
11+
/// </summary>
12+
/// <typeparam name="T"></typeparam>
13+
public class ModuleConfig<T> : IModuleConfig<T> where T : new()
14+
{
15+
private readonly ILogger _logger;
16+
private readonly Timer _saveTimer;
17+
private readonly string _configPath;
18+
19+
private readonly JsonSerializerOptions _options;
20+
21+
/// <summary>
22+
/// Module config constructor
23+
/// </summary>
24+
/// <param name="configPath"></param>
25+
/// <param name="logger"></param>
26+
/// <param name="additionalConverters"></param>
27+
private ModuleConfig(string configPath, ILogger logger, IEnumerable<JsonConverter> additionalConverters)
28+
{
29+
_logger = logger;
30+
_configPath = configPath;
31+
32+
_saveTimer = new Timer(_ => { OsTask.Run(Save); });
33+
34+
_options = new JsonSerializerOptions
35+
{
36+
WriteIndented = true,
37+
Converters = { new JsonStringEnumConverter(), new SemVersionJsonConverter(), new OneOfConverterFactory() }
38+
};
39+
40+
foreach (var converter in additionalConverters)
41+
{
42+
_options.Converters.Add(converter);
43+
}
44+
}
45+
46+
public static async Task<ModuleConfig<T>> Create(string configPath, ILogger logger, params IEnumerable<JsonConverter> additionalConverters)
47+
{
48+
var config = new ModuleConfig<T>(configPath, logger, additionalConverters);
49+
await config.LoadConfig();
50+
return config;
51+
}
52+
53+
private async Task LoadConfig()
54+
{
55+
// Load config
56+
var config = default(T);
57+
58+
59+
if (File.Exists(_configPath))
60+
{
61+
_logger.LogInformation("Config file found, trying to load config from {Path}", _configPath);
62+
var json = await File.ReadAllTextAsync(_configPath);
63+
if (!string.IsNullOrWhiteSpace(json))
64+
{
65+
_logger.LogTrace("Config file is not empty");
66+
try
67+
{
68+
config = JsonSerializer.Deserialize<T>(json, _options);
69+
}
70+
catch (Exception e)
71+
{
72+
_logger.LogCritical(e, "Error during deserialization/loading of config");
73+
_logger.LogWarning("Attempting to move old config and generate a new one");
74+
var configName = Path.GetFileName(_configPath);
75+
File.Move(configName, $"{configName}.old");
76+
}
77+
}
78+
}
79+
80+
if (config != null)
81+
{
82+
Config = config;
83+
_logger.LogInformation("Successfully loaded config");
84+
return;
85+
}
86+
87+
_logger.LogInformation(
88+
"No config file found (does not exist or empty or invalid), generating new one at {Path}", _configPath);
89+
Config = new T();
90+
await Save();
91+
_logger.LogInformation("New configuration file generated!");
92+
}
93+
94+
public T Config { get; private set; }
95+
96+
public void SaveDeferred()
97+
{
98+
_saveTimer.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan);
99+
}
100+
101+
private readonly SemaphoreSlim _saveLock = new(1, 1);
102+
103+
public async Task Save()
104+
{
105+
await _saveLock.WaitAsync().ConfigureAwait(false);
106+
try
107+
{
108+
_logger.LogTrace("Saving config");
109+
var directory = Path.GetDirectoryName(_configPath);
110+
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
111+
Directory.CreateDirectory(directory);
112+
113+
await File.WriteAllTextAsync(_configPath, JsonSerializer.Serialize(Config, _options)).ConfigureAwait(false);
114+
_logger.LogInformation("Config saved");
115+
}
116+
catch (Exception e)
117+
{
118+
_logger.LogError(e, "Error occurred while saving new config file");
119+
}
120+
finally
121+
{
122+
_saveLock.Release();
123+
}
124+
}
125+
}

Desktop/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public static class Constants
88
public static readonly string AppdataFolder =
99
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), @"OpenShock\Desktop");
1010
public static readonly string LogsFolder = Path.Combine(AppdataFolder, "logs");
11+
public static readonly string ModuleData = Path.Combine(AppdataFolder, "moduleData");
1112

1213
public static readonly SemVersion Version = SemVersion.Parse(typeof(Constants).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion, SemVersionStyles.Strict);
1314
public static readonly SemVersion VersionWithoutMetadata = Version.WithoutMetadata();

Desktop/MauiProgram.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#if MAUI
22
using System.Globalization;
33
using System.Text;
4+
using Microsoft.AspNetCore.Components;
45
using Microsoft.Maui.LifecycleEvents;
56
using OpenShock.Desktop.Config;
67
using OpenShock.Desktop.Services.Pipes;
@@ -37,6 +38,7 @@ private static Microsoft.Maui.Hosting.MauiApp CreateMauiAppInternal()
3738
builder.Services.AddOpenShockDesktopServices();
3839
builder.Services.AddCommonBlazorServices();
3940
builder.Services.AddMauiBlazorWebView();
41+
builder.Services.AddScoped<IComponentActivator, OpenShockModuleComponentActivator>();
4042

4143
#if WINDOWS
4244
builder.Services.AddWindowsServices();
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using OpenShock.Desktop.Config;
2+
using OpenShock.Desktop.ModuleBase;
3+
using OpenShock.Desktop.ModuleBase.Config;
4+
5+
namespace OpenShock.Desktop.ModuleManager;
6+
7+
public class ModuleInstanceManager : IModuleInstanceManager
8+
{
9+
private readonly LoadedModule _loadedModule;
10+
private readonly ILoggerFactory _loggerFactory;
11+
12+
public ModuleInstanceManager(LoadedModule loadedModule, ILoggerFactory loggerFactory)
13+
{
14+
_loadedModule = loadedModule;
15+
_loggerFactory = loggerFactory;
16+
}
17+
18+
public async Task<IModuleConfig<T>> GetModuleConfig<T>() where T : new()
19+
{
20+
var moduleConfigPath = Path.Combine(Constants.ModuleData, _loadedModule.Module.Id);
21+
22+
if(!Directory.Exists(moduleConfigPath)) Directory.CreateDirectory(moduleConfigPath);
23+
24+
var moduleConfigFile = Path.Combine(moduleConfigPath, "config.json");
25+
26+
return await ModuleConfig<T>.Create(moduleConfigFile,
27+
_loggerFactory.CreateLogger("ModuleConfig+" + _loadedModule.Module.Id));
28+
}
29+
30+
public required IServiceProvider ServiceProvider { get; init; }
31+
}

Desktop/ModuleManager/ModuleManager.cs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public sealed class ModuleManager
2121
private readonly ConfigManager _configManager;
2222

2323
public event Action? ModulesLoaded;
24-
24+
2525
private static string ModuleDirectory => Path.Combine(Constants.AppdataFolder, "modules");
2626

2727
private static readonly HttpClient HttpClient = new()
@@ -41,20 +41,21 @@ public ModuleManager(IServiceProvider serviceProvider, ILogger<ModuleManager> lo
4141
public readonly ConcurrentDictionary<string, LoadedModule> Modules = new();
4242

4343
#region Tasks
44-
44+
4545
public async Task ProcessTaskList()
4646
{
4747
foreach (var moduleTask in _configManager.Config.Modules.ModuleTasks)
4848
{
4949
try
5050
{
5151
await ProcessTask(moduleTask);
52-
} catch (Exception ex)
52+
}
53+
catch (Exception ex)
5354
{
5455
_logger.LogError(ex, "Failed to process task for module {ModuleId}", moduleTask.Key);
5556
}
5657
}
57-
58+
5859
_configManager.Config.Modules.ModuleTasks.Clear();
5960
_configManager.Save();
6061
}
@@ -63,8 +64,9 @@ private async Task ProcessTask(KeyValuePair<string, ModuleTask> moduleTask)
6364
{
6465
await moduleTask.Value.Match(async install =>
6566
{
66-
_logger.LogInformation("Installing module {ModuleId} version {Version}", moduleTask.Key, install.Version);
67-
await DownloadModule(moduleTask.Key, install.Version);
67+
_logger.LogInformation("Installing module {ModuleId} version {Version}", moduleTask.Key,
68+
install.Version);
69+
await DownloadModule(moduleTask.Key, install.Version);
6870
},
6971
remove =>
7072
{
@@ -73,7 +75,7 @@ await moduleTask.Value.Match(async install =>
7375
return Task.FromResult(Task.CompletedTask);
7476
});
7577
}
76-
78+
7779
#endregion
7880

7981
private void RemoveModule(string moduleId)
@@ -170,6 +172,11 @@ private void LoadModule(string moduleFolderPath)
170172
Module = module
171173
};
172174

175+
module.SetContext(new ModuleInstanceManager(loadedModule, _serviceProvider.GetRequiredService<ILoggerFactory>())
176+
{
177+
ServiceProvider = _serviceProvider
178+
});
179+
173180
var moduleFolder =
174181
Path.GetFileName(moduleFolderPath); // now this seems odd, but this gives me the modules folder name
175182

@@ -203,7 +210,7 @@ internal void LoadAll()
203210
_logger.LogError(ex, "Failed to load plugin");
204211
}
205212
}
206-
213+
207214
ModulesLoaded?.Invoke();
208215
}
209216
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using Microsoft.AspNetCore.Components;
2+
3+
namespace OpenShock.Desktop;
4+
5+
public class OpenShockModuleComponentActivator : IComponentActivator
6+
{
7+
private readonly IServiceProvider _defaultProvider;
8+
9+
public OpenShockModuleComponentActivator(IServiceProvider defaultProvider)
10+
{
11+
_defaultProvider = defaultProvider;
12+
}
13+
14+
public IComponent CreateInstance(Type componentType)
15+
{
16+
// Determine if this component should be created with a module-specific provider.
17+
if (typeof(IModuleComponent).IsAssignableFrom(componentType))
18+
{
19+
// For example, look up a module-specific provider using a custom mechanism.
20+
// You might have a module manager that tracks service providers per module.
21+
var moduleProvider = ModuleManager.GetModuleServiceProvider(componentType);
22+
if (moduleProvider != null)
23+
{
24+
// Use the module-specific provider to create the component.
25+
return (IComponent)ActivatorUtilities.CreateInstance(moduleProvider, componentType);
26+
}
27+
}
28+
29+
// Fallback: use the default (host) service provider.
30+
return (IComponent)ActivatorUtilities.CreateInstance(_defaultProvider, componentType);
31+
}
32+
}

Desktop/Services/StartupService.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,20 @@ public async Task StartupApp()
8282
_logger.LogError(e, "Error while loading modules");
8383
}
8484

85+
_logger.LogDebug("Setting up modules");
86+
Status.Update("Setting up modules");
87+
88+
foreach (var moduleManagerModule in _moduleManager.Modules)
89+
{
90+
await moduleManagerModule.Value.Module.Setup();
91+
}
92+
93+
_logger.LogDebug("Starting modules");
94+
Status.Update("Starting modules");
95+
8596
foreach (var moduleManagerModule in _moduleManager.Modules)
8697
{
87-
moduleManagerModule.Value.Module.Start();
98+
await moduleManagerModule.Value.Module.Start();
8899
}
89100
}
90101

ExampleModule/ExampleDesktopModule.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ public class ExampleDesktopModule : DesktopModuleBase
2626
}
2727
];
2828

29-
public ExampleDesktopModule()
29+
public override async Task Start()
3030
{
31+
var config = await ModuleInstanceManager.GetModuleConfig<ExampleModuleConfig>();
3132

33+
Console.WriteLine(config.Config.SomeConfigOption);
3234
}
3335
}

ExampleModule/ExampleModuleConfig.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace OpenShock.Desktop.Modules.ExampleModule;
2+
3+
public sealed class ExampleModuleConfig
4+
{
5+
public string SomeConfigOption { get; set; } = "defaultvalue";
6+
7+
}

ModuleBase/Config/IModuleConfig.cs

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,8 @@
1-
using System.Text.Json;
2-
using System.Text.Json.Serialization;
1+
namespace OpenShock.Desktop.ModuleBase.Config;
32

4-
namespace OpenShock.Desktop.ModuleBase.Config;
5-
6-
public abstract class ModuleConfig<T>
3+
public interface IModuleConfig<T>
74
{
85
public T Config { get; }
9-
10-
public void SaveDeferred()
11-
{
12-
13-
}
14-
15-
private readonly SemaphoreSlim _saveLock = new(1, 1);
16-
17-
private static readonly JsonSerializerOptions Options = new()
18-
{
19-
WriteIndented = true,
20-
Converters = { new JsonStringEnumConverter() }
21-
};
22-
23-
public async Task SaveNow()
24-
{
25-
await _saveLock.WaitAsync().ConfigureAwait(false);
26-
try
27-
{
28-
_logger.LogTrace("Saving config");
29-
var directory = System.IO.Path.GetDirectoryName(ConfigPath);
30-
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
31-
Directory.CreateDirectory(directory);
32-
33-
await File.WriteAllTextAsync(ConfigPath, JsonSerializer.Serialize(Config, Options)).ConfigureAwait(false);
34-
_logger.LogInformation("Config saved");
35-
}
36-
catch (Exception e)
37-
{
38-
_logger.LogError(e, "Error occurred while saving new config file");
39-
}
40-
finally
41-
{
42-
_saveLock.Release();
43-
}
44-
}
45-
46-
public void Save()
47-
{
48-
lock (_saveTimer)
49-
{
50-
_saveTimer.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan);
51-
}
52-
}
6+
public void SaveDeferred();
7+
public Task Save();
538
}

0 commit comments

Comments
 (0)