2 # -*- coding: utf-8 -*-
6 # Copyright © 2013-2019 RebornOS
8 # This file is part of Cnchi.
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.
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.
20 # The following additional terms are in effect as per Section 7 of the license:
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.
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/>.
29 """ Main Cnchi (Antergos Installer) module """
35 import logging.handlers
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"))
53 gi.require_version('Gtk', '3.0')
54 from gi.repository import Gio, Gtk, GObject
56 import misc.extra as misc
57 from misc.run_cmd import call
58 import show_message as show
61 from logging_utils import ContextFilter
65 # from bugsnag.handlers import BugsnagHandler
67 # BUGSNAG_ERROR = None
68 #except ImportError as err:
69 # BUGSNAG_ERROR = str(err)
71 BUGSNAG_ERROR = "Bugsnag disabled. Makes requests raise several exceptions. Need to check what is wrong"
73 # When testing, no _() is available
76 except NameError as err:
81 class CnchiApp(Gtk.Application):
82 """ Main Cnchi App class """
84 TEMP_FOLDER = '/var/tmp/cnchi'
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
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. """
100 except ImportError as err:
101 msg = "Cannot create Cnchi main window: {0}".format(err)
105 # Check if we have administrative privileges
107 msg = _('This installer must be run with administrative privileges, '
108 'and cannot continue without them.')
109 show.error(None, msg)
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)
122 window = main_window.MainWindow(self, self.cmd_line)
123 self.add_window(window)
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())
131 except PermissionError as err:
133 show.error(None, err)
135 # This is unnecessary as show_all is called in MainWindow
138 # def do_startup(self):
139 # """ Override the 'startup' signal of GLib.Application. """
140 # Gtk.Application.do_startup(self)
142 # Application main menu (we don't need one atm)
143 # Leaving this here for future reference
145 # menu.append("About", "win.about")
146 # menu.append("Quit", "app.quit")
147 # self.set_app_menu(menu)
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()
157 pid = int(lines[1].strip('\n'))
158 except ValueError as err:
160 logging.debug("Cannot read PID value.")
163 logging.debug("Cannot read PID value.")
166 if misc.check_pid(pid):
167 logging.info("Cnchi with pid '%d' already running.", pid)
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)
176 """ Initializes Cnchi """
178 # Useful vars for gettext (translations)
180 LOCALE_DIR = "/usr/share/locale"
182 # At least this GTK version is needed
183 GTK_VERSION_NEEDED = "3.18.0"
185 LOG_FOLDER = '/var/log/cnchi'
186 TEMP_FOLDER = '/var/tmp/cnchi'
189 """ This function initialises Cnchi """
191 # Sets SIGTERM handler, so Cnchi can clean up before exiting
192 # signal.signal(signal.SIGTERM, sigterm_handler)
194 # Create Cnchi's temporary folder
195 with misc.raised_privileges():
196 os.makedirs(CnchiInit.TEMP_FOLDER, mode=0o755, exist_ok=True)
198 # Configures gettext to be able to translate messages, using _()
201 # Command line options
202 self.cmd_line = self.parse_options()
204 if self.cmd_line.version:
205 print(_("Cnchi (RebornOS Installer) version {0}").format(
209 if self.cmd_line.force:
210 misc.remove_temp_files(CnchiInit.TEMP_FOLDER)
212 # Drop root privileges
213 misc.drop_privileges()
215 # Setup our logging framework
218 # Enables needed repositories only if it's not enabled
219 self.enable_repositories()
221 # Check that all repositories are present in pacman.conf file
222 if not self.check_pacman_conf("/etc/pacman.conf"):
225 # Check Cnchi is correctly installed
226 if not self.check_for_files():
229 # Check installed GTK version
230 if not self.check_gtk_version():
233 # Check installed pyalpm and libalpm versions
234 if not self.check_pyalpm_version():
237 # Check ISO version where Cnchi is running from
238 if not self.check_iso_version():
241 # Disable suspend to RAM
242 self.disable_suspend()
244 # Init PyObject Threads
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) """
252 with open(path, 'rt') as pacman:
253 lines = pacman.readlines()
256 "[Reborn-OS]", "[core]", "[extra]", "[community]", "[multilib]"]
259 line = line.strip('\n')
264 logging.error("Repository %s not in pacman.conf file", repo)
267 logging.debug("All repositories are present in pacman.conf file")
270 def setup_logging(self):
271 """ Configure our logger """
273 with misc.raised_privileges():
274 os.makedirs(CnchiInit.LOG_FOLDER, mode=0o755, exist_ok=True)
276 logger = logging.getLogger()
280 if self.cmd_line.debug:
281 log_level = logging.DEBUG
283 log_level = logging.INFO
285 logger.setLevel(log_level)
287 context_filter = ContextFilter()
288 logger.addFilter(context_filter.filter)
290 datefmt = "%Y-%m-%d %H:%M:%S"
292 fmt = "%(asctime)s [%(levelname)-7s] %(message)s (%(filename)s:%(lineno)d)"
293 formatter = logging.Formatter(fmt, datefmt)
296 "%(asctime)s [%(levelname)-18s] %(message)s "
297 "($BOLD%(filename)s$RESET:%(lineno)d)")
298 color_formatter = logging_color.ColoredFormatter(color_fmt, datefmt)
301 log_path = os.path.join(CnchiInit.LOG_FOLDER, 'cnchi.log')
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)
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)
319 if not BUGSNAG_ERROR:
321 bugsnag_api = context_filter.api_key
322 if bugsnag_api is not None:
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)
336 "Sending Cnchi log messages to bugsnag server (using python-bugsnag).")
339 "Cannot read the bugsnag api key, logging to bugsnag is not possible.")
341 logging.warning(BUGSNAG_ERROR)
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])
351 # Check system GTK Version
352 major = Gtk.get_major_version()
353 minor = Gtk.get_minor_version()
354 micro = Gtk.get_micro_version()
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
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)
372 show.error(None, text)
373 except ImportError as import_error:
374 logging.error(import_error)
376 logging.info("Using GTK v%d.%d.%d", major, minor, micro)
381 def check_pyalpm_version():
382 """ Checks python alpm binding and alpm library versions """
386 txt = "Using pyalpm v{0} as interface to libalpm v{1}"
387 txt = txt.format(pyalpm.version(), pyalpm.alpmversion())
389 except (NameError, ImportError) as err:
391 show.error(None, err)
393 except ImportError as import_error:
394 logging.error(import_error)
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
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)]
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
422 logging.warning("Not running from ISO")
427 """ argparse http://docs.python.org/3/howto/argparse.html """
431 desc = _("Cnchi v{0} - RebornOS Installer").format(info.CNCHI_VERSION)
432 parser = argparse.ArgumentParser(description=desc)
435 "-a", "--a11y", help=_("Set accessibility feature on by default"),
438 "-c", "--cache", help=_("Use pre-downloaded xz packages when possible"),
441 "-d", "--debug", help=_("Sets Cnchi log level to 'debug'"),
444 "-e", "--environment", help=_("Sets the Desktop Environment that will be installed"),
447 "-f", "--force", help=_("Runs cnchi even when another instance is running"),
450 "-n", "--no-check", help=_("Makes checks optional in check screen"),
453 "-p", "--packagelist", help=_("Install packages referenced by a local XML file"),
456 "-s", "--logserver", help=_("Log server (deprecated, always uses bugsnag)"),
459 "-t", "--no-tryit", help=_("Disables first screen's 'try it' option"),
462 "-v", "--verbose", help=_("Show logging messages to stdout"),
465 "-V", "--version", help=_("Show Cnchi version and quit"),
468 "-z", "--hidden", help=_("Show options in development (use at your own risk!)"),
471 return parser.parse_args()
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().
483 minor = Gtk.get_minor_version()
484 micro = Gtk.get_micro_version()
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
491 threading.Thread(target=lambda: None).start()
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()
501 """ This allows to translate all py texts (not the glade ones) """
503 gettext.textdomain(CnchiInit.APP_NAME)
504 gettext.bindtextdomain(CnchiInit.APP_NAME, CnchiInit.LOCALE_DIR)
506 locale_code, _encoding = locale.getdefaultlocale()
507 lang = gettext.translation(
508 CnchiInit.APP_NAME, CnchiInit.LOCALE_DIR, [locale_code], None, True)
513 def check_for_files():
514 """ Check for some necessary files. Cnchi can't run without them """
517 "/usr/share/cnchi/ui",
518 "/usr/share/cnchi/data",
519 "/usr/share/cnchi/data/locale"]
522 if not os.path.exists(path):
523 print(_("Cnchi files not found. Please, install Cnchi using pacman"))
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) """
533 repositories = ['Reborn-OS', 'core', 'extra', 'community', 'multilib']
535 # Read pacman.conf file
536 with open("/etc/pacman.conf", 'rt') as pconf:
537 lines = pconf.readlines()
539 # For each repository, check if it is enabled or not
541 for repo in repositories:
542 enabled[repo] = False
544 if line.startswith('[' + repo + ']'):
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")
560 pconf.write("Include = /etc/pacman.d/mirrorlist\n\n")
562 def disable_suspend(self):
563 """ Disable gnome settings suspend to ram """
564 """ Change logging warning. Original: 'User "antergos" does not exist' """
566 pwd.getpwnam('antergos')
567 schema = 'org.gnome.settings-daemon.plugins.power'
568 keys = ['sleep-inactive-ac-type', 'sleep-inactive-battery-type']
571 self.gsettings_set('antergos', schema, key, value)
573 logging.warning('User Rebornos exist')
576 def gsettings_set(user, schema, key, value):
577 """ Set a gnome setting """
581 '-c', "dbus-launch gsettings set " + schema + " " + key + " " + value]
583 logging.debug("Running set on gsettings: %s", ''.join(str(e) + ' ' for e in cmd))
584 with misc.raised_privileges():
588 """ Main function. Initializes Cnchi and creates it as a GTK App """
590 cnchi_init = CnchiInit()
591 # Create Gtk Application
592 my_app = CnchiApp(cnchi_init.cmd_line)
593 status = my_app.run(None)
596 if __name__ == '__main__':