/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 4 -*- * * Copyright (C) 2011 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Authored by: Robert Ancell */ public const int grid_size = 40; public class ArcticaGreeter { public static ArcticaGreeter singleton; public signal void show_message (string text, LightDM.MessageType type); public signal void show_prompt (string text, LightDM.PromptType type); public signal void authentication_complete (); public signal void starting_session (); public bool test_mode = false; private string state_file; private KeyFile state; private Cairo.XlibSurface background_surface; private SettingsDaemon settings_daemon; public bool orca_needs_kick; private MainWindow main_window; private LightDM.Greeter greeter; private Canberra.Context canberra_context; private static Timer log_timer; private DialogDBusInterface dbus_object; private SettingsDaemonDBusInterface settings_daemon_proxy; public signal void xsettings_ready (); public signal void greeter_ready (); private ArcticaGreeter (bool test_mode_) { singleton = this; test_mode = test_mode_; greeter = new LightDM.Greeter (); greeter.show_message.connect ((text, type) => { show_message (text, type); }); greeter.show_prompt.connect ((text, type) => { show_prompt (text, type); }); greeter.autologin_timer_expired.connect (() => { try { greeter.authenticate_autologin (); } catch (Error e) { warning ("Failed to autologin authenticate: %s", e.message); } }); greeter.authentication_complete.connect (() => { authentication_complete (); }); var connected = false; try { connected = greeter.connect_to_daemon_sync (); } catch (Error e) { warning ("Failed to connect to LightDM daemon: %s", e.message); } if (!connected && !test_mode) Posix.exit (Posix.EXIT_FAILURE); if (!test_mode) { settings_daemon = new SettingsDaemon (); settings_daemon.start (); } var state_dir = Path.build_filename (Environment.get_user_cache_dir (), "arctica-greeter"); DirUtils.create_with_parents (state_dir, 0775); var xdg_seat = GLib.Environment.get_variable("XDG_SEAT"); var state_file_name = xdg_seat != null && xdg_seat != "seat0" ? xdg_seat + "-state" : "state"; state_file = Path.build_filename (state_dir, state_file_name); state = new KeyFile (); try { state.load_from_file (state_file, KeyFileFlags.NONE); } catch (Error e) { if (!(e is FileError.NOENT)) warning ("Failed to load state from %s: %s\n", state_file, e.message); } if (!test_mode) { /* Render things after xsettings is ready */ xsettings_ready.connect ( xsettings_ready_cb ); GLib.Bus.watch_name (BusType.SESSION, "org.mate.SettingsDaemon", BusNameWatcherFlags.NONE, (c, name, owner) => { try { settings_daemon_proxy = GLib.Bus.get_proxy_sync ( BusType.SESSION, "org.mate.SettingsDaemon", "/org/mate/SettingsDaemon"); settings_daemon_proxy.plugin_activated.connect ( (name) => { if (name == "xsettings") { debug ("xsettings is ready"); xsettings_ready (); } } ); } catch (Error e) { debug ("Failed to get USD proxy, proceed anyway"); xsettings_ready (); } }, null); } else xsettings_ready_cb (); } public string? get_state (string key) { try { return state.get_value ("greeter", key); } catch (Error e) { return null; } } public void set_state (string key, string value) { state.set_value ("greeter", key, value); var data = state.to_data (); try { FileUtils.set_contents (state_file, data); } catch (Error e) { debug ("Failed to write state: %s", e.message); } } public void push_list (GreeterList widget) { main_window.push_list (widget); } public void pop_list () { main_window.pop_list (); } public static void add_style_class (Gtk.Widget widget) { /* Add style context class lightdm-user-list */ var ctx = widget.get_style_context (); ctx.add_class ("lightdm"); } public static string? get_default_session () { var sessions = new List (); sessions.append ("lightdm-xsession"); // FIXME: this list should be obtained from AGSettings, ideally... sessions.append ("mate"); sessions.append ("xfce"); sessions.append ("kde-plasma"); sessions.append ("kde"); sessions.append ("gnome"); sessions.append ("cinnamon"); foreach (string session in sessions) { var path = Path.build_filename ("/usr/share/xsessions/", session.concat(".desktop"), null); if (FileUtils.test (path, FileTest.EXISTS)) { return session; } } warning ("Could not find a default session."); return null; } public static string validate_session (string? session) { /* Make sure the given session actually exists. Return it if it does. * otherwise, return the default session. */ if (session != null) { var path = Path.build_filename ("/usr/share/xsessions/", session.concat(".desktop"), null); if (!FileUtils.test (path, FileTest.EXISTS) ) { debug ("Invalid session: '%s'", session); session = null; } } if (session == null) { var default_session = ArcticaGreeter.get_default_session (); debug ("Invalid session: '%s'. Using session '%s' instead.", session, default_session); return default_session; } return session; } public bool start_session (string? session, Background bg) { /* Explicitly set the right scale before closing window */ var screen = Gdk.Screen.get_default (); var display = Gdk.Display.get_default(); var monitor = display.get_primary_monitor(); var scale = monitor.get_scale_factor (); background_surface.set_device_scale (scale, scale); /* Paint our background onto the root window before we close our own window */ var c = new Cairo.Context (background_surface); bg.draw_full (c, Background.DrawFlags.NONE); c = null; refresh_background (screen, background_surface); main_window.before_session_start(); if (test_mode) { debug ("Successfully logged in! Quitting..."); Gtk.main_quit (); return true; } if (!session_is_valid (session)) { debug ("Session %s is not available, using system default %s instead", session, greeter.default_session_hint); session = greeter.default_session_hint; } var result = false; try { result = LightDM.greeter_start_session_sync (greeter, session); } catch (Error e) { warning ("Failed to start session: %s", e.message); } if (result) starting_session (); return result; } private bool session_is_valid (string? session) { if (session == null) return true; foreach (var s in LightDM.get_sessions ()) if (s.key == session) return true; return false; } private bool ready_cb () { debug ("starting system-ready sound"); /* Launch canberra */ Canberra.Context.create (out canberra_context); if (AGSettings.get_boolean (AGSettings.KEY_PLAY_READY_SOUND)) canberra_context.play (0, Canberra.PROP_CANBERRA_XDG_THEME_NAME, "arctica-greeter", Canberra.PROP_EVENT_ID, "system-ready"); return false; } public void show () { debug ("Showing main window"); main_window.show (); main_window.get_window ().focus (Gdk.CURRENT_TIME); main_window.set_keyboard_state (); } public bool is_authenticated () { return greeter.is_authenticated; } public void authenticate (string? userid = null) { try { greeter.authenticate (userid); } catch (Error e) { warning ("Failed to authenticate: %s", e.message); } } public void authenticate_as_guest () { try { greeter.authenticate_as_guest (); } catch (Error e) { warning ("Failed to authenticate as guest: %s", e.message); } } public void authenticate_remote (string? session, string? userid) { try { ArcticaGreeter.singleton.greeter.authenticate_remote (session, userid); } catch (Error e) { warning ("Failed to remote authenticate: %s", e.message); } } public void cancel_authentication () { try { greeter.cancel_authentication (); } catch (Error e) { warning ("Failed to cancel authentication: %s", e.message); } } public void respond (string response) { try { greeter.respond (response); } catch (Error e) { warning ("Failed to respond: %s", e.message); } } public string authentication_user () { return greeter.authentication_user; } public string default_session_hint () { return greeter.default_session_hint; } public string select_user_hint () { return greeter.select_user_hint; } public bool show_manual_login_hint () { return greeter.show_manual_login_hint; } public bool show_remote_login_hint () { return greeter.show_remote_login_hint; } public bool hide_users_hint () { return greeter.hide_users_hint; } public bool has_guest_account_hint () { return greeter.has_guest_account_hint; } private Gdk.FilterReturn focus_upon_map (Gdk.XEvent gxevent, Gdk.Event event) { var xevent = (X.Event*)gxevent; if (xevent.type == X.EventType.MapNotify) { var display = Gdk.X11.Display.lookup_for_xdisplay (xevent.xmap.display); var xwin = xevent.xmap.window; var win = new Gdk.X11.Window.foreign_for_display (display, xwin); if (win != null && !xevent.xmap.override_redirect) { /* Check to see if this window is our onboard window, since we don't want to focus it. */ X.Window keyboard_xid = 0; if (main_window.menubar.keyboard_window != null) keyboard_xid = (main_window.menubar.keyboard_window.get_window () as Gdk.X11.Window).get_xid (); if (xwin != keyboard_xid && win.get_type_hint() != Gdk.WindowTypeHint.NOTIFICATION) { win.focus (Gdk.CURRENT_TIME); /* Make sure to keep keyboard above */ if (main_window.menubar.keyboard_window != null) main_window.menubar.keyboard_window.get_window ().raise (); } } } else if (xevent.type == X.EventType.UnmapNotify) { // Since we aren't keeping track of focus (for example, we don't // track the Z stack of windows) like a normal WM would, when we // decide here where to return focus after another window unmaps, // we don't have much to go on. X will tell us if we should take // focus back. (I could not find an obvious way to determine this, // but checking if the X input focus is RevertTo.None seems // reliable.) X.Window xwin; int revert_to; xevent.xunmap.display.get_input_focus (out xwin, out revert_to); if (revert_to == X.RevertTo.None) { main_window.get_window ().focus (Gdk.CURRENT_TIME); /* Make sure to keep keyboard above */ if (main_window.menubar.keyboard_window != null) main_window.menubar.keyboard_window.get_window ().raise (); } } return Gdk.FilterReturn.CONTINUE; } private void start_fake_wm () { /* We want new windows (e.g. the shutdown dialog) to gain focus. We don't really need anything more than that (don't need alt-tab since any dialog should be "modal" or at least dealt with before continuing even if not actually marked as modal) */ var root = Gdk.get_default_root_window (); root.set_events (root.get_events () | Gdk.EventMask.SUBSTRUCTURE_MASK); root.add_filter (focus_upon_map); } private void kill_fake_wm () { var root = Gdk.get_default_root_window (); root.remove_filter (focus_upon_map); } private static Cairo.XlibSurface? create_root_surface (Gdk.Screen screen) { var visual = screen.get_system_visual (); unowned X.Display display = (screen.get_display () as Gdk.X11.Display).get_xdisplay (); unowned X.Screen xscreen = (screen as Gdk.X11.Screen).get_xscreen (); var pixmap = X.CreatePixmap (display, (screen.get_root_window () as Gdk.X11.Window).get_xid (), xscreen.width_of_screen (), xscreen.height_of_screen (), visual.get_depth ()); /* Convert into a Cairo surface */ var surface = new Cairo.XlibSurface (display, pixmap, (visual as Gdk.X11.Visual).get_xvisual (), xscreen.width_of_screen (), xscreen.height_of_screen ()); return surface; } private static void refresh_background (Gdk.Screen screen, Cairo.XlibSurface surface) { Gdk.flush (); unowned X.Display display = (screen.get_display () as Gdk.X11.Display).get_xdisplay (); /* Ensure Cairo has actually finished its drawing */ surface.flush (); /* Use this pixmap for the background */ X.SetWindowBackgroundPixmap (display, (screen.get_root_window () as Gdk.X11.Window).get_xid (), surface.get_drawable ()); X.ClearWindow (display, (screen.get_root_window () as Gdk.X11.Window).get_xid ()); } private static void log_cb (string? log_domain, LogLevelFlags log_level, string message) { string prefix; switch (log_level & LogLevelFlags.LEVEL_MASK) { case LogLevelFlags.LEVEL_ERROR: prefix = "ERROR:"; break; case LogLevelFlags.LEVEL_CRITICAL: prefix = "CRITICAL:"; break; case LogLevelFlags.LEVEL_WARNING: prefix = "WARNING:"; break; case LogLevelFlags.LEVEL_MESSAGE: prefix = "MESSAGE:"; break; case LogLevelFlags.LEVEL_INFO: prefix = "INFO:"; break; case LogLevelFlags.LEVEL_DEBUG: prefix = "DEBUG:"; break; default: prefix = "LOG:"; break; } stderr.printf ("[%+.2fs] %s %s\n", log_timer.elapsed (), prefix, message); } private void xsettings_ready_cb () { /* Prepare to set the background */ debug ("Creating background surface"); background_surface = create_root_surface (Gdk.Screen.get_default ()); main_window = new MainWindow (); main_window.destroy.connect(() => { kill_fake_wm (); }); main_window.delete_event.connect(() => { Gtk.main_quit(); return false; }); Bus.own_name (BusType.SESSION, "org.ayatana.Greeter", BusNameOwnerFlags.NONE); dbus_object = new DialogDBusInterface (); dbus_object.open_dialog.connect ((type) => { ShutdownDialogType dialog_type; switch (type) { default: case 1: dialog_type = ShutdownDialogType.SHUTDOWN; break; case 2: dialog_type = ShutdownDialogType.RESTART; break; } main_window.show_shutdown_dialog (dialog_type); }); dbus_object.close_dialog.connect ((type) => { main_window.close_shutdown_dialog (); }); Bus.own_name (BusType.SESSION, "org.ayatana.Desktop", BusNameOwnerFlags.NONE, (c) => { try { c.register_object ("/org/gnome/SessionManager/EndSessionDialog", dbus_object); } catch (Error e) { warning ("Failed to register /org/gnome/SessionManager/EndSessionDialog: %s", e.message); } }, null, () => debug ("Failed to acquire name org.ayatana.Desktop")); start_fake_wm (); Gdk.threads_add_idle (ready_cb); greeter_ready (); } public static int main (string[] args) { /* Protect memory from being paged to disk, as we deal with passwords */ Posix.mlockall (Posix.MCL_CURRENT | Posix.MCL_FUTURE); /* Disable the stupid global menubar */ Environment.unset_variable ("UBUNTU_MENUPROXY"); /* Initialize i18n */ Intl.setlocale (LocaleCategory.ALL, ""); Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR); Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8"); Intl.textdomain (Config.GETTEXT_PACKAGE); /* Set up the accessibility stack, in case the user needs it for screen reading etc. */ Environment.set_variable ("GTK_MODULES", "atk-bridge", false); Pid atspi_pid = 0; try { string[] argv = null; if (FileUtils.test ("/usr/lib/at-spi2-core/at-spi-bus-launcher", FileTest.EXISTS)) { // Debian & derivatives... Shell.parse_argv ("/usr/lib/at-spi2-core/at-spi-bus-launcher --launch-immediately", out argv); } else if (FileUtils.test ("/usr/libexec/at-spi-bus-launcher", FileTest.EXISTS)) { // Fedora & derivatives... Shell.parse_argv ("/usr/libexec/at-spi-bus-launcher --launch-immediately", out argv); } if (argv != null) Process.spawn_async (null, argv, null, SpawnFlags.SEARCH_PATH, null, out atspi_pid); } catch (Error e) { warning ("Error starting the at-spi registry: %s", e.message); } Gtk.init (ref args); Ido.init (); log_timer = new Timer (); Log.set_default_handler (log_cb); debug ("Starting arctica-greeter %s UID=%d LANG=%s", Config.VERSION, (int) Posix.getuid (), Environment.get_variable ("LANG")); /* Set the cursor to not be the crap default */ debug ("Setting cursor"); Gdk.get_default_root_window ().set_cursor (new Gdk.Cursor.for_display (Gdk.Display.get_default (), Gdk.CursorType.LEFT_PTR)); bool do_show_version = false; bool do_test_mode = false; OptionEntry versionOption = { "version", 'v', 0, OptionArg.NONE, ref do_show_version, /* Help string for command line --version flag */ N_("Show release version"), null }; OptionEntry testOption = { "test-mode", 0, 0, OptionArg.NONE, ref do_test_mode, /* Help string for command line --test-mode flag */ N_("Run in test mode"), null }; OptionEntry nullOption = { null }; OptionEntry[] options = { versionOption, testOption, nullOption }; debug ("Loading command line options"); var c = new OptionContext (/* Arguments and description for --help text */ _("- Arctica Greeter")); c.add_main_entries (options, Config.GETTEXT_PACKAGE); c.add_group (Gtk.get_option_group (true)); try { c.parse (ref args); } catch (Error e) { stderr.printf ("%s\n", e.message); stderr.printf (/* Text printed out when an unknown command-line argument provided */ _("Run '%s --help' to see a full list of available command line options."), args[0]); stderr.printf ("\n"); return Posix.EXIT_FAILURE; } if (do_show_version) { /* Note, not translated so can be easily parsed */ stderr.printf ("arctica-greeter %s\n", Config.VERSION); return Posix.EXIT_SUCCESS; } if (do_test_mode) debug ("Running in test mode"); /* Set GTK+ settings */ debug ("Setting GTK+ settings"); var settings = Gtk.Settings.get_default (); var value = AGSettings.get_string (AGSettings.KEY_THEME_NAME); if (value != "") settings.set ("gtk-theme-name", value, null); value = AGSettings.get_string (AGSettings.KEY_ICON_THEME_NAME); if (value != "") settings.set ("gtk-icon-theme-name", value, null); value = AGSettings.get_string (AGSettings.KEY_FONT_NAME); if (value != "") settings.set ("gtk-font-name", value, null); var double_value = AGSettings.get_double (AGSettings.KEY_XFT_DPI); if (double_value != 0.0) settings.set ("gtk-xft-dpi", (int) (1024 * double_value), null); var boolean_value = AGSettings.get_boolean (AGSettings.KEY_XFT_ANTIALIAS); settings.set ("gtk-xft-antialias", boolean_value, null); value = AGSettings.get_string (AGSettings.KEY_XFT_HINTSTYLE); if (value != "") settings.set ("gtk-xft-hintstyle", value, null); value = AGSettings.get_string (AGSettings.KEY_XFT_RGBA); if (value != "") settings.set ("gtk-xft-rgba", value, null); debug ("Creating Arctica Greeter"); var greeter = new ArcticaGreeter (do_test_mode); string systemd_stderr; int systemd_exitcode = 0; Pid nmapplet_pid = 0; var indicator_list = AGSettings.get_strv(AGSettings.KEY_INDICATORS); var update_indicator_list = false; for (var i = 0; i < indicator_list.length; i++) { if (indicator_list[i] == "ug-keyboard") { indicator_list[i] = "org.ayatana.indicator.keyboard"; update_indicator_list = true; } } if (update_indicator_list) AGSettings.set_strv(AGSettings.KEY_INDICATORS, indicator_list); var launched_indicator_services = new List(); if (!do_test_mode) { greeter.greeter_ready.connect (() => { debug ("Showing greeter"); greeter.show (); }); var indicator_service = ""; foreach (unowned string indicator in indicator_list) { if ("ug-" in indicator && ! ("." in indicator)) continue; if ("org.ayatana.indicator." in indicator) indicator_service = "ayatana-indicator-%s".printf(indicator.split_set(".")[3]); else if ("ayatana-" in indicator) indicator_service = "ayatana-indicator-%s".printf(indicator.split_set("-")[1]); else indicator_service = indicator; try { /* Start the indicator service */ string[] argv; Shell.parse_argv ("systemctl --user start %s".printf(indicator_service), out argv); Process.spawn_sync (null, argv, null, SpawnFlags.SEARCH_PATH, null, null, out systemd_stderr, out systemd_exitcode); if (systemd_exitcode == 0) { launched_indicator_services.append(indicator_service); debug ("Successfully started Indicator Service '%s'", indicator_service); } else { warning ("Systemd failed to start Indicator Service '%s': %s", indicator_service, systemd_stderr); } } catch (Error e) { warning ("Error starting Indicator Service '%s': %s", indicator_service, e.message); } } /* Make nm-applet hide items the user does not have permissions to interact with */ Environment.set_variable ("NM_APPLET_HIDE_POLICY_ITEMS", "1", true); try { string[] argv; Shell.parse_argv ("nm-applet --indicator", out argv); Process.spawn_async (null, argv, null, SpawnFlags.SEARCH_PATH, null, out nmapplet_pid); } catch (Error e) { warning ("Error starting the Network Manager Applet: %s", e.message); } } else greeter.show (); /* Setup a handler for TERM so we quit cleanly */ GLib.Unix.signal_add(GLib.ProcessSignal.TERM, () => { debug("Got a SIGTERM"); Gtk.main_quit(); return true; }); debug ("Starting main loop"); Gtk.main (); debug ("Cleaning up"); if (!do_test_mode) { foreach (unowned string indicator_service in launched_indicator_services) { try { /* Stop this indicator service */ string[] argv; Shell.parse_argv ("systemctl --user stop %s".printf(indicator_service), out argv); Process.spawn_sync (null, argv, null, SpawnFlags.SEARCH_PATH, null, null, out systemd_stderr, out systemd_exitcode); if (systemd_exitcode == 0) { debug ("Successfully stopped Indicator Service '%s' via systemd", indicator_service); } else { warning ("Systemd failed to stop Indicator Service '%s': %s", indicator_service, systemd_stderr); } } catch (Error e) { warning ("Error stopping Indicator Service '%s': %s", indicator_service, e.message); } } } greeter.settings_daemon.stop(); if (nmapplet_pid != 0) { Posix.kill (nmapplet_pid, Posix.SIGTERM); int status; Posix.waitpid (nmapplet_pid, out status, 0); if (Process.if_exited (status)) debug ("Network Manager Applet exited with return value %d", Process.exit_status (status)); else debug ("Network Manager Applet terminated with signal %d", Process.term_sig (status)); nmapplet_pid = 0; } if (atspi_pid != 0) { Posix.kill (atspi_pid, Posix.SIGKILL); int status; Posix.waitpid (atspi_pid, out status, 0); if (Process.if_exited (status)) debug ("AT-SPI exited with return value %d", Process.exit_status (status)); else debug ("AT-SPI terminated with signal %d", Process.term_sig (status)); atspi_pid = 0; } debug ("Exiting"); return Posix.EXIT_SUCCESS; } } [DBus (name="org.gnome.SessionManager.EndSessionDialog")] public class DialogDBusInterface : Object { public signal void open_dialog (uint32 type); public signal void close_dialog (); public void open (uint32 type, uint32 timestamp, uint32 seconds_to_stay_open, ObjectPath[] inhibitor_object_paths) { open_dialog (type); } public void close () { close_dialog (); } } [DBus (name="org.mate.SettingsDaemon")] private interface SettingsDaemonDBusInterface : Object { public signal void plugin_activated (string name); public signal void plugin_deactivated (string name); }