Added logic to stop FileMonitor immediately

Added command line options --stop, --restart, --url, and --trace.
Moved logging logic to StashPluginHelper.py
This commit is contained in:
David Maisonave
2024-08-05 20:23:39 -04:00
parent 1aef74b993
commit a1cec1fdbd
10 changed files with 484 additions and 173 deletions

View File

@@ -1,86 +1,29 @@
# Description: This is a Stash plugin which updates Stash if any changes occurs in the Stash library paths.
# By David Maisonave (aka Axter) Jul-2024 (https://www.axter.com/)
# Get the latest developers version from following link: https://github.com/David-Maisonave/Axter-Stash/tree/main/plugins/FileMonitor
# Note: To call this script outside of Stash, pass any argument.
# Example: python filemonitor.py start
# Note: To call this script outside of Stash, pass --url and the Stash URL.
# Example: python filemonitor.py --url http://localhost:9999
import os
import sys
import json
from pathlib import Path
import logging
from logging.handlers import RotatingFileHandler
from stashapi.stashapp import StashInterface
import time
import pathlib
import argparse
from StashPluginHelper import StashPluginHelper
from watchdog.observers import Observer # This is also needed for event attributes
import watchdog # pip install watchdog # https://pythonhosted.org/watchdog/
from threading import Lock, Condition
from multiprocessing import shared_memory
from filemonitor_config import config # Import settings from filemonitor_config.py
# **********************************************************************
# Constant global variables --------------------------------------------
LOG_FILE_PATH = log_file_path = f"{Path(__file__).resolve().parent}\\{Path(__file__).stem}.log"
FORMAT = "[%(asctime)s - LN:%(lineno)s] %(message)s"
PLUGIN_ARGS_MODE = False
PLUGIN_ID = Path(__file__).stem.lower()
RFH = RotatingFileHandler(
filename=LOG_FILE_PATH,
mode='a',
maxBytes=2*1024*1024, # Configure logging for this script with max log file size of 2000K
backupCount=2,
encoding=None,
delay=0
)
TIMEOUT = 5
CONTINUE_RUNNING_SIG = 99
STOP_RUNNING_SIG = 32
# **********************************************************************
# Global variables --------------------------------------------
exitMsg = "Change success!!"
mutex = Lock()
signal = Condition(mutex)
shouldUpdate = False
TargetPaths = []
runningInPluginMode = False
parser = argparse.ArgumentParser()
parser.add_argument('--url', '-u', dest='stash_url', type=str, help='Add Stash URL')
parser.add_argument('--trace', '-t', dest='trace', action='store_true', help='Enables debug trace mode.')
parser.add_argument('--stop', '-s', dest='stop', action='store_true', help='Stop (kill) a running FileMonitor task.')
parser.add_argument('--restart', '-r', dest='restart', action='store_true', help='Restart FileMonitor.')
parse_args = parser.parse_args()
# Configure local log file for plugin within plugin folder having a limited max log file size
logging.basicConfig(level=logging.INFO, format=FORMAT, datefmt="%y%m%d %H:%M:%S", handlers=[RFH])
logger = logging.getLogger(Path(__file__).stem)
# **********************************************************************
# ----------------------------------------------------------------------
# Code section to fetch variables from Plugin UI and from filemonitor_settings.py
# Check if being called as Stash plugin
gettingCalledAsStashPlugin = True
stopLibraryMonitoring = False
StdInRead = None
try:
if len(sys.argv) == 1:
print(f"Attempting to read stdin. (len(sys.argv)={len(sys.argv)})", file=sys.stderr)
StdInRead = sys.stdin.read()
else:
if len(sys.argv) > 1 and sys.argv[1].lower() == "stop":
stopLibraryMonitoring = True
raise Exception("Not called in plugin mode.")
except:
gettingCalledAsStashPlugin = False
print(f"Either len(sys.argv) not expected value OR sys.stdin.read() failed! (stopLibraryMonitoring={stopLibraryMonitoring}) (StdInRead={StdInRead}) (len(sys.argv)={len(sys.argv)})", file=sys.stderr)
pass
if gettingCalledAsStashPlugin and StdInRead:
print(f"StdInRead={StdInRead} (len(sys.argv)={len(sys.argv)})", file=sys.stderr)
runningInPluginMode = True
json_input = json.loads(StdInRead)
FRAGMENT_SERVER = json_input["server_connection"]
else:
runningInPluginMode = False
FRAGMENT_SERVER = {'Scheme': config['endpoint_Scheme'], 'Host': config['endpoint_Host'], 'Port': config['endpoint_Port'], 'SessionCookie': {'Name': 'session', 'Value': '', 'Path': '', 'Domain': '', 'Expires': '0001-01-01T00:00:00Z', 'RawExpires': '', 'MaxAge': 0, 'Secure': False, 'HttpOnly': False, 'SameSite': 0, 'Raw': '', 'Unparsed': None}, 'Dir': os.path.dirname(Path(__file__).resolve().parent), 'PluginDir': Path(__file__).resolve().parent}
print("Running in non-plugin mode!", file=sys.stderr)
stash = StashInterface(FRAGMENT_SERVER)
PLUGINCONFIGURATION = stash.get_configuration()["plugins"]
STASHCONFIGURATION = stash.get_configuration()["general"]
STASHPATHSCONFIG = STASHCONFIGURATION['stashes']
stashPaths = []
settings = {
"recursiveDisabled": False,
"runCleanAfterDelete": False,
@@ -88,39 +31,46 @@ settings = {
"zzdebugTracing": False,
"zzdryRun": False,
}
plugin = StashPluginHelper(
stash_url=parse_args.stash_url,
debugTracing=parse_args.trace,
settings=settings,
config=config,
mainScriptName = __file__)
plugin.Status()
plugin.Log(f"\nStarting (__file__={__file__}) (plugin.CALLED_AS_STASH_PLUGIN={plugin.CALLED_AS_STASH_PLUGIN}) (plugin.DEBUG_TRACING={plugin.DEBUG_TRACING}) (plugin.DRY_RUN={plugin.DRY_RUN}) (plugin.PLUGIN_TASK_NAME={plugin.PLUGIN_TASK_NAME})************************************************")
if PLUGIN_ID in PLUGINCONFIGURATION:
settings.update(PLUGINCONFIGURATION[PLUGIN_ID])
# ----------------------------------------------------------------------
debugTracing = settings["zzdebugTracing"]
RECURSIVE = settings["recursiveDisabled"] == False
SCAN_MODIFIED = settings["scanModified"]
RUN_CLEAN_AFTER_DELETE = settings["runCleanAfterDelete"]
RUN_GENERATE_CONTENT = config['runGenerateContent']
exitMsg = "Change success!!"
mutex = Lock()
signal = Condition(mutex)
shouldUpdate = False
TargetPaths = []
RECURSIVE = plugin.pluginSettings["recursiveDisabled"] == False
SCAN_MODIFIED = plugin.pluginSettings["scanModified"]
RUN_CLEAN_AFTER_DELETE = plugin.pluginSettings["runCleanAfterDelete"]
RUN_GENERATE_CONTENT = plugin.pluginConfig['runGenerateContent']
SCAN_ON_ANY_EVENT = plugin.pluginConfig['onAnyEvent']
SIGNAL_TIMEOUT = plugin.pluginConfig['timeOut']
CREATE_SPECIAL_FILE_TO_EXIT = plugin.pluginConfig['createSpecFileToExit']
DELETE_SPECIAL_FILE_ON_STOP = plugin.pluginConfig['deleteSpecFileInStop']
SPECIAL_FILE_DIR = f"{plugin.LOG_FILE_DIR}{os.sep}working"
if not os.path.exists(SPECIAL_FILE_DIR) and CREATE_SPECIAL_FILE_TO_EXIT:
os.makedirs(SPECIAL_FILE_DIR)
# Unique name to trigger shutting down FileMonitor
SPECIAL_FILE_NAME = f"{SPECIAL_FILE_DIR}{os.sep}trigger_to_kill_filemonitor_by_david_maisonave.txt"
STASHPATHSCONFIG = plugin.STASH_CONFIGURATION['stashes']
stashPaths = []
for item in STASHPATHSCONFIG:
stashPaths.append(item["path"])
stashPaths.append(SPECIAL_FILE_DIR)
plugin.Trace(f"(stashPaths={stashPaths})")
# Extract dry_run setting from settings
DRY_RUN = settings["zzdryRun"]
dry_run_prefix = ''
try:
PLUGIN_ARGS_MODE = json_input['args']["mode"]
except:
pass
logger.info(f"\nStarting (runningInPluginMode={runningInPluginMode}) (debugTracing={debugTracing}) (DRY_RUN={DRY_RUN}) (PLUGIN_ARGS_MODE={PLUGIN_ARGS_MODE})************************************************")
if debugTracing: logger.info(f"Debug Tracing (stash.get_configuration()={stash.get_configuration()})................")
if debugTracing: logger.info("settings: %s " % (settings,))
if debugTracing: logger.info(f"Debug Tracing (STASHCONFIGURATION={STASHCONFIGURATION})................")
if debugTracing: logger.info(f"Debug Tracing (stashPaths={stashPaths})................")
if DRY_RUN:
logger.info("Dry run mode is enabled.")
dry_run_prefix = "Would've "
if debugTracing: logger.info("Debug Tracing................")
# ----------------------------------------------------------------------
# **********************************************************************
if debugTracing: logger.info(f"Debug Tracing (SCAN_MODIFIED={SCAN_MODIFIED}) (RECURSIVE={RECURSIVE})................")
if plugin.DRY_RUN:
plugin.Log("Dry run mode is enabled.")
plugin.Trace(f"(SCAN_MODIFIED={SCAN_MODIFIED}) (SCAN_ON_ANY_EVENT={SCAN_ON_ANY_EVENT}) (RECURSIVE={RECURSIVE})")
def start_library_monitor():
global shouldUpdate
@@ -129,14 +79,13 @@ def start_library_monitor():
# Create shared memory buffer which can be used as singleton logic or to get a signal to quit task from external script
shm_a = shared_memory.SharedMemory(name="DavidMaisonaveAxter_FileMonitor", create=True, size=4)
except:
pass
logger.info("Could not open shared memory map. Change File Monitor must be running. Can not run multiple instance of Change File Monitor.")
plugin.Error("Could not open shared memory map. Change File Monitor must be running. Can not run multiple instance of Change File Monitor.")
return
type(shm_a.buf)
shm_buffer = shm_a.buf
len(shm_buffer)
shm_buffer[0] = CONTINUE_RUNNING_SIG
if debugTracing: logger.info(f"Shared memory map opended, and flag set to {shm_buffer[0]}")
plugin.Trace(f"Shared memory map opended, and flag set to {shm_buffer[0]}")
RunCleanMetadata = False
event_handler = watchdog.events.FileSystemEventHandler()
@@ -144,7 +93,7 @@ def start_library_monitor():
global shouldUpdate
global TargetPaths
TargetPaths.append(event.src_path)
logger.info(f"CREATE *** '{event.src_path}'")
plugin.Log(f"CREATE *** '{event.src_path}'")
with mutex:
shouldUpdate = True
signal.notify()
@@ -154,7 +103,7 @@ def start_library_monitor():
global TargetPaths
nonlocal RunCleanMetadata
TargetPaths.append(event.src_path)
logger.info(f"DELETE *** '{event.src_path}'")
plugin.Log(f"DELETE *** '{event.src_path}'")
with mutex:
shouldUpdate = True
RunCleanMetadata = True
@@ -165,106 +114,136 @@ def start_library_monitor():
global TargetPaths
if SCAN_MODIFIED:
TargetPaths.append(event.src_path)
logger.info(f"MODIFIED *** '{event.src_path}'")
plugin.Log(f"MODIFIED *** '{event.src_path}'")
with mutex:
shouldUpdate = True
signal.notify()
else:
if debugTracing: logger.info(f"Ignoring modifications due to plugin UI setting. path='{event.src_path}'")
plugin.Trace(f"Ignoring modifications due to plugin UI setting. path='{event.src_path}'")
def on_moved(event):
global shouldUpdate
global TargetPaths
TargetPaths.append(event.src_path)
TargetPaths.append(event.dest_path)
logger.info(f"MOVE *** from '{event.src_path}' to '{event.dest_path}'")
plugin.Log(f"MOVE *** from '{event.src_path}' to '{event.dest_path}'")
with mutex:
shouldUpdate = True
signal.notify()
if debugTracing: logger.info("Debug Trace........")
def on_any_event(event):
global shouldUpdate
global TargetPaths
if SCAN_ON_ANY_EVENT:
plugin.Log(f"Any-Event *** '{event.src_path}'")
TargetPaths.append(event.src_path)
with mutex:
shouldUpdate = True
signal.notify()
else:
plugin.Trace("Ignoring on_any_event trigger.")
plugin.Trace()
event_handler.on_created = on_created
event_handler.on_deleted = on_deleted
event_handler.on_modified = on_modified
event_handler.on_moved = on_moved
event_handler.on_any_event = on_any_event
observer = Observer()
# Iterate through stashPaths
for path in stashPaths:
observer.schedule(event_handler, path, recursive=RECURSIVE)
if debugTracing: logger.info(f"Observing {path}")
plugin.Trace(f"Observing {path}")
observer.start()
if debugTracing: logger.info("Starting loop................")
plugin.Trace("Starting loop")
try:
while True:
TmpTargetPaths = []
with mutex:
while not shouldUpdate:
if debugTracing: logger.info("Wait start................")
signal.wait()
if debugTracing: logger.info("Wait end................")
plugin.Trace("Wait start")
signal.wait(timeout=SIGNAL_TIMEOUT)
plugin.Trace("Wait end")
if shm_buffer[0] != CONTINUE_RUNNING_SIG:
plugin.Log(f"Breaking out of loop. (shm_buffer[0]={shm_buffer[0]})")
break
shouldUpdate = False
TmpTargetPaths = []
for TargetPath in TargetPaths:
TmpTargetPaths.append(os.path.dirname(TargetPath))
if TargetPath == SPECIAL_FILE_DIR:
if os.path.isfile(SPECIAL_FILE_NAME):
shm_buffer[0] = STOP_RUNNING_SIG
plugin.Log(f"Detected trigger file to kill FileMonitor. {SPECIAL_FILE_NAME}", printTo = plugin.LOG_TO_FILE + plugin.LOG_TO_CONSOLE + plugin.LOG_TO_STASH)
TargetPaths = []
TmpTargetPaths = list(set(TmpTargetPaths))
if TmpTargetPaths != []:
logger.info(f"Triggering stash scan for path(s) {TmpTargetPaths}")
if not DRY_RUN:
stash.metadata_scan(paths=TmpTargetPaths)
if RUN_CLEAN_AFTER_DELETE and RunCleanMetadata:
stash.metadata_clean(paths=TmpTargetPaths, dry_run=DRY_RUN)
if RUN_GENERATE_CONTENT:
stash.metadata_generate()
if gettingCalledAsStashPlugin and shm_buffer[0] == CONTINUE_RUNNING_SIG:
stash.run_plugin_task(plugin_id=PLUGIN_ID, task_name="Start Library Monitor")
if debugTracing: logger.info("Exiting plugin so that metadata_scan task can run.")
plugin.Log(f"Triggering Stash scan for path(s) {TmpTargetPaths}")
if len(TmpTargetPaths) > 1 or TmpTargetPaths[0] != SPECIAL_FILE_DIR:
if not plugin.DRY_RUN:
plugin.STASH_INTERFACE.metadata_scan(paths=TmpTargetPaths)
if RUN_CLEAN_AFTER_DELETE and RunCleanMetadata:
plugin.STASH_INTERFACE.metadata_clean(paths=TmpTargetPaths, dry_run=plugin.DRY_RUN)
if RUN_GENERATE_CONTENT:
plugin.STASH_INTERFACE.metadata_generate()
if plugin.CALLED_AS_STASH_PLUGIN and shm_buffer[0] == CONTINUE_RUNNING_SIG:
plugin.STASH_INTERFACE.run_plugin_task(plugin_id=plugin.PLUGIN_ID, task_name="Start Library Monitor")
plugin.Trace("Exiting plugin so that metadata_scan task can run.")
return
else:
if debugTracing: logger.info("Nothing to scan.")
plugin.Trace("Nothing to scan.")
if shm_buffer[0] != CONTINUE_RUNNING_SIG:
logger.info(f"Exiting Change File Monitor. (shm_buffer[0]={shm_buffer[0]})")
plugin.Log(f"Exiting Change File Monitor. (shm_buffer[0]={shm_buffer[0]})")
shm_a.close()
shm_a.unlink() # Call unlink only once to release the shared memory
raise KeyboardInterrupt
except KeyboardInterrupt:
observer.stop()
if debugTracing: logger.info("Stopping observer................")
plugin.Trace("Stopping observer")
if os.path.isfile(SPECIAL_FILE_NAME):
os.remove(SPECIAL_FILE_NAME)
observer.join()
if debugTracing: logger.info("Exiting function................")
plugin.Trace("Exiting function")
# This function is only useful when called outside of Stash.
# Example: python filemonitor.py stop
# Stops monitoring after triggered by the next file change.
# ToDo: Add logic so it doesn't have to wait until the next file change
# Example: python filemonitor.py --stop
def stop_library_monitor():
import time
if debugTracing: logger.info("Opening shared memory map.")
if CREATE_SPECIAL_FILE_TO_EXIT:
if os.path.isfile(SPECIAL_FILE_NAME):
os.remove(SPECIAL_FILE_NAME)
pathlib.Path(SPECIAL_FILE_NAME).touch()
if DELETE_SPECIAL_FILE_ON_STOP:
os.remove(SPECIAL_FILE_NAME)
plugin.Trace("Opening shared memory map.")
try:
shm_a = shared_memory.SharedMemory(name="DavidMaisonaveAxter_FileMonitor", create=False, size=4)
except:
pass
logger.info("Could not open shared memory map. Change File Monitor must not be running.")
plugin.Log("Could not open shared memory map. Change File Monitor must not be running.")
return
type(shm_a.buf)
shm_buffer = shm_a.buf
len(shm_buffer)
shm_buffer[0] = 123
if debugTracing: logger.info(f"Shared memory map opended, and flag set to {shm_buffer[0]}")
shm_buffer[0] = STOP_RUNNING_SIG
plugin.Trace(f"Shared memory map opended, and flag set to {shm_buffer[0]}")
shm_a.close()
shm_a.unlink() # Call unlink only once to release the shared memory
time.sleep(1)
return
if stopLibraryMonitoring:
if parse_args.stop or parse_args.restart:
stop_library_monitor()
if debugTracing: logger.info(f"stop_library_monitor EXIT................")
elif PLUGIN_ARGS_MODE == "start_library_monitor" or not gettingCalledAsStashPlugin:
if parse_args.restart:
time.sleep(5)
plugin.STASH_INTERFACE.run_plugin_task(plugin_id=plugin.PLUGIN_ID, task_name="Start Library Monitor")
plugin.Trace(f"Restart FileMonitor EXIT")
else:
plugin.Trace(f"Stop FileMonitor EXIT")
elif plugin.PLUGIN_TASK_NAME == "start_library_monitor" or not plugin.CALLED_AS_STASH_PLUGIN:
start_library_monitor()
if debugTracing: logger.info(f"start_library_monitor EXIT................")
plugin.Trace(f"start_library_monitor EXIT")
else:
logger.info(f"Nothing to do!!! (PLUGIN_ARGS_MODE={PLUGIN_ARGS_MODE})")
plugin.Log(f"Nothing to do!!! (plugin.PLUGIN_TASK_NAME={plugin.PLUGIN_TASK_NAME})")
if debugTracing: logger.info("\n*********************************\nEXITING ***********************\n*********************************")
plugin.Trace("\n*********************************\nEXITING ***********************\n*********************************")