mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
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:
@@ -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!
|
||||
|
||||
"""
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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!) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user