/// <summary> /// Application specific configuration /// This method should initialize any IoC resources utilized by your web service classes. /// </summary> public override void Configure(Container container) { // inject the concrete logging implementation Log.Debug($"Entering AppHost.Configure method, container is {container.ToString()}"); // AppSettings is a first class object on the Container, so it will be auto-wired // In any other assembly, AppSettings is read-only, so it must be populated in this assembly // Location of the configuration files will depend on running as LifeCycle Production/QA/Dev as well as Debug and Release settings //It would be nice if ServiceStack implemented the User Secrets pattern that ASP Core provides // The Environment variable may define the location of a text file to add to the AppSettings string indirectSettingsTextFilepath; try { indirectSettingsTextFilepath = Environment.GetEnvironmentVariable(agentEnvironmentIndirectSettingsTextFileNameKey); } catch (SecurityException e) { Log.Error($"{cannotReadEnvironmentVariablesSecurityExceptionMessage}: {e.Message}"); throw new SecurityException(cannotReadEnvironmentVariablesSecurityExceptionMessage, e); } // ToDo: If the Environment variable does define a location for another text fle, ensure it can be read if (indirectSettingsTextFilepath != null) { try { } catch { } } // Create the AppSettingsBuilder, and add command line arguments to it var multiAppSettingsBuilder = new MultiAppSettingsBuilder(); // Highest priority is any command line variable values, so add command line arguments to the AppSettingsBuilder // ToDo: .Add?? // Next in priority are all environment values, so append AddEnvironmentalVariables to the AppSettingsBuilder multiAppSettingsBuilder.AddEnvironmentalVariables(); // Next in priority are contents of any indirect files mentioned in the environment variables (e.g. User Secrets ) if (indirectSettingsTextFilepath != null) { multiAppSettingsBuilder.AddTextFile(indirectSettingsTextFilepath); } // Next in priority are any configuration settings in a text file named on the command line. // ToDo: if a configuration settings text file is specified on the command line, ensure we have permission to read it // ToDo: append AddTextFile for configuration settings in a text file specified on the command line to the AppSettingsBuilder // Next in priority are any configuration settings in a text file named as a constant string in the app. // Location of the text file is relative to the current working directory at the point in time when this method executes. // If this file does not exist, it is not considered an error, but if it does exist, not having read permission is an error // ToDo: if a configuration settings text file is specified as a constant string in the app, ensure we have permission to read it // ToDo: if it exists, append AddTextFile for configuration settings in a text file specified as a constant string in the app to the AppSettingsBuilder multiAppSettingsBuilder.AddTextFile(agentSettingsTextFileNameString); // Next in priority are any configuration settings in the application config file (AKA AceAgent.exe.config at runtime) // Location of the application config file is relative to the current working directory at the point in time when this method executes. // If this file does not exist, it is not considered an error, but if it does exist, not having read permission is an error // ToDo: if a application config file is specified as a constant string in the app, ensure we have permission to read it // ToDo: if it exists, append AddTextFile for configuration settings in a text file specified as a constant string in the app to the AppSettingsBuilder multiAppSettingsBuilder.AddAppSettings() // Builtin (compiled in) configuration settings have the lowest priority // Add these to the AppSettingsBuilder .AddDictionarySettings(DefaultConfiguration.Configuration()); //Build the AppSettings AppSettings = multiAppSettingsBuilder.Build(); // Create the BaseServices data structure and register it in the container // The AppHost (here, ServiceStack running as a Windows service) has some configuration that is common // to both Frameworks (.Net and .Net Core), which will be setup in a common assembly, so this instance of // the appHost is being passed to the BaseServicesData constructor. // this also registers a BaseServicesData instance in the container // ToDo: implement Singleton pattern for BaseServicesData in the DI Container var baseServicesData = new BaseServicesData(this); container.Register <BaseServicesData>(c => baseServicesData); // ToDo: Get the list of plugins to install from the configuration settings, currently hard coded // Create the list of PlugIns to load var plugInList = new List <IPlugin>() { //new RealEstateServicesPlugin(), //new MinerServicesPlugin(), new DiskAnalysisServicesPlugin(), new GUIServicesPlugin() }; // Load each plugin here. foreach (var pl in plugInList) { Plugins.Add(pl); } // ToDo: See Issue #8 // ToDo place a static, deep-copy of the current application'instance of the configuration settings as the first object in the application's configuration settings history list. // start all the timers Log.Debug("In AppHost.Configure: starting all timers"); var timers = Container.Resolve <Dictionary <string, System.Timers.Timer> >(); var longRunningTasksCheckTimer = timers[Ace.Agent.BaseServices.BaseServicesData.LongRunningTasksCheckTimerName]; longRunningTasksCheckTimer.Start(); /* * // ToDo: create a NotifyIcon equivalent for a Windows Service or Linux Daemon. Notifiy Icon itself will not work, as that is for WinForms only * Log.Debug("in AppHost.Configure: Create a NotifyIcon for AceCommander"); * Application.EnableVisualStyles(); * Application.SetCompatibleTextRenderingDefault(false); * NotifyIcon notifyIcon1 = new NotifyIcon(); * ContextMenu contextMenu1 = new ContextMenu(); * MenuItem menuItem1 = new MenuItem(); * contextMenu1.MenuItems * .AddRange(new MenuItem[] { menuItem1 }); * menuItem1.Index=0; * menuItem1.Text="E&xit"; * menuItem1.Click+=new EventHandler(menuItem1_Click); * notifyIcon1.Icon=new Icon("atap.ico"); * notifyIcon1.Text="AceCommander"; * notifyIcon1.ContextMenu=contextMenu1; * notifyIcon1.Visible=true; * // Log.Debug("Calling a Web Forms Application instance's static Run method"); * // Application.Run(); * // notifyIcon1.Visible = false; * Log.Debug("NotifyIcon for AceCommander created"); */ Log.Debug("Leaving AppHost.Configure"); }
/// Configure its PlugInAppSettings and ConfigurationData public void BeforePluginsLoaded(IAppHost appHost) { // Get the IHostEnvironment type object from the ServiceStack Container HostEnvironment = appHost.GetContainer().Resolve <Microsoft.Extensions.Hosting.IHostEnvironment>(); // Determine the environment this PlugIn has been activated in string envName = HostEnvironment.EnvironmentName; // Populate this PlugIn's AppSettings Configuration Settings and place it in the appSettingsDictionary // ToDo: figure out how to place /resolve text files from relative to the location of the PlugIn assembly var pluginAppSettingsBuilder = new MultiAppSettingsBuilder(); // ToDo: command line settings have the highest priority // Environment variables have 2nd highest priority pluginAppSettingsBuilder.AddEnvironmentalVariables(); // third priority are Non-Production Configuration settings in a text file if (!this.HostEnvironment.IsProduction()) { var settingsTextFileName = Ace.Agent.GUIServices.StringConstants.PluginSettingsTextFileName + '.' + envName + StringConstants.PluginSettingsTextFileSuffix; // ToDo: ensure it exists and the ensure we have permission to read it // ToDo: Security: There is something called a Time-To-Check / Time-To-Use vulnerability, ensure the way we check then access the text file does not open the program to this vulnerability pluginAppSettingsBuilder.AddTextFile(settingsTextFileName); } // next in priority are Production Configuration settings in a text file pluginAppSettingsBuilder.AddTextFile(StringConstants.PluginSettingsTextFileName + StringConstants.PluginSettingsTextFileSuffix); // BuiltIn (compiled in) have the lowest priority pluginAppSettingsBuilder.AddDictionarySettings(DefaultConfiguration.Production); // Populate the PlugInAppSettings property for this PlugIn from the builder PlugInAppSettings = pluginAppSettingsBuilder.Build(); // Populate the ConfigurationData property with an empty configuration data instance // populate the GUIS property of the ConfigurationData with a new GUIS having an empty list of GUIs ConfigurationData = new ConfigurationData(); ConfigurationData.GUIS = new GUIS() { GUIs = new List <GUI>() }; // Get the GUIMaps POCO from PlugInAppSettings // Ensure the PlugInAppSettings has a non-empty ConfigKey for the GUIMaps if (!PlugInAppSettings.Exists(StringConstants.GUIMapsConfigKey) || (PlugInAppSettings.GetString(StringConstants.GUIMapsConfigKey) == string.Empty)) { throw new Exception(StringConstants.GUISKeyOrValueNotFoundExceptionMessage); } // ToDo: Security: this is a place where character strings from external sources are processed, check carefully // any typo in the text file can make the conversion to a POCO break GUIMaps gUIMaps; try { gUIMaps = PlugInAppSettings.Get <GUIMaps>(StringConstants.GUIMapsConfigKey); } catch (Exception) { //ToDo: Better exception handling throw; } // ToDo: Throw exception if the IEnumerable count = 0 char[] invalidChars = Path.GetInvalidPathChars(); // Loop over each GUI defined in pluginAppSettings GUIs, create a virtualFileMapping foreach (GUIMap gUIMap in gUIMaps._GUIMaps) { if (gUIMap.RelativeToContentRootPath.Any(x => invalidChars.Contains(x))) { throw new Exception(StringConstants.RelativeRootPathValueContainsIlegalCharacterExceptionMessage); } // The GenericHost's ContentRootPath is found on the HostEnvironment injected during the .ctor var physicalPath = Path.Combine(this.HostEnvironment.ContentRootPath, gUIMap.RelativeToContentRootPath); //Log.Debug("in GUIServicesPlugin.Configure, physicalPath = {PhysicalPath}, ContentRootPath = {ContentRootPath} relativeRootPathValue = {RelativeToContentRootPath}", physicalPath, this.HostEnvironment.ContentRootPath, gUIMap.RelativeToContentRootPath); //Log.Debug("in GUIServicesPlugin.Configure, index.html exists in physicalpath = {0}", File.Exists(Path.Combine(physicalPath, "index.html"))); //Log.Debug("in GUIServicesPlugin.Configure, virtualRootPath = {GUIMapVirtualRootPath}", gUIMap.VirtualRootPath); // if the VirtualRootPath is empty, setup the webserver's wwwroot to point to the physicalpath if (gUIMap.VirtualRootPath == null) { Log.Debug("current wwwroot = {Wwwroot}", (appHost as AppHostBase).HostingEnvironment.WebRootPath); // If the webserver's wwwroot is already populated, throw an exception // Set the WebRootPath to the physicalpath // (appHost as AppHostBase).HostingEnvironment.WebRootPath = physicalPath; // Log.Debug("current wwwroot = {Wwwroot}", (appHost as AppHostBase).HostingEnvironment.WebRootPath); appHost.AddVirtualFileSources .Add(new FileSystemMapping("", physicalPath)); } else { // Map the virtualRootPath to the physicalpath of the root of the GUI // Wrap in a try catch block in case the physicalRootPath does not exists // ToDo: test for failure condition instead of letting it throw an exception try { appHost.AddVirtualFileSources .Add(new FileSystemMapping(gUIMap.VirtualRootPath, physicalPath)); } catch (Exception e) { // ToDo: research how best to log an exception with Serilog, ServiceStack and/or MS // ToDo: USe stringconstant for exception message Log.Debug(e, "in GUIServicesPlugin.Configure, Adding a new VirtualFileSource failed with : {Message}", e.Message); // ToDo wrap in an Aggregate when doing loop throw; // ToDo: figure out how to log this and fallback to something useful } } // ToDo: Get Version and Description from assembly metadata ConfigurationData.GUIS.GUIs.Add(new GUI() { Version = "0.0.1", Description = "A sample", GUIMap = gUIMap }); } // ToDo: add support for probing for additional GUIS at runtime, add them to ConfigurationData.GUIS.GUIs // If a request comes in for the root of the web site, redirect it to the default GUI's index.html file // ToDo: Security: this is a place where character strings from external sources are processed, check carefully // Get the DefaultGUIVirtualRootPath from PlugInAppSettings // Ensure the PlugInAppSettings has a (possibly empty) ConfigKey for the DefaultGUIVirtualRootPath if (!PlugInAppSettings.Exists(StringConstants.DefaultGUIVirtualRootPathConfigKey)) { throw new Exception(StringConstants.DefaultGUIVirtualRootPathKeyOrValueNotFoundExceptionMessage); } // Ensure the value of the defaultGUIVirtualRootPath matches the value of the VirtualRootPath of one of the GUIMaps string defaultGUIVirtualRootPath = PlugInAppSettings.Get <string>(StringConstants.DefaultGUIVirtualRootPathConfigKey); GUI defaultGUI; try { defaultGUI = ConfigurationData.GUIS.GUIs.Single(gUI => gUI.GUIMap.VirtualRootPath == defaultGUIVirtualRootPath); } catch { throw new Exception(StringConstants.DefaultGUIVirtualRootPathDoesNotMatchExactlyOneGUIVirtualRootPathExceptionMessage); } // Set the default redirect. If defaultGUIVirtualRootPath is null, ensure there is not a double // in the path appHost.Config.DefaultRedirectPath = String.Format(StringConstants.DefaultRedirectPathTemplate, defaultGUI.GUIMap.VirtualRootPath); // Blazor requires the delivery of static files ending in certain file suffixes. // SS disallows some of these by default, so here we tell SS to allow certain file suffixes appHost.Config.AllowFileExtensions.Add("dll"); appHost.Config.AllowFileExtensions.Add("json"); appHost.Config.AllowFileExtensions.Add("pdb"); // Blazor requires CORS support, enable the ServiceStack CORS feature appHost.Plugins.Add(new CorsFeature( allowedMethods: "GET, POST, PUT, DELETE, OPTIONS", allowedOrigins: "*", allowCredentials: true, allowedHeaders: "content-type, Authorization, Accept")); // create the PlugIn's data object GUIServicesData gUIServicesData = new GUIServicesData(PlugInAppSettings, ConfigurationData); // Pass the Plugin's data structure to the container so it will be available to every other module and services appHost.GetContainer().Register <GUIServicesData>(d => gUIServicesData); // ToDo: enable the mechanisms that monitors each plugin-specific data sensor, and start them running // ToDo: Need to figure out if / how a PlugIn can add a PlugIn to the parent AppHost, if it detects there is a dependency }