#! /usr/bi n/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2020 , 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 < http : / / www . gnu . org / licenses / > .
"""
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 ( )
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 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 :
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
#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 " ]
#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 )