OSDN Git Service

Updating info
[rebornos/cnchi-gnome-osdn.git] / Cnchi / cnchi.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 #  cnchi.py
5 #
6 #  Copyright © 2013-2019 RebornOS
7 #
8 #  This file is part of Cnchi.
9 #
10 #  Cnchi is free software; you can redistribute it and/or modify
11 #  it under the terms of the GNU General Public License as published by
12 #  the Free Software Foundation; either version 3 of the License, or
13 #  (at your option) any later version.
14 #
15 #  Cnchi is distributed in the hope that it will be useful,
16 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
17 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 #  GNU General Public License for more details.
19 #
20 #  The following additional terms are in effect as per Section 7 of the license:
21 #
22 #  The preservation of all legal notices and author attributions in
23 #  the material or in the Appropriate Legal Notices displayed
24 #  by works containing it is required.
25 #
26 #  You should have received a copy of the GNU General Public License
27 #  along with Cnchi; If not, see <http://www.gnu.org/licenses/>.
28
29 """ Main Cnchi (Antergos Installer) module """
30
31 import os
32 import sys
33 import shutil
34 import logging
35 import logging.handlers
36 import gettext
37 import locale
38 import gi
39 import pwd
40
41 CNCHI_PATH = "/usr/share/cnchi"
42 sys.path.append(CNCHI_PATH)
43 sys.path.append(os.path.join(CNCHI_PATH, "src"))
44 sys.path.append(os.path.join(CNCHI_PATH, "src/download"))
45 sys.path.append(os.path.join(CNCHI_PATH, "src/hardware"))
46 sys.path.append(os.path.join(CNCHI_PATH, "src/installation"))
47 sys.path.append(os.path.join(CNCHI_PATH, "src/misc"))
48 sys.path.append(os.path.join(CNCHI_PATH, "src/pacman"))
49 sys.path.append(os.path.join(CNCHI_PATH, "src/pages"))
50 sys.path.append(os.path.join(CNCHI_PATH, "src/pages/dialogs"))
51 sys.path.append(os.path.join(CNCHI_PATH, "src/parted3"))
52
53 gi.require_version('Gtk', '3.0')
54 from gi.repository import Gio, Gtk, GObject
55
56 import misc.extra as misc
57 from misc.run_cmd import call
58 import show_message as show
59 import info
60
61 from logging_utils import ContextFilter
62 import logging_color
63
64 #try:
65 #    from bugsnag.handlers import BugsnagHandler
66 #    import bugsnag
67 #    BUGSNAG_ERROR = None
68 #except ImportError as err:
69 #    BUGSNAG_ERROR = str(err)
70
71 BUGSNAG_ERROR = "Bugsnag disabled. Makes requests raise several exceptions. Need to check what is wrong"
72
73 # When testing, no _() is available
74 try:
75     _("")
76 except NameError as err:
77     def _(message):
78         return message
79
80
81 class CnchiApp(Gtk.Application):
82     """ Main Cnchi App class """
83
84     TEMP_FOLDER = '/var/tmp/cnchi'
85
86     def __init__(self, cmd_line):
87         """ Constructor. Call base class """
88         Gtk.Application.__init__(self,
89                                  application_id="com.antergos.cnchi",
90                                  flags=Gio.ApplicationFlags.FLAGS_NONE)
91         self.tmp_running = os.path.join(CnchiApp.TEMP_FOLDER, ".setup-running")
92         self.cmd_line = cmd_line
93
94     def do_activate(self):
95         """ Override the 'activate' signal of GLib.Application.
96             Shows the default first window of the application (like a new document).
97             This corresponds to the application being launched by the desktop environment. """
98         try:
99             import main_window
100         except ImportError as err:
101             msg = "Cannot create Cnchi main window: {0}".format(err)
102             logging.error(msg)
103             sys.exit(1)
104
105         # Check if we have administrative privileges
106         if os.getuid() != 0:
107             msg = _('This installer must be run with administrative privileges, '
108                     'and cannot continue without them.')
109             show.error(None, msg)
110             return
111
112         # Check if we're already running
113         if self.already_running():
114             msg = _("You cannot run two instances of this installer.\n\n"
115                     "If you are sure that the installer is not already running\n"
116                     "you can run this installer using the --force option\n"
117                     "or you can manually delete the offending file.\n\n"
118                     "Offending file: '{0}'").format(self.tmp_running)
119             show.error(None, msg)
120             return
121
122         window = main_window.MainWindow(self, self.cmd_line)
123         self.add_window(window)
124         window.show()
125
126         try:
127             with misc.raised_privileges():
128                 with open(self.tmp_running, 'w') as tmp_file:
129                     txt = "Cnchi {0}\n{1}\n".format(info.CNCHI_VERSION, os.getpid())
130                     tmp_file.write(txt)
131         except PermissionError as err:
132             logging.error(err)
133             show.error(None, err)
134
135         # This is unnecessary as show_all is called in MainWindow
136         # window.show_all()
137
138         # def do_startup(self):
139         # """ Override the 'startup' signal of GLib.Application. """
140         # Gtk.Application.do_startup(self)
141
142         # Application main menu (we don't need one atm)
143         # Leaving this here for future reference
144         # menu = Gio.Menu()
145         # menu.append("About", "win.about")
146         # menu.append("Quit", "app.quit")
147         # self.set_app_menu(menu)
148
149     def already_running(self):
150         """ Check if we're already running """
151         if os.path.exists(self.tmp_running):
152             logging.debug("File %s already exists.", self.tmp_running)
153             with open(self.tmp_running) as setup:
154                 lines = setup.readlines()
155             if len(lines) >= 2:
156                 try:
157                     pid = int(lines[1].strip('\n'))
158                 except ValueError as err:
159                     logging.debug(err)
160                     logging.debug("Cannot read PID value.")
161                     return True
162             else:
163                 logging.debug("Cannot read PID value.")
164                 return True
165
166             if misc.check_pid(pid):
167                 logging.info("Cnchi with pid '%d' already running.", pid)
168                 return True
169
170             # Cnchi with pid 'pid' is no longer running, we can safely
171             # remove the offending file and continue.
172             os.remove(self.tmp_running)
173         return False
174
175 class CnchiInit():
176     """ Initializes Cnchi """
177
178     # Useful vars for gettext (translations)
179     APP_NAME = "cnchi"
180     LOCALE_DIR = "/usr/share/locale"
181
182     # At least this GTK version is needed
183     GTK_VERSION_NEEDED = "3.18.0"
184
185     LOG_FOLDER = '/var/log/cnchi'
186     TEMP_FOLDER = '/var/tmp/cnchi'
187
188     def __init__(self):
189         """ This function initialises Cnchi """
190
191         # Sets SIGTERM handler, so Cnchi can clean up before exiting
192         # signal.signal(signal.SIGTERM, sigterm_handler)
193
194         # Create Cnchi's temporary folder
195         with misc.raised_privileges():
196             os.makedirs(CnchiInit.TEMP_FOLDER, mode=0o755, exist_ok=True)
197
198         # Configures gettext to be able to translate messages, using _()
199         self.setup_gettext()
200
201         # Command line options
202         self.cmd_line = self.parse_options()
203
204         if self.cmd_line.version:
205             print(_("Cnchi (RebornOS Installer) version {0}").format(
206                 info.CNCHI_VERSION))
207             sys.exit(0)
208
209         if self.cmd_line.force:
210             misc.remove_temp_files(CnchiInit.TEMP_FOLDER)
211
212         # Drop root privileges
213         misc.drop_privileges()
214
215         # Setup our logging framework
216         self.setup_logging()
217
218         # Enables needed repositories only if it's not enabled
219         self.enable_repositories()
220
221         # Check that all repositories are present in pacman.conf file
222         if not self.check_pacman_conf("/etc/pacman.conf"):
223             sys.exit(1)
224
225         # Check Cnchi is correctly installed
226         if not self.check_for_files():
227             sys.exit(1)
228
229         # Check installed GTK version
230         if not self.check_gtk_version():
231             sys.exit(1)
232
233         # Check installed pyalpm and libalpm versions
234         if not self.check_pyalpm_version():
235             sys.exit(1)
236
237         # Check ISO version where Cnchi is running from
238         if not self.check_iso_version():
239             sys.exit(1)
240
241         # Disable suspend to RAM
242         self.disable_suspend()
243
244         # Init PyObject Threads
245         self.threads_init()
246
247     @staticmethod
248     def check_pacman_conf(path):
249         """ Check that pacman.conf has the correct options """
250         """ Remove antergos repo, and add Reborn-OS repo (Rafael) """
251
252         with open(path, 'rt') as pacman:
253             lines = pacman.readlines()
254
255         repos = [
256             "[Reborn-OS]", "[core]", "[extra]", "[community]", "[multilib]"]
257
258         for line in lines:
259             line = line.strip('\n')
260             if line in repos:
261                 repos.remove(line)
262         if repos:
263             for repo in repos:
264                 logging.error("Repository %s not in pacman.conf file", repo)
265             return False
266
267         logging.debug("All repositories are present in pacman.conf file")
268         return True
269
270     def setup_logging(self):
271         """ Configure our logger """
272
273         with misc.raised_privileges():
274             os.makedirs(CnchiInit.LOG_FOLDER, mode=0o755, exist_ok=True)
275
276         logger = logging.getLogger()
277
278         logger.handlers = []
279
280         if self.cmd_line.debug:
281             log_level = logging.DEBUG
282         else:
283             log_level = logging.INFO
284
285         logger.setLevel(log_level)
286
287         context_filter = ContextFilter()
288         logger.addFilter(context_filter.filter)
289
290         datefmt = "%Y-%m-%d %H:%M:%S"
291
292         fmt = "%(asctime)s [%(levelname)-7s] %(message)s  (%(filename)s:%(lineno)d)"
293         formatter = logging.Formatter(fmt, datefmt)
294
295         color_fmt = (
296             "%(asctime)s [%(levelname)-18s] %(message)s  "
297             "($BOLD%(filename)s$RESET:%(lineno)d)")
298         color_formatter = logging_color.ColoredFormatter(color_fmt, datefmt)
299
300         # File logger
301         log_path = os.path.join(CnchiInit.LOG_FOLDER, 'cnchi.log')
302         try:
303             with misc.raised_privileges():
304                 file_handler = logging.FileHandler(log_path, mode='w')
305             file_handler.setLevel(log_level)
306             file_handler.setFormatter(formatter)
307             logger.addHandler(file_handler)
308         except PermissionError as permission_error:
309             print("Can't open ", log_path, " : ", permission_error)
310
311         # Stdout logger
312         if self.cmd_line.verbose:
313             # Show log messages to stdout in color (color_formatter)
314             stream_handler = logging.StreamHandler()
315             stream_handler.setLevel(log_level)
316             stream_handler.setFormatter(color_formatter)
317             logger.addHandler(stream_handler)
318
319         if not BUGSNAG_ERROR:
320             # Bugsnag logger
321             bugsnag_api = context_filter.api_key
322             if bugsnag_api is not None:
323                 bugsnag.configure(
324                     api_key=bugsnag_api,
325                     app_version=info.CNCHI_VERSION,
326                     project_root='/usr/share/cnchi/cnchi',
327                     release_stage=info.CNCHI_RELEASE_STAGE)
328                 bugsnag_handler = BugsnagHandler(api_key=bugsnag_api)
329                 bugsnag_handler.setLevel(logging.WARNING)
330                 bugsnag_handler.setFormatter(formatter)
331                 bugsnag_handler.addFilter(context_filter.filter)
332                 bugsnag.before_notify(
333                     context_filter.bugsnag_before_notify_callback)
334                 logger.addHandler(bugsnag_handler)
335                 logging.info(
336                     "Sending Cnchi log messages to bugsnag server (using python-bugsnag).")
337             else:
338                 logging.warning(
339                     "Cannot read the bugsnag api key, logging to bugsnag is not possible.")
340         else:
341             logging.warning(BUGSNAG_ERROR)
342
343     @staticmethod
344     def check_gtk_version():
345         """ Check GTK version """
346         # Check desired GTK Version
347         major_needed = int(CnchiInit.GTK_VERSION_NEEDED.split(".")[0])
348         minor_needed = int(CnchiInit.GTK_VERSION_NEEDED.split(".")[1])
349         micro_needed = int(CnchiInit.GTK_VERSION_NEEDED.split(".")[2])
350
351         # Check system GTK Version
352         major = Gtk.get_major_version()
353         minor = Gtk.get_minor_version()
354         micro = Gtk.get_micro_version()
355
356         # Cnchi will be called from our liveCD that already
357         # has the latest GTK version. This is here just to
358         # help testing Cnchi in our environment.
359         wrong_gtk_version = False
360         if major_needed > major:
361             wrong_gtk_version = True
362         if major_needed == major and minor_needed > minor:
363             wrong_gtk_version = True
364         if major_needed == major and minor_needed == minor and micro_needed > micro:
365             wrong_gtk_version = True
366
367         if wrong_gtk_version:
368             text = "Detected GTK version {0}.{1}.{2} but version >= {3} is needed."
369             text = text.format(major, minor, micro,
370                                CnchiInit.GTK_VERSION_NEEDED)
371             try:
372                 show.error(None, text)
373             except ImportError as import_error:
374                 logging.error(import_error)
375             return False
376         logging.info("Using GTK v%d.%d.%d", major, minor, micro)
377
378         return True
379
380     @staticmethod
381     def check_pyalpm_version():
382         """ Checks python alpm binding and alpm library versions """
383         try:
384             import pyalpm
385
386             txt = "Using pyalpm v{0} as interface to libalpm v{1}"
387             txt = txt.format(pyalpm.version(), pyalpm.alpmversion())
388             logging.info(txt)
389         except (NameError, ImportError) as err:
390             try:
391                 show.error(None, err)
392                 logging.error(err)
393             except ImportError as import_error:
394                 logging.error(import_error)
395             return False
396
397         return True
398
399     def check_iso_version(self):
400         """ Hostname contains the ISO version """
401         from socket import gethostname
402         hostname = gethostname()
403         # antergos-year.month-iso
404         prefix = "ant-"
405         suffix = "-min"
406         if hostname.startswith(prefix) or hostname.endswith(suffix):
407             # We're running form the ISO, register which version.
408             if suffix in hostname:
409                 version = hostname[len(prefix):-len(suffix)]
410             else:
411                 version = hostname[len(prefix):]
412             logging.debug("Running from ISO version %s", version)
413             # Delete user's chromium cache (just in case)
414             cache_dir = "/home/antergos/.cache/chromium"
415             if os.path.exists(cache_dir):
416                 shutil.rmtree(path=cache_dir, ignore_errors=True)
417                 logging.debug("User's chromium cache deleted")
418             # If we're running from sonar iso force a11y parameter to true
419             if hostname.endswith("sonar"):
420                 self.cmd_line.a11y = True
421         else:
422             logging.warning("Not running from ISO")
423         return True
424
425     @staticmethod
426     def parse_options():
427         """ argparse http://docs.python.org/3/howto/argparse.html """
428
429         import argparse
430
431         desc = _("Cnchi v{0} - RebornOS Installer").format(info.CNCHI_VERSION)
432         parser = argparse.ArgumentParser(description=desc)
433
434         parser.add_argument(
435             "-a", "--a11y", help=_("Set accessibility feature on by default"),
436             action="store_true")
437         parser.add_argument(
438             "-c", "--cache", help=_("Use pre-downloaded xz packages when possible"),
439             nargs='?')
440         parser.add_argument(
441             "-d", "--debug", help=_("Sets Cnchi log level to 'debug'"),
442             action="store_true")
443         parser.add_argument(
444             "-e", "--environment", help=_("Sets the Desktop Environment that will be installed"),
445             nargs='?')
446         parser.add_argument(
447             "-f", "--force", help=_("Runs cnchi even when another instance is running"),
448             action="store_true")
449         parser.add_argument(
450             "-n", "--no-check", help=_("Makes checks optional in check screen"),
451             action="store_true")
452         parser.add_argument(
453             "-p", "--packagelist", help=_("Install packages referenced by a local XML file"),
454             nargs='?')
455         parser.add_argument(
456             "-s", "--logserver", help=_("Log server (deprecated, always uses bugsnag)"),
457             nargs='?')
458         parser.add_argument(
459             "-t", "--no-tryit", help=_("Disables first screen's 'try it' option"),
460             action="store_true")
461         parser.add_argument(
462             "-v", "--verbose", help=_("Show logging messages to stdout"),
463             action="store_true")
464         parser.add_argument(
465             "-V", "--version", help=_("Show Cnchi version and quit"),
466             action="store_true")
467         parser.add_argument(
468             "-z", "--hidden", help=_("Show options in development (use at your own risk!)"),
469             action="store_true")
470
471         return parser.parse_args()
472
473     @staticmethod
474     def threads_init():
475         """
476         For applications that wish to use Python threads to interact with the GNOME platform,
477         GObject.threads_init() must be called prior to running or creating threads and starting
478         main loops (see notes below for PyGObject 3.10 and greater). Generally, this should be done
479         in the first stages of an applications main entry point or right after importing GObject.
480         For multi-threaded GUI applications Gdk.threads_init() must also be called prior to running
481         Gtk.main() or Gio/Gtk.Application.run().
482         """
483         minor = Gtk.get_minor_version()
484         micro = Gtk.get_micro_version()
485
486         if minor == 10 and micro < 2:
487             # Unfortunately these versions of PyGObject suffer a bug
488             # which require a workaround to get threading working properly.
489             # Workaround: Force GIL creation
490             import threading
491             threading.Thread(target=lambda: None).start()
492
493         # Since version 3.10.2, calling threads_init is no longer needed.
494         # See: https://wiki.gnome.org/PyGObject/Threading
495         if minor < 10 or (minor == 10 and micro < 2):
496             GObject.threads_init()
497             # Gdk.threads_init()
498
499     @staticmethod
500     def setup_gettext():
501         """ This allows to translate all py texts (not the glade ones) """
502
503         gettext.textdomain(CnchiInit.APP_NAME)
504         gettext.bindtextdomain(CnchiInit.APP_NAME, CnchiInit.LOCALE_DIR)
505
506         locale_code, _encoding = locale.getdefaultlocale()
507         lang = gettext.translation(
508             CnchiInit.APP_NAME, CnchiInit.LOCALE_DIR, [locale_code], None, True)
509         lang.install()
510
511
512     @staticmethod
513     def check_for_files():
514         """ Check for some necessary files. Cnchi can't run without them """
515         paths = [
516             "/usr/share/cnchi",
517             "/usr/share/cnchi/ui",
518             "/usr/share/cnchi/data",
519             "/usr/share/cnchi/data/locale"]
520
521         for path in paths:
522             if not os.path.exists(path):
523                 print(_("Cnchi files not found. Please, install Cnchi using pacman"))
524                 return False
525
526         return True
527
528     @staticmethod
529     def enable_repositories():
530         """ Enable needed repositories in /etc/pacman.conf (just in case) """
531         """ Remove antergos repo, and add Reborn-OS repo (Rafael) """
532
533         repositories = ['Reborn-OS', 'core', 'extra', 'community', 'multilib']
534
535         # Read pacman.conf file
536         with open("/etc/pacman.conf", 'rt') as pconf:
537             lines = pconf.readlines()
538
539         # For each repository, check if it is enabled or not
540         enabled = {}
541         for repo in repositories:
542             enabled[repo] = False
543             for line in lines:
544                 if line.startswith('[' + repo + ']'):
545                     enabled[repo] = True
546                     break
547
548         # For each repository, add it's definition if it is not enabled
549         # Remove antergos repo definition, and add Reborn-OS repo definition (Rafael)
550         for repo in repositories:
551             if not enabled[repo]:
552                 logging.debug("Adding %s repository to /etc/pacman.conf", repo)
553                 with misc.raised_privileges():
554                     with open("/etc/pacman.conf", 'at') as pconf:
555                         pconf.write("[{}]\n".format(repo))
556                         if repo == 'Reborn-OS':
557                             pconf.write("SigLevel = Optional TrustAll\n")
558                             pconf.write("Include = /etc/pacman.d/reborn-mirrorlist\n\n")
559                         else:
560                             pconf.write("Include = /etc/pacman.d/mirrorlist\n\n")
561
562     def disable_suspend(self):
563         """ Disable gnome settings suspend to ram """
564         """ Change logging warning. Original: 'User "antergos" does not exist'  """
565         try:
566             pwd.getpwnam('antergos')
567             schema = 'org.gnome.settings-daemon.plugins.power'
568             keys = ['sleep-inactive-ac-type', 'sleep-inactive-battery-type']
569             value = 'nothing'
570             for key in keys:
571                 self.gsettings_set('antergos', schema, key, value)
572         except KeyError:
573             logging.warning('User Rebornos exist')
574
575     @staticmethod
576     def gsettings_set(user, schema, key, value):
577         """ Set a gnome setting """
578         cmd = [
579             'runuser',
580             '-l', user,
581             '-c', "dbus-launch gsettings set " + schema + " " + key + " " + value]
582
583         logging.debug("Running set on gsettings: %s", ''.join(str(e) + ' ' for e in cmd))
584         with misc.raised_privileges():
585             return call(cmd)
586
587 def main():
588     """ Main function. Initializes Cnchi and creates it as a GTK App """
589     # Init cnchi
590     cnchi_init = CnchiInit()
591     # Create Gtk Application
592     my_app = CnchiApp(cnchi_init.cmd_line)
593     status = my_app.run(None)
594     sys.exit(status)
595
596 if __name__ == '__main__':
597     main()