1 2# Copyright 2013-2016 Jaap Karssenberg <jaap.karssenberg@gmail.com> 3 4'''This module defines the L{main()} function for executing the zim 5application. It also defines a number of command classes that implement 6specific commandline commands and an singleton application object that 7takes core of the process life cycle. 8''' 9 10# TODO: 11# - implement weakvalue dict to ensure uniqueness of notebook objects 12 13 14import os 15import sys 16import logging 17import signal 18 19logger = logging.getLogger('zim') 20 21import zim 22import zim.fs 23import zim.errors 24import zim.config 25import zim.config.basedirs 26 27from zim import __version__ 28 29from zim.utils import get_module, lookup_subclass 30from zim.errors import Error 31from zim.notebook import Notebook, Path, \ 32 get_notebook_list, resolve_notebook, build_notebook 33from zim.formats import get_format 34 35from zim.config import ConfigManager 36from zim.plugins import PluginManager 37 38from .command import Command, GtkCommand, UsageError, GetoptError 39from .ipc import dispatch as _ipc_dispatch 40from .ipc import start_listening as _ipc_start_listening 41 42 43class HelpCommand(Command): 44 '''Class implementing the C{--help} command''' 45 46 usagehelp = '''\ 47usage: zim [OPTIONS] [NOTEBOOK [PAGE]] 48 or: zim --server [OPTIONS] [NOTEBOOK] 49 or: zim --export [OPTIONS] NOTEBOOK [PAGE] 50 or: zim --search NOTEBOOK QUERY 51 or: zim --index NOTEBOOK 52 or: zim --plugin PLUGIN [ARGUMENTS] 53 or: zim --manual [OPTIONS] [PAGE] 54 or: zim --help 55''' 56 optionhelp = '''\ 57General Options: 58 --gui run the editor (this is the default) 59 --server run the web server 60 --export export to a different format 61 --search run a search query on a notebook 62 --index build an index for a notebook 63 --plugin call a specific plugin function 64 --manual open the user manual 65 -V, --verbose print information to terminal 66 -D, --debug print debug messages 67 -v, --version print version and exit 68 -h, --help print this text 69 70GUI Options: 71 --list show the list with notebooks instead of 72 opening the default notebook 73 --geometry window size and position as WxH+X+Y 74 --fullscreen start in fullscreen mode 75 --standalone start a single instance, no background process 76 77Server Options: 78 --port port to use (defaults to 8080) 79 --template name of the template to use 80 --private serve only to localhost 81 --gui run the gui wrapper for the server 82 83Export Options: 84 -o, --output output directory (mandatory option) 85 --format format to use (defaults to 'html') 86 --template name of the template to use 87 --root-url url to use for the document root 88 --index-page index page name 89 -r, --recursive when exporting a page, also export sub-pages 90 -s, --singlefile export all pages to a single output file 91 -O, --overwrite force overwriting existing file(s) 92 93Search Options: 94 None 95 96Index Options: 97 -f, --flush flush the index first and force re-building 98 99Try 'zim --manual' for more help. 100''' 101 102 def run(self): 103 print(self.usagehelp) 104 print(self.optionhelp) # TODO - generate from commands 105 106 107class VersionCommand(Command): 108 '''Class implementing the C{--version} command''' 109 110 def run(self): 111 print('zim %s\n' % zim.__version__) 112 print(zim.__copyright__, '\n') 113 print(zim.__license__) 114 115 116class NotebookLookupError(Error): 117 '''Error when failing to locate a notebook''' 118 119 description = _('Could not find the file or folder for this notebook') 120 # T: Error verbose description 121 122 123class NotebookCommand(Command): 124 '''Base class for commands that act on a notebook''' 125 126 def get_default_or_only_notebook(self): 127 '''Helper to get a default notebook''' 128 notebooks = get_notebook_list() 129 if notebooks.default: 130 uri = notebooks.default.uri 131 elif len(notebooks) == 1: 132 uri = notebooks[0].uri 133 else: 134 return None 135 136 return resolve_notebook(uri, pwd=self.pwd) # None if not found 137 138 def get_notebook_argument(self): 139 '''Get the notebook and page arguments for this command 140 @returns: a 2-tuple of an L{NotebookInfo} object and an 141 optional L{Path} or C{(None, None)} if the notebook 142 argument is optional and not given 143 @raises NotebookLookupError: if the notebook is mandatory and 144 not given, or if it is given but could not be resolved 145 ''' 146 assert self.arguments[0] in ('NOTEBOOK', '[NOTEBOOK]') 147 args = self.get_arguments() 148 notebook = args[0] 149 150 if notebook is None: 151 if self.arguments[0] == 'NOTEBOOK': # not optional 152 raise NotebookLookupError(_('Please specify a notebook')) 153 # T: Error when looking up a notebook 154 else: 155 return None, None 156 157 notebookinfo = resolve_notebook(notebook, pwd=self.pwd) 158 if not notebookinfo: 159 raise NotebookLookupError(_('Could not find notebook: %s') % notebook) 160 # T: error message 161 162 if len(self.arguments) > 1 \ 163 and self.arguments[1] in ('PAGE', '[PAGE]') \ 164 and args[1] is not None: 165 pagename = Path.makeValidPageName(args[1]) 166 return notebookinfo, Path(pagename) 167 else: 168 return notebookinfo, None 169 170 def build_notebook(self, ensure_uptodate=True): 171 '''Get the L{Notebook} object for this command 172 Tries to automount the file location if needed. 173 @param ensure_uptodate: if C{True} index is updated when needed. 174 Only set to C{False} when index update is handled explicitly 175 (e.g. in the main gui). 176 @returns: a L{Notebook} object and a L{Path} object or C{None} 177 @raises NotebookLookupError: if the notebook could not be 178 resolved or is not given 179 @raises FileNotFoundError: if the notebook location does not 180 exist and could not be mounted. 181 ''' 182 # Explicit page argument has priority over implicit from uri 183 # mounting is attempted by zim.notebook.build_notebook() 184 notebookinfo, page = self.get_notebook_argument() # can raise NotebookLookupError 185 if not notebookinfo: 186 raise NotebookLookupError(_('Please specify a notebook')) 187 notebook, uripage = build_notebook(notebookinfo) # can raise FileNotFound 188 189 if ensure_uptodate and not notebook.index.is_uptodate: 190 for info in notebook.index.update_iter(): 191 #logger.info('Indexing %s', info) 192 pass # TODO meaningful info for above message 193 194 return notebook, page or uripage 195 196 197class GuiCommand(NotebookCommand, GtkCommand): 198 '''Class implementing the C{--gui} command and run the gtk interface''' 199 200 arguments = ('[NOTEBOOK]', '[PAGE]') 201 options = ( 202 ('list', '', 'show the list with notebooks instead of\nopening the default notebook'), 203 ('geometry=', '', 'window size and position as WxH+X+Y'), 204 ('fullscreen', '', 'start in fullscreen mode'), 205 ('standalone', '', 'start a single instance, no background process'), 206 ) 207 208 def build_notebook(self, ensure_uptodate=False): 209 # Bit more complicated here due to options to use default and 210 # allow using notebookdialog to prompt 211 212 # Explicit page argument has priority over implicit from uri 213 # mounting is attempted by zim.notebook.build_notebook() 214 215 from zim.notebook import FileNotFoundError 216 217 def prompt_notebook_list(): 218 import zim.gui.notebookdialog 219 return zim.gui.notebookdialog.prompt_notebook() 220 # Can return None if dialog is cancelled 221 222 used_default = False 223 page = None 224 if self.opts.get('list'): 225 notebookinfo = prompt_notebook_list() 226 else: 227 notebookinfo, page = self.get_notebook_argument() 228 229 if notebookinfo is None: 230 notebookinfo = self.get_default_or_only_notebook() 231 used_default = notebookinfo is not None 232 233 if notebookinfo is None: 234 notebookinfo = prompt_notebook_list() 235 236 if notebookinfo is None: 237 return None, None # Cancelled prompt 238 239 try: 240 notebook, uripage = build_notebook(notebookinfo) # can raise FileNotFound 241 except FileNotFoundError: 242 if used_default: 243 # Default notebook went missing? Fallback to dialog to allow changing it 244 notebookinfo = prompt_notebook_list() 245 if notebookinfo is None: 246 return None, None # Cancelled prompt 247 notebook, uripage = build_notebook(notebookinfo) # can raise FileNotFound 248 else: 249 raise 250 251 if ensure_uptodate and not notebook.index.is_uptodate: 252 for info in notebook.index.update_iter(): 253 #logger.info('Indexing %s', info) 254 pass # TODO meaningful info for above message 255 256 return notebook, page or uripage 257 258 def run(self): 259 from gi.repository import Gtk 260 261 from zim.gui.mainwindow import MainWindow 262 263 windows = [ 264 w for w in Gtk.Window.list_toplevels() 265 if isinstance(w, MainWindow) 266 ] 267 268 notebook, page = self.build_notebook() 269 if notebook is None: 270 logger.debug('NotebookDialog cancelled - exit') 271 return 272 273 for window in windows: 274 if window.notebook.uri == notebook.uri: 275 self._present_window(window, page) 276 return window 277 else: 278 return self._run_new_window(notebook, page) 279 280 def _present_window(self, window, page): 281 window.present() 282 283 if page: 284 window.open_page(page) 285 286 geometry = self.opts.get('geometry', None) 287 if geometry is not None: 288 window.parse_geometry(geometry) 289 290 if self.opts.get('fullscreen', False): 291 window.toggle_fullscreen(True) 292 293 def _run_new_window(self, notebook, page): 294 from gi.repository import GObject 295 296 from zim.gui.mainwindow import MainWindow 297 298 pluginmanager = PluginManager() 299 300 preferences = ConfigManager.preferences['General'] 301 preferences.setdefault('plugins_list_version', 'none') 302 if preferences['plugins_list_version'] != '0.70': 303 if not preferences['plugins']: 304 pluginmanager.load_plugins_from_preferences( 305 [ # Default plugins 306 'pageindex', 'pathbar', 'toolbar', 307 'insertsymbol', 'printtobrowser', 308 'versioncontrol', 'osx_menubar' 309 ] 310 ) 311 else: 312 # Upgrade version <0.70 where these were core functions 313 pluginmanager.load_plugins_from_preferences(['pageindex', 'pathbar']) 314 315 if 'calendar' in pluginmanager.failed: 316 ConfigManager.preferences['JournalPlugin'] = \ 317 ConfigManager.preferences['CalendarPlugin'] 318 pluginmanager.load_plugins_from_preferences(['journal']) 319 320 preferences['plugins_list_version'] = '0.70' 321 322 window = MainWindow( 323 notebook, 324 page=page, 325 **self.get_options('geometry', 'fullscreen') 326 ) 327 window.present() 328 329 if not window.notebook.index.is_uptodate: 330 window._uiactions.check_and_update_index(update_only=True) # XXX 331 else: 332 # Start a lightweight background check of the index 333 # put a small delay to ensure window is shown before we start 334 def start_background_check(): 335 notebook.index.start_background_check(notebook) 336 return False # only run once 337 GObject.timeout_add(500, start_background_check) 338 339 return window 340 341 342class ManualCommand(GuiCommand): 343 '''Like L{GuiCommand} but always opens the manual''' 344 345 arguments = ('[PAGE]',) 346 options = tuple(t for t in GuiCommand.options if t[0] != 'list') 347 # exclude --list 348 349 def run(self): 350 from zim.config import data_dir 351 self.arguments = ('NOTEBOOK', '[PAGE]') # HACK 352 self.args.insert(0, data_dir('manual').path) 353 return GuiCommand.run(self) 354 355 356class ServerCommand(NotebookCommand): 357 '''Class implementing the C{--server} command and running the web 358 server. 359 ''' 360 361 arguments = ('NOTEBOOK',) 362 options = ( 363 ('port=', 'p', 'port number to use (defaults to 8080)'), 364 ('template=', 't', 'name or path of the template to use'), 365 ('standalone', '', 'start a single instance, no background process'), 366 ('private', '', 'serve only to localhost') 367 ) 368 369 def run(self): 370 import zim.www 371 self.opts['port'] = int(self.opts.get('port', 8080)) 372 self.opts.setdefault('template', 'Default') 373 notebook, page = self.build_notebook() 374 is_public = not self.opts.get('private', False) 375 376 self.server = httpd = zim.www.make_server(notebook, public=is_public, **self.get_options('template', 'port')) 377 # server attribute used in testing to stop sever in thread 378 logger.info("Serving HTTP on %s port %i...", httpd.server_name, httpd.server_port) 379 httpd.serve_forever() 380 381 382class ServerGuiCommand(NotebookCommand, GtkCommand): 383 '''Like L{ServerCommand} but uses the graphical interface for the 384 server defined in L{zim.gui.server}. 385 ''' 386 387 arguments = ('[NOTEBOOK]',) 388 options = ( 389 ('port=', 'p', 'port number to use (defaults to 8080)'), 390 ('template=', 't', 'name or path of the template to use'), 391 ('standalone', '', 'start a single instance, no background process'), 392 ) 393 394 def run(self): 395 import zim.gui.server 396 self.opts['port'] = int(self.opts.get('port', 8080)) 397 notebookinfo, page = self.get_notebook_argument() 398 if notebookinfo is None: 399 # Prefer default to be selected in drop down, user can still change 400 notebookinfo = self.get_default_or_only_notebook() 401 402 window = zim.gui.server.ServerWindow( 403 notebookinfo, 404 public=True, 405 **self.get_options('template', 'port') 406 ) 407 window.show_all() 408 return window 409 410 411class ExportCommand(NotebookCommand): 412 '''Class implementing the C{--export} command''' 413 414 arguments = ('NOTEBOOK', '[PAGE]') 415 options = ( 416 ('format=', '', 'format to use (defaults to \'html\')'), 417 ('template=', '', 'name or path of the template to use'), 418 ('output=', 'o', 'output folder, or output file name'), 419 ('root-url=', '', 'url to use for the document root'), 420 ('index-page=', '', 'index page name'), 421 ('recursive', 'r', 'when exporting a page, also export sub-pages'), 422 ('singlefile', 's', 'export all pages to a single output file'), 423 ('overwrite', 'O', 'overwrite existing file(s)'), 424 ) 425 426 def get_exporter(self, page): 427 from zim.fs import File, Dir 428 from zim.export import \ 429 build_mhtml_file_exporter, \ 430 build_single_file_exporter, \ 431 build_page_exporter, \ 432 build_notebook_exporter 433 434 format = self.opts.get('format', 'html') 435 if not 'output' in self.opts: 436 raise UsageError(_('Output location needed for export')) # T: error in export command 437 output = Dir(self.opts['output']) 438 if not output.isdir(): 439 output = File(self.opts.get('output')) 440 template = self.opts.get('template', 'Default') 441 442 if output.exists() and not self.opts.get('overwrite'): 443 if output.isdir(): 444 if len(output.list()) > 0: 445 raise Error(_('Output folder exists and not empty, specify "--overwrite" to force export')) # T: error message for export 446 else: 447 pass 448 else: 449 raise Error(_('Output file exists, specify "--overwrite" to force export')) # T: error message for export 450 451 if format == 'mhtml': 452 self.ignore_options('index-page') 453 if output.isdir(): 454 raise UsageError(_('Need output file to export MHTML')) # T: error message for export 455 456 exporter = build_mhtml_file_exporter( 457 output, template, 458 document_root_url=self.opts.get('root-url'), 459 ) 460 elif self.opts.get('singlefile'): 461 self.ignore_options('index-page') 462 if output.exists() and output.isdir(): 463 ext = get_format(format).info['extension'] 464 output = output.file(page.basename) + '.' + ext 465 466 exporter = build_single_file_exporter( 467 output, format, template, namespace=page, 468 document_root_url=self.opts.get('root-url'), 469 ) 470 elif page: 471 self.ignore_options('index-page') 472 if output.exists() and output.isdir(): 473 ext = get_format(format).info['extension'] 474 output = output.file(page.basename) + '.' + ext 475 476 exporter = build_page_exporter( 477 output, format, template, page, 478 document_root_url=self.opts.get('root-url'), 479 ) 480 else: 481 if not output.exists(): 482 output = Dir(output.path) 483 elif not output.isdir(): 484 raise UsageError(_('Need output folder to export full notebook')) # T: error message for export 485 486 exporter = build_notebook_exporter( 487 output, format, template, 488 index_page=self.opts.get('index-page'), 489 document_root_url=self.opts.get('root-url'), 490 ) 491 492 return exporter 493 494 def run(self): 495 from zim.export.selections import AllPages, SinglePage, SubPages 496 497 notebook, page = self.build_notebook() 498 notebook.index.check_and_update() 499 500 if page and self.opts.get('recursive'): 501 selection = SubPages(notebook, page) 502 elif page: 503 selection = SinglePage(notebook, page) 504 else: 505 selection = AllPages(notebook) 506 507 exporter = self.get_exporter(page) 508 exporter.export(selection) 509 510 511 512class SearchCommand(NotebookCommand): 513 '''Class implementing the C{--search} command''' 514 515 arguments = ('NOTEBOOK', 'QUERY') 516 517 def run(self): 518 from zim.search import SearchSelection, Query 519 520 notebook, p = self.build_notebook() 521 n, query = self.get_arguments() 522 523 if query and not query.isspace(): 524 logger.info('Searching for: %s', query) 525 query = Query(query) 526 else: 527 raise ValueError('Empty query') 528 529 selection = SearchSelection(notebook) 530 selection.search(query) 531 for path in sorted(selection, key=lambda p: p.name): 532 print(path.name) 533 534 535class IndexCommand(NotebookCommand): 536 '''Class implementing the C{--index} command''' 537 538 arguments = ('NOTEBOOK',) 539 options = ( 540 ('flush', 'f', 'flush the index first and force re-building'), 541 ) 542 543 def run(self): 544 # Elevate logging level of indexer to ensure "zim --index -V" gives 545 # some meaningfull output 546 def elevate_index_logging(log_record): 547 if log_record.levelno == logging.DEBUG: 548 log_record.levelno = logging.INFO 549 log_record.levelname = 'INFO' 550 return True 551 552 mylogger = logging.getLogger('zim.notebook.index') 553 mylogger.setLevel(logging.DEBUG) 554 mylogger.addFilter(elevate_index_logging) 555 556 notebook, p = self.build_notebook(ensure_uptodate=False) 557 if self.opts.get('flush'): 558 notebook.index.flush() 559 notebook.index.update() 560 else: 561 # Effectively the same as check_and_update_index ui action 562 logger.info('Checking notebook index') 563 notebook.index.check_and_update() 564 565 logger.info('Index up to date!') 566 567 568commands = { 569 'help': HelpCommand, 570 'version': VersionCommand, 571 'gui': GuiCommand, 572 'manual': ManualCommand, 573 'server': ServerCommand, 574 'servergui': ServerGuiCommand, 575 'export': ExportCommand, 576 'search': SearchCommand, 577 'index': IndexCommand, 578} 579 580 581def build_command(args, pwd=None): 582 '''Parse all commandline options 583 @returns: a L{Command} object 584 @raises UsageError: if args is not correct 585 ''' 586 args = list(args) 587 588 if args and args[0] == '--plugin': 589 args.pop(0) 590 try: 591 cmd = args.pop(0) 592 except IndexError: 593 raise UsageError('Missing plugin name') 594 595 try: 596 mod = get_module('zim.plugins.' + cmd) 597 klass = lookup_subclass(mod, Command) 598 except: 599 if '-D' in args or '--debug' in args: 600 logger.exception('Error while loading: zim.plugins.%s.Command', cmd) 601 # Can't use following because log level not yet set: 602 # logger.debug('Error while loading: zim.plugins.%s.Command', cmd, exc_info=sys.exc_info()) 603 raise UsageError('Could not load commandline command for plugin "%s"' % cmd) 604 else: 605 if args and args[0].startswith('--') and args[0][2:] in commands: 606 cmd = args.pop(0)[2:] 607 if cmd == 'server' and '--gui' in args: 608 args.remove('--gui') 609 cmd = 'servergui' 610 elif args and args[0] == '-v': 611 args.pop(0) 612 cmd = 'version' 613 elif args and args[0] == '-h': 614 args.pop(0) 615 cmd = 'help' 616 else: 617 cmd = 'gui' # default 618 619 klass = commands[cmd] 620 621 obj = klass(cmd, pwd=pwd) 622 obj.parse_options(*args) 623 return obj 624 625 626 627class ZimApplication(object): 628 '''This object is repsonsible for managing the life cycle of the 629 application process. 630 631 To do so, it decides whether to dispatch a command to an already 632 running zim process or to handle it in the current process. 633 For gtk based commands it keeps track of the toplevel objects 634 for re-use and to be able to end the process when no toplevel 635 objects are left. 636 ''' 637 638 def __init__(self): 639 self._running = False 640 self._log_started = False 641 self._standalone = False 642 self._windows = set() 643 644 @property 645 def toplevels(self): 646 return iter(self._windows) 647 648 @property 649 def notebooks(self): 650 return frozenset( 651 w.notebook for w in self.toplevels 652 if hasattr(w, 'notebook') 653 ) 654 655 def get_mainwindow(self, notebook, _class=None): 656 '''Returns an existing L{MainWindow} for C{notebook} or C{None}''' 657 from zim.gui.mainwindow import MainWindow 658 _class = _class or MainWindow # test seam 659 for w in self.toplevels: 660 if isinstance(w, _class) and w.notebook.uri == notebook.uri: 661 return w 662 else: 663 return None 664 665 def present(self, notebook, page=None): 666 '''Present notebook and page in a mainwindow, may not return for 667 standalone processes. 668 ''' 669 uri = notebook if isinstance(notebook, str) else notebook.uri 670 pagename = page if isinstance(page, str) else page.name 671 self.run('--gui', uri, pagename) 672 673 def add_window(self, window): 674 if not window in self._windows: 675 logger.debug('Add window: %s', window.__class__.__name__) 676 677 assert hasattr(window, 'destroy') 678 window.connect('destroy', self._on_destroy_window) 679 self._windows.add(window) 680 681 def remove_window(self, window): 682 logger.debug('Remove window: %s', window.__class__.__name__) 683 try: 684 self._windows.remove(window) 685 except KeyError: 686 pass 687 688 def _on_destroy_window(self, window): 689 self.remove_window(window) 690 if not self._windows: 691 from gi.repository import Gtk 692 693 logger.debug('Last toplevel destroyed, quit') 694 if Gtk.main_level() > 0: 695 Gtk.main_quit() 696 697 def run(self, *args, **kwargs): 698 '''Run a commandline command, either in this process, an 699 existing process, or a new process. 700 @param args: commandline arguments 701 @param kwargs: optional arguments for L{build_command} 702 ''' 703 PluginManager().load_plugins_from_preferences( 704 ConfigManager.preferences['General']['plugins'] 705 ) 706 cmd = build_command(args, **kwargs) 707 self._run_cmd(cmd, args) # test seam 708 709 def _run_cmd(self, cmd, args): 710 if not self._log_started: 711 self._log_start() 712 713 if self._running: 714 # This is not the first command that we process 715 if isinstance(cmd, GtkCommand): 716 if self._standalone or cmd.standalone_process: 717 self._spawn_standalone(args) 718 else: 719 w = cmd.run() 720 if w is not None: 721 self.add_window(w) 722 w.present() 723 else: 724 cmd.run() 725 else: 726 # Although a-typical, this path could be re-entrant if a 727 # run_local() dispatches another command - therefore we set 728 # standalone before calling run_local() 729 if isinstance(cmd, GtkCommand): 730 self._standalone = self._standalone or cmd.standalone_process 731 if cmd.run_local(): 732 return 733 734 if not self._standalone and self._try_dispatch(args, cmd.pwd): 735 pass # We are done 736 else: 737 self._running = True 738 self._run_main_loop(cmd) 739 else: 740 cmd.run() 741 742 def _run_main_loop(self, cmd): 743 # Run for the 1st gtk command in a primary process, 744 # but can still be standalone process 745 from gi.repository import Gtk 746 from gi.repository import GObject 747 748 ####################################################################### 749 # WARNING: commented out "GObject.threads_init()" because it leads to 750 # various segfaults on linux. See github issue #7 751 # However without this init, gobject does not properly release the 752 # python GIL during C calls, so threads may block while main loop is 753 # waiting. Thus threads become very slow and unpredictable unless we 754 # actively monitor them from the mainloop, causing python to run 755 # frequently. So be very carefull relying on threads. 756 # Re-evaluate when we are above PyGObject 3.10.2 - threading should 757 # wotk bettter there even without this statement. (But even then, 758 # no Gtk calls from threads, just "GObject.idle_add()". ) 759 # Kept for windows, because we need thread to run ipc listener, and no 760 # crashes observed there. 761 if os.name == 'nt': 762 GObject.threads_init() 763 ####################################################################### 764 765 from zim.gui.widgets import gtk_window_set_default_icon 766 gtk_window_set_default_icon() 767 768 zim.errors.set_use_gtk(True) 769 self._setup_signal_handling() 770 771 if self._standalone: 772 logger.debug('Starting standalone process') 773 else: 774 logger.debug('Starting primary process') 775 self._daemonize() 776 if not _ipc_start_listening(self._handle_incoming): 777 logger.warn('Failure to setup socket, falling back to "--standalone" mode') 778 self._standalone = True 779 780 w = cmd.run() 781 if w is not None: 782 self.add_window(w) 783 784 while self._windows: 785 Gtk.main() 786 787 for toplevel in list(self._windows): 788 try: 789 toplevel.destroy() 790 except: 791 logger.exception('Exception while destroying window') 792 self.remove_window(toplevel) # force removal 793 794 # start main again if toplevels remaining .. 795 796 # exit immediatly if no toplevel created 797 798 def _log_start(self): 799 self._log_started = True 800 801 logger.info('This is zim %s', __version__) 802 level = logger.getEffectiveLevel() 803 if level == logging.DEBUG: 804 import sys 805 import os 806 import zim.config 807 808 logger.debug('Python version is %s', str(sys.version_info)) 809 logger.debug('Platform is %s', os.name) 810 zim.config.log_basedirs() 811 812 def _setup_signal_handling(self): 813 def handle_sigterm(signal, frame): 814 from gi.repository import Gtk 815 816 logger.info('Got SIGTERM, quit') 817 if Gtk.main_level() > 0: 818 Gtk.main_quit() 819 820 signal.signal(signal.SIGTERM, handle_sigterm) 821 822 def _spawn_standalone(self, args): 823 from zim import ZIM_EXECUTABLE 824 from zim.applications import Application 825 826 args = list(args) 827 if not '--standalone' in args: 828 args.append('--standalone') 829 830 # more detailed logging has lower number, so WARN > INFO > DEBUG 831 loglevel = logging.getLogger().getEffectiveLevel() 832 if loglevel <= logging.DEBUG: 833 args.append('-D',) 834 elif loglevel <= logging.INFO: 835 args.append('-V',) 836 837 Application([ZIM_EXECUTABLE] + args).spawn() 838 839 def _try_dispatch(self, args, pwd): 840 try: 841 _ipc_dispatch(pwd, *args) 842 except AssertionError as err: 843 logger.debug('Got error in dispatch: %s', str(err)) 844 return False 845 except Exception: 846 logger.exception('Got error in dispatch') 847 return False 848 else: 849 logger.debug('Dispatched command %r', args) 850 return True 851 852 def _handle_incoming(self, pwd, *args): 853 self.run(*args, pwd=pwd) 854 855 def _daemonize(self): 856 # Decouple from parent environment 857 # and redirect standard file descriptors 858 os.chdir(zim.fs.Dir('~').path) 859 # Using HOME because this folder will not disappear normally 860 # and because it is a sane starting point for file choosers etc. 861 862 try: 863 si = file(os.devnull, 'r') 864 os.dup2(si.fileno(), sys.stdin.fileno()) 865 except: 866 pass 867 868 loglevel = logging.getLogger().getEffectiveLevel() 869 if loglevel <= logging.INFO and sys.stdout.isatty() and sys.stderr.isatty(): 870 # more detailed logging has lower number, so WARN > INFO > DEBUG 871 # log to file unless output is a terminal and logging <= INFO 872 pass 873 else: 874 # Redirect output to file 875 dir = zim.fs.get_tmpdir() 876 zim.debug_log_file = os.path.join(dir.path, "zim.log") 877 err_stream = open(zim.debug_log_file, "w") 878 879 # Try to flush standards out and error, if there 880 for pipe in (sys.stdout, sys.stderr): 881 if pipe is not None: 882 try: 883 pipe.flush() 884 except OSError: 885 pass 886 887 # First try to dup handles for anyone who still has a reference 888 # if that fails, just set them (maybe not real files in the first place) 889 try: 890 os.dup2(err_stream.fileno(), sys.stdout.fileno()) 891 os.dup2(err_stream.fileno(), sys.stderr.fileno()) 892 except: 893 sys.stdout = err_stream 894 sys.stderr = err_stream 895 896 # Re-initialize logging handler, in case it keeps reference 897 # to the old stderr object 898 rootlogger = logging.getLogger() 899 try: 900 for handler in rootlogger.handlers: 901 rootlogger.removeHandler(handler) 902 903 handler = logging.StreamHandler() 904 handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) 905 rootlogger.addHandler(handler) 906 except: 907 pass 908 909 if rootlogger.getEffectiveLevel() != logging.DEBUG: 910 rootlogger.setLevel(logging.DEBUG) # else file remains empty 911 self._log_start() 912 913 914 915ZIM_APPLICATION = ZimApplication() # Singleton per process 916 917 918def main(*argv): 919 '''Run full zim application 920 @returns: exit code (if error handled, else just raises) 921 ''' 922 923 import zim.config 924 925 # Check if we can find our own data files 926 _file = zim.config.data_file('zim.png') 927 if not (_file and _file.exists()): #pragma: no cover 928 raise AssertionError( 929 'ERROR: Could not find data files in path: \n' 930 '%s\n' 931 'Try setting XDG_DATA_DIRS' 932 % list(map(str, zim.config.data_dirs())) 933 ) 934 935 try: 936 ZIM_APPLICATION.run(*argv[1:]) 937 except KeyboardInterrupt: 938 # Don't show error dialog for this error.. 939 logger.error('KeyboardInterrupt') 940 return 1 941 except Exception: 942 zim.errors.exception_handler('Exception in main()') 943 return 1 944 else: 945 return 0 946