//(object/*ApiResponse | IActionResult*/, RoutineExecution, CommonReturnValue) public static async Task <ExecuteRoutineAsyncResult> ExecuteRoutineAsync(ExecOptions execOptions, Dictionary <string, string> requestHeaders, string referer, string remoteIpAddress, string appTitle, string appVersion) { string debugInfo = null; Project project = null; Application app = null; Endpoint endpoint = null; Dictionary <string, string> responseHeaders = null; List <ExecutionPlugin> pluginList = null; RoutineExecution routineExecutionMetric = null; responseHeaders = new Dictionary <string, string>(); try { if (!ControllerHelper.GetProjectAndAppAndEndpoint(execOptions.project, execOptions.application, execOptions.endpoint, out project, out app, out endpoint, out var resp)) { return(new ExecuteRoutineAsyncResult(resp, null, null, responseHeaders)); } routineExecutionMetric = new RoutineExecution(endpoint, execOptions.schema, execOptions.routine); RealtimeTrackerThread.Instance.Enqueue(routineExecutionMetric); // debugInfo += $"[{execOptions.schema}].[{execOptions.routine}]"; string jsDALApiKey = null; if (requestHeaders.ContainsKey("api-key")) { jsDALApiKey = requestHeaders["api-key"]; } // make sure the source domain/IP is allowed access var mayAccess = app.MayAccessDbSource(referer, jsDALApiKey); if (!mayAccess.IsSuccess) { return(new ExecuteRoutineAsyncResult(null, null, mayAccess, responseHeaders)); } Dictionary <string, dynamic> outputParameters; int commandTimeOutInSeconds = 60; // PLUGINS var pluginsInitMetric = routineExecutionMetric.BeginChildStage("Init plugins"); pluginList = InitPlugins(app, execOptions.inputParameters, requestHeaders); pluginsInitMetric.End(); //////////////////// // Auth stage /////////////////// { // ask all ExecPlugins to authenticate foreach (var plugin in pluginList) { if (!plugin.IsAuthenticated(execOptions.schema, execOptions.routine, out var error)) { responseHeaders.Add("Plugin-AuthFailed", plugin.Name); return(new ExecuteRoutineAsyncResult(new UnauthorizedObjectResult(error), null, null, responseHeaders)); } } } var execRoutineQueryMetric = routineExecutionMetric.BeginChildStage("execRoutineQuery"); string dataCollectorEntryShortId = DataCollectorThread.Enqueue(endpoint, execOptions); /////////////////// // Database call /////////////////// OrmDAL.ExecutionResult executionResult = null; try { executionResult = await OrmDAL.ExecRoutineQueryAsync( execOptions.CancellationToken, execOptions.type, execOptions.schema, execOptions.routine, endpoint, execOptions.inputParameters, requestHeaders, remoteIpAddress, pluginList, commandTimeOutInSeconds, execRoutineQueryMetric, responseHeaders ); outputParameters = executionResult.OutputParameterDictionary; responseHeaders = executionResult.ResponseHeaders; execRoutineQueryMetric.End(); ulong?rows = null; if (executionResult?.RowsAffected.HasValue ?? false) { rows = (ulong)executionResult.RowsAffected.Value; } DataCollectorThread.End(dataCollectorEntryShortId, rowsAffected: rows, durationInMS: execRoutineQueryMetric.DurationInMS, bytesReceived: executionResult.BytesReceived, networkServerTimeMS: executionResult.NetworkServerTimeInMS); } catch (Exception execEx) { DataCollectorThread.End(dataCollectorEntryShortId, ex: execEx); // if (execOptions != null && endpoint != null) // { // var wrapEx = new Exception($"Failed to execute {endpoint?.Pedigree}/{execOptions?.schema}/{execOptions?.routine}", execEx); // // create a fake frame to include the exec detail - this way any logger logging the StackTrace will always include the relevant execution detail // StackFrame sf = new($"{execOptions.type} {endpoint?.Pedigree}/{execOptions?.schema}/{execOptions?.routine}", 0); // StackTrace st = new(sf); // //?wrapEx.SetStackTrace(st); // var allFields = wrapEx.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance); // var fffff =string.Join("\r\n", allFields.Select(f=>f.Name).ToArray()); // var fi = wrapEx.GetType().GetField("_stackTraceString", BindingFlags.NonPublic | BindingFlags.Instance); // fi.SetValue(wrapEx, $"{endpoint?.Pedigree}/{execOptions?.schema}/{execOptions?.routine}"); // throw wrapEx; // } // else throw; throw; // rethrow } var prepareResultsMetric = routineExecutionMetric.BeginChildStage("Prepare results"); if (!string.IsNullOrEmpty(executionResult.userError)) { return(new ExecuteRoutineAsyncResult(ApiResponse.ExclamationModal(executionResult.userError), routineExecutionMetric, mayAccess, responseHeaders)); } var retVal = (IDictionary <string, object>) new System.Dynamic.ExpandoObject(); var ret = ApiResponse.Payload(retVal); retVal.Add("OutputParms", outputParameters); if (outputParameters != null) { // TODO: Consider making this a plugin var possibleUEParmNames = (new string[] { "usererrormsg", "usrerrmsg", "usererrormessage", "usererror", "usererrmsg" }).ToList(); var ueKey = outputParameters.Keys.FirstOrDefault(k => possibleUEParmNames.Contains(k.ToLower())); // if a user error msg is defined. if (!string.IsNullOrWhiteSpace(ueKey) && !string.IsNullOrWhiteSpace(outputParameters[ueKey])) { ret.Message = outputParameters[ueKey]; ret.Title = "Action failed"; ret.Type = ApiResponseType.ExclamationModal; } } if (execOptions.type == ExecType.Query) { if (executionResult.ReaderResults != null) { var keys = executionResult.ReaderResults.Keys.ToList(); for (var i = 0; i < keys.Count; i++) { retVal.Add(keys[i], executionResult.ReaderResults[keys[i]]); } retVal.Add("HasResultSets", keys.Count > 0); retVal.Add("ResultSetKeys", keys.ToArray()); } else { var dataSet = executionResult.DataSet; var dataContainers = dataSet.ToJsonDS(); var keys = dataContainers.Keys.ToList(); for (var i = 0; i < keys.Count; i++) { retVal.Add(keys[i], dataContainers[keys[i]]); } retVal.Add("HasResultSets", keys.Count > 0); retVal.Add("ResultSetKeys", keys.ToArray()); } } else if (execOptions.type == ExecType.NonQuery) { // nothing to do } else if (execOptions.type == ExecType.Scalar) { if (executionResult.ScalarValue is DateTime) { var dt = (DateTime)executionResult.ScalarValue; // convert to Javascript Date ticks var ticks = dt.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds; ret = ApiResponseScalar.Payload(ticks, true); } else { ret = ApiResponse.Payload(executionResult.ScalarValue); } } prepareResultsMetric.End(); routineExecutionMetric.End(executionResult.RowsAffected ?? 0); // enqueue a second time as we now have an End date and rowsAffected RealtimeTrackerThread.Instance.Enqueue(routineExecutionMetric); return(new ExecuteRoutineAsyncResult(ret, routineExecutionMetric, mayAccess, responseHeaders)); } catch (SqlException ex) when(execOptions.CancellationToken.IsCancellationRequested && ex.Number == 0 && ex.State == 0 && ex.Class == 11) { // if we ended up here with a SqlException and a Cancel has been requested, we are very likely here because of the exception "Operation cancelled by user." // since MS does not provide an easy way (like a specific error code) to detect this scenario we have to guess routineExecutionMetric?.Exception(ex); throw new OperationCancelledByUserException(ex); } catch (Exception ex) { routineExecutionMetric?.Exception(ex); Connection dbConn = null; if (endpoint != null) { // TODO: Fix! dbConn = endpoint.GetSqlConnection(); // if (debugInfo == null) debugInfo = ""; // if (dbConn != null) // { // debugInfo = $"{ endpoint.Pedigree } - { dbConn.InitialCatalog } - { debugInfo }"; // } // else // { // debugInfo = $"{ endpoint.Pedigree } - (no connection) - { debugInfo }"; // } } var exceptionResponse = ApiResponse.ExecException(ex, execOptions, out var exceptionId, debugInfo, appTitle, appVersion); if (debugInfo == null) { debugInfo = ""; } debugInfo = exceptionId + " " + debugInfo; // TODO: Get Execution plugin list specifically if (pluginList != null) { string externalRef; if (dbConn != null) { using (var con = new SqlConnection(dbConn.ConnectionStringDecrypted)) { try { string additionalInfo = debugInfo; if (execOptions?.inputParameters != null) { additionalInfo += " "; additionalInfo += string.Join(",", execOptions.inputParameters.Select(kv => $"{kv.Key}={kv.Value}").ToArray()); } con.Open(); ProcessPluginExecutionExceptionHandlers(pluginList, con, ex, additionalInfo, appTitle, appVersion, out externalRef); ((dynamic)exceptionResponse.Data).ExternalRef = externalRef; } catch (Exception e) { ExceptionLogger.LogException(e, "ProcessPluginExecutionExceptionHandlers", "jsdal-server"); } } } // else: TODO: Log fact that we dont have a proper connection string.. or should plugins handle that? } // return it as "200 (Ok)" because the exception has been handled return(new ExecuteRoutineAsyncResult(exceptionResponse, routineExecutionMetric, null, responseHeaders)); } }
public void CreateEndpointInstances(IHubContext <Hubs.BackgroundPluginHub> hub) { IHubClients hubClients = hub.Clients; // TODO: Need to look at EP Creation Event and EP stopped & deleted event. var apps = Settings.SettingsInstance.Instance .ProjectList .SelectMany(proj => proj.Applications) .Where(app => app.IsPluginIncluded(this.PluginGuid.ToString())); var endpointCollection = apps.SelectMany(app => app.Endpoints); // create a default instance just to read the Default Value collection var defaultInstance = (BackgroundThreadPlugin)this.Assembly.CreateInstance(this.TypeInfo.FullName); var defaultConfig = defaultInstance.GetDefaultConfig(); if (defaultConfig?.ContainsKey("IsEnabled") ?? false) { var defIsEnabled = defaultConfig["IsEnabled"]; // TODO: Convert to better typed class (e.g. true/false) // TODO: Match up with Endpoint config. EP Config needs to be persisted somewhere } // TODO: For each BG Plugin catalog the 'server methods' available. (do this once per assembly, not per EP as they are the same for all EPs) foreach (var endpoint in endpointCollection) { try { // TODO: Look for an existing instance on the EP // TODO: If no longer ENABLED on EP kill instance? Won't currently be in collection above var existingInstance = FindPluginInstance(endpoint); if (existingInstance != null) { // no need to instantiate again continue; } var pluginInstance = (BackgroundThreadPlugin)this.Assembly.CreateInstance(this.TypeInfo.FullName); var initMethod = typeof(BackgroundThreadPlugin).GetMethod("Init", BindingFlags.Instance | BindingFlags.NonPublic); // var initMethod = typeof(BackgroundThreadPlugin).GetMethod("Init", BindingFlags.Instance | BindingFlags.NonPublic); if (initMethod != null) { var instanceWrapper = new BackgroundThreadPluginInstance(endpoint, pluginInstance); var logExceptionCallback = new Action <Exception, string>((exception, additionalInfo) => { // TODO: Throttle logging if it happens too frequently. Possibly stop plugin if too many exceptions? ExceptionLogger.LogException(exception, new Controllers.ExecController.ExecOptions() { project = endpoint.Application.Project.Name, application = endpoint.Application.Name, endpoint = endpoint.Name, schema = "BG PLUGIN", routine = this.PluginName, type = Controllers.ExecController.ExecType.BackgroundThread }, additionalInfo, $"BG PLUGIN - {this.PluginName}", endpoint.Pedigree); }); var openSqlConnectionCallback = new Func <SqlConnection>(() => { try { var execConn = endpoint.GetSqlConnection(); if (execConn == null) { throw new Exception($"Execution connection not configured on endpoint {endpoint.Pedigree}"); } var cb = new SqlConnectionStringBuilder(execConn.ConnectionStringDecrypted); cb.ApplicationName = $"{this.PluginName}"; var sqlCon = new SqlConnection(cb.ConnectionString); sqlCon.Open(); return(sqlCon); } catch (Exception ex) { ExceptionLogger.LogExceptionThrottled(ex, $"{this.PluginName}-{endpoint.Pedigree}::OpenSqlConnection", 20); throw; } }); var updateDataCallback = new Func <ExpandoObject, bool>(data => { dynamic eo = data; eo.InstanceId = instanceWrapper.Id; eo.Endpoint = endpoint.Pedigree; hubClients.Group(Hubs.BackgroundPluginHub.ADMIN_GROUP_NAME).SendAsync("updateData", (object)eo); return(true); }); var browserConsoleSendCallback = new Func <string, string, bool>((method, line) => { hubClients.Group(Hubs.BackgroundPluginHub.BROWSER_CONSOLE_GROUP_NAME).SendAsync(method, new { InstanceId = instanceWrapper.Id, Endpoint = endpoint.Pedigree, Line = line }); return(true); }); var addToGroupAsync = new Func <string, string, CancellationToken, Task>((connectionId, groupName, cancellationToken) => { return(hub.Groups.AddToGroupAsync(connectionId, $"{endpoint.Pedigree}.{groupName}", cancellationToken)); }); var sendToGroupsAsync = new Func <string, string, object[], Task>((groupName, methodName, args) => { groupName = $"{endpoint.Pedigree}.{groupName}"; if (args == null || args.Length == 0) { return(hub.Clients.Groups(groupName).SendAsync(methodName)); } else if (args.Length == 1) { return(hub.Clients.Groups(groupName).SendAsync(methodName, args[0])); } else if (args.Length == 2) { return(hub.Clients.Groups(groupName).SendAsync(methodName, args[0], args[1])); } else if (args.Length == 3) { return(hub.Clients.Groups(groupName).SendAsync(methodName, args[0], args[1], args[2])); } else if (args.Length == 4) { return(hub.Clients.Groups(groupName).SendAsync(methodName, args[0], args[1], args[2], args[3])); } else if (args.Length == 5) { return(hub.Clients.Groups(groupName).SendAsync(methodName, args[0], args[1], args[2], args[3], args[4])); } else if (args.Length == 6) { return(hub.Clients.Groups(groupName).SendAsync(methodName, args[0], args[1], args[2], args[3], args[4], args[5])); } else if (args.Length == 7) { return(hub.Clients.Groups(groupName).SendAsync(methodName, args[0], args[1], args[2], args[3], args[4], args[5], args[6])); } else if (args.Length == 8) { return(hub.Clients.Groups(groupName).SendAsync(methodName, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7])); } else if (args.Length == 9) { return(hub.Clients.Groups(groupName).SendAsync(methodName, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8])); } else if (args.Length == 10) { return(hub.Clients.Groups(groupName).SendAsync(methodName, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9])); } return(null); }); var dataCollectorBeginCallback = new Func <string, string, string>((schema, routine) => { string dataCollectorEntryShortId = DataCollectorThread.Enqueue(endpoint, new Controllers.ExecController.ExecOptions() { project = endpoint.Application.Project.Name, application = endpoint.Application.Name, endpoint = endpoint.Name, schema = schema, routine = routine, type = Controllers.ExecController.ExecType.BackgroundThread, inputParameters = new Dictionary <string, string>() // TODO: needs to be an input parameter }); return(dataCollectorEntryShortId); }); initMethod.Invoke(pluginInstance, new object[] { endpoint.Pedigree, logExceptionCallback, openSqlConnectionCallback, updateDataCallback, browserConsoleSendCallback, addToGroupAsync, sendToGroupsAsync, null /*configKeys*/, null /*configSource*/ }); { var setGetServicesFuncMethod = typeof(PluginBase).GetMethod("SetGetServicesFunc", BindingFlags.Instance | BindingFlags.NonPublic); if (setGetServicesFuncMethod != null) { setGetServicesFuncMethod.Invoke(pluginInstance, new object[] { new Func <Type, PluginService>(serviceType => { if (serviceType == typeof(BlobStoreBase)) { return(BlobStore.Instance); } return(null); }) }); } } this.AddEnpointInstance(endpoint, instanceWrapper); SessionLog.Info($"BG plugin '{this.PluginName}' instantiated successfully on endpoint {endpoint.Pedigree}"); } else { throw new Exception("Expected Init method not found"); } } catch (Exception ex) { SessionLog.Error($"Failed to instantiate plugin '{this.PluginName}' ({this.PluginGuid}) from assembly {this.Assembly.FullName} on endpoint {endpoint.Pedigree}. See exception that follows."); SessionLog.Exception(ex); } } }