diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 9db3d90ab53..d1994378ead 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -389,12 +389,29 @@ 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! - args: [PluginArgInput!] + "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! + + """ + 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 8aa520c08c5..15d3e47218c 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,72 @@ 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, + description *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() + jobID := m.RunPluginTask(ctx, pluginID, taskName, description, argsMap) + 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() - m.RunPluginTask(ctx, pluginID, taskName, args) - return "todo", nil + return m.PluginCache.RunPlugin(ctx, pluginID, args) } 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..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.PluginArgInput) 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/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/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/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/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 b92539529dc..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" @@ -225,8 +226,10 @@ 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 { + if operation != nil { + applyDefaultArgs(args, operation.DefaultArgs) + } serverConnection.PluginDir = plugin.getConfigPath() return common.PluginInput{ ServerConnection: serverConnection, @@ -255,7 +258,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) { @@ -269,9 +272,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{ @@ -285,6 +291,68 @@ func (c Cache) CreateTask(ctx context.Context, pluginID string, operationName st 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, @@ -343,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() 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() 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,