Music production session manager https://www.laborejo.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

174 lines
7.6 KiB

4 years ago
#! /usr/bi n/env python3
# -*- coding: utf-8 -*-
"""
2 years ago
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
4 years ago
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
4 years ago
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):
4 years ago
"""
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 = []
4 years ago
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:
4 years ago
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
4 years ago
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.
4 years ago
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 :)
4 years ago
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
4 years ago
self._nsmServerControl = nsmServerControl
4 years ago
assert self._nsmServerControl.sessionRoot
4 years ago
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.
4 years ago
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()
4 years ago
logger.info("Watcher resumed")
4 years ago
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
4 years ago
def process(self):
"""Add this to your event loop.
4 years ago
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
4 years ago
if self.sessionsChangedHook:
self._update()
4 years ago
#Now check the incremental hooks.
#No hooks, no reason to process
if not (self.timeStampHook or self.lockFileHook):
4 years ago
logger.info("No watcher-hooks to process")
return
4 years ago
for entry in self._lastExport:
4 years ago
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()