From e36999584b0f350639a10adaffbc9b4ee0d4beff Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 21 Feb 2024 11:55:49 +1100 Subject: [PATCH 1/5] Accept plain map for runPluginTask --- graphql/schema/schema.graphql | 3 +- internal/api/resolver_mutation_plugin.go | 55 ++++++++++++++++++++++-- internal/manager/task_plugin.go | 2 +- pkg/plugin/args.go | 27 +++--------- pkg/plugin/convert.go | 33 ++------------ pkg/plugin/plugins.go | 6 +-- 6 files changed, 67 insertions(+), 59 deletions(-) diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 9db3d90ab53..ba95e1fe6ea 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -393,7 +393,8 @@ type Mutation { runPluginTask( plugin_id: ID! task_name: String! - args: [PluginArgInput!] + args: [PluginArgInput!] @deprecated(reason: "Use args_map instead") + args_map: Map ): ID! reloadPlugins: Boolean! diff --git a/internal/api/resolver_mutation_plugin.go b/internal/api/resolver_mutation_plugin.go index 8aa520c08c5..0d9e5edb616 100644 --- a/internal/api/resolver_mutation_plugin.go +++ b/internal/api/resolver_mutation_plugin.go @@ -2,6 +2,7 @@ package api import ( "context" + "strconv" "github.com/stashapp/stash/internal/manager" "github.com/stashapp/stash/internal/manager/config" @@ -9,10 +10,58 @@ import ( "github.com/stashapp/stash/pkg/sliceutil" ) -func (r *mutationResolver) RunPluginTask(ctx context.Context, pluginID string, taskName string, args []*plugin.PluginArgInput) (string, error) { +func toPluginArgs(args []*plugin.PluginArgInput) plugin.OperationInput { + ret := make(plugin.OperationInput) + for _, a := range args { + ret[a.Key] = toPluginArgValue(a.Value) + } + + return ret +} + +func toPluginArgValue(arg *plugin.PluginValueInput) interface{} { + if arg == nil { + return nil + } + + switch { + case arg.Str != nil: + return *arg.Str + case arg.I != nil: + return *arg.I + case arg.B != nil: + return *arg.B + case arg.F != nil: + return *arg.F + case arg.O != nil: + return toPluginArgs(arg.O) + case arg.A != nil: + var ret []interface{} + for _, v := range arg.A { + ret = append(ret, toPluginArgValue(v)) + } + return ret + } + + return nil +} + +func (r *mutationResolver) RunPluginTask( + ctx context.Context, + pluginID string, + taskName string, + args []*plugin.PluginArgInput, + argsMap map[string]interface{}, +) (string, error) { + if argsMap == nil { + // convert args to map + // otherwise ignore args in favour of args map + argsMap = toPluginArgs(args) + } + m := manager.GetInstance() - m.RunPluginTask(ctx, pluginID, taskName, args) - return "todo", nil + jobID := m.RunPluginTask(ctx, pluginID, taskName, argsMap) + return strconv.Itoa(jobID), nil } func (r *mutationResolver) ReloadPlugins(ctx context.Context) (bool, error) { diff --git a/internal/manager/task_plugin.go b/internal/manager/task_plugin.go index 78cd5db0282..142b0ecaf29 100644 --- a/internal/manager/task_plugin.go +++ b/internal/manager/task_plugin.go @@ -9,7 +9,7 @@ import ( "github.com/stashapp/stash/pkg/plugin" ) -func (s *Manager) RunPluginTask(ctx context.Context, pluginID string, taskName string, args []*plugin.PluginArgInput) int { +func (s *Manager) RunPluginTask(ctx context.Context, pluginID string, taskName string, args plugin.OperationInput) int { j := job.MakeJobExec(func(jobCtx context.Context, progress *job.Progress) { pluginProgress := make(chan float64) task, err := s.PluginCache.CreateTask(ctx, pluginID, taskName, args, pluginProgress) diff --git a/pkg/plugin/args.go b/pkg/plugin/args.go index d85a624c77e..8b8f9c2e291 100644 --- a/pkg/plugin/args.go +++ b/pkg/plugin/args.go @@ -1,5 +1,7 @@ package plugin +type OperationInput map[string]interface{} + type PluginArgInput struct { Key string `json:"key"` Value *PluginValueInput `json:"value"` @@ -14,28 +16,11 @@ type PluginValueInput struct { A []*PluginValueInput `json:"a"` } -func findArg(args []*PluginArgInput, name string) *PluginArgInput { - for _, v := range args { - if v.Key == name { - return v - } - } - - return nil -} - -func applyDefaultArgs(args []*PluginArgInput, defaultArgs map[string]string) []*PluginArgInput { +func applyDefaultArgs(args OperationInput, defaultArgs map[string]string) { for k, v := range defaultArgs { - if arg := findArg(args, k); arg == nil { - v := v // Copy v, because it's being exported out of the loop - args = append(args, &PluginArgInput{ - Key: k, - Value: &PluginValueInput{ - Str: &v, - }, - }) + _, found := args[k] + if !found { + args[k] = v } } - - return args } diff --git a/pkg/plugin/convert.go b/pkg/plugin/convert.go index 7aeb9598319..6b910428574 100644 --- a/pkg/plugin/convert.go +++ b/pkg/plugin/convert.go @@ -4,38 +4,11 @@ import ( "github.com/stashapp/stash/pkg/plugin/common" ) -func toPluginArgs(args []*PluginArgInput) common.ArgsMap { +func toPluginArgs(args OperationInput) common.ArgsMap { ret := make(common.ArgsMap) - for _, a := range args { - ret[a.Key] = toPluginArgValue(a.Value) + for k, a := range args { + ret[k] = common.PluginArgValue(a) } return ret } - -func toPluginArgValue(arg *PluginValueInput) common.PluginArgValue { - if arg == nil { - return nil - } - - switch { - case arg.Str != nil: - return common.PluginArgValue(*arg.Str) - case arg.I != nil: - return common.PluginArgValue(*arg.I) - case arg.B != nil: - return common.PluginArgValue(*arg.B) - case arg.F != nil: - return common.PluginArgValue(*arg.F) - case arg.O != nil: - return common.PluginArgValue(toPluginArgs(arg.O)) - case arg.A != nil: - var ret []common.PluginArgValue - for _, v := range arg.A { - ret = append(ret, toPluginArgValue(v)) - } - return common.PluginArgValue(ret) - } - - return nil -} diff --git a/pkg/plugin/plugins.go b/pkg/plugin/plugins.go index b92539529dc..cc158bb6135 100644 --- a/pkg/plugin/plugins.go +++ b/pkg/plugin/plugins.go @@ -225,8 +225,8 @@ func (c Cache) ListPluginTasks() []*PluginTask { return ret } -func buildPluginInput(plugin *Config, operation *OperationConfig, serverConnection common.StashServerConnection, args []*PluginArgInput) common.PluginInput { - args = applyDefaultArgs(args, operation.DefaultArgs) +func buildPluginInput(plugin *Config, operation *OperationConfig, serverConnection common.StashServerConnection, args OperationInput) common.PluginInput { + applyDefaultArgs(args, operation.DefaultArgs) serverConnection.PluginDir = plugin.getConfigPath() return common.PluginInput{ ServerConnection: serverConnection, @@ -255,7 +255,7 @@ func (c Cache) makeServerConnection(ctx context.Context) common.StashServerConne // CreateTask runs the plugin operation for the pluginID and operation // name provided. Returns an error if the plugin or the operation could not be // resolved. -func (c Cache) CreateTask(ctx context.Context, pluginID string, operationName string, args []*PluginArgInput, progress chan float64) (Task, error) { +func (c Cache) CreateTask(ctx context.Context, pluginID string, operationName string, args OperationInput, progress chan float64) (Task, error) { serverConnection := c.makeServerConnection(ctx) if c.pluginDisabled(pluginID) { From 07002f440baa4f79304402ae9eb01461235160dd Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:32:06 +1100 Subject: [PATCH 2/5] Support running plugin task without task name --- graphql/schema/schema.graphql | 13 +++++++++++-- internal/api/resolver_mutation_plugin.go | 5 +++-- internal/manager/task_plugin.go | 17 +++++++++++++++-- pkg/plugin/config.go | 4 +++- pkg/plugin/plugins.go | 15 ++++++++++----- pkg/plugin/raw.go | 2 +- pkg/plugin/rpc.go | 2 +- 7 files changed, 44 insertions(+), 14 deletions(-) diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index ba95e1fe6ea..f70857e2cf5 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -389,10 +389,19 @@ type Mutation { """ setPluginsEnabled(enabledMap: BoolMap!): Boolean! - "Run plugin task. Returns the job ID" + """ + Run a plugin task. + If task_name is provided, then the task must exist in the plugin config and the tasks configuration + will be used to run the plugin. + If no task_name is provided, then the plugin will be executed with the arguments provided only. + Returns the job ID + """ runPluginTask( plugin_id: ID! - task_name: String! + "if provided, then the default args will be applied" + task_name: String + "displayed in the task queue" + description: String args: [PluginArgInput!] @deprecated(reason: "Use args_map instead") args_map: Map ): ID! diff --git a/internal/api/resolver_mutation_plugin.go b/internal/api/resolver_mutation_plugin.go index 0d9e5edb616..b6e10d8efc4 100644 --- a/internal/api/resolver_mutation_plugin.go +++ b/internal/api/resolver_mutation_plugin.go @@ -49,7 +49,8 @@ func toPluginArgValue(arg *plugin.PluginValueInput) interface{} { func (r *mutationResolver) RunPluginTask( ctx context.Context, pluginID string, - taskName string, + taskName *string, + description *string, args []*plugin.PluginArgInput, argsMap map[string]interface{}, ) (string, error) { @@ -60,7 +61,7 @@ func (r *mutationResolver) RunPluginTask( } m := manager.GetInstance() - jobID := m.RunPluginTask(ctx, pluginID, taskName, argsMap) + jobID := m.RunPluginTask(ctx, pluginID, taskName, description, argsMap) return strconv.Itoa(jobID), nil } diff --git a/internal/manager/task_plugin.go b/internal/manager/task_plugin.go index 142b0ecaf29..c8be8dfd5c7 100644 --- a/internal/manager/task_plugin.go +++ b/internal/manager/task_plugin.go @@ -9,7 +9,13 @@ import ( "github.com/stashapp/stash/pkg/plugin" ) -func (s *Manager) RunPluginTask(ctx context.Context, pluginID string, taskName string, args plugin.OperationInput) int { +func (s *Manager) RunPluginTask( + ctx context.Context, + pluginID string, + taskName *string, + description *string, + args plugin.OperationInput, +) int { j := job.MakeJobExec(func(jobCtx context.Context, progress *job.Progress) { pluginProgress := make(chan float64) task, err := s.PluginCache.CreateTask(ctx, pluginID, taskName, args, pluginProgress) @@ -56,5 +62,12 @@ func (s *Manager) RunPluginTask(ctx context.Context, pluginID string, taskName s } }) - return s.JobManager.Add(ctx, fmt.Sprintf("Running plugin task: %s", taskName), j) + displayName := pluginID + if taskName != nil { + displayName = *taskName + } + if description != nil { + displayName = *description + } + return s.JobManager.Add(ctx, fmt.Sprintf("Running plugin task: %s", displayName), j) } diff --git a/pkg/plugin/config.go b/pkg/plugin/config.go index 325ba56e8e3..88e7e73247c 100644 --- a/pkg/plugin/config.go +++ b/pkg/plugin/config.go @@ -295,7 +295,9 @@ func (c Config) getConfigPath() string { func (c Config) getExecCommand(task *OperationConfig) []string { ret := c.Exec - ret = append(ret, task.ExecArgs...) + if task != nil { + ret = append(ret, task.ExecArgs...) + } if len(ret) > 0 { _, err := exec.LookPath(ret[0]) diff --git a/pkg/plugin/plugins.go b/pkg/plugin/plugins.go index cc158bb6135..dd102470922 100644 --- a/pkg/plugin/plugins.go +++ b/pkg/plugin/plugins.go @@ -226,7 +226,9 @@ func (c Cache) ListPluginTasks() []*PluginTask { } func buildPluginInput(plugin *Config, operation *OperationConfig, serverConnection common.StashServerConnection, args OperationInput) common.PluginInput { - applyDefaultArgs(args, operation.DefaultArgs) + if operation != nil { + applyDefaultArgs(args, operation.DefaultArgs) + } serverConnection.PluginDir = plugin.getConfigPath() return common.PluginInput{ ServerConnection: serverConnection, @@ -255,7 +257,7 @@ func (c Cache) makeServerConnection(ctx context.Context) common.StashServerConne // CreateTask runs the plugin operation for the pluginID and operation // name provided. Returns an error if the plugin or the operation could not be // resolved. -func (c Cache) CreateTask(ctx context.Context, pluginID string, operationName string, args OperationInput, progress chan float64) (Task, error) { +func (c Cache) CreateTask(ctx context.Context, pluginID string, operationName *string, args OperationInput, progress chan float64) (Task, error) { serverConnection := c.makeServerConnection(ctx) if c.pluginDisabled(pluginID) { @@ -269,9 +271,12 @@ func (c Cache) CreateTask(ctx context.Context, pluginID string, operationName st return nil, fmt.Errorf("no plugin with ID %s", pluginID) } - operation := plugin.getTask(operationName) - if operation == nil { - return nil, fmt.Errorf("no task with name %s in plugin %s", operationName, plugin.getName()) + var operation *OperationConfig + if operationName != nil { + operation = plugin.getTask(*operationName) + if operation == nil { + return nil, fmt.Errorf("no task with name %s in plugin %s", *operationName, plugin.getName()) + } } task := pluginTask{ diff --git a/pkg/plugin/raw.go b/pkg/plugin/raw.go index f276e139c4d..6b78451effe 100644 --- a/pkg/plugin/raw.go +++ b/pkg/plugin/raw.go @@ -41,7 +41,7 @@ func (t *rawPluginTask) Start() error { command := t.plugin.getExecCommand(t.operation) if len(command) == 0 { - return fmt.Errorf("empty exec value in operation %s", t.operation.Name) + return fmt.Errorf("empty exec value") } var cmd *exec.Cmd diff --git a/pkg/plugin/rpc.go b/pkg/plugin/rpc.go index 49955a55ba4..ebe6a404440 100644 --- a/pkg/plugin/rpc.go +++ b/pkg/plugin/rpc.go @@ -53,7 +53,7 @@ func (t *rpcPluginTask) Start() error { command := t.plugin.getExecCommand(t.operation) if len(command) == 0 { - return fmt.Errorf("empty exec value in operation %s", t.operation.Name) + return fmt.Errorf("empty exec value") } pluginErrReader, pluginErrWriter := io.Pipe() From b29f1c9519caf45536bcc23a74797c8162151106 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:12:42 +1100 Subject: [PATCH 3/5] Add interface to run plugin operations --- graphql/schema/schema.graphql | 10 +++ internal/api/resolver_mutation_plugin.go | 13 ++++ pkg/plugin/examples/js/js.js | 42 ++++++++----- pkg/plugin/js.go | 4 +- pkg/plugin/plugins.go | 80 +++++++++++++++++++----- 5 files changed, 119 insertions(+), 30 deletions(-) diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index f70857e2cf5..f7299dc3414 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -405,6 +405,16 @@ type Mutation { args: [PluginArgInput!] @deprecated(reason: "Use args_map instead") args_map: Map ): ID! + + """ + Runs a plugin operation. The operation is run immediately and does not use the job queue. + Returns a map of the result. + """ + runPluginOperation( + plugin_id: ID! + args: Map + ): Any + reloadPlugins: Boolean! """ diff --git a/internal/api/resolver_mutation_plugin.go b/internal/api/resolver_mutation_plugin.go index b6e10d8efc4..15d3e47218c 100644 --- a/internal/api/resolver_mutation_plugin.go +++ b/internal/api/resolver_mutation_plugin.go @@ -65,6 +65,19 @@ func (r *mutationResolver) RunPluginTask( return strconv.Itoa(jobID), nil } +func (r *mutationResolver) RunPluginOperation( + ctx context.Context, + pluginID string, + args map[string]interface{}, +) (interface{}, error) { + if args == nil { + args = make(map[string]interface{}) + } + + m := manager.GetInstance() + return m.PluginCache.RunPlugin(ctx, pluginID, args) +} + func (r *mutationResolver) ReloadPlugins(ctx context.Context) (bool, error) { manager.GetInstance().RefreshPluginCache() return true, nil diff --git a/pkg/plugin/examples/js/js.js b/pkg/plugin/examples/js/js.js index 39ba1f5c6de..b9c8063c4e2 100644 --- a/pkg/plugin/examples/js/js.js +++ b/pkg/plugin/examples/js/js.js @@ -2,26 +2,40 @@ var tagName = "Hawwwwt" function main() { var modeArg = input.Args.mode; - try { - if (modeArg == "" || modeArg == "add") { - addTag(); - } else if (modeArg == "remove") { - removeTag(); - } else if (modeArg == "long") { - doLongTask(); - } else if (modeArg == "indef") { - doIndefiniteTask(); - } else if (modeArg == "hook") { - doHookTask(); + if (modeArg !== undefined) { + try { + if (modeArg == "" || modeArg == "add") { + addTag(); + } else if (modeArg == "remove") { + removeTag(); + } else if (modeArg == "long") { + doLongTask(); + } else if (modeArg == "indef") { + doIndefiniteTask(); + } else if (modeArg == "hook") { + doHookTask(); + } + } catch (err) { + return { + Error: err + }; } - } catch (err) { + + return { + Output: "ok" + }; + } + + if (input.Args.error) { return { - Error: err + Error: input.Args.error }; } + // immediate mode + // just return the args return { - Output: "ok" + Output: input.Args }; } diff --git a/pkg/plugin/js.go b/pkg/plugin/js.go index 2ebbb6ccc08..e50376c5474 100644 --- a/pkg/plugin/js.go +++ b/pkg/plugin/js.go @@ -45,7 +45,9 @@ func (t *jsPluginTask) makeOutput(o otto.Value) { return } - t.result.Output, _ = asObj.Get("Output") + output, _ := asObj.Get("Output") + t.result.Output, _ = output.Export() + err, _ := asObj.Get("Error") if !err.IsUndefined() { errStr := err.String() diff --git a/pkg/plugin/plugins.go b/pkg/plugin/plugins.go index dd102470922..d6f7161f78b 100644 --- a/pkg/plugin/plugins.go +++ b/pkg/plugin/plugins.go @@ -9,6 +9,7 @@ package plugin import ( "context" + "errors" "fmt" "net/http" "os" @@ -290,6 +291,68 @@ func (c Cache) CreateTask(ctx context.Context, pluginID string, operationName *s return task.createTask(), nil } +func (c Cache) RunPlugin(ctx context.Context, pluginID string, args OperationInput) (interface{}, error) { + serverConnection := c.makeServerConnection(ctx) + + if c.pluginDisabled(pluginID) { + return nil, fmt.Errorf("plugin %s is disabled", pluginID) + } + + // find the plugin + plugin := c.getPlugin(pluginID) + + pluginInput := buildPluginInput(plugin, nil, serverConnection, args) + + pt := pluginTask{ + plugin: plugin, + input: pluginInput, + gqlHandler: c.gqlHandler, + serverConfig: c.config, + } + + task := pt.createTask() + if err := task.Start(); err != nil { + return nil, err + } + + if err := waitForTask(ctx, task); err != nil { + return nil, err + } + + output := task.GetResult() + if output == nil { + logger.Debugf("%s: returned no result", pluginID) + return nil, nil + } else { + if output.Error != nil { + return nil, errors.New(*output.Error) + } + + return output.Output, nil + } +} + +func waitForTask(ctx context.Context, task Task) error { + // handle cancel from context + c := make(chan struct{}) + go func() { + task.Wait() + close(c) + }() + + select { + case <-ctx.Done(): + if err := task.Stop(); err != nil { + logger.Warnf("could not stop task: %v", err) + } + return fmt.Errorf("operation cancelled") + case <-c: + // task finished normally + } + + return nil +} + func (c Cache) ExecutePostHooks(ctx context.Context, id int, hookType HookTriggerEnum, input interface{}, inputFields []string) { if err := c.executePostHooks(ctx, hookType, common.HookContext{ ID: id, @@ -348,21 +411,8 @@ func (c Cache) executePostHooks(ctx context.Context, hookType HookTriggerEnum, h return err } - // handle cancel from context - c := make(chan struct{}) - go func() { - task.Wait() - close(c) - }() - - select { - case <-ctx.Done(): - if err := task.Stop(); err != nil { - logger.Warnf("could not stop task: %v", err) - } - return fmt.Errorf("operation cancelled") - case <-c: - // task finished normally + if err := waitForTask(ctx, task); err != nil { + return err } output := task.GetResult() From 4008dc8828e687b551b27b6a0b9315d98681f556 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:38:11 +1100 Subject: [PATCH 4/5] Update RunPluginTask client mutation --- ui/v2.5/graphql/mutations/plugins.graphql | 12 ++++++------ ui/v2.5/src/core/StashService.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ui/v2.5/graphql/mutations/plugins.graphql b/ui/v2.5/graphql/mutations/plugins.graphql index e39611625ba..c4bd3cad9c5 100644 --- a/ui/v2.5/graphql/mutations/plugins.graphql +++ b/ui/v2.5/graphql/mutations/plugins.graphql @@ -2,12 +2,12 @@ mutation ReloadPlugins { reloadPlugins } -mutation RunPluginTask( - $plugin_id: ID! - $task_name: String! - $args: [PluginArgInput!] -) { - runPluginTask(plugin_id: $plugin_id, task_name: $task_name, args: $args) +mutation RunPluginTask($plugin_id: ID!, $task_name: String!, $args_map: Map) { + runPluginTask( + plugin_id: $plugin_id + task_name: $task_name + args_map: $args_map + ) } mutation ConfigurePlugin($plugin_id: ID!, $input: Map!) { diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index 8ff6e388a72..aa0f6131302 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -2360,7 +2360,7 @@ export const mutateMetadataClean = (input: GQL.CleanMetadataInput) => export const mutateRunPluginTask = ( pluginId: string, taskName: string, - args?: GQL.PluginArgInput[] + args?: GQL.Scalars["Map"]["input"] ) => client.mutate({ mutation: GQL.RunPluginTaskDocument, From bd7ae7b4df5802e17e6fee1b90c79ceb850f7d95 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 21 Feb 2024 13:38:18 +1100 Subject: [PATCH 5/5] Pretty --- graphql/schema/schema.graphql | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index f7299dc3414..d1994378ead 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -390,7 +390,7 @@ type Mutation { setPluginsEnabled(enabledMap: BoolMap!): Boolean! """ - Run a plugin task. + Run a plugin task. If task_name is provided, then the task must exist in the plugin config and the tasks configuration will be used to run the plugin. If no task_name is provided, then the plugin will be executed with the arguments provided only. @@ -410,10 +410,7 @@ type Mutation { Runs a plugin operation. The operation is run immediately and does not use the job queue. Returns a map of the result. """ - runPluginOperation( - plugin_id: ID! - args: Map - ): Any + runPluginOperation(plugin_id: ID!, args: Map): Any reloadPlugins: Boolean!