Plugin API improvements (#4603)

* Accept plain map for runPluginTask
* Support running plugin task without task name
* Add interface to run plugin operations
* Update RunPluginTask client mutation
This commit is contained in:
WithoutPants
2024-02-22 11:20:21 +11:00
committed by GitHub
parent a8c909e0c9
commit 0c2a2190e5
13 changed files with 229 additions and 105 deletions

View File

@@ -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!
"""

View File

@@ -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()
m.RunPluginTask(ctx, pluginID, taskName, args)
return "todo", nil
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()
return m.PluginCache.RunPlugin(ctx, pluginID, args)
}
func (r *mutationResolver) ReloadPlugins(ctx context.Context) (bool, error) {

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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])

View File

@@ -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
}

View File

@@ -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 {
Error: err
Output: "ok"
};
}
if (input.Args.error) {
return {
Error: input.Args.error
};
}
// immediate mode
// just return the args
return {
Output: "ok"
Output: input.Args
};
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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!) {

View File

@@ -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<GQL.RunPluginTaskMutation>({
mutation: GQL.RunPluginTaskDocument,