File: xsession_chooser_linux.cc

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (380 lines) | stat: -rw-r--r-- 14,139 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

// This file implements a dialog allowing the user to pick between installed
// X session types. It finds sessions by looking for .desktop files in
// /etc/X11/sessions and in the xsessions folder (if any) in each XDG system
// data directory. (By default, this will be /usr/local/share/xsessions and
// /usr/share/xsessions.) Once the user selects a session, it will be launched
// via /etc/X11/Xsession. There will additionally be an "Xsession" will will
// invoke Xsession without arguments to launch a "default" session based on the
// system's configuration. If no session .desktop files are found, this will be
// the only option present.

#include <gtk/gtk.h>
#include <unistd.h>

#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "base/environment.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/i18n/icu_util.h"
#include "base/logging.h"
#include "base/message_loop/message_pump_type.h"
#include "base/nix/xdg_util.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_executor.h"
#include "remoting/base/logging.h"
#include "remoting/base/string_resources.h"
#include "remoting/host/xsession_chooser_ui.inc"
#include "third_party/icu/source/common/unicode/unistr.h"
#include "third_party/icu/source/i18n/unicode/coll.h"
#include "ui/base/glib/scoped_gobject.h"
#include "ui/base/glib/scoped_gsignal.h"
#include "ui/base/l10n/l10n_util.h"

namespace remoting {

namespace {

struct XSession {
  std::string name;
  std::string comment;
  std::vector<std::string> desktop_names;
  std::string exec;
};

class SessionDialog {
 public:
  SessionDialog(std::vector<XSession> choices,
                base::OnceCallback<void(XSession)> callback,
                base::OnceClosure cancel_callback)
      : choices_(std::move(choices)),
        callback_(std::move(callback)),
        cancel_callback_(std::move(cancel_callback)),
        ui_(TakeGObject(gtk_builder_new_from_string(UI, -1))) {
    gtk_label_set_text(
        GTK_LABEL(gtk_builder_get_object(ui_, "message")),
        l10n_util::GetStringUTF8(IDS_SESSION_DIALOG_MESSAGE).c_str());
    gtk_tree_view_column_set_title(
        GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(ui_, "name_column")),
        l10n_util::GetStringUTF8(IDS_SESSION_DIALOG_NAME_COLUMN).c_str());
    gtk_tree_view_column_set_title(
        GTK_TREE_VIEW_COLUMN(gtk_builder_get_object(ui_, "comment_column")),
        l10n_util::GetStringUTF8(IDS_SESSION_DIALOG_COMMENT_COLUMN).c_str());

    GtkListStore* session_store =
        GTK_LIST_STORE(gtk_builder_get_object(ui_, "session_store"));
    for (std::size_t i = 0; i < choices_.size(); ++i) {
      GtkTreeIter iter;
      gtk_list_store_append(session_store, &iter);
      // gtk_list_store_set makes its own internal copy of the strings.
      gtk_list_store_set(session_store, &iter, INDEX_COLUMN,
                         static_cast<guint>(i), NAME_COLUMN,
                         choices_[i].name.c_str(), COMMENT_COLUMN,
                         choices_[i].comment.c_str(), -1);
    }

    auto connect = [&](auto* sender, const char* detailed_signal,
                       auto receiver) {
      // Unretained() is safe since SessionDialog will own the ScopedGSignal.
      signals_.emplace_back(
          sender, detailed_signal,
          base::BindRepeating(receiver, base::Unretained(this)));
    };
    connect(GTK_TREE_VIEW(gtk_builder_get_object(ui_, "session_list")),
            "row-activated", &SessionDialog::OnRowActivated);
    connect(GTK_BUTTON(gtk_builder_get_object(ui_, "ok_button")), "clicked",
            &SessionDialog::OnOkClicked);
    connect(GTK_WIDGET(gtk_builder_get_object(ui_, "dialog")), "delete-event",
            &SessionDialog::OnClose);
  }

  void Show() {
    gtk_widget_show(GTK_WIDGET(gtk_builder_get_object(ui_, "dialog")));
  }

 private:
  void ActivateChoice(std::size_t index) {
    gtk_widget_hide(GTK_WIDGET(gtk_builder_get_object(ui_, "dialog")));
    if (callback_) {
      std::move(callback_).Run(std::move(choices_.at(index)));
    }
  }

  void OnRowActivated(GtkTreeView* session_list,
                      GtkTreePath* path,
                      GtkTreeViewColumn*);
  void OnOkClicked(GtkButton* button);
  gboolean OnClose(GtkWidget* dialog, GdkEvent*);

  enum Columns { INDEX_COLUMN, NAME_COLUMN, COMMENT_COLUMN, NUM_COLUMNS };
  std::vector<XSession> choices_;
  base::OnceCallback<void(XSession)> callback_;
  base::OnceClosure cancel_callback_;
  ScopedGObject<GtkBuilder> ui_;
  std::vector<ScopedGSignal> signals_;

  SessionDialog(const SessionDialog&) = delete;
  SessionDialog& operator=(const SessionDialog&) = delete;
};

void SessionDialog::OnRowActivated(GtkTreeView* session_list,
                                   GtkTreePath* path,
                                   GtkTreeViewColumn*) {
  GtkTreeModel* model = gtk_tree_view_get_model(session_list);
  GtkTreeIter iter;
  guint index;
  if (!gtk_tree_model_get_iter(model, &iter, path)) {
    // Strange, but the user should still be able to click OK to progress.
    return;
  }
  gtk_tree_model_get(model, &iter, INDEX_COLUMN, &index, -1);
  ActivateChoice(index);
}

void SessionDialog::OnOkClicked(GtkButton*) {
  GtkTreeSelection* selection = gtk_tree_view_get_selection(
      GTK_TREE_VIEW(gtk_builder_get_object(ui_, "session_list")));
  GtkTreeModel* model;
  GtkTreeIter iter;
  guint index;
  if (!gtk_tree_selection_get_selected(selection, &model, &iter)) {
    // Nothing selected, so do nothing. Note that the selection mode is set to
    // "browse", which should, under most circumstances, ensure that exactly one
    // item is selected, preventing this from being reached. However, it does
    // not completely guarantee that it can never happen.
    return;
  }
  gtk_tree_model_get(model, &iter, INDEX_COLUMN, &index, -1);
  ActivateChoice(index);
}

gboolean SessionDialog::OnClose(GtkWidget* dialog, GdkEvent*) {
  gtk_widget_hide(dialog);
  if (cancel_callback_) {
    std::move(cancel_callback_).Run();
  }
  return true;
}

std::optional<XSession> TryLoadSession(base::FilePath path) {
  std::unique_ptr<GKeyFile, void (*)(GKeyFile*)> key_file(g_key_file_new(),
                                                          &g_key_file_free);
  GError* error;

  if (!g_key_file_load_from_file(key_file.get(), path.value().c_str(),
                                 G_KEY_FILE_NONE, &error)) {
    LOG(WARNING) << "Failed to load " << path << ": " << error->message;
    g_error_free(error);
    return std::nullopt;
  }

  // Files without a "Desktop Entry" group can be ignored. (An empty file can be
  // put in a higher-priority directory to hide entries from a lower-priority
  // directory.)
  if (!g_key_file_has_group(key_file.get(), G_KEY_FILE_DESKTOP_GROUP)) {
    return std::nullopt;
  }

  // Files with "NoDisplay" or "Hidden" set should be ignored.
  for (const char* key :
       {G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY, G_KEY_FILE_DESKTOP_KEY_HIDDEN}) {
    if (g_key_file_get_boolean(key_file.get(), G_KEY_FILE_DESKTOP_GROUP, key,
                               nullptr)) {
      return std::nullopt;
    }
  }

  // If there's a "TryExec" key, we need to check if the specified path is
  // executable and ignore the entry if not. (However, we should not try to
  // actually execute the specified path.)
  if (gchar* try_exec =
          g_key_file_get_string(key_file.get(), G_KEY_FILE_DESKTOP_GROUP,
                                G_KEY_FILE_DESKTOP_KEY_TRY_EXEC, nullptr)) {
    base::FilePath try_exec_path(
        base::TrimWhitespaceASCII(try_exec, base::TRIM_ALL));
    g_free(try_exec);

    if (try_exec_path.IsAbsolute()
            ? access(try_exec_path.value().c_str(), X_OK) != 0
            : !base::ExecutableExistsInPath(base::Environment::Create().get(),
                                            try_exec_path.value())) {
      LOG(INFO) << "Rejecting " << path << " due to TryExec=" << try_exec_path;
      return std::nullopt;
    }
  }

  XSession session;
  // Required fields.
  if (gchar* localized_name = g_key_file_get_locale_string(
          key_file.get(), G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME,
          nullptr, nullptr)) {
    session.name = localized_name;
    g_free(localized_name);
  } else {
    LOG(WARNING) << "Failed to load value of " << G_KEY_FILE_DESKTOP_KEY_NAME
                 << " from " << path;
    return std::nullopt;
  }

  if (gchar* exec =
          g_key_file_get_string(key_file.get(), G_KEY_FILE_DESKTOP_GROUP,
                                G_KEY_FILE_DESKTOP_KEY_EXEC, nullptr)) {
    session.exec = exec;
    g_free(exec);
  } else {
    LOG(WARNING) << "Failed to load value of " << G_KEY_FILE_DESKTOP_KEY_EXEC
                 << " from " << path;
    return std::nullopt;
  }

  // Optional fields.
  if (gchar* localized_comment = g_key_file_get_locale_string(
          key_file.get(), G_KEY_FILE_DESKTOP_GROUP,
          G_KEY_FILE_DESKTOP_KEY_COMMENT, nullptr, nullptr)) {
    session.comment = localized_comment;
    g_free(localized_comment);
  }

  // DesktopNames does not yet have a constant in glib.
  if (gchar** desktop_names =
          g_key_file_get_string_list(key_file.get(), G_KEY_FILE_DESKTOP_GROUP,
                                     "DesktopNames", nullptr, nullptr)) {
    for (std::size_t i = 0; desktop_names[i]; ++i) {
      session.desktop_names.push_back(desktop_names[i]);
    }
    g_strfreev(desktop_names);
  }

  return session;
}

std::vector<XSession> CollectXSessions() {
  std::vector<base::FilePath> session_search_dirs;
  session_search_dirs.emplace_back("/etc/X11/sessions");

  // Returned list is owned by GLib and should not be modified or freed.
  const gchar* const* system_data_dirs = g_get_system_data_dirs();
  // List is null-terminated.
  for (std::size_t i = 0; system_data_dirs[i]; ++i) {
    session_search_dirs.push_back(
        base::FilePath(system_data_dirs[i]).Append("xsessions"));
  }

  std::map<base::FilePath, base::FilePath> session_files;

  for (const base::FilePath& search_dir : session_search_dirs) {
    base::FileEnumerator file_enumerator(search_dir, false /* recursive */,
                                         base::FileEnumerator::FILES,
                                         "*.desktop");
    base::FilePath session_path;
    while (!(session_path = file_enumerator.Next()).empty()) {
      base::FilePath basename = session_path.BaseName().RemoveFinalExtension();
      // Files in higher-priority directory should shadow those from lower-
      // priority directories. Emplace will only insert if an entry with the
      // same basename wasn't found in a previous directory.
      session_files.emplace(basename, session_path);
    }
  }

  std::vector<XSession> sessions;

  // Ensure there's always at least one session.
  sessions.push_back(
      {l10n_util::GetStringUTF8(IDS_SESSION_DIALOG_DEFAULT_SESSION_NAME),
       l10n_util::GetStringUTF8(IDS_SESSION_DIALOG_DEFAULT_SESSION_COMMENT),
       {},
       "default"});

  for (const auto& session : session_files) {
    std::optional<XSession> loaded_session = TryLoadSession(session.second);
    if (loaded_session) {
      sessions.push_back(std::move(*loaded_session));
    }
  }

  UErrorCode err = U_ZERO_ERROR;
  std::unique_ptr<icu::Collator> collator(icu::Collator::createInstance(err));
  if (U_SUCCESS(err)) {
    std::sort(sessions.begin() + 1, sessions.end(),
              [&](const XSession& first, const XSession& second) {
                UErrorCode err = U_ZERO_ERROR;
                UCollationResult result = collator->compare(
                    icu::UnicodeString::fromUTF8(first.name),
                    icu::UnicodeString::fromUTF8(second.name), err);
                // The icu documentation isn't clear under what circumstances
                // this can fail. base::i18n::CompareString16WithCollator just
                // does a DCHECK of the result, so do the same here for now.
                DCHECK(U_SUCCESS(err));
                return result == UCOL_LESS;
              });
  } else {
    LOG(WARNING) << "Error creating collator. Not sorting list. ("
                 << u_errorName(err) << ")";
  }

  return sessions;
}

void ExecXSession(base::OnceClosure quit_closure, XSession session) {
  base::FilePath xsession_script;
  if (!base::PathService::Get(base::DIR_EXE, &xsession_script)) {
    PLOG(ERROR) << "Failed to get CRD install path";
    std::move(quit_closure).Run();
    return;
  }
  xsession_script = xsession_script.Append("Xsession");
  LOG(INFO) << "Running " << xsession_script << " " << session.exec;
  if (!session.desktop_names.empty()) {
    std::unique_ptr<base::Environment> environment =
        base::Environment::Create();
    environment->SetVar(base::nix::kXdgCurrentDesktopEnvVar,
                        base::JoinString(session.desktop_names, ":"));
  }
  execl(xsession_script.value().c_str(), xsession_script.value().c_str(),
        session.exec.c_str(), nullptr);
  PLOG(ERROR) << "Failed to exec XSession";
  std::move(quit_closure).Run();
}

}  // namespace

int XSessionChooserMain() {
#if GTK_CHECK_VERSION(3, 90, 0)
  gtk_init();
#else
  gtk_init(nullptr, nullptr);
#endif

  base::SingleThreadTaskExecutor task_executor(base::MessagePumpType::UI);
  base::RunLoop run_loop;

  SessionDialog dialog(CollectXSessions(),
                       base::BindOnce(&ExecXSession, run_loop.QuitClosure()),
                       run_loop.QuitClosure());
  dialog.Show();

  run_loop.Run();

  // Control only gets to here if something went wrong.
  return 1;
}

}  // namespace remoting