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