mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +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!
|
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(
|
runPluginTask(
|
||||||
plugin_id: ID!
|
plugin_id: ID!
|
||||||
task_name: String!
|
"if provided, then the default args will be applied"
|
||||||
args: [PluginArgInput!]
|
task_name: String
|
||||||
|
"displayed in the task queue"
|
||||||
|
description: String
|
||||||
|
args: [PluginArgInput!] @deprecated(reason: "Use args_map instead")
|
||||||
|
args_map: Map
|
||||||
): ID!
|
): 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!
|
reloadPlugins: Boolean!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/internal/manager"
|
"github.com/stashapp/stash/internal/manager"
|
||||||
"github.com/stashapp/stash/internal/manager/config"
|
"github.com/stashapp/stash/internal/manager/config"
|
||||||
@@ -9,10 +10,72 @@ import (
|
|||||||
"github.com/stashapp/stash/pkg/sliceutil"
|
"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 := manager.GetInstance()
|
||||||
m.RunPluginTask(ctx, pluginID, taskName, args)
|
jobID := m.RunPluginTask(ctx, pluginID, taskName, description, argsMap)
|
||||||
return "todo", nil
|
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) {
|
func (r *mutationResolver) ReloadPlugins(ctx context.Context) (bool, error) {
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import (
|
|||||||
"github.com/stashapp/stash/pkg/plugin"
|
"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) {
|
j := job.MakeJobExec(func(jobCtx context.Context, progress *job.Progress) {
|
||||||
pluginProgress := make(chan float64)
|
pluginProgress := make(chan float64)
|
||||||
task, err := s.PluginCache.CreateTask(ctx, pluginID, taskName, args, pluginProgress)
|
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
|
package plugin
|
||||||
|
|
||||||
|
type OperationInput map[string]interface{}
|
||||||
|
|
||||||
type PluginArgInput struct {
|
type PluginArgInput struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Value *PluginValueInput `json:"value"`
|
Value *PluginValueInput `json:"value"`
|
||||||
@@ -14,28 +16,11 @@ type PluginValueInput struct {
|
|||||||
A []*PluginValueInput `json:"a"`
|
A []*PluginValueInput `json:"a"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func findArg(args []*PluginArgInput, name string) *PluginArgInput {
|
func applyDefaultArgs(args OperationInput, defaultArgs map[string]string) {
|
||||||
for _, v := range args {
|
|
||||||
if v.Key == name {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyDefaultArgs(args []*PluginArgInput, defaultArgs map[string]string) []*PluginArgInput {
|
|
||||||
for k, v := range defaultArgs {
|
for k, v := range defaultArgs {
|
||||||
if arg := findArg(args, k); arg == nil {
|
_, found := args[k]
|
||||||
v := v // Copy v, because it's being exported out of the loop
|
if !found {
|
||||||
args = append(args, &PluginArgInput{
|
args[k] = v
|
||||||
Key: k,
|
|
||||||
Value: &PluginValueInput{
|
|
||||||
Str: &v,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,7 +295,9 @@ func (c Config) getConfigPath() string {
|
|||||||
func (c Config) getExecCommand(task *OperationConfig) []string {
|
func (c Config) getExecCommand(task *OperationConfig) []string {
|
||||||
ret := c.Exec
|
ret := c.Exec
|
||||||
|
|
||||||
|
if task != nil {
|
||||||
ret = append(ret, task.ExecArgs...)
|
ret = append(ret, task.ExecArgs...)
|
||||||
|
}
|
||||||
|
|
||||||
if len(ret) > 0 {
|
if len(ret) > 0 {
|
||||||
_, err := exec.LookPath(ret[0])
|
_, err := exec.LookPath(ret[0])
|
||||||
|
|||||||
@@ -4,38 +4,11 @@ import (
|
|||||||
"github.com/stashapp/stash/pkg/plugin/common"
|
"github.com/stashapp/stash/pkg/plugin/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func toPluginArgs(args []*PluginArgInput) common.ArgsMap {
|
func toPluginArgs(args OperationInput) common.ArgsMap {
|
||||||
ret := make(common.ArgsMap)
|
ret := make(common.ArgsMap)
|
||||||
for _, a := range args {
|
for k, a := range args {
|
||||||
ret[a.Key] = toPluginArgValue(a.Value)
|
ret[k] = common.PluginArgValue(a)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
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,6 +2,7 @@ var tagName = "Hawwwwt"
|
|||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
var modeArg = input.Args.mode;
|
var modeArg = input.Args.mode;
|
||||||
|
if (modeArg !== undefined) {
|
||||||
try {
|
try {
|
||||||
if (modeArg == "" || modeArg == "add") {
|
if (modeArg == "" || modeArg == "add") {
|
||||||
addTag();
|
addTag();
|
||||||
@@ -23,6 +24,19 @@ function main() {
|
|||||||
return {
|
return {
|
||||||
Output: "ok"
|
Output: "ok"
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.Args.error) {
|
||||||
|
return {
|
||||||
|
Error: input.Args.error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// immediate mode
|
||||||
|
// just return the args
|
||||||
|
return {
|
||||||
|
Output: input.Args
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResult(result) {
|
function getResult(result) {
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ func (t *jsPluginTask) makeOutput(o otto.Value) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.result.Output, _ = asObj.Get("Output")
|
output, _ := asObj.Get("Output")
|
||||||
|
t.result.Output, _ = output.Export()
|
||||||
|
|
||||||
err, _ := asObj.Get("Error")
|
err, _ := asObj.Get("Error")
|
||||||
if !err.IsUndefined() {
|
if !err.IsUndefined() {
|
||||||
errStr := err.String()
|
errStr := err.String()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ package plugin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -225,8 +226,10 @@ func (c Cache) ListPluginTasks() []*PluginTask {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildPluginInput(plugin *Config, operation *OperationConfig, serverConnection common.StashServerConnection, args []*PluginArgInput) common.PluginInput {
|
func buildPluginInput(plugin *Config, operation *OperationConfig, serverConnection common.StashServerConnection, args OperationInput) common.PluginInput {
|
||||||
args = applyDefaultArgs(args, operation.DefaultArgs)
|
if operation != nil {
|
||||||
|
applyDefaultArgs(args, operation.DefaultArgs)
|
||||||
|
}
|
||||||
serverConnection.PluginDir = plugin.getConfigPath()
|
serverConnection.PluginDir = plugin.getConfigPath()
|
||||||
return common.PluginInput{
|
return common.PluginInput{
|
||||||
ServerConnection: serverConnection,
|
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
|
// CreateTask runs the plugin operation for the pluginID and operation
|
||||||
// name provided. Returns an error if the plugin or the operation could not be
|
// name provided. Returns an error if the plugin or the operation could not be
|
||||||
// resolved.
|
// 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)
|
serverConnection := c.makeServerConnection(ctx)
|
||||||
|
|
||||||
if c.pluginDisabled(pluginID) {
|
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)
|
return nil, fmt.Errorf("no plugin with ID %s", pluginID)
|
||||||
}
|
}
|
||||||
|
|
||||||
operation := plugin.getTask(operationName)
|
var operation *OperationConfig
|
||||||
|
if operationName != nil {
|
||||||
|
operation = plugin.getTask(*operationName)
|
||||||
if operation == nil {
|
if operation == nil {
|
||||||
return nil, fmt.Errorf("no task with name %s in plugin %s", operationName, plugin.getName())
|
return nil, fmt.Errorf("no task with name %s in plugin %s", *operationName, plugin.getName())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task := pluginTask{
|
task := pluginTask{
|
||||||
@@ -285,6 +291,68 @@ func (c Cache) CreateTask(ctx context.Context, pluginID string, operationName st
|
|||||||
return task.createTask(), nil
|
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) {
|
func (c Cache) ExecutePostHooks(ctx context.Context, id int, hookType HookTriggerEnum, input interface{}, inputFields []string) {
|
||||||
if err := c.executePostHooks(ctx, hookType, common.HookContext{
|
if err := c.executePostHooks(ctx, hookType, common.HookContext{
|
||||||
ID: id,
|
ID: id,
|
||||||
@@ -343,21 +411,8 @@ func (c Cache) executePostHooks(ctx context.Context, hookType HookTriggerEnum, h
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle cancel from context
|
if err := waitForTask(ctx, task); err != nil {
|
||||||
c := make(chan struct{})
|
return err
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output := task.GetResult()
|
output := task.GetResult()
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (t *rawPluginTask) Start() error {
|
|||||||
|
|
||||||
command := t.plugin.getExecCommand(t.operation)
|
command := t.plugin.getExecCommand(t.operation)
|
||||||
if len(command) == 0 {
|
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
|
var cmd *exec.Cmd
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (t *rpcPluginTask) Start() error {
|
|||||||
|
|
||||||
command := t.plugin.getExecCommand(t.operation)
|
command := t.plugin.getExecCommand(t.operation)
|
||||||
if len(command) == 0 {
|
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()
|
pluginErrReader, pluginErrWriter := io.Pipe()
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ mutation ReloadPlugins {
|
|||||||
reloadPlugins
|
reloadPlugins
|
||||||
}
|
}
|
||||||
|
|
||||||
mutation RunPluginTask(
|
mutation RunPluginTask($plugin_id: ID!, $task_name: String!, $args_map: Map) {
|
||||||
$plugin_id: ID!
|
runPluginTask(
|
||||||
$task_name: String!
|
plugin_id: $plugin_id
|
||||||
$args: [PluginArgInput!]
|
task_name: $task_name
|
||||||
) {
|
args_map: $args_map
|
||||||
runPluginTask(plugin_id: $plugin_id, task_name: $task_name, args: $args)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
mutation ConfigurePlugin($plugin_id: ID!, $input: Map!) {
|
mutation ConfigurePlugin($plugin_id: ID!, $input: Map!) {
|
||||||
|
|||||||
@@ -2360,7 +2360,7 @@ export const mutateMetadataClean = (input: GQL.CleanMetadataInput) =>
|
|||||||
export const mutateRunPluginTask = (
|
export const mutateRunPluginTask = (
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
taskName: string,
|
taskName: string,
|
||||||
args?: GQL.PluginArgInput[]
|
args?: GQL.Scalars["Map"]["input"]
|
||||||
) =>
|
) =>
|
||||||
client.mutate<GQL.RunPluginTaskMutation>({
|
client.mutate<GQL.RunPluginTaskMutation>({
|
||||||
mutation: GQL.RunPluginTaskDocument,
|
mutation: GQL.RunPluginTaskDocument,
|
||||||
|
|||||||
Reference in New Issue
Block a user