Improve plugin hook cyclic detection (#4625)

* Move and rename HookTriggerEnum into separate package
* Move visited plugin hook handler code
* Allow up to ten plugin hook loops
This commit is contained in:
WithoutPants
2024-02-28 08:29:25 +11:00
committed by GitHub
parent 3a56dd98db
commit fcf249e5f6
20 changed files with 345 additions and 271 deletions

View File

@@ -9,6 +9,7 @@ import (
"sort"
"strings"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/utils"
"gopkg.in/yaml.v2"
)
@@ -195,7 +196,7 @@ func (c Config) getPluginHooks(includePlugin bool) []*PluginHook {
return ret
}
func convertHooks(hooks []HookTriggerEnum) []string {
func convertHooks(hooks []hook.TriggerEnum) []string {
var ret []string
for _, h := range hooks {
ret = append(ret, h.String())
@@ -275,7 +276,7 @@ func (c Config) getTask(name string) *OperationConfig {
return nil
}
func (c Config) getHooks(hookType HookTriggerEnum) []*HookConfig {
func (c Config) getHooks(hookType hook.TriggerEnum) []*HookConfig {
var ret []*HookConfig
for _, h := range c.Hooks {
for _, t := range h.TriggeredBy {
@@ -399,7 +400,7 @@ type HookConfig struct {
OperationConfig `yaml:",inline"`
// A list of stash operations that will be used to trigger this hook operation.
TriggeredBy []HookTriggerEnum `yaml:"triggeredBy"`
TriggeredBy []hook.TriggerEnum `yaml:"triggeredBy"`
}
func loadPluginFromYAML(reader io.Reader) (*Config, error) {

131
pkg/plugin/hook/hooks.go Normal file
View File

@@ -0,0 +1,131 @@
package hook
type TriggerEnum string
// Scan-related hooks are current disabled until post-hook execution is
// integrated.
const (
SceneMarkerCreatePost TriggerEnum = "SceneMarker.Create.Post"
SceneMarkerUpdatePost TriggerEnum = "SceneMarker.Update.Post"
SceneMarkerDestroyPost TriggerEnum = "SceneMarker.Destroy.Post"
SceneCreatePost TriggerEnum = "Scene.Create.Post"
SceneUpdatePost TriggerEnum = "Scene.Update.Post"
SceneDestroyPost TriggerEnum = "Scene.Destroy.Post"
ImageCreatePost TriggerEnum = "Image.Create.Post"
ImageUpdatePost TriggerEnum = "Image.Update.Post"
ImageDestroyPost TriggerEnum = "Image.Destroy.Post"
GalleryCreatePost TriggerEnum = "Gallery.Create.Post"
GalleryUpdatePost TriggerEnum = "Gallery.Update.Post"
GalleryDestroyPost TriggerEnum = "Gallery.Destroy.Post"
GalleryChapterCreatePost TriggerEnum = "GalleryChapter.Create.Post"
GalleryChapterUpdatePost TriggerEnum = "GalleryChapter.Update.Post"
GalleryChapterDestroyPost TriggerEnum = "GalleryChapter.Destroy.Post"
MovieCreatePost TriggerEnum = "Movie.Create.Post"
MovieUpdatePost TriggerEnum = "Movie.Update.Post"
MovieDestroyPost TriggerEnum = "Movie.Destroy.Post"
PerformerCreatePost TriggerEnum = "Performer.Create.Post"
PerformerUpdatePost TriggerEnum = "Performer.Update.Post"
PerformerDestroyPost TriggerEnum = "Performer.Destroy.Post"
StudioCreatePost TriggerEnum = "Studio.Create.Post"
StudioUpdatePost TriggerEnum = "Studio.Update.Post"
StudioDestroyPost TriggerEnum = "Studio.Destroy.Post"
TagCreatePost TriggerEnum = "Tag.Create.Post"
TagUpdatePost TriggerEnum = "Tag.Update.Post"
TagMergePost TriggerEnum = "Tag.Merge.Post"
TagDestroyPost TriggerEnum = "Tag.Destroy.Post"
)
var AllHookTriggerEnum = []TriggerEnum{
SceneMarkerCreatePost,
SceneMarkerUpdatePost,
SceneMarkerDestroyPost,
SceneCreatePost,
SceneUpdatePost,
SceneDestroyPost,
ImageCreatePost,
ImageUpdatePost,
ImageDestroyPost,
GalleryCreatePost,
GalleryUpdatePost,
GalleryDestroyPost,
GalleryChapterCreatePost,
GalleryChapterUpdatePost,
GalleryChapterDestroyPost,
MovieCreatePost,
MovieUpdatePost,
MovieDestroyPost,
PerformerCreatePost,
PerformerUpdatePost,
PerformerDestroyPost,
StudioCreatePost,
StudioUpdatePost,
StudioDestroyPost,
TagCreatePost,
TagUpdatePost,
TagMergePost,
TagDestroyPost,
}
func (e TriggerEnum) IsValid() bool {
switch e {
case SceneMarkerCreatePost,
SceneMarkerUpdatePost,
SceneMarkerDestroyPost,
SceneCreatePost,
SceneUpdatePost,
SceneDestroyPost,
ImageCreatePost,
ImageUpdatePost,
ImageDestroyPost,
GalleryCreatePost,
GalleryUpdatePost,
GalleryDestroyPost,
GalleryChapterCreatePost,
GalleryChapterUpdatePost,
GalleryChapterDestroyPost,
MovieCreatePost,
MovieUpdatePost,
MovieDestroyPost,
PerformerCreatePost,
PerformerUpdatePost,
PerformerDestroyPost,
StudioCreatePost,
StudioUpdatePost,
StudioDestroyPost,
TagCreatePost,
TagUpdatePost,
TagDestroyPost:
return true
}
return false
}
func (e TriggerEnum) String() string {
return string(e)
}

View File

@@ -12,136 +12,6 @@ type PluginHook struct {
Plugin *Plugin `json:"plugin"`
}
type HookTriggerEnum string
// Scan-related hooks are current disabled until post-hook execution is
// integrated.
const (
SceneMarkerCreatePost HookTriggerEnum = "SceneMarker.Create.Post"
SceneMarkerUpdatePost HookTriggerEnum = "SceneMarker.Update.Post"
SceneMarkerDestroyPost HookTriggerEnum = "SceneMarker.Destroy.Post"
SceneCreatePost HookTriggerEnum = "Scene.Create.Post"
SceneUpdatePost HookTriggerEnum = "Scene.Update.Post"
SceneDestroyPost HookTriggerEnum = "Scene.Destroy.Post"
ImageCreatePost HookTriggerEnum = "Image.Create.Post"
ImageUpdatePost HookTriggerEnum = "Image.Update.Post"
ImageDestroyPost HookTriggerEnum = "Image.Destroy.Post"
GalleryCreatePost HookTriggerEnum = "Gallery.Create.Post"
GalleryUpdatePost HookTriggerEnum = "Gallery.Update.Post"
GalleryDestroyPost HookTriggerEnum = "Gallery.Destroy.Post"
GalleryChapterCreatePost HookTriggerEnum = "GalleryChapter.Create.Post"
GalleryChapterUpdatePost HookTriggerEnum = "GalleryChapter.Update.Post"
GalleryChapterDestroyPost HookTriggerEnum = "GalleryChapter.Destroy.Post"
MovieCreatePost HookTriggerEnum = "Movie.Create.Post"
MovieUpdatePost HookTriggerEnum = "Movie.Update.Post"
MovieDestroyPost HookTriggerEnum = "Movie.Destroy.Post"
PerformerCreatePost HookTriggerEnum = "Performer.Create.Post"
PerformerUpdatePost HookTriggerEnum = "Performer.Update.Post"
PerformerDestroyPost HookTriggerEnum = "Performer.Destroy.Post"
StudioCreatePost HookTriggerEnum = "Studio.Create.Post"
StudioUpdatePost HookTriggerEnum = "Studio.Update.Post"
StudioDestroyPost HookTriggerEnum = "Studio.Destroy.Post"
TagCreatePost HookTriggerEnum = "Tag.Create.Post"
TagUpdatePost HookTriggerEnum = "Tag.Update.Post"
TagMergePost HookTriggerEnum = "Tag.Merge.Post"
TagDestroyPost HookTriggerEnum = "Tag.Destroy.Post"
)
var AllHookTriggerEnum = []HookTriggerEnum{
SceneMarkerCreatePost,
SceneMarkerUpdatePost,
SceneMarkerDestroyPost,
SceneCreatePost,
SceneUpdatePost,
SceneDestroyPost,
ImageCreatePost,
ImageUpdatePost,
ImageDestroyPost,
GalleryCreatePost,
GalleryUpdatePost,
GalleryDestroyPost,
GalleryChapterCreatePost,
GalleryChapterUpdatePost,
GalleryChapterDestroyPost,
MovieCreatePost,
MovieUpdatePost,
MovieDestroyPost,
PerformerCreatePost,
PerformerUpdatePost,
PerformerDestroyPost,
StudioCreatePost,
StudioUpdatePost,
StudioDestroyPost,
TagCreatePost,
TagUpdatePost,
TagMergePost,
TagDestroyPost,
}
func (e HookTriggerEnum) IsValid() bool {
switch e {
case SceneMarkerCreatePost,
SceneMarkerUpdatePost,
SceneMarkerDestroyPost,
SceneCreatePost,
SceneUpdatePost,
SceneDestroyPost,
ImageCreatePost,
ImageUpdatePost,
ImageDestroyPost,
GalleryCreatePost,
GalleryUpdatePost,
GalleryDestroyPost,
GalleryChapterCreatePost,
GalleryChapterUpdatePost,
GalleryChapterDestroyPost,
MovieCreatePost,
MovieUpdatePost,
MovieDestroyPost,
PerformerCreatePost,
PerformerUpdatePost,
PerformerDestroyPost,
StudioCreatePost,
StudioUpdatePost,
StudioDestroyPost,
TagCreatePost,
TagUpdatePost,
TagDestroyPost:
return true
}
return false
}
func (e HookTriggerEnum) String() string {
return string(e)
}
func addHookContext(argsMap common.ArgsMap, hookContext common.HookContext) {
argsMap[common.HookContextKey] = hookContext
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin/common"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/session"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/txn"
@@ -356,7 +357,7 @@ func waitForTask(ctx context.Context, task Task) error {
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 hook.TriggerEnum, input interface{}, inputFields []string) {
if err := c.executePostHooks(ctx, hookType, common.HookContext{
ID: id,
Type: hookType.String(),
@@ -367,7 +368,7 @@ func (c Cache) ExecutePostHooks(ctx context.Context, id int, hookType HookTrigge
}
}
func (c Cache) RegisterPostHooks(ctx context.Context, id int, hookType HookTriggerEnum, input interface{}, inputFields []string) {
func (c Cache) RegisterPostHooks(ctx context.Context, id int, hookType hook.TriggerEnum, input interface{}, inputFields []string) {
txn.AddPostCommitHook(ctx, func(ctx context.Context) {
c.ExecutePostHooks(ctx, id, hookType, input, inputFields)
})
@@ -379,23 +380,28 @@ func (c Cache) ExecuteSceneUpdatePostHooks(ctx context.Context, input models.Sce
logger.Errorf("error converting id in SceneUpdatePostHooks: %v", err)
return
}
c.ExecutePostHooks(ctx, id, SceneUpdatePost, input, inputFields)
c.ExecutePostHooks(ctx, id, hook.SceneUpdatePost, input, inputFields)
}
func (c Cache) executePostHooks(ctx context.Context, hookType HookTriggerEnum, hookContext common.HookContext) error {
visitedPlugins := session.GetVisitedPlugins(ctx)
// maxCyclicLoopDepth is the maximum number of identical plugin hook calls that
// can be made before a cyclic loop is detected. It is set to an arbitrary value
// that should not be hit under normal circumstances.
const maxCyclicLoopDepth = 10
func (c Cache) executePostHooks(ctx context.Context, hookType hook.TriggerEnum, hookContext common.HookContext) error {
visitedPluginHookCounts := getVisitedPluginHookCounts(ctx)
for _, p := range c.enabledPlugins() {
hooks := p.getHooks(hookType)
// don't revisit a plugin we've already visited
// only log if there's hooks that we're skipping
if len(hooks) > 0 && sliceutil.Contains(visitedPlugins, p.id) {
logger.Debugf("plugin ID '%s' already triggered, not re-triggering", p.id)
if len(hooks) > 0 && visitedPluginHookCounts.For(p.id, hookType) >= maxCyclicLoopDepth {
logger.Debugf("cyclic loop detected: plugin ID '%s' hook %s, not re-triggering", p.id, hookType)
continue
}
for _, h := range hooks {
newCtx := session.AddVisitedPlugin(ctx, p.id)
newCtx := session.AddVisitedPluginHook(ctx, p.id, hookType)
serverConnection := c.makeServerConnection(newCtx)
pluginInput := buildPluginInput(&p, &h.OperationConfig, serverConnection, nil)
@@ -434,6 +440,46 @@ func (c Cache) executePostHooks(ctx context.Context, hookType HookTriggerEnum, h
return nil
}
type visitedPluginHookCount struct {
session.VisitedPluginHook
Count int
}
type visitedPluginHookCounts []visitedPluginHookCount
func (v visitedPluginHookCounts) For(pluginID string, hookType hook.TriggerEnum) int {
for _, c := range v {
if c.VisitedPluginHook.PluginID == pluginID && c.VisitedPluginHook.HookType == hookType {
return c.Count
}
}
return 0
}
func getVisitedPluginHookCounts(ctx context.Context) visitedPluginHookCounts {
visitedPluginHooks := session.GetVisitedPluginHooks(ctx)
visitedPluginHookCounts := make([]visitedPluginHookCount, 0)
for _, p := range visitedPluginHooks {
found := false
for i, v := range visitedPluginHookCounts {
if v.VisitedPluginHook == p {
visitedPluginHookCounts[i].Count++
found = true
break
}
}
if !found {
visitedPluginHookCounts = append(visitedPluginHookCounts, visitedPluginHookCount{
VisitedPluginHook: p,
Count: 1,
})
}
}
return visitedPluginHookCounts
}
func (c Cache) getPlugin(pluginID string) *Config {
for _, s := range c.plugins {
if s.id == pluginID {