""" ======================================================================== NO-HANG IO (part of Frigcal) Avoid hangs in filesystem (or other) calls with thread timeouts. FC: this does not apply to SAF ops on Android, but it may apply to POSIX/filepath ops on PCs and possibly to file descriptors on Android. TBD: is this still useful in FC 4.0? YE: this file is currently used in FC 4.0 but only for folder listings on PCs to check access. It is not used for the SAF chooser on Android (which runs atomically), the Kivy MD chooser on PCs (which doesn't have a filesystem-wrpper API), SAF pre-read/write calls on Android, or POSIX read/write calls for files and descriptors on all. Android SAF calls before read/write might use this too but currently do not, on the assumption that the rarely accessed calendar files in this app are not hosted on hangable mediums. See storage.py for most file-ccess code in this app. ======================================================================== """ from common import * def try_any_nohang(func, *args, waitsecs=0.25, onfail=None, log=True): """ -------------------------------------------------------------------- [from PPUS] Run any system call with a timeout to avoid GUI hangs. Returns onfail for exception or timeout, else func(*args)'s result. onfail means immediate fail (unmounted?) or hung (inaccessible?). For portability, runs func(*args) in a thread with a timed join. Also wraps the call in a try handler to catch any/all exceptions. Initially coded for os.listdir() only, then updated for any func. LATER: os.isdir() hangs on Windows (but not macOS) too: generalize this to call any folder-access function with a timeout. This now returns onfail if func(*args) either raised an exc before timeout or hung; else returns func result if it ended without exc or hang. Still not universal (no **kargs), but handles this app's use cases. Windows drive enumeration now also uses this, not a custom version. Timeouts can also be had with signals on Unix but not on Windows, and they are also available in multiprocessing but not on Android. Threads always finish even if timeout: no simple way to kill, but a shared result object per thread ensures that new results aren't overwritten. Hung thread is left to finish (exit on target return) and modify a result object that is ignored by the calling thread. But set daemon=True so thread auto-killed at app exit if hung that long (unlikely); else inherits False from app process and may delay app exit (which _may_ have triggered a kivy logger loop on exit). INITIAL: run an os.listdir() to verify folder access before ops that rely on it, but run it in a thread with a timeout to avoid lags/hangs for drives that are inaccessible - temporarily or permanently. This includes mounted but disconnected network drives, but also others (e.g., USB drives not yet fully mounted). Returns False iff os.listdir() either failed immediately or hung. Used on all platforms and all contexts - chooser start folder, chooser storage tap/reset, and Main action folder prechecks. Also used for an Android hang in storage_path_app_specific(). LATER: change waitsecs default timer duration from 0.50 to 0.25 seconds; half a second is a bit jarring when a Windows mapped network drive is offline (as they regularly are), and a quarter of a second should be plenty to notice a ghost. Also added logs=True parameter to allow callers to turn off traces (traces can be too much, but never happen in released Android apps). -------------------------------------------------------------------- """ def try_any(func, args, result, onfail): try: got = func(*args) # run folder-access call: hangware except: result.append(onfail) # failure, immediate, or later if hung else: result.append(got) # success, now or later; got may == onfail result = [] # new shared object for each thread nohang = threading.Thread( target=try_any, args=(func, args, result, onfail), daemon=True) nohang.start() nohang.join(timeout=waitsecs) # wait till thread finished or timeout if nohang.is_alive(): if log: trace('try thread timeout') sendback = onfail # let thread finish and set its ignored result else: if log: trace('try thread exit') sendback = result[0] # thead finished and set result: ok or exc return sendback # can be onfail for exc, timeout, or got #---------------------------------------------------------------------------- # No-hang helpers # # try_listdir_nohang() => True IFF listable, empty or not: no list returned; # use a try_any_nohang() instead if list is needed too (see examples ahead); # try_isdir_nohang() => False if exc, timeout, or false (callers don't care); #---------------------------------------------------------------------------- def try_listdir_nohang(path): return try_any_nohang(os.listdir, path, onfail=None) != None def try_isdir_nohang(path): return try_any_nohang(os.path.isdir, path, onfail=False) # Other try_any() clients: Windows drive enumeration, Android drive name, file-system class FileSystemLocalNoHang(FileSystemLocal): """ ----------------------------------------------------------- TBD: is this used by KivyMD's MDFileManager too? And will MDFileManager be used by this app on PCs? Wrap Kivy folder-chooser's filesystem ops to avoid hangs. One instance, created by and stored in App instance. Wrap methods of the Kivy FileChooser's FileSystemLocal interface class (os module calls, mostly) in thread timouts. Else the chooser can stall (block and possibly hang) as users navigate by tapping its UI. Network drives, cameras, etc., may go offline, especially on Windows, causing raw, unwrapped calls to block indefinitely. This happens deep in the Kivy FileChooser code, but its file_system hook supports mods. Code here fixes the chooser widget; filesystem calls in the app itself are also wrapped in thread timeouts ahead. Coding notes: this doesn't call os functions directly, because Kivy's FileSystemLocal.is_hidden() uses tight magic to access and use a private win32 call on Windows. APPROOT is the global App instance: see App.on_start(). This also adds a thread per chooser nav tap, but PCs and phones are fast. TBD: is this useful only, or at least primarily, on Windows? If so, could avoid one thread per os-module call elsewhere. To date, this is the only platform where nav hangs have been seen, though PCs and phones are both quick... ----------------------------------------------------------- """ def __init__(self, app): self.app = app # link back to main.py's App instance def listdir(self, filepath, warn=True): """ called for both test and result: warn user """ trace('NoHang.listdir') listed = try_any_nohang(super().listdir, filepath, onfail=None, log=False) if listed != None: return listed # empty or not (no exc or timeout) else: trace('Skipping dir: listdir') if warn: self.app.info_message('Cannot access folder', usetoast=True) raise OSError # kill dir in filechooser (run in try) def getsize(self, filepath, warn=False): """ may be used if is_dir fails, but rare """ trace('NoHang.getsize') getsize = try_any_nohang(super().getsize, filepath, onfail=None, log=False) if getsize != None: return getsize # dir or not (no exc or timeout) else: trace('Skipping file: getsize') if warn: self.app.info_message('Cannot access file', usetoast=True) raise OSError # kill dir in filechooser (run in try) def is_hidden(self, filepath, warn=False): """ win32 privates used in super called numerous times: don't warn user """ #trace('NoHang.is_hidden') ishidden = try_any_nohang(super().is_hidden, filepath, onfail=None, log=False) if ishidden != None: return ishidden # hidden or not (no exc or timeout) else: trace('Skipping dir: is_hidden') if warn: self.app.info_message('Cannot access folder', usetoast=True) raise False # kill dir in filechooser (run in try) def is_dir(self, filepath, warn=False): """ called an insane number of times: don't warn user """ #trace('NoHang.is_dir') isdir = try_any_nohang(super().is_dir, filepath, onfail=None, log=False) if isdir != None: return isdir # dir or not (no exc or timeout) else: trace('Skipping dir: is_dir') if warn: self.app.info_message('Cannot access folder', usetoast=True) return False # kill dir in filechooser