//(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));
            }
        }
示例#2
0
        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);
                }
            }
        }