xsession_chooser_linux.cc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. // Copyright 2019 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. // This file implements a dialog allowing the user to pick between installed
  5. // X session types. It finds sessions by looking for .desktop files in
  6. // /etc/X11/sessions and in the xsessions folder (if any) in each XDG system
  7. // data directory. (By default, this will be /usr/local/share/xsessions and
  8. // /usr/share/xsessions.) Once the user selects a session, it will be launched
  9. // via /etc/X11/Xsession. There will additionally be an "Xsession" will will
  10. // invoke Xsession without arguments to launch a "default" session based on the
  11. // system's configuration. If no session .desktop files are found, this will be
  12. // the only option present.
  13. #include <gtk/gtk.h>
  14. #include <unistd.h>
  15. #include <map>
  16. #include <memory>
  17. #include <string>
  18. #include <vector>
  19. #include "base/bind.h"
  20. #include "base/callback.h"
  21. #include "base/environment.h"
  22. #include "base/files/file_enumerator.h"
  23. #include "base/files/file_path.h"
  24. #include "base/files/file_util.h"
  25. #include "base/i18n/icu_util.h"
  26. #include "base/logging.h"
  27. #include "base/message_loop/message_pump_type.h"
  28. #include "base/nix/xdg_util.h"
  29. #include "base/path_service.h"
  30. #include "base/run_loop.h"
  31. #include "base/strings/string_util.h"
  32. #include "base/task/single_thread_task_executor.h"
  33. #include "remoting/base/logging.h"
  34. #include "remoting/base/string_resources.h"
  35. #include "third_party/abseil-cpp/absl/types/optional.h"
  36. #include "third_party/icu/source/common/unicode/unistr.h"
  37. #include "third_party/icu/source/i18n/unicode/coll.h"
  38. #include "ui/base/glib/glib_signal.h"
  39. #include "ui/base/glib/scoped_gobject.h"
  40. #include "ui/base/l10n/l10n_util.h"
  41. #include "remoting/host/xsession_chooser_ui.inc"
  42. namespace remoting {
  43. namespace {
  44. struct XSession {
  45. std::string name;
  46. std::string comment;
  47. std::vector<std::string> desktop_names;
  48. std::string exec;
  49. };
  50. class SessionDialog {
  51. public:
  52. SessionDialog(std::vector<XSession> choices,
  53. base::OnceCallback<void(XSession)> callback,
  54. base::OnceClosure cancel_callback)
  55. : choices_(std::move(choices)),
  56. callback_(std::move(callback)),
  57. cancel_callback_(std::move(cancel_callback)),
  58. ui_(TakeGObject(gtk_builder_new_from_string(UI, -1))) {
  59. gtk_label_set_text(
  60. GTK_LABEL(gtk_builder_get_object(ui_, "message")),
  61. l10n_util::GetStringUTF8(IDS_SESSION_DIALOG_MESSAGE).c_str());
  62. gtk_tree_view_column_set_title(
  63. GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(ui_, "name_column")),
  64. l10n_util::GetStringUTF8(IDS_SESSION_DIALOG_NAME_COLUMN).c_str());
  65. gtk_tree_view_column_set_title(
  66. GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(ui_, "comment_column")),
  67. l10n_util::GetStringUTF8(IDS_SESSION_DIALOG_COMMENT_COLUMN).c_str());
  68. GtkListStore* session_store =
  69. GTK_LIST_STORE(gtk_builder_get_object(ui_, "session_store"));
  70. for (std::size_t i = 0; i < choices_.size(); ++i) {
  71. GtkTreeIter iter;
  72. gtk_list_store_append(session_store, &iter);
  73. // gtk_list_store_set makes its own internal copy of the strings.
  74. gtk_list_store_set(session_store, &iter,
  75. INDEX_COLUMN, static_cast<guint>(i),
  76. NAME_COLUMN, choices_[i].name.c_str(),
  77. COMMENT_COLUMN, choices_[i].comment.c_str(),
  78. -1);
  79. }
  80. g_signal_connect(gtk_builder_get_object(ui_, "session_list"),
  81. "row-activated", G_CALLBACK(OnRowActivatedThunk), this);
  82. g_signal_connect(gtk_builder_get_object(ui_, "ok_button"), "clicked",
  83. G_CALLBACK(OnOkClickedThunk), this);
  84. g_signal_connect(gtk_builder_get_object(ui_, "dialog"), "delete-event",
  85. G_CALLBACK(OnCloseThunk), this);
  86. }
  87. void Show() {
  88. gtk_widget_show(GTK_WIDGET(gtk_builder_get_object(ui_, "dialog")));
  89. }
  90. private:
  91. void ActivateChoice(std::size_t index) {
  92. gtk_widget_hide(GTK_WIDGET(gtk_builder_get_object(ui_, "dialog")));
  93. if (callback_) {
  94. std::move(callback_).Run(std::move(choices_.at(index)));
  95. }
  96. }
  97. CHROMEG_CALLBACK_2(SessionDialog,
  98. void,
  99. OnRowActivated,
  100. GtkTreeView*,
  101. GtkTreePath*,
  102. GtkTreeViewColumn*);
  103. CHROMEG_CALLBACK_0(SessionDialog, void, OnOkClicked, GtkButton*);
  104. CHROMEG_CALLBACK_1(SessionDialog, gboolean, OnClose, GtkWidget*, GdkEvent*);
  105. enum Columns { INDEX_COLUMN, NAME_COLUMN, COMMENT_COLUMN, NUM_COLUMNS };
  106. std::vector<XSession> choices_;
  107. base::OnceCallback<void(XSession)> callback_;
  108. base::OnceClosure cancel_callback_;
  109. ScopedGObject<GtkBuilder> ui_;
  110. SessionDialog(const SessionDialog&) = delete;
  111. SessionDialog& operator=(const SessionDialog&) = delete;
  112. };
  113. void SessionDialog::OnRowActivated(GtkTreeView* session_list,
  114. GtkTreePath* path,
  115. GtkTreeViewColumn*) {
  116. GtkTreeModel* model = gtk_tree_view_get_model(session_list);
  117. GtkTreeIter iter;
  118. guint index;
  119. if (!gtk_tree_model_get_iter(model, &iter, path)) {
  120. // Strange, but the user should still be able to click OK to progress.
  121. return;
  122. }
  123. gtk_tree_model_get(model, &iter, INDEX_COLUMN, &index, -1);
  124. ActivateChoice(index);
  125. }
  126. void SessionDialog::OnOkClicked(GtkButton*) {
  127. GtkTreeSelection* selection = gtk_tree_view_get_selection(
  128. GTK_TREE_VIEW(gtk_builder_get_object(ui_, "session_list")));
  129. GtkTreeModel* model;
  130. GtkTreeIter iter;
  131. guint index;
  132. if (!gtk_tree_selection_get_selected(selection, &model, &iter)) {
  133. // Nothing selected, so do nothing. Note that the selection mode is set to
  134. // "browse", which should, under most circumstances, ensure that exactly one
  135. // item is selected, preventing this from being reached. However, it does
  136. // not completely guarantee that it can never happen.
  137. return;
  138. }
  139. gtk_tree_model_get(model, &iter, INDEX_COLUMN, &index, -1);
  140. ActivateChoice(index);
  141. }
  142. gboolean SessionDialog::OnClose(GtkWidget* dialog, GdkEvent*) {
  143. gtk_widget_hide(dialog);
  144. if (cancel_callback_) {
  145. std::move(cancel_callback_).Run();
  146. }
  147. return true;
  148. }
  149. absl::optional<XSession> TryLoadSession(base::FilePath path) {
  150. std::unique_ptr<GKeyFile, void (*)(GKeyFile*)> key_file(g_key_file_new(),
  151. &g_key_file_free);
  152. GError* error;
  153. if (!g_key_file_load_from_file(key_file.get(), path.value().c_str(),
  154. G_KEY_FILE_NONE, &error)) {
  155. LOG(WARNING) << "Failed to load " << path << ": " << error->message;
  156. g_error_free(error);
  157. return absl::nullopt;
  158. }
  159. // Files without a "Desktop Entry" group can be ignored. (An empty file can be
  160. // put in a higher-priority directory to hide entries from a lower-priority
  161. // directory.)
  162. if (!g_key_file_has_group(key_file.get(), G_KEY_FILE_DESKTOP_GROUP)) {
  163. return absl::nullopt;
  164. }
  165. // Files with "NoDisplay" or "Hidden" set should be ignored.
  166. for (const char* key :
  167. {G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY, G_KEY_FILE_DESKTOP_KEY_HIDDEN}) {
  168. if (g_key_file_get_boolean(key_file.get(), G_KEY_FILE_DESKTOP_GROUP, key,
  169. nullptr)) {
  170. return absl::nullopt;
  171. }
  172. }
  173. // If there's a "TryExec" key, we need to check if the specified path is
  174. // executable and ignore the entry if not. (However, we should not try to
  175. // actually execute the specified path.)
  176. if (gchar* try_exec =
  177. g_key_file_get_string(key_file.get(), G_KEY_FILE_DESKTOP_GROUP,
  178. G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, nullptr)) {
  179. base::FilePath try_exec_path(
  180. base::TrimWhitespaceASCII(try_exec, base::TRIM_ALL));
  181. g_free(try_exec);
  182. if (try_exec_path.IsAbsolute()
  183. ? access(try_exec_path.value().c_str(), X_OK) != 0
  184. : !base::ExecutableExistsInPath(base::Environment::Create().get(),
  185. try_exec_path.value())) {
  186. LOG(INFO) << "Rejecting " << path << " due to TryExec=" << try_exec_path;
  187. return absl::nullopt;
  188. }
  189. }
  190. XSession session;
  191. // Required fields.
  192. if (gchar* localized_name = g_key_file_get_locale_string(
  193. key_file.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME,
  194. nullptr, nullptr)) {
  195. session.name = localized_name;
  196. g_free(localized_name);
  197. } else {
  198. LOG(WARNING) << "Failed to load value of " << G_KEY_FILE_DESKTOP_KEY_NAME
  199. << " from " << path;
  200. return absl::nullopt;
  201. }
  202. if (gchar* exec =
  203. g_key_file_get_string(key_file.get(), G_KEY_FILE_DESKTOP_GROUP,
  204. G_KEY_FILE_DESKTOP_KEY_EXEC, nullptr)) {
  205. session.exec = exec;
  206. g_free(exec);
  207. } else {
  208. LOG(WARNING) << "Failed to load value of " << G_KEY_FILE_DESKTOP_KEY_EXEC
  209. << " from " << path;
  210. return absl::nullopt;
  211. }
  212. // Optional fields.
  213. if (gchar* localized_comment = g_key_file_get_locale_string(
  214. key_file.get(), G_KEY_FILE_DESKTOP_GROUP,
  215. G_KEY_FILE_DESKTOP_KEY_COMMENT, nullptr, nullptr)) {
  216. session.comment = localized_comment;
  217. g_free(localized_comment);
  218. }
  219. // DesktopNames does not yet have a constant in glib.
  220. if (gchar** desktop_names =
  221. g_key_file_get_string_list(key_file.get(), G_KEY_FILE_DESKTOP_GROUP,
  222. "DesktopNames", nullptr, nullptr)) {
  223. for (std::size_t i = 0; desktop_names[i]; ++i) {
  224. session.desktop_names.push_back(desktop_names[i]);
  225. }
  226. g_strfreev(desktop_names);
  227. }
  228. return session;
  229. }
  230. std::vector<XSession> CollectXSessions() {
  231. std::vector<base::FilePath> session_search_dirs;
  232. session_search_dirs.emplace_back("/etc/X11/sessions");
  233. // Returned list is owned by GLib and should not be modified or freed.
  234. const gchar* const* system_data_dirs = g_get_system_data_dirs();
  235. // List is null-terminated.
  236. for (std::size_t i = 0; system_data_dirs[i]; ++i) {
  237. session_search_dirs.push_back(
  238. base::FilePath(system_data_dirs[i]).Append("xsessions"));
  239. }
  240. std::map<base::FilePath, base::FilePath> session_files;
  241. for (const base::FilePath& search_dir : session_search_dirs) {
  242. base::FileEnumerator file_enumerator(search_dir, false /* recursive */,
  243. base::FileEnumerator::FILES,
  244. "*.desktop");
  245. base::FilePath session_path;
  246. while (!(session_path = file_enumerator.Next()).empty()) {
  247. base::FilePath basename = session_path.BaseName().RemoveFinalExtension();
  248. // Files in higher-priority directory should shadow those from lower-
  249. // priority directories. Emplace will only insert if an entry with the
  250. // same basename wasn't found in a previous directory.
  251. session_files.emplace(basename, session_path);
  252. }
  253. }
  254. std::vector<XSession> sessions;
  255. // Ensure there's always at least one session.
  256. sessions.push_back(
  257. {l10n_util::GetStringUTF8(IDS_SESSION_DIALOG_DEFAULT_SESSION_NAME),
  258. l10n_util::GetStringUTF8(IDS_SESSION_DIALOG_DEFAULT_SESSION_COMMENT),
  259. {},
  260. "default"});
  261. for (const auto& session : session_files) {
  262. absl::optional<XSession> loaded_session = TryLoadSession(session.second);
  263. if (loaded_session) {
  264. sessions.push_back(std::move(*loaded_session));
  265. }
  266. }
  267. UErrorCode err = U_ZERO_ERROR;
  268. std::unique_ptr<icu::Collator> collator(icu::Collator::createInstance(err));
  269. if (U_SUCCESS(err)) {
  270. std::sort(sessions.begin() + 1, sessions.end(),
  271. [&](const XSession& first, const XSession& second) {
  272. UErrorCode err = U_ZERO_ERROR;
  273. UCollationResult result = collator->compare(
  274. icu::UnicodeString::fromUTF8(first.name),
  275. icu::UnicodeString::fromUTF8(second.name), err);
  276. // The icu documentation isn't clear under what circumstances
  277. // this can fail. base::i18n::CompareString16WithCollator just
  278. // does a DCHECK of the result, so do the same here for now.
  279. DCHECK(U_SUCCESS(err));
  280. return result == UCOL_LESS;
  281. });
  282. } else {
  283. LOG(WARNING) << "Error creating collator. Not sorting list. ("
  284. << u_errorName(err) << ")";
  285. }
  286. return sessions;
  287. }
  288. void ExecXSession(base::OnceClosure quit_closure, XSession session) {
  289. base::FilePath xsession_script;
  290. if (!base::PathService::Get(base::DIR_EXE, &xsession_script)) {
  291. PLOG(ERROR) << "Failed to get CRD install path";
  292. std::move(quit_closure).Run();
  293. return;
  294. }
  295. xsession_script = xsession_script.Append("Xsession");
  296. LOG(INFO) << "Running " << xsession_script << " " << session.exec;
  297. if (!session.desktop_names.empty()) {
  298. std::unique_ptr<base::Environment> environment =
  299. base::Environment::Create();
  300. environment->SetVar(base::nix::kXdgCurrentDesktopEnvVar,
  301. base::JoinString(session.desktop_names, ":"));
  302. }
  303. execl(xsession_script.value().c_str(), xsession_script.value().c_str(),
  304. session.exec.c_str(), nullptr);
  305. PLOG(ERROR) << "Failed to exec XSession";
  306. std::move(quit_closure).Run();
  307. }
  308. } // namespace
  309. int XSessionChooserMain() {
  310. #if GTK_CHECK_VERSION(3, 90, 0)
  311. gtk_init();
  312. #else
  313. gtk_init(nullptr, nullptr);
  314. #endif
  315. base::SingleThreadTaskExecutor task_executor(base::MessagePumpType::UI);
  316. base::RunLoop run_loop;
  317. SessionDialog dialog(CollectXSessions(),
  318. base::BindOnce(&ExecXSession, run_loop.QuitClosure()),
  319. run_loop.QuitClosure());
  320. dialog.Show();
  321. run_loop.Run();
  322. // Control only gets to here if something went wrong.
  323. return 1;
  324. }
  325. } // namespace remoting