#! /usr/bi n/env python3 # -*- coding: utf-8 -*- """ Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), This application is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library from datetime import datetime import pathlib import os #Our Modules from engine.start import PATHS def fast_scandir(dir): """ Get all subdirectories recursively. https://stackoverflow.com/questions/973473/getting-a-list-of-all-subdirectories-in-the-current-directory""" try: subfolders= [f.path for f in os.scandir(dir) if f.is_dir()] except PermissionError: subfolders = [] for dir in list(subfolders): subfolders.extend(fast_scandir(dir)) return subfolders class Watcher(object): """ Initialize with the server controller The watcher will should only run when the program is in the mode to choose a session. If a session is loaded it will technically work, but will be a waste of resources. The watcher will also trigger the nsmController to redundantly query for a session list, for example when a new empty sessions gets created or duplicated. Once because nsmd sends the "new" signal, and then our watcher will notice the change itself. However, this happens only so often and can be accepted, so the program does not need more checks and exceptions. Inform you via callback when: a) a session dir was deleted b) a new directory got created. c) session directory renamed or moved d) session.nsm changed timestamp e) A lockfile appeared or disappeared We cannot poll nsmServerControl.exportSessionsAsDicts() because that triggers an nsmd response including log message and will flood their console. Instead this class only offers incremental updates. Another way is to watch the dir for changes ourselves in in some cases request a new project list from nsmd. Lockfile functionality goes beyond what NSM offers. A session-daemon can open only one project at a time. If you try to open another project with a second GUI (but same NSM-URL) it goes wrong a bit, at least in the new-session-manager GUI. They will leave a zombie lockfile. However, it still is possible to open a second nsmd instance. In this case the lockfile prevents opening a session twice. And we are reflecting the opened state of the project, no matter from which daemon. Horray for us :) Clients can only be added or removed while a session is locked. We do not check nor update number of clients or symlinks. Therefore we advice a GUI to deactivate the display of these two values while a session is locked (together with size) """ def __init__(self, nsmServerControl): self.active = True self._nsmServerControl = nsmServerControl assert self._nsmServerControl.sessionRoot self._directories = fast_scandir(self._nsmServerControl.sessionRoot) logger.info("Requestion our own copy of the session list. Don't worry about the apparent redundant call :)") self._lastExport = self._nsmServerControl.exportSessionsAsDicts() #list of dicts self._lastTimestamp = {d["nsmSessionName"]:d["lastSavedDate"] for d in self._lastExport} #str:str #Init all values with None will send the initial state via callback on program start, which is what the user wants to know. self._lastLockfile = {d["nsmSessionName"]:None for d in self._lastExport} #str:bool self.timeStampHook = None # a single function that gets informed of changes, most likely the api callback self.lockFileHook = None # a single function that gets informed of changes, most likely the api callback self.sessionsChangedHook = None # the api callback function api.callbacks._sessionsChanged. Rarely used. def resume(self, *args): """For api callbacks""" self.active = True #If we returned from an open session that will surely have changed. Trigger a single poll self.process() logger.info("Watcher resumed") def suspend(self, *args): """For api callbacks""" self.active = False logger.info("Watcher suspended") def _update(self): current_directories = fast_scandir(self._nsmServerControl.sessionRoot) if not self._directories == current_directories: self._directories = current_directories self._lastExport = self.sessionsChangedHook() #will gather its own data, send it to api callbacks, but also return for us. self._lastTimestamp = {d["nsmSessionName"]:d["lastSavedDate"] for d in self._lastExport} #str:str self._lastLockfile = {d["nsmSessionName"]:None for d in self._lastExport} #str:bool def process(self): """Add this to your event loop. We look for any changes in the directory structure. If we detect any we simply trigger a new NSM export and a new NSM generated project list via callback. We do not expect this to happen often. This will also trigger if we add a new session ourselves. This *is* our way to react to new Sessions. """ if not self.active: return if self.sessionsChangedHook: self._update() #Now check the incremental hooks. #No hooks, no reason to process if not (self.timeStampHook or self.lockFileHook): logger.info("No watcher-hooks to process") return for entry in self._lastExport: nsmSessionName = entry["nsmSessionName"] try: #Timestamp of session.nsm if self.timeStampHook: timestamp = datetime.fromtimestamp(entry["sessionFile"].stat().st_mtime).isoformat(sep=" ", timespec='minutes') #same format as server control export if not timestamp == self._lastTimestamp[nsmSessionName]: #This will only trigger on a minute-based slot, which is all we want and need. This is for relaying information to the user, not for advanced processing. self._lastTimestamp[nsmSessionName] = timestamp self.timeStampHook(nsmSessionName, timestamp) #Lockfiles if self.lockFileHook: lockfileState = entry["lockFile"].is_file() if not self._lastLockfile[nsmSessionName] == lockfileState: self._lastLockfile[nsmSessionName] = lockfileState self.lockFileHook(nsmSessionName, lockfileState) except PermissionError: logger.warning(f"File Permission error for {entry}") self._lastExport.remove(entry) #avoid stumbling upon this again self._update() except FileNotFoundError: logger.warning(f"File not found error for {entry}") self._lastExport.remove(entry) #avoid stumbling upon this again self._update()