diff --git a/Makefile b/Makefile index 09622c76..c672c4b7 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,7 @@ _autoortho_win.exe: autoortho/.version --include-data-file=./autoortho/aoimage/*.dll=aoimage/ \ --include-data-dir=./autoortho/imgs=imgs \ --onefile \ + --disable-console \ ./autoortho/__main__.py -o autoortho_win.exe __main__.dist: autoortho/.version @@ -61,6 +62,7 @@ __main__.dist: autoortho/.version --include-data-file=./autoortho/aoimage/*.dll=aoimage/ \ --include-data-dir=./autoortho/imgs=imgs \ --standalone \ + --disable-console \ ./autoortho/__main__.py -o autoortho_win.exe win_exe: AutoOrtho_win_$(VERSION).exe diff --git a/autoortho/__main__.py b/autoortho/__main__.py index 1429058b..b5b52245 100644 --- a/autoortho/__main__.py +++ b/autoortho/__main__.py @@ -22,7 +22,7 @@ def setuplogs(): maxBytes=10485760, backupCount=5 ), - logging.StreamHandler() + logging.StreamHandler() if sys.stdout is not None else logging.NullHandler() ] ) log = logging.getLogger(__name__) diff --git a/autoortho/autoortho.py b/autoortho/autoortho.py index 15ae4777..698c688d 100644 --- a/autoortho/autoortho.py +++ b/autoortho/autoortho.py @@ -148,62 +148,131 @@ def diagnose(CFG): log.warning("Please review logs and setup.") log.warning("***************") log.warning("***************") + return False else: log.info(" Diagnostics done. All checks passed") + return True log.info("------------------------------------\n\n") -def run(root, mountpoint, threading=True): - global RUNNING - if threading: - log.info("Running in multi-threaded mode.") - nothreads = False - else: - log.info("Running in single-threaded mode.") - nothreads = True - - root = os.path.expanduser(root) - - try: - if platform.system() == 'Windows': - systemtype, libpath = winsetup.find_win_libs() - with setupmount(mountpoint, systemtype) as mount: - log.info(f"AutoOrtho: root: {root} mountpoint: {mount}") - import autoortho_fuse - from refuse import high - high._libfuse = ctypes.CDLL(libpath) - autoortho_fuse.run( - autoortho_fuse.AutoOrtho(root), - mount, - nothreads - ) - else: - with setupmount(mountpoint, "Linux-FUSE") as mount: - log.info("Running in FUSE mode.") - log.info(f"AutoOrtho: root: {root} mountpoint: {mount}") - import autoortho_fuse - autoortho_fuse.run( - autoortho_fuse.AutoOrtho(root), - mount, - nothreads + +class AOMount: + mounts_running = False + + def __init__(self, cfg): + self.cfg = cfg + self.mount_threads = [] + + def mount_sceneries(self, blocking=True): + self.mounts_running = True + for scenery in self.cfg.scenery_mounts: + t = threading.Thread( + target=self.domount, + daemon=False, + args=( + scenery.get('root'), + scenery.get('mount'), + self.cfg.fuse.threading ) + ) + t.start() + self.mount_threads.append(t) + + if not blocking: + log.info("Running mounts in non-blocking mode.") + time.sleep(1) + diagnose(self.cfg) + return - except Exception as err: - log.error(f"Exception detected when running FUSE mount: {err}. Exiting...") - RUNNING = False - time.sleep(5) + try: + def handle_sigterm(sig, frame): + raise(SystemExit) + signal.signal(signal.SIGTERM, handle_sigterm) + + time.sleep(1) + # Check things out + diagnose(self.cfg) + + while self.mounts_running: + time.sleep(1) + + except (KeyboardInterrupt, SystemExit) as err: + self.running = False + log.info(f"Exiting due to {err}") + finally: + log.info("Shutting down ...") + self.unmount_sceneries() -def unmount(mountpoint): - mounted = True - while mounted: - print(f"Shutting down {mountpoint}") - print("Send poison pill ...") - mounted = os.path.isfile(os.path.join( - mountpoint, - ".poison" - )) - time.sleep(0.5) + + def unmount_sceneries(self): + log.info("Unmounting ...") + self.mounts_running = False + for scenery in self.cfg.scenery_mounts: + self.unmount(scenery.get('mount')) + + log.info("Wait on threads...") + for t in self.mount_threads: + t.join(5) + log.info(f"Thread {t.ident} exited.") + log.info("Unmount complete") + + + def domount(self, root, mountpoint, threading=True): + + if threading: + log.info("Running in multi-threaded mode.") + nothreads = False + else: + log.info("Running in single-threaded mode.") + nothreads = True + + root = os.path.expanduser(root) + + try: + if platform.system() == 'Windows': + systemtype, libpath = winsetup.find_win_libs() + with setupmount(mountpoint, systemtype) as mount: + log.info(f"AutoOrtho: root: {root} mountpoint: {mount}") + import autoortho_fuse + from refuse import high + high._libfuse = ctypes.CDLL(libpath) + autoortho_fuse.run( + autoortho_fuse.AutoOrtho(root), + mount, + nothreads + ) + else: + with setupmount(mountpoint, "Linux-FUSE") as mount: + log.info("Running in FUSE mode.") + log.info(f"AutoOrtho: root: {root} mountpoint: {mount}") + import autoortho_fuse + autoortho_fuse.run( + autoortho_fuse.AutoOrtho(root), + mount, + nothreads + ) + + except Exception as err: + log.error(f"Exception detected when running FUSE mount: {err}. Exiting...") + time.sleep(5) + + def unmount(self, mountpoint): + mounted = True + while mounted: + print(f"Shutting down {mountpoint}") + print("Send poison pill ...") + mounted = os.path.isfile(os.path.join( + mountpoint, + ".poison" + )) + time.sleep(0.5) + + +class AOMountUI(config_ui.ConfigUI, AOMount): + def __init__(self, *args, **kwargs): + self.mount_threads = [] + super().__init__(*args, **kwargs) def main(): @@ -240,18 +309,12 @@ def main(): args = parser.parse_args() CFG = aoconfig.CFG - cfgui = config_ui.ConfigUI(CFG) - if (not CFG.ready) or args.configure or (CFG.general.showconfig and not args.headless): - cfgui.setup(headless = args.headless) - - if not args.root or not args.mountpoint: - cfgui.verify() + if args.configure or (CFG.general.showconfig and not args.headless): + # Show cfgui at start + run_headless = False else: - root = args.root - mountpoint = args.mountpoint - print("root:", root) - print("mountpoint:", mountpoint) - + # Don't show cfgui + run_headless = True stats = aostats.AOStats() @@ -259,69 +322,43 @@ def main(): log.warning(f"No installed sceneries detected. Exiting.") sys.exit(0) - #if CFG.cache.clean_on_start: - # aoconfig.clean_cache(CFG.paths.cache_dir, int(float(CFG.cache.file_cache_size))) - import flighttrack ftrack = threading.Thread( target=flighttrack.run, daemon=True ) + + # Start helper threads ftrack.start() - stats.start() - global RUNNING - RUNNING = True - do_threads = True - if do_threads: - mount_threads = [] - for scenery in CFG.scenery_mounts: - t = threading.Thread( - target=run, - daemon=False, - args=( - scenery.get('root'), - scenery.get('mount'), - CFG.fuse.threading - ) - ) - t.start() - mount_threads.append(t) - - try: - def handle_sigterm(sig, frame): - raise(SystemExit) - - signal.signal(signal.SIGTERM, handle_sigterm) - - time.sleep(1) - # Check things out - diagnose(CFG) - - while RUNNING: - time.sleep(1) - except (KeyboardInterrupt, SystemExit): - RUNNING = False - pass - finally: - log.info("Shutting down ...") - for scenery in CFG.scenery_mounts: - unmount(scenery.get('mount')) - for t in mount_threads: - t.join(5) - print(f"Thread {t.ident} exited.") - else: - scenery = CFG.scenery_mounts[0] - run( - scenery.get('root'), - scenery.get('mount'), + # Run things + if args.root and args.mountpoint: + # Just mount specific requested dirs + root = args.root + mountpoint = args.mountpoint + print("root:", root) + print("mountpoint:", mountpoint) + aom = AOMount(CFG) + aom.domount( + root, + mountpoint, CFG.fuse.threading ) + elif run_headless: + log.info("Running headless") + aom = AOMount(CFG) + aom.mount_sceneries() + else: + log.info("Running CFG UI") + cfgui = AOMountUI(CFG) + cfgui.setup() stats.stop() flighttrack.ft.stop() + log.info("AutoOrtho exit.") + if __name__ == '__main__': main() diff --git a/autoortho/autoortho_fuse.py b/autoortho/autoortho_fuse.py index b3cc4d9e..aab9b05a 100644 --- a/autoortho/autoortho_fuse.py +++ b/autoortho/autoortho_fuse.py @@ -224,7 +224,7 @@ def getattr(self, path, fh=None): @lru_cache def readdir(self, path, fh): - log.info(f"READDIR: {path} {fh}") + #log.info(f"READDIR: {path} {fh}") if path in ["/textures"]: return ['.', '..', '.AOISWORKING', '24832_12416_BI16.dds'] elif path in ["/terrain"]: @@ -264,7 +264,7 @@ def mkdir(self, path, mode): @lru_cache def statfs(self, path): - log.info(f"STATFS: {path}") + #log.info(f"STATFS: {path}") full_path = self._full_path(path) if platform.system() == 'Windows': stats = { diff --git a/autoortho/config_ui.py b/autoortho/config_ui.py index 66e46a39..0029cade 100644 --- a/autoortho/config_ui.py +++ b/autoortho/config_ui.py @@ -25,7 +25,6 @@ CUR_PATH = os.path.dirname(os.path.realpath(__file__)) - class ConfigUI(object): status = None @@ -35,10 +34,13 @@ class ConfigUI(object): window = None running = False ready = None + splash_w = None def __init__(self, cfg): self.ready = threading.Event() self.ready.clear() + + self.start_splash() self.cfg = cfg self.dl = downloader.OrthoManager( @@ -57,7 +59,18 @@ def __init__(self, cfg): else: self.icon_path =os.path.join(CUR_PATH, 'imgs', 'ao-icon.png') - def setup(self, headless=False): + def start_splash(self): + splash_path = os.path.join(CUR_PATH, 'imgs', 'splash.png') + self.splash_w = sg.Window( + 'Window Title', [[sg.Image(splash_path, subsample=2)]], + transparent_color=sg.theme_background_color(), no_titlebar=True, + keep_on_top=True, finalize=True + ) + event, values = self.splash_w.read(timeout=100) + return + + + def setup(self): scenery_path = self.cfg.paths.scenery_path showconfig = self.cfg.general.showconfig maptype = self.cfg.autoortho.maptype_override @@ -65,23 +78,8 @@ def setup(self, headless=False): if not os.path.exists(self.cfg.paths.cache_dir): os.makedirs(self.cfg.paths.cache_dir) - if not headless: - self.ui_loop() - else: + self.ui_loop() - log.info("-"*28) - log.info(f"Running setup!") - log.info("-"*28) - scenery_path = input(f"Enter path to scenery install directory ({scenery_path}) : ") or scenery_path - xplane_path = input(f"Enter path to X-Plane install directory ({xplane_path}) : ") or xplane_path - - self.config['paths']['scenery_path'] = scenery_path - self.config['paths']['xplane_path'] = xplane_path - self.config['general']['showconfig'] = str(showconfig) - self.config['autoortho']['maptype_override'] = maptype - - self.save() - self.load() def refresh_scenery(self): self.dl.regions = {} @@ -95,7 +93,7 @@ def refresh_scenery(self): def ui_loop(self): # Main GUI loop - + scenery_path = self.cfg.paths.scenery_path showconfig = self.cfg.general.showconfig maptype = self.cfg.autoortho.maptype_override @@ -103,6 +101,9 @@ def ui_loop(self): sg.theme('DarkAmber') + # + # Setup/config tab + # setup = [ [ #sg.Column( @@ -180,6 +181,9 @@ def ui_loop(self): ] + # + # Setup scenery tab + # scenery = [ ] self.dl.find_regions() @@ -211,13 +215,32 @@ def ui_loop(self): #scenery.append([sg.Text(key='-EXPAND-', font='ANY 1', pad=(0,0))]) #scenery.append([sg.StatusBar("...", size=(74,3), key="status", auto_size_text=True, expand_x=True)]) + # + # Console logs tab + # logs = [ + [sg.Multiline( + "", + key="log", + size=(80,20), + autoscroll=True, + reroute_stdout=True, + reroute_stderr=True, + #echo_stdout_stderr=True, + expand_x=True, + expand_y=True + ) + ] ] scenery_column = sg.Column(scenery, expand_x=True, expand_y=True, scrollable=True, vertical_scroll_only=True) layout = [ [sg.TabGroup( - [[sg.Tab('Setup', setup), sg.Tab('Scenery', [[scenery_column]])]]) + [[ + sg.Tab('Setup', setup), + sg.Tab('Scenery', [[scenery_column]]), + sg.Tab('Logs', logs) + ]]) ], [sg.Text(key='-EXPAND-', font='ANY 1', pad=(0,0))], [sg.StatusBar("...", size=(74,3), key="status", auto_size_text=True, expand_x=True)], @@ -234,81 +257,106 @@ def ui_loop(self): #print = lambda *args, **kwargs: window['output'].print(*args, **kwargs) self.window['-EXPAND-'].expand(True, True, True) self.status = self.window['status'] - + self.log = self.window['log'] + self.running = True close = False - - t = threading.Thread(target=self.scenery_setup) - t.start() + scenery_t = threading.Thread(target=self.scenery_setup) + scenery_t.start() + + if self.splash_w is not None: + # GUI starting, close splash screen + self.splash_w.close() + self.ready.set() - while self.running: - event, values = self.window.read(timeout=100) - #log.info(f'VALUES: {values}') - #print(f"VALUES {values}") - #print(f"EVENT: {event}") - if event == sg.WIN_CLOSED: - print("Not saving changes ...") - close = True - break - elif event == 'Quit': - print("Quiting ...") - close = True - break - elif event == "Run": - print("Updating config.") - self.save() - self.cfg.load() - break - elif event == 'Save': - print("Updating config.") - self.save() - self.cfg.load() - print(self.cfg.paths) - elif event == 'Clean Cache': - cbutton = self.window["Clean Cache"] - rbutton = self.window["Run"] - cbutton.update("Working") - cbutton.update(disabled=True) - rbutton.update(disabled=True) + try: + while self.running: + event, values = self.window.read(timeout=1000) + #log.info(f'VALUES: {values}') + #print(f"VALUES {values}") + #print(f"EVENT: {event}") + if event == sg.WIN_CLOSED: + print("Exiting ...") + #print("Not saving changes ...") + #self.show_status("Exiting") + close = True + self.running = False + elif event == 'Quit': + self.show_status("Quiting") + print("Quiting ...") + close = True + self.running = False + self.show_status("Quiting") + elif event == "Run": + print("Updating config.") + self.show_status("Updating config") + self.save() + self.cfg.load() + self.show_status("Mounting sceneries") + self.mount_sceneries(blocking=False) + self.show_status("Verifying") + self.verify() + self.show_status("Running") + self.window.minimize() + elif event == 'Save': + print("Updating config.") + self.show_status("Updating config") + self.save() + self.cfg.load() + print(self.cfg.paths) + elif event == 'Clean Cache': + self.show_status("Cleaning cache") + cbutton = self.window["Clean Cache"] + rbutton = self.window["Run"] + cbutton.update("Working") + cbutton.update(disabled=True) + rbutton.update(disabled=True) + self.window.refresh() + self.clean_cache( + self.cfg.paths.cache_dir, + int(float(self.cfg.cache.file_cache_size)) + ) + sg.popup("Done cleaning cache!") + cbutton.update("Clean Cache") + cbutton.update(disabled=False) + rbutton.update(disabled=False) + elif event.startswith("scenery-"): + self.save() + self.cfg.load() + button = self.window[event] + button.update(disabled=True) + regionid = event.split("-")[1] + self.scenery_q.put(regionid) + elif self.show_errs: + font = ("Helventica", 14) + sg.popup("\n".join(self.show_errs), title="ERROR!", font=font) + self.show_errs.clear() + + self.update_logs() self.window.refresh() - self.clean_cache( - self.cfg.paths.cache_dir, - int(float(self.cfg.cache.file_cache_size)) - ) - sg.popup("Done cleaning cache!") - cbutton.update("Clean Cache") - cbutton.update(disabled=False) - rbutton.update(disabled=False) - elif event.startswith("scenery-"): - self.save() - self.cfg.load() - button = self.window[event] - button.update(disabled=True) - regionid = event.split("-")[1] - self.scenery_q.put(regionid) - elif self.show_errs: - font = ("Helventica", 14) - sg.popup("\n".join(self.show_errs), title="ERROR!", font=font) - self.show_errs.clear() - - self.window.refresh() - - print("Exiting ...") - self.running = False - t.join() - self.window.close() + finally: + log.info("GUI exiting...") + self.stop() + log.info("Join scenery thread") + scenery_t.join() + log.info("Exiting UI") - if close: - sys.exit(0) def stop(self): self.running = False + self.unmount_sceneries() self.window.close() - def scenery_setup(self): + def update_logs(self): + with open(self.cfg.paths.log_file) as h: + lines = h.readlines()[-25:] + self.log.update(''.join(lines)) + + + def scenery_setup(self): while self.running: try: regionid = self.scenery_q.get(timeout=2) @@ -423,6 +471,7 @@ def show_status(self, msg): self.status.update(msg) self.window.refresh() + def clean_cache(self, cache_dir, size_gb): self.show_status(f"Cleaning up cache_dir {cache_dir}. Please wait ...") @@ -459,6 +508,7 @@ def clean_cache(self, cache_dir, size_gb): self.status.update(f"Cache cleanup done.") + def _check_ortho_dir(self, path): ret = True @@ -468,6 +518,7 @@ def _check_ortho_dir(self, path): return ret + def _check_xplane_dir(self, path): if not os.path.isdir(path): @@ -479,6 +530,3 @@ def _check_xplane_dir(self, path): return False return True - - - diff --git a/autoortho/imgs/splash.png b/autoortho/imgs/splash.png new file mode 100644 index 00000000..9800cf32 Binary files /dev/null and b/autoortho/imgs/splash.png differ diff --git a/requirements-build.in b/requirements-build.in new file mode 100644 index 00000000..86df3f38 --- /dev/null +++ b/requirements-build.in @@ -0,0 +1,3 @@ +nuitka==1.6.6 +zstandard +ordered-set diff --git a/requirements-build.txt b/requirements-build.txt index 64406fab..52e9ad72 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -1,4 +1,16 @@ -nuitka -#orderedset -zstandard -ordered-set +# +# This file is autogenerated by pip-compile with python 3.10 +# To update, run: +# +# pip-compile requirements-build.in +# +nuitka==1.6.6 + # via -r requirements-build.in +ordered-set==4.1.0 + # via + # -r requirements-build.in + # nuitka +zstandard==0.21.0 + # via + # -r requirements-build.in + # nuitka diff --git a/run.bat b/run.bat index d34192a8..845b4e0c 100644 --- a/run.bat +++ b/run.bat @@ -1,3 +1,3 @@ @echo on py -m pip install -r requirements.txt -py autoortho +pythonw.exe -i autoortho