/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ #include "config.h" #include #include #include #include #include #include #include #include #ifdef HAVE_SYS_RESOURCE_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define GNOME_DESKTOP_USE_UNSTABLE_API #include #if defined __OpenBSD__ || defined __FreeBSD__ #include #endif #include "shell-enum-types.h" #include "shell-global-private.h" #include "shell-perf-log.h" #include "shell-window-tracker.h" #include "shell-wm.h" #include "shell-util.h" #include "st.h" #include "switcheroo-control.h" static ShellGlobal *the_object = NULL; struct _ShellGlobal { GObject parent; ClutterStage *stage; MetaBackend *backend; MetaContext *meta_context; MetaDisplay *meta_display; MetaWorkspaceManager *workspace_manager; Display *xdisplay; char *session_mode; XserverRegion input_region; GjsContext *js_context; MetaPlugin *plugin; ShellWM *wm; GSettings *settings; const char *datadir; char *imagedir; char *userdatadir; GFile *userdatadir_path; GFile *runtime_state_path; StFocusManager *focus_manager; guint work_count; GSList *leisure_closures; guint leisure_function_id; GHashTable *save_ops; gboolean frame_timestamps; gboolean frame_finish_timestamp; GDBusProxy *switcheroo_control; GCancellable *switcheroo_cancellable; }; enum { PROP_0, PROP_SESSION_MODE, PROP_BACKEND, PROP_CONTEXT, PROP_DISPLAY, PROP_WORKSPACE_MANAGER, PROP_SCREEN_WIDTH, PROP_SCREEN_HEIGHT, PROP_STAGE, PROP_WINDOW_GROUP, PROP_TOP_WINDOW_GROUP, PROP_WINDOW_MANAGER, PROP_SETTINGS, PROP_DATADIR, PROP_IMAGEDIR, PROP_USERDATADIR, PROP_FOCUS_MANAGER, PROP_FRAME_TIMESTAMPS, PROP_FRAME_FINISH_TIMESTAMP, PROP_SWITCHEROO_CONTROL, N_PROPS }; static GParamSpec *props[N_PROPS] = { NULL, }; /* Signals */ enum { NOTIFY_ERROR, LOCATE_POINTER, LAST_SIGNAL }; G_DEFINE_TYPE(ShellGlobal, shell_global, G_TYPE_OBJECT); static guint shell_global_signals [LAST_SIGNAL] = { 0 }; static void got_switcheroo_control_gpus_property_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) { ShellGlobal *global; GError *error = NULL; GVariant *gpus; gpus = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &error); if (!gpus) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_debug ("Could not get GPUs property from switcheroo-control: %s", error->message); g_clear_error (&error); return; } global = user_data; g_dbus_proxy_set_cached_property (global->switcheroo_control, "GPUs", gpus); g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SWITCHEROO_CONTROL]); } static void switcheroo_control_ready_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) { ShellGlobal *global; GError *error = NULL; ShellNetHadessSwitcherooControl *control; g_auto(GStrv) cached_props = NULL; control = shell_net_hadess_switcheroo_control_proxy_new_for_bus_finish (res, &error); if (!control) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_debug ("Could not get switcheroo-control GDBusProxy: %s", error->message); g_clear_error (&error); return; } global = user_data; global->switcheroo_control = G_DBUS_PROXY (control); g_debug ("Got switcheroo-control proxy successfully"); cached_props = g_dbus_proxy_get_cached_property_names (global->switcheroo_control); if (cached_props != NULL && g_strv_contains ((const gchar * const *) cached_props, "GPUs")) { g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SWITCHEROO_CONTROL]); return; } /* Delay property notification until we have all the properties gathered */ g_dbus_connection_call (g_dbus_proxy_get_connection (global->switcheroo_control), g_dbus_proxy_get_name (global->switcheroo_control), g_dbus_proxy_get_object_path (global->switcheroo_control), "org.freedesktop.DBus.Properties", "Get", g_variant_new ("(ss)", g_dbus_proxy_get_interface_name (global->switcheroo_control), "GPUs"), NULL, G_DBUS_CALL_FLAGS_NONE, -1, global->switcheroo_cancellable, got_switcheroo_control_gpus_property_cb, user_data); } static void shell_global_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { ShellGlobal *global = SHELL_GLOBAL (object); switch (prop_id) { case PROP_SESSION_MODE: g_clear_pointer (&global->session_mode, g_free); global->session_mode = g_ascii_strdown (g_value_get_string (value), -1); break; case PROP_FRAME_TIMESTAMPS: { gboolean enable = g_value_get_boolean (value); if (global->frame_timestamps != enable) { global->frame_timestamps = enable; g_object_notify_by_pspec (object, props[PROP_FRAME_TIMESTAMPS]); } } break; case PROP_FRAME_FINISH_TIMESTAMP: { gboolean enable = g_value_get_boolean (value); if (global->frame_finish_timestamp != enable) { global->frame_finish_timestamp = enable; g_object_notify_by_pspec (object, props[PROP_FRAME_FINISH_TIMESTAMP]); } } break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void shell_global_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { ShellGlobal *global = SHELL_GLOBAL (object); switch (prop_id) { case PROP_SESSION_MODE: g_value_set_string (value, shell_global_get_session_mode (global)); break; case PROP_BACKEND: g_value_set_object (value, global->backend); break; case PROP_CONTEXT: g_value_set_object (value, global->meta_context); break; case PROP_DISPLAY: g_value_set_object (value, global->meta_display); break; case PROP_WORKSPACE_MANAGER: g_value_set_object (value, global->workspace_manager); break; case PROP_SCREEN_WIDTH: { int width, height; meta_display_get_size (global->meta_display, &width, &height); g_value_set_int (value, width); } break; case PROP_SCREEN_HEIGHT: { int width, height; meta_display_get_size (global->meta_display, &width, &height); g_value_set_int (value, height); } break; case PROP_STAGE: g_value_set_object (value, global->stage); break; case PROP_WINDOW_GROUP: g_value_set_object (value, meta_get_window_group_for_display (global->meta_display)); break; case PROP_TOP_WINDOW_GROUP: g_value_set_object (value, meta_get_top_window_group_for_display (global->meta_display)); break; case PROP_WINDOW_MANAGER: g_value_set_object (value, global->wm); break; case PROP_SETTINGS: g_value_set_object (value, global->settings); break; case PROP_DATADIR: g_value_set_string (value, global->datadir); break; case PROP_IMAGEDIR: g_value_set_string (value, global->imagedir); break; case PROP_USERDATADIR: g_value_set_string (value, global->userdatadir); break; case PROP_FOCUS_MANAGER: g_value_set_object (value, global->focus_manager); break; case PROP_FRAME_TIMESTAMPS: g_value_set_boolean (value, global->frame_timestamps); break; case PROP_FRAME_FINISH_TIMESTAMP: g_value_set_boolean (value, global->frame_finish_timestamp); break; case PROP_SWITCHEROO_CONTROL: g_value_set_object (value, global->switcheroo_control); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void switcheroo_appeared_cb (GDBusConnection *connection, const char *name, const char *name_owner, gpointer user_data) { ShellGlobal *global = user_data; g_debug ("switcheroo-control appeared"); shell_net_hadess_switcheroo_control_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, "net.hadess.SwitcherooControl", "/net/hadess/SwitcherooControl", global->switcheroo_cancellable, switcheroo_control_ready_cb, global); } static void switcheroo_vanished_cb (GDBusConnection *connection, const char *name, gpointer user_data) { ShellGlobal *global = user_data; g_debug ("switcheroo-control vanished"); g_clear_object (&global->switcheroo_control); g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SWITCHEROO_CONTROL]); } static void shell_global_init (ShellGlobal *global) { const char *datadir = g_getenv ("GNOME_SHELL_DATADIR"); const char *shell_js = g_getenv("GNOME_SHELL_JS"); char *imagedir, **search_path; char *path; const char *byteorder_string; if (!datadir) datadir = GNOME_SHELL_DATADIR; global->datadir = datadir; /* We make sure imagedir ends with a '/', since the JS won't have * access to g_build_filename() and so will end up just * concatenating global.imagedir to a filename. */ imagedir = g_build_filename (datadir, "images/", NULL); if (g_file_test (imagedir, G_FILE_TEST_IS_DIR)) global->imagedir = imagedir; else { g_free (imagedir); global->imagedir = g_strdup_printf ("%s/", datadir); } /* Ensure config dir exists for later use */ global->userdatadir = g_build_filename (g_get_user_data_dir (), "gnome-shell", NULL); g_mkdir_with_parents (global->userdatadir, 0700); global->userdatadir_path = g_file_new_for_path (global->userdatadir); #if G_BYTE_ORDER == G_LITTLE_ENDIAN byteorder_string = "LE"; #else byteorder_string = "BE"; #endif /* And the runtime state */ path = g_strdup_printf ("%s/gnome-shell/runtime-state-%s.%s", g_get_user_runtime_dir (), byteorder_string, XDisplayName (NULL)); (void) g_mkdir_with_parents (path, 0700); global->runtime_state_path = g_file_new_for_path (path); g_free (path); global->settings = g_settings_new ("org.gnome.shell"); if (shell_js) { int i, j; search_path = g_strsplit (shell_js, ":", -1); /* The naive g_strsplit above will split 'resource:///foo/bar' into 'resource', * '///foo/bar'. Combine these back together by looking for a literal 'resource' * in the array. */ for (i = 0, j = 0; search_path[i];) { char *out; if (strcmp (search_path[i], "resource") == 0 && search_path[i + 1] != NULL) { out = g_strconcat (search_path[i], ":", search_path[i + 1], NULL); g_free (search_path[i]); g_free (search_path[i + 1]); i += 2; } else { out = search_path[i]; i += 1; } search_path[j++] = out; } search_path[j] = NULL; /* NULL-terminate the now possibly shorter array */ } else { search_path = g_malloc0 (2 * sizeof (char *)); search_path[0] = g_strdup ("resource:///org/gnome/shell"); } global->js_context = g_object_new (GJS_TYPE_CONTEXT, "search-path", search_path, NULL); g_strfreev (search_path); global->save_ops = g_hash_table_new_full (g_file_hash, (GEqualFunc) g_file_equal, g_object_unref, g_object_unref); global->switcheroo_cancellable = g_cancellable_new (); g_bus_watch_name (G_BUS_TYPE_SYSTEM, "net.hadess.SwitcherooControl", G_BUS_NAME_WATCHER_FLAGS_NONE, switcheroo_appeared_cb, switcheroo_vanished_cb, global, NULL); } static void shell_global_finalize (GObject *object) { ShellGlobal *global = SHELL_GLOBAL (object); g_clear_object (&global->js_context); g_object_unref (global->settings); the_object = NULL; g_cancellable_cancel (global->switcheroo_cancellable); g_clear_object (&global->switcheroo_cancellable); g_clear_object (&global->userdatadir_path); g_clear_object (&global->runtime_state_path); g_free (global->session_mode); g_free (global->imagedir); g_free (global->userdatadir); g_hash_table_unref (global->save_ops); G_OBJECT_CLASS(shell_global_parent_class)->finalize (object); } static void shell_global_class_init (ShellGlobalClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); gobject_class->get_property = shell_global_get_property; gobject_class->set_property = shell_global_set_property; gobject_class->finalize = shell_global_finalize; shell_global_signals[NOTIFY_ERROR] = g_signal_new ("notify-error", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING); shell_global_signals[LOCATE_POINTER] = g_signal_new ("locate-pointer", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); props[PROP_SESSION_MODE] = g_param_spec_string ("session-mode", "Session Mode", "The session mode to use", "user", G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); props[PROP_SCREEN_WIDTH] = g_param_spec_int ("screen-width", "Screen Width", "Screen width, in pixels", 0, G_MAXINT, 1, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_SCREEN_HEIGHT] = g_param_spec_int ("screen-height", "Screen Height", "Screen height, in pixels", 0, G_MAXINT, 1, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_BACKEND] = g_param_spec_object ("backend", "Backend", "MetaBackend object", META_TYPE_BACKEND, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_CONTEXT] = g_param_spec_object ("context", "Context", "MetaContext object", META_TYPE_CONTEXT, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_DISPLAY] = g_param_spec_object ("display", "Display", "Metacity display object for the shell", META_TYPE_DISPLAY, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_WORKSPACE_MANAGER] = g_param_spec_object ("workspace-manager", "Workspace manager", "Workspace manager", META_TYPE_WORKSPACE_MANAGER, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_STAGE] = g_param_spec_object ("stage", "Stage", "Stage holding the desktop scene graph", CLUTTER_TYPE_ACTOR, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_WINDOW_GROUP] = g_param_spec_object ("window-group", "Window Group", "Actor holding window actors", CLUTTER_TYPE_ACTOR, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_TOP_WINDOW_GROUP] = g_param_spec_object ("top-window-group", "Top Window Group", "Actor holding override-redirect windows", CLUTTER_TYPE_ACTOR, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_WINDOW_MANAGER] = g_param_spec_object ("window-manager", "Window Manager", "Window management interface", SHELL_TYPE_WM, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_SETTINGS] = g_param_spec_object ("settings", "Settings", "GSettings instance for gnome-shell configuration", G_TYPE_SETTINGS, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_DATADIR] = g_param_spec_string ("datadir", "Data directory", "Directory containing gnome-shell data files", NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_IMAGEDIR] = g_param_spec_string ("imagedir", "Image directory", "Directory containing gnome-shell image files", NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_USERDATADIR] = g_param_spec_string ("userdatadir", "User data directory", "Directory containing gnome-shell user data", NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_FOCUS_MANAGER] = g_param_spec_object ("focus-manager", "Focus manager", "The shell's StFocusManager", ST_TYPE_FOCUS_MANAGER, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); props[PROP_FRAME_TIMESTAMPS] = g_param_spec_boolean ("frame-timestamps", "Frame Timestamps", "Whether to log frame timestamps in the performance log", FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); props[PROP_FRAME_FINISH_TIMESTAMP] = g_param_spec_boolean ("frame-finish-timestamp", "Frame Finish Timestamps", "Whether at the end of a frame to call glFinish and log paintCompletedTimestamp", FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); props[PROP_SWITCHEROO_CONTROL] = g_param_spec_object ("switcheroo-control", "switcheroo-control", "D-Bus Proxy for switcheroo-control daemon", G_TYPE_DBUS_PROXY, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (gobject_class, N_PROPS, props); } /* * _shell_global_init: (skip) * @first_property_name: the name of the first property * @...: the value of the first property, followed optionally by more * name/value pairs, followed by %NULL * * Initializes the shell global singleton with the construction-time * properties. * * There are currently no such properties, so @first_property_name should * always be %NULL. * * This call must be called before shell_global_get() and shouldn't be called * more than once. */ void _shell_global_init (const char *first_property_name, ...) { va_list argument_list; g_return_if_fail (the_object == NULL); va_start (argument_list, first_property_name); the_object = SHELL_GLOBAL (g_object_new_valist (SHELL_TYPE_GLOBAL, first_property_name, argument_list)); va_end (argument_list); } /** * shell_global_get: * * Gets the singleton global object that represents the desktop. * * Return value: (transfer none): the singleton global object */ ShellGlobal * shell_global_get (void) { return the_object; } /** * _shell_global_destroy_gjs_context: (skip) * @self: global object * * Destroys the GjsContext held by ShellGlobal, in order to break reference * counting cycles. (The GjsContext holds a reference to ShellGlobal because * it's available as window.global inside JS.) */ void _shell_global_destroy_gjs_context (ShellGlobal *self) { g_clear_object (&self->js_context); } static guint32 get_current_time_maybe_roundtrip (ShellGlobal *global) { guint32 time; time = shell_global_get_current_time (global); if (time != CurrentTime) return time; return meta_display_get_current_time_roundtrip (global->meta_display); } static void focus_window_changed (MetaDisplay *display, GParamSpec *param, gpointer user_data) { ShellGlobal *global = user_data; /* If the stage window became unfocused, drop the key focus * on Clutter's side. */ if (!meta_stage_is_focused (global->meta_display)) clutter_stage_set_key_focus (global->stage, NULL); } static ClutterActor * get_key_focused_actor (ShellGlobal *global) { ClutterActor *actor; actor = clutter_stage_get_key_focus (global->stage); /* If there's no explicit key focus, clutter_stage_get_key_focus() * returns the stage. This is a terrible API. */ if (actor == CLUTTER_ACTOR (global->stage)) actor = NULL; return actor; } static void sync_stage_window_focus (ShellGlobal *global) { ClutterActor *actor; actor = get_key_focused_actor (global); /* An actor got key focus and the stage needs to be focused. */ if (actor != NULL && !meta_stage_is_focused (global->meta_display)) meta_focus_stage_window (global->meta_display, get_current_time_maybe_roundtrip (global)); /* An actor dropped key focus. Focus the default window. */ else if (actor == NULL && meta_stage_is_focused (global->meta_display)) meta_display_focus_default_window (global->meta_display, get_current_time_maybe_roundtrip (global)); } static void focus_actor_changed (ClutterStage *stage, GParamSpec *param, gpointer user_data) { ShellGlobal *global = user_data; sync_stage_window_focus (global); } static void sync_input_region (ShellGlobal *global) { MetaDisplay *display = global->meta_display; MetaX11Display *x11_display = meta_display_get_x11_display (display); meta_x11_display_set_stage_input_region (x11_display, global->input_region); } /** * shell_global_set_stage_input_region: * @global: the #ShellGlobal * @rectangles: (element-type Meta.Rectangle): a list of #MetaRectangle * describing the input region. * * Sets the area of the stage that is responsive to mouse clicks when * we don't have a modal or grab. */ void shell_global_set_stage_input_region (ShellGlobal *global, GSList *rectangles) { MetaRectangle *rect; XRectangle *rects; int nrects, i; GSList *r; g_return_if_fail (SHELL_IS_GLOBAL (global)); if (meta_is_wayland_compositor ()) return; nrects = g_slist_length (rectangles); rects = g_new (XRectangle, nrects); for (r = rectangles, i = 0; r; r = r->next, i++) { rect = (MetaRectangle *)r->data; rects[i].x = rect->x; rects[i].y = rect->y; rects[i].width = rect->width; rects[i].height = rect->height; } if (global->input_region) XFixesDestroyRegion (global->xdisplay, global->input_region); global->input_region = XFixesCreateRegion (global->xdisplay, rects, nrects); g_free (rects); sync_input_region (global); } /** * shell_global_get_context: * * Return value: (transfer none): The #MetaContext */ MetaContext * shell_global_get_context (ShellGlobal *global) { return global->meta_context; } /** * shell_global_get_stage: * * Return value: (transfer none): The default #ClutterStage */ ClutterStage * shell_global_get_stage (ShellGlobal *global) { return global->stage; } /** * shell_global_get_display: * * Return value: (transfer none): The default #MetaDisplay */ MetaDisplay * shell_global_get_display (ShellGlobal *global) { return global->meta_display; } /** * shell_global_get_workspace_manager: * * Return value: (transfer none): The default #MetaWorkspaceManager */ MetaWorkspaceManager * shell_global_get_workspace_manager (ShellGlobal *global) { return global->workspace_manager; } /** * shell_global_get_window_actors: * * Gets the list of #MetaWindowActor for the plugin's screen * * Return value: (element-type Meta.WindowActor) (transfer container): the list of windows */ GList * shell_global_get_window_actors (ShellGlobal *global) { GList *filtered = NULL; GList *l; g_return_val_if_fail (SHELL_IS_GLOBAL (global), NULL); for (l = meta_get_window_actors (global->meta_display); l; l = l->next) if (!meta_window_actor_is_destroyed (l->data)) filtered = g_list_prepend (filtered, l->data); return g_list_reverse (filtered); } static void global_stage_notify_width (GObject *gobject, GParamSpec *pspec, gpointer data) { ShellGlobal *global = SHELL_GLOBAL (data); g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SCREEN_WIDTH]); } static void global_stage_notify_height (GObject *gobject, GParamSpec *pspec, gpointer data) { ShellGlobal *global = SHELL_GLOBAL (data); g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SCREEN_HEIGHT]); } static gboolean global_stage_before_paint (gpointer data) { ShellGlobal *global = SHELL_GLOBAL (data); if (global->frame_timestamps) shell_perf_log_event (shell_perf_log_get_default (), "clutter.stagePaintStart"); return TRUE; } static gboolean load_gl_symbol (const char *name, void **func) { *func = cogl_get_proc_address (name); if (!*func) { g_warning ("failed to resolve required GL symbol \"%s\"\n", name); return FALSE; } return TRUE; } static void global_stage_after_paint (ClutterStage *stage, ClutterStageView *stage_view, ShellGlobal *global) { /* At this point, we've finished all layout and painting, but haven't * actually flushed or swapped */ if (global->frame_timestamps && global->frame_finish_timestamp) { /* It's interesting to find out when the paint actually finishes * on the GPU. We could wait for this asynchronously with * ARB_timer_query (see https://bugzilla.gnome.org/show_bug.cgi?id=732350 * for an implementation of this), but what we actually would * find out then is the latency for drawing a frame, not how much * GPU work was needed, since frames can overlap. Calling glFinish() * is a fairly reliable way to separate out adjacent frames * and measure the amount of GPU work. This is turned on with a * separate property from ::frame-timestamps, since it should not * be turned on if we're trying to actual measure latency or frame * rate. */ static void (*finish) (void); if (!finish) load_gl_symbol ("glFinish", (void **)&finish); cogl_flush (); finish (); shell_perf_log_event (shell_perf_log_get_default (), "clutter.paintCompletedTimestamp"); } } static gboolean global_stage_after_swap (gpointer data) { /* Everything is done, we're ready for a new frame */ ShellGlobal *global = SHELL_GLOBAL (data); if (global->frame_timestamps) shell_perf_log_event (shell_perf_log_get_default (), "clutter.stagePaintDone"); return TRUE; } static void update_scaling_factor (ShellGlobal *global, MetaSettings *settings) { ClutterStage *stage = CLUTTER_STAGE (global->stage); StThemeContext *context = st_theme_context_get_for_stage (stage); int scaling_factor; scaling_factor = meta_settings_get_ui_scaling_factor (settings); g_object_set (context, "scale-factor", scaling_factor, NULL); } static void ui_scaling_factor_changed (MetaSettings *settings, ShellGlobal *global) { update_scaling_factor (global, settings); } static void entry_cursor_func (StEntry *entry, gboolean use_ibeam, gpointer user_data) { ShellGlobal *global = user_data; meta_display_set_cursor (global->meta_display, use_ibeam ? META_CURSOR_IBEAM : META_CURSOR_DEFAULT); } static void on_x11_display_closed (MetaDisplay *display, ShellGlobal *global) { g_signal_handlers_disconnect_by_data (global->stage, global); } void _shell_global_set_plugin (ShellGlobal *global, MetaPlugin *plugin) { MetaContext *context; MetaDisplay *display; MetaBackend *backend; MetaSettings *settings; g_return_if_fail (SHELL_IS_GLOBAL (global)); g_return_if_fail (global->plugin == NULL); display = meta_plugin_get_display (plugin); context = meta_display_get_context (display); backend = meta_context_get_backend (context); global->plugin = plugin; global->wm = shell_wm_new (plugin); global->meta_display = display; global->meta_context = meta_display_get_context (display); global->backend = meta_context_get_backend (context); global->workspace_manager = meta_display_get_workspace_manager (display); global->stage = CLUTTER_STAGE (meta_get_stage_for_display (display)); if (!meta_is_wayland_compositor ()) { MetaX11Display *x11_display = meta_display_get_x11_display (display); global->xdisplay = meta_x11_display_get_xdisplay (x11_display); } st_entry_set_cursor_func (entry_cursor_func, global); st_clipboard_set_selection (meta_display_get_selection (display)); g_signal_connect (global->stage, "notify::width", G_CALLBACK (global_stage_notify_width), global); g_signal_connect (global->stage, "notify::height", G_CALLBACK (global_stage_notify_height), global); clutter_threads_add_repaint_func_full (CLUTTER_REPAINT_FLAGS_PRE_PAINT, global_stage_before_paint, global, NULL); g_signal_connect (global->stage, "after-paint", G_CALLBACK (global_stage_after_paint), global); clutter_threads_add_repaint_func_full (CLUTTER_REPAINT_FLAGS_POST_PAINT, global_stage_after_swap, global, NULL); shell_perf_log_define_event (shell_perf_log_get_default(), "clutter.stagePaintStart", "Start of stage page repaint", ""); shell_perf_log_define_event (shell_perf_log_get_default(), "clutter.paintCompletedTimestamp", "Paint completion on GPU", ""); shell_perf_log_define_event (shell_perf_log_get_default(), "clutter.stagePaintDone", "End of frame, possibly including swap time", ""); g_signal_connect (global->stage, "notify::key-focus", G_CALLBACK (focus_actor_changed), global); g_signal_connect (global->meta_display, "notify::focus-window", G_CALLBACK (focus_window_changed), global); if (global->xdisplay) g_signal_connect_object (global->meta_display, "x11-display-closing", G_CALLBACK (on_x11_display_closed), global, 0); backend = meta_context_get_backend (shell_global_get_context (global)); settings = meta_backend_get_settings (backend); g_signal_connect (settings, "ui-scaling-factor-changed", G_CALLBACK (ui_scaling_factor_changed), global); global->focus_manager = st_focus_manager_get_for_stage (global->stage); update_scaling_factor (global, settings); } GjsContext * _shell_global_get_gjs_context (ShellGlobal *global) { return global->js_context; } /* Code to close all file descriptors before we exec; copied from gspawn.c in GLib. * * Authors: Padraig O'Briain, Matthias Clasen, Lennart Poettering * * http://bugzilla.gnome.org/show_bug.cgi?id=469231 * http://bugzilla.gnome.org/show_bug.cgi?id=357585 */ static int set_cloexec (void *data, gint fd) { if (fd >= GPOINTER_TO_INT (data)) fcntl (fd, F_SETFD, FD_CLOEXEC); return 0; } #ifndef HAVE_FDWALK static int fdwalk (int (*cb)(void *data, int fd), void *data) { gint open_max; gint fd; gint res = 0; #ifdef HAVE_SYS_RESOURCE_H struct rlimit rl; #endif #ifdef __linux__ DIR *d; if ((d = opendir("/proc/self/fd"))) { struct dirent *de; while ((de = readdir(d))) { glong l; gchar *e = NULL; if (de->d_name[0] == '.') continue; errno = 0; l = strtol(de->d_name, &e, 10); if (errno != 0 || !e || *e) continue; fd = (gint) l; if ((glong) fd != l) continue; if (fd == dirfd(d)) continue; if ((res = cb (data, fd)) != 0) break; } closedir(d); return res; } /* If /proc is not mounted or not accessible we fall back to the old * rlimit trick */ #endif #ifdef HAVE_SYS_RESOURCE_H if (getrlimit(RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY) open_max = rl.rlim_max; else #endif open_max = sysconf (_SC_OPEN_MAX); for (fd = 0; fd < open_max; fd++) if ((res = cb (data, fd)) != 0) break; return res; } #endif static void pre_exec_close_fds(void) { fdwalk (set_cloexec, GINT_TO_POINTER(3)); } /** * shell_global_reexec_self: * @global: A #ShellGlobal * * Restart the current process. Only intended for development purposes. */ void shell_global_reexec_self (ShellGlobal *global) { GPtrArray *arr; gsize len; MetaContext *meta_context; #if defined __linux__ || defined __sun char *buf; char *buf_p; char *buf_end; g_autoptr (GError) error = NULL; if (!g_file_get_contents ("/proc/self/cmdline", &buf, &len, &error)) { g_warning ("failed to get /proc/self/cmdline: %s", error->message); return; } buf_end = buf+len; arr = g_ptr_array_new (); /* The cmdline file is NUL-separated */ for (buf_p = buf; buf_p < buf_end; buf_p = buf_p + strlen (buf_p) + 1) g_ptr_array_add (arr, buf_p); g_ptr_array_add (arr, NULL); #elif defined __OpenBSD__ gchar **args, **args_p; gint mib[] = { CTL_KERN, KERN_PROC_ARGS, getpid(), KERN_PROC_ARGV }; if (sysctl (mib, G_N_ELEMENTS (mib), NULL, &len, NULL, 0) == -1) return; args = g_malloc0 (len); if (sysctl (mib, G_N_ELEMENTS (mib), args, &len, NULL, 0) == -1) { g_warning ("failed to get command line args: %d", errno); g_free (args); return; } arr = g_ptr_array_new (); for (args_p = args; *args_p != NULL; args_p++) { g_ptr_array_add (arr, *args_p); } g_ptr_array_add (arr, NULL); #elif defined __FreeBSD__ char *buf; char *buf_p; char *buf_end; gint mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_ARGS, getpid() }; if (sysctl (mib, G_N_ELEMENTS (mib), NULL, &len, NULL, 0) == -1) return; buf = g_malloc0 (len); if (sysctl (mib, G_N_ELEMENTS (mib), buf, &len, NULL, 0) == -1) { g_warning ("failed to get command line args: %d", errno); g_free (buf); return; } buf_end = buf+len; arr = g_ptr_array_new (); /* The value returned by sysctl is NUL-separated */ for (buf_p = buf; buf_p < buf_end; buf_p = buf_p + strlen (buf_p) + 1) g_ptr_array_add (arr, buf_p); g_ptr_array_add (arr, NULL); #else return; #endif /* Close all file descriptors other than stdin/stdout/stderr, otherwise * they will leak and stay open after the exec. In particular, this is * important for file descriptors that represent mapped graphics buffer * objects. */ pre_exec_close_fds (); meta_context = shell_global_get_context (global); meta_context_restore_rlimit_nofile (meta_context, NULL); meta_display_close (shell_global_get_display (global), shell_global_get_current_time (global)); execvp (arr->pdata[0], (char**)arr->pdata); g_warning ("failed to reexec: %s", g_strerror (errno)); g_ptr_array_free (arr, TRUE); #if defined __linux__ || defined __FreeBSD__ g_free (buf); #elif defined __OpenBSD__ g_free (args); #endif } /** * shell_global_notify_error: * @global: a #ShellGlobal * @msg: Error message * @details: Error details * * Show a system error notification. Use this function * when a user-initiated action results in a non-fatal problem * from causes that may not be under system control. For * example, an application crash. */ void shell_global_notify_error (ShellGlobal *global, const char *msg, const char *details) { g_signal_emit_by_name (global, "notify-error", msg, details); } /** * shell_global_get_pointer: * @global: the #ShellGlobal * @x: (out): the X coordinate of the pointer, in global coordinates * @y: (out): the Y coordinate of the pointer, in global coordinates * @mods: (out): the current set of modifier keys that are pressed down * * Gets the pointer coordinates and current modifier key state. */ void shell_global_get_pointer (ShellGlobal *global, int *x, int *y, ClutterModifierType *mods) { ClutterModifierType raw_mods; MetaCursorTracker *tracker; graphene_point_t point; tracker = meta_cursor_tracker_get_for_display (global->meta_display); meta_cursor_tracker_get_pointer (tracker, &point, &raw_mods); if (x) *x = point.x; if (y) *y = point.y; *mods = raw_mods & CLUTTER_MODIFIER_MASK; } /** * shell_global_get_switcheroo_control: * @global: A #ShellGlobal * * Get the global #GDBusProxy instance for the switcheroo-control * daemon. * * Return value: (transfer none): the #GDBusProxy for the daemon, * or %NULL on error. */ GDBusProxy * shell_global_get_switcheroo_control (ShellGlobal *global) { return global->switcheroo_control; } /** * shell_global_get_settings: * @global: A #ShellGlobal * * Get the global GSettings instance. * * Return value: (transfer none): The GSettings object */ GSettings * shell_global_get_settings (ShellGlobal *global) { return global->settings; } /** * shell_global_get_current_time: * @global: A #ShellGlobal * * Returns: the current X server time from the current Clutter, Gdk, or X * event. If called from outside an event handler, this may return * %Clutter.CURRENT_TIME (aka 0), or it may return a slightly * out-of-date timestamp. */ guint32 shell_global_get_current_time (ShellGlobal *global) { guint32 time; /* meta_display_get_current_time() will return the correct time when handling an X or Gdk event, but will return CurrentTime from some Clutter event callbacks. clutter_get_current_event_time() will return the correct time from a Clutter event callback, but may return CLUTTER_CURRENT_TIME timestamp if called at other times. So we try meta_display_get_current_time() first, since we can recognize a "wrong" answer from that, and then fall back to clutter_get_current_event_time(). */ time = meta_display_get_current_time (global->meta_display); if (time != CLUTTER_CURRENT_TIME) return time; return clutter_get_current_event_time (); } static void shell_global_app_launched_cb (GAppLaunchContext *context, GAppInfo *info, GVariant *platform_data, gpointer user_data) { gint32 pid; const gchar *app_name; if (!g_variant_lookup (platform_data, "pid", "i", &pid)) return; /* If pid == 0 the application was launched through D-Bus * activation, therefore it's already in its own unit */ if (pid == 0) return; app_name = g_app_info_get_id (info); if (app_name == NULL) app_name = g_app_info_get_executable (info); /* Start async request; we don't care about the result */ gnome_start_systemd_scope (app_name, pid, NULL, NULL, NULL, NULL, NULL); } /** * shell_global_create_app_launch_context: * @global: A #ShellGlobal * @timestamp: the timestamp for the launch (or 0 for current time) * @workspace: a workspace index, or -1 to indicate the current one * * Create a #GAppLaunchContext set up with the correct timestamp, and * targeted to activate on the current workspace. * * Return value: (transfer full): A new #GAppLaunchContext */ GAppLaunchContext * shell_global_create_app_launch_context (ShellGlobal *global, guint32 timestamp, int workspace) { MetaWorkspaceManager *workspace_manager = global->workspace_manager; MetaStartupNotification *sn; MetaLaunchContext *context; MetaWorkspace *ws = NULL; sn = meta_display_get_startup_notification (global->meta_display); context = meta_startup_notification_create_launcher (sn); if (timestamp == 0) timestamp = shell_global_get_current_time (global); meta_launch_context_set_timestamp (context, timestamp); if (workspace < 0) ws = meta_workspace_manager_get_active_workspace (workspace_manager); else ws = meta_workspace_manager_get_workspace_by_index (workspace_manager, workspace); meta_launch_context_set_workspace (context, ws); g_signal_connect (context, "launched", G_CALLBACK (shell_global_app_launched_cb), NULL); return (GAppLaunchContext *) context; } typedef struct { ShellLeisureFunction func; gpointer user_data; GDestroyNotify notify; } LeisureClosure; static gboolean run_leisure_functions (gpointer data) { ShellGlobal *global = data; GSList *closures; GSList *iter; global->leisure_function_id = 0; /* We started more work since we scheduled the idle */ if (global->work_count > 0) return FALSE; /* No leisure closures, so we are done */ if (global->leisure_closures == NULL) return FALSE; closures = global->leisure_closures; global->leisure_closures = NULL; for (iter = closures; iter; iter = iter->next) { LeisureClosure *closure = closures->data; closure->func (closure->user_data); if (closure->notify) closure->notify (closure->user_data); g_free (closure); } g_slist_free (closures); return FALSE; } static void schedule_leisure_functions (ShellGlobal *global) { /* This is called when we think we are ready to run leisure functions * by our own accounting. We try to handle other types of business * (like ClutterAnimation) by adding a low priority idle function. * * This won't work properly if the mainloop goes idle waiting for * the vertical blanking interval or waiting for work being done * in another thread. */ if (!global->leisure_function_id) { global->leisure_function_id = g_idle_add_full (G_PRIORITY_LOW, run_leisure_functions, global, NULL); g_source_set_name_by_id (global->leisure_function_id, "[gnome-shell] run_leisure_functions"); } } /** * shell_global_begin_work: * @global: the #ShellGlobal * * Marks that we are currently doing work. This is used to to track * whether we are busy for the purposes of shell_global_run_at_leisure(). * A count is kept and shell_global_end_work() must be called exactly * as many times as shell_global_begin_work(). */ void shell_global_begin_work (ShellGlobal *global) { global->work_count++; } /** * shell_global_end_work: * @global: the #ShellGlobal * * Marks the end of work that we started with shell_global_begin_work(). * If no other work is ongoing and functions have been added with * shell_global_run_at_leisure(), they will be run at the next * opportunity. */ void shell_global_end_work (ShellGlobal *global) { g_return_if_fail (global->work_count > 0); global->work_count--; if (global->work_count == 0) schedule_leisure_functions (global); } /** * shell_global_run_at_leisure: * @global: the #ShellGlobal * @func: function to call at leisure * @user_data: data to pass to @func * @notify: function to call to free @user_data * * Schedules a function to be called the next time the shell is idle. * Idle means here no animations, no redrawing, and no ongoing background * work. Since there is currently no way to hook into the Clutter master * clock and know when is running, the implementation here is somewhat * approximation. Animations may be detected as terminating early if they * can be drawn fast enough so that the event loop goes idle between frames. * * The intent of this function is for performance measurement runs * where a number of actions should be run serially and each action is * timed individually. Using this function for other purposes will * interfere with the ability to use it for performance measurement so * should be avoided. */ void shell_global_run_at_leisure (ShellGlobal *global, ShellLeisureFunction func, gpointer user_data, GDestroyNotify notify) { LeisureClosure *closure = g_new (LeisureClosure, 1); closure->func = func; closure->user_data = user_data; closure->notify = notify; global->leisure_closures = g_slist_append (global->leisure_closures, closure); if (global->work_count == 0) schedule_leisure_functions (global); } const char * shell_global_get_session_mode (ShellGlobal *global) { g_return_val_if_fail (SHELL_IS_GLOBAL (global), "user"); return global->session_mode; } static void delete_variant_cb (GObject *object, GAsyncResult *result, gpointer user_data) { ShellGlobal *global = user_data; GError *error = NULL; if (!g_file_delete_finish (G_FILE (object), result, &error)) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_warning ("Could not delete runtime/persistent state file: %s\n", error->message); } g_error_free (error); } g_hash_table_remove (global->save_ops, object); } static void replace_contents_worker (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { GFile *file = source_object; GBytes *bytes = task_data; GError *error = NULL; const gchar *data; gsize len; data = g_bytes_get_data (bytes, &len); if (!g_file_replace_contents (file, data, len, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, NULL, cancellable, &error)) g_task_return_error (task, g_steal_pointer (&error)); else g_task_return_boolean (task, TRUE); } static void replace_contents_async (GFile *path, GBytes *bytes, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(GTask) task = NULL; g_assert (G_IS_FILE (path)); g_assert (bytes != NULL); g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); task = g_task_new (path, cancellable, callback, user_data); g_task_set_source_tag (task, replace_contents_async); g_task_set_task_data (task, g_bytes_ref (bytes), (GDestroyNotify)g_bytes_unref); g_task_run_in_thread (task, replace_contents_worker); } static gboolean replace_contents_finish (GFile *file, GAsyncResult *result, GError **error) { return g_task_propagate_boolean (G_TASK (result), error); } static void replace_variant_cb (GObject *object, GAsyncResult *result, gpointer user_data) { ShellGlobal *global = user_data; GError *error = NULL; if (!replace_contents_finish (G_FILE (object), result, &error)) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { g_warning ("Could not replace runtime/persistent state file: %s\n", error->message); } g_error_free (error); } g_hash_table_remove (global->save_ops, object); } static void save_variant (ShellGlobal *global, GFile *dir, const char *property_name, GVariant *variant) { GFile *path = g_file_get_child (dir, property_name); GCancellable *cancellable; cancellable = g_hash_table_lookup (global->save_ops, path); g_cancellable_cancel (cancellable); cancellable = g_cancellable_new (); g_hash_table_insert (global->save_ops, g_object_ref (path), cancellable); if (variant == NULL || g_variant_get_data (variant) == NULL) { g_file_delete_async (path, G_PRIORITY_DEFAULT, cancellable, delete_variant_cb, global); } else { g_autoptr(GBytes) bytes = NULL; bytes = g_bytes_new_with_free_func (g_variant_get_data (variant), g_variant_get_size (variant), (GDestroyNotify)g_variant_unref, g_variant_ref (variant)); /* g_file_replace_contents_async() can potentially fsync() from the * calling thread when completing the asynchronous task. Instead, we * want to force that fsync() to a thread to avoid blocking the * compositor main loop. Using our own replace_contents_async() * simply executes the operation synchronously from a thread. */ replace_contents_async (path, bytes, cancellable, replace_variant_cb, global); } g_object_unref (path); } static GVariant * load_variant (GFile *dir, const char *property_type, const char *property_name) { GVariant *res = NULL; GMappedFile *mfile; GFile *path = g_file_get_child (dir, property_name); char *pathstr; GError *local_error = NULL; pathstr = g_file_get_path (path); mfile = g_mapped_file_new (pathstr, FALSE, &local_error); if (!mfile) { if (!g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) { g_warning ("Failed to open runtime state: %s", local_error->message); } g_clear_error (&local_error); } else { GBytes *bytes = g_mapped_file_get_bytes (mfile); res = g_variant_new_from_bytes (G_VARIANT_TYPE (property_type), bytes, FALSE); g_bytes_unref (bytes); g_mapped_file_unref (mfile); } g_object_unref (path); g_free (pathstr); return res; } /** * shell_global_set_runtime_state: * @global: a #ShellGlobal * @property_name: Name of the property * @variant: (nullable): A #GVariant, or %NULL to unset * * Change the value of serialized runtime state. */ void shell_global_set_runtime_state (ShellGlobal *global, const char *property_name, GVariant *variant) { save_variant (global, global->runtime_state_path, property_name, variant); } /** * shell_global_get_runtime_state: * @global: a #ShellGlobal * @property_type: Expected data type * @property_name: Name of the property * * The shell maintains "runtime" state which does not persist across * logout or reboot. * * Returns: (transfer floating): The value of a serialized property, or %NULL if none stored */ GVariant * shell_global_get_runtime_state (ShellGlobal *global, const char *property_type, const char *property_name) { return load_variant (global->runtime_state_path, property_type, property_name); } /** * shell_global_set_persistent_state: * @global: a #ShellGlobal * @property_name: Name of the property * @variant: (nullable): A #GVariant, or %NULL to unset * * Change the value of serialized persistent state. */ void shell_global_set_persistent_state (ShellGlobal *global, const char *property_name, GVariant *variant) { save_variant (global, global->userdatadir_path, property_name, variant); } /** * shell_global_get_persistent_state: * @global: a #ShellGlobal * @property_type: Expected data type * @property_name: Name of the property * * The shell maintains "persistent" state which will persist after * logout or reboot. * * Returns: (transfer none): The value of a serialized property, or %NULL if none stored */ GVariant * shell_global_get_persistent_state (ShellGlobal *global, const char *property_type, const char *property_name) { return load_variant (global->userdatadir_path, property_type, property_name); } void _shell_global_locate_pointer (ShellGlobal *global) { g_signal_emit (global, shell_global_signals[LOCATE_POINTER], 0); }