using System.IO.Compression; using System.Reflection; using System.Runtime.Loader; using System.Diagnostics; using Oqtane.Modules; using Oqtane.Services; using System.Globalization; using System.Text.Json; using System.Text.Json.Nodes; namespace Oqtane.Maui; public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder.UseMauiApp() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); builder.Services.AddMauiBlazorWebView(); #if DEBUG builder.Services.AddBlazorWebViewDeveloperTools(); #endif var apiurl = LoadAppSettings(); if (!string.IsNullOrEmpty(apiurl)) { var httpClient = new HttpClient { BaseAddress = new Uri(GetBaseUrl(apiurl)) }; httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Shared.Constants.MauiUserAgent); httpClient.DefaultRequestHeaders.Add(Shared.Constants.MauiAliasPath, GetUrlPath(apiurl).Replace("/", "")); builder.Services.AddSingleton(httpClient); builder.Services.AddHttpClient(); // IHttpClientFactory for calling remote services via RemoteServiceBase // dynamically load client assemblies LoadClientAssemblies(httpClient, apiurl); } // register localization services builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); // register auth services builder.Services.AddOqtaneAuthentication(); // register scoped core services builder.Services.AddOqtaneClientScopedServices(); var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies(); foreach (var assembly in assemblies) { // dynamically register module services RegisterModuleServices(assembly, builder.Services); // register client startup services RegisterClientStartups(assembly, builder.Services); } return builder.Build(); } private static string LoadAppSettings() { var url = MauiConstants.ApiUrl; if (MauiConstants.UseAppSettings) { string file = Path.Combine(FileSystem.Current.AppDataDirectory, "appsettings.json"); if (File.Exists(file)) { using FileStream stream = File.OpenRead(file); using StreamReader reader = new StreamReader(stream); var content = reader.ReadToEnd(); var obj = JsonSerializer.Deserialize(content)!; if (!string.IsNullOrEmpty((string)obj["Url"])) { url = (string)obj["Url"]; } } else { // create template appsettings.json file using (StreamWriter writer = File.CreateText(file)) { writer.WriteLine("{ \"Url\": \"\" }"); } } } return url; } private static void LoadClientAssemblies(HttpClient http, string apiurl) { try { // ensure local assembly folder exists string folder = Path.Combine(FileSystem.Current.AppDataDirectory, "oqtane"); if (!Directory.Exists(folder)) { Directory.CreateDirectory(folder); } var dlls = new Dictionary(); var pdbs = new Dictionary(); var list = new List(); var files = new List(); foreach (var file in Directory.EnumerateFiles(folder, "*.dll", SearchOption.AllDirectories)) { files.Add(file.Substring(folder.Length + 1).Replace("\\", "/")); } if (files.Count() != 0) { // get list of assemblies from server var json = Task.Run(() => http.GetStringAsync($"{GetUrlPath(apiurl)}api/Installation/list")).GetAwaiter().GetResult(); var assemblies = JsonSerializer.Deserialize>(json); // determine which assemblies need to be downloaded foreach (var assembly in assemblies) { var file = files.FirstOrDefault(item => item.Contains(assembly)); if (file == null) { list.Add(assembly); } else { // check if newer version available if (GetFileDate(assembly) > GetFileDate(file)) { list.Add(assembly); } } } // get assemblies already downloaded foreach (var file in files) { if (assemblies.Contains(file) && !list.Contains(file)) { try { dlls.Add(file, File.ReadAllBytes(Path.Combine(folder, file))); var pdb = file.Replace(".dll", ".pdb"); if (File.Exists(Path.Combine(folder, pdb))) { pdbs.Add(pdb, File.ReadAllBytes(Path.Combine(folder, pdb))); } } catch { // ignore } } else // file is deprecated { try { foreach (var path in Directory.EnumerateFiles(folder, Path.GetFileNameWithoutExtension(file) + ".*")) { File.Delete(path); } } catch { // ignore } } } } else { list.Add("*"); } if (list.Count != 0) { // get assemblies from server var zip = Task.Run(() => http.GetByteArrayAsync($"{GetUrlPath(apiurl)}api/Installation/load?list=" + string.Join(",", list))).GetAwaiter().GetResult(); // asemblies and debug symbols are packaged in a zip file using (ZipArchive archive = new ZipArchive(new MemoryStream(zip))) { foreach (ZipArchiveEntry entry in archive.Entries) { using (var memoryStream = new MemoryStream()) { entry.Open().CopyTo(memoryStream); byte[] file = memoryStream.ToArray(); // save assembly to local folder try { using var stream = File.Create(Path.Combine(folder, entry.FullName)); stream.Write(file, 0, file.Length); } catch { // ignore } if (Path.GetExtension(entry.FullName) == ".dll") { dlls.Add(entry.FullName, file); } else { pdbs.Add(entry.FullName, file); } } } } } // load assemblies into app domain foreach (var item in dlls) { if (pdbs.ContainsKey(item.Key.Replace(".dll", ".pdb"))) { AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value), new MemoryStream(pdbs[item.Key.Replace(".dll", ".pdb")])); } else { AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(item.Value)); } } } catch (Exception ex) { Debug.WriteLine($"Error Loading Client Assemblies From {apiurl} - {ex}"); } } private static DateTime GetFileDate(string filepath) { var segments = filepath.Split('.'); return DateTime.ParseExact(segments[segments.Length - 2], "yyyyMMddHHmmss", CultureInfo.InvariantCulture); } private static void RegisterModuleServices(Assembly assembly, IServiceCollection services) { // dynamically register module scoped services try { var implementationTypes = assembly.GetInterfaces(); foreach (var implementationType in implementationTypes) { if (implementationType.AssemblyQualifiedName != null) { var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}")); services.AddScoped(serviceType ?? implementationType, implementationType); } } implementationTypes = assembly.GetInterfaces(); foreach (var implementationType in implementationTypes) { if (implementationType.AssemblyQualifiedName != null) { var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}")); services.AddScoped(serviceType ?? implementationType, implementationType); } } } catch { // could not interrogate assembly - likely missing dependencies } } private static void RegisterClientStartups(Assembly assembly, IServiceCollection services) { try { var startUps = assembly.GetInstances(); foreach (var startup in startUps) { startup.ConfigureServices(services); } } catch { // could not interrogate assembly - likely missing dependencies } } private static string GetBaseUrl(string url) { var uri = new Uri(url); return uri.Scheme + "://"+ uri.Authority + "/"; } private static string GetUrlPath(string url) { var path = new Uri(url).AbsolutePath.Substring(1); path = (!string.IsNullOrEmpty(path) && !path.EndsWith("/")) ? path + "/" : path; return path; } }