File: notification_helper_launches_chrome_unittest.cc

package info (click to toggle)
chromium 138.0.7204.183-1~deb12u1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm-proposed-updates
  • size: 6,080,960 kB
  • sloc: cpp: 34,937,079; ansic: 7,176,967; javascript: 4,110,704; python: 1,419,954; asm: 946,768; xml: 739,971; pascal: 187,324; sh: 89,623; perl: 88,663; objc: 79,944; sql: 50,304; cs: 41,786; fortran: 24,137; makefile: 21,811; php: 13,980; tcl: 13,166; yacc: 8,925; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (334 lines) | stat: -rw-r--r-- 13,097 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
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This test file is an evolution of
// chrome/notification_helper/notification_helper_process_unittest.cc. In
// addition to testing launching notification_helper.exe by the OS via registry
// which is what notification_helper_process_unittest is all about, this test
// also tests if chrome.exe can be successfully launched by
// notification_helper.exe via the NotificationActivator::Activate function.
//
// This test is compiled into unit_tests.exe rather than
// notification_helper_unittests.exe. This is because unit_tests.exe has data
// dependency on chrome.exe which is required by this test, and it's undesired
// to make notification_helper_unittests.exe have data dependency on chrome.exe.

#include <memory>
#include <string>

#include <NotificationActivationCallback.h>
#include <wrl/client.h>

#include "base/base_paths.h"
#include "base/containers/flat_map.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/path_service.h"
#include "base/process/kill.h"
#include "base/process/process.h"
#include "base/process/process_iterator.h"
#include "base/test/test_timeouts.h"
#include "base/win/scoped_com_initializer.h"
#include "build/build_config.h"
#include "chrome/install_static/install_util.h"
#include "chrome/installer/setup/install_worker.h"
#include "chrome/installer/util/install_util.h"
#include "chrome/installer/util/util_constants.h"
#include "chrome/installer/util/work_item.h"
#include "chrome/installer/util/work_item_list.h"
#include "chrome/test/base/process_inspector_win.h"
#include "content/public/common/result_codes.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

constexpr wchar_t kLaunchId[] =
    L"0|0|Default|0|https://example.com/|notification_id";

// Returns a handle to the process of id |pid| if it is an immediate child of
// |parent|.
base::Process OpenProcessIfChildOf(base::ProcessId pid,
                                   const base::Process& parent) {
  DCHECK(parent.IsValid());
  // PROCESS_VM_READ access right is required for ProcessInspector::Create()
  // below.
  auto process = base::Process::OpenWithExtraPrivileges(pid);
  if (!process.IsValid())
    return process;
  auto inspector = ProcessInspector::Create(process);
  if (!inspector || inspector->GetParentPid() != parent.Pid())
    process.Close();
  return process;
}

// Used to filter all the descendant processes of the given process.
class ProcessTreeFilter : public base::ProcessFilter {
 public:
  explicit ProcessTreeFilter(base::Process process)
      : parent_pid_(process.Pid()) {
    ancestor_processes_[process.Pid()] = std::move(process);
  }
  ProcessTreeFilter(const ProcessTreeFilter&) = delete;
  ProcessTreeFilter& operator=(const ProcessTreeFilter&) = delete;

  bool Includes(const base::ProcessEntry& entry) const override {
    auto iter = ancestor_processes_.find(entry.parent_pid());
    if (iter != ancestor_processes_.end()) {
      base::Process process = OpenProcessIfChildOf(entry.pid(), iter->second);

      // If the process is invalid, it could be an immediate child of
      // iter->second but has been killed resulting from its parent proc's being
      // killed. Despite this, its child processes may not be killed yet. So in
      // theory, we need to add its pid to ancestor_processes_ map and continue
      // hunting for its descendant processes.
      //
      // However, it is possible that the pid was reused, and we don't want to
      // kill the new proc's child processes. With this possibility, we choose
      // not to kill the new process if it is invalid. This works fine for this
      // test as chrome puts its sub-procs in a job object so that they all
      // should die with the parent.
      if (!process.IsValid())
        return false;

      has_child_process_alive_ = true;
      ancestor_processes_[entry.pid()] = std::move(process);
      return true;
    }
    return false;
  }

  bool has_child_process_alive() { return has_child_process_alive_; }

  void set_has_child_process_alive(bool has_child_process_alive) {
    has_child_process_alive_ = has_child_process_alive;
  }

 private:
  // The handles of the ancestor processes, indexed by process id.
  // Must be mutable because override function Includes() is const.
  mutable base::flat_map<base::ProcessId, base::Process> ancestor_processes_;

  // Id of the parent process.
  const base::ProcessId parent_pid_;

  // A flag indicating if there is any child process alive.
  // Must be mutable because override function Includes() is const.
  mutable bool has_child_process_alive_ = false;
};

// Kills |process| and all of its descendants. Child processes are explicitly
// killed to ensure that they do not outlive the test.
void KillProcessTree(base::Process process) {
  ProcessTreeFilter process_tree_filter(process.Duplicate());

  // Start by explicitly killing the main process.
  ASSERT_TRUE(process.Terminate(content::RESULT_CODE_KILLED, true /* wait */));

  // base::KillProcesses used in conjuction with KillProcessTree kills
  // processes from parent to child. Loop until all descendant processes are
  // killed with no more than kMaxTries tries.
  static constexpr int kMaxTries = 10;
  int num_tries = 0;
  base::FilePath::StringType exe_name = installer::kChromeExe;
  do {
    process_tree_filter.set_has_child_process_alive(false);
    base::KillProcesses(exe_name, content::RESULT_CODE_KILLED,
                        &process_tree_filter);
  } while (process_tree_filter.has_child_process_alive() &&
           ++num_tries < kMaxTries);

  DLOG_IF(ERROR, num_tries >= kMaxTries) << "Failed to kill all processes!";
}

// Returns the process with name |name| if it is found.
base::Process FindProcess(const std::wstring& name) {
  unsigned int pid;
  {
    base::NamedProcessIterator iter(name, nullptr);
    const auto* entry = iter.NextProcessEntry();
    if (!entry)
      return base::Process();
    pid = entry->pid();
  }

  auto process = base::Process::Open(pid);
  if (!process.IsValid())
    return process;

  // Since the process could go away suddenly before we open a handle to it,
  // it's possible that a different process was just opened and assigned the
  // same PID due to aggressive PID reuse. Now that a handle is held to *some*
  // process, take another run through the snapshot to see if the process with
  // this PID has the right exe name.
  base::NamedProcessIterator iter(name, nullptr);
  while (const auto* entry = iter.NextProcessEntry()) {
    if (entry->pid() == pid)
      return process;  // PID was not reused since the PID's match.
  }
  return base::Process();  // The PID was reused.
}

// Used to filter all the immediate child processes by process id.
class ChildProcessFilter : public base::ProcessFilter {
 public:
  explicit ChildProcessFilter(base::ProcessId parent_pid)
      : parent_pid_(parent_pid) {}
  ChildProcessFilter(const ChildProcessFilter&) = delete;
  ChildProcessFilter& operator=(const ChildProcessFilter&) = delete;

  bool Includes(const base::ProcessEntry& entry) const override {
    return parent_pid_ == entry.parent_pid();
  }

 private:
  const base::ProcessId parent_pid_;
};

}  // namespace

class NotificationHelperLaunchesChrome : public testing::Test {
 public:
  NotificationHelperLaunchesChrome(const NotificationHelperLaunchesChrome&) =
      delete;
  NotificationHelperLaunchesChrome& operator=(
      const NotificationHelperLaunchesChrome&) = delete;

 protected:
  NotificationHelperLaunchesChrome() : root_(HKEY_CURRENT_USER) {}

  ~NotificationHelperLaunchesChrome() override = default;

  void SetUp() override { ASSERT_NO_FATAL_FAILURE(RegisterServer()); }

  void TearDown() override {
    // The test creates a notification_helper process. When the test fails, this
    // process and its child processes can be left behind. We should clean it up
    // in this scenario.
    base::Process process = FindProcess(installer::kNotificationHelperExe);
    if (process.IsValid())
      KillProcessTree(std::move(process));

    ASSERT_NO_FATAL_FAILURE(UnregisterServer());
  }

 private:
  // Registers notification_helper.exe as the server.
  void RegisterServer() {
    ASSERT_TRUE(scoped_com_initializer_.Succeeded());

    // Notification_helper.exe is in the build output directory next to this
    // test executable, as the test build target has a data_deps dependency on
    // it.
    base::FilePath dir_exe;
    ASSERT_TRUE(base::PathService::Get(base::DIR_EXE, &dir_exe));
    base::FilePath notification_helper_path =
        dir_exe.Append(installer::kNotificationHelperExe);

    work_item_list_ = base::WrapUnique(WorkItem::CreateWorkItemList());

    installer::AddNativeNotificationWorkItems(root_, notification_helper_path,
                                              work_item_list_.get());

    ASSERT_TRUE(work_item_list_->Do());
  }

  // Unregisters the server by rolling back the work item list.
  void UnregisterServer() {
    if (work_item_list_)
      work_item_list_->Rollback();
  }

  // Predefined handle to the registry.
  const HKEY root_;

  // A list of work items on the registry.
  std::unique_ptr<WorkItemList> work_item_list_;

  base::win::ScopedCOMInitializer scoped_com_initializer_;
};

TEST_F(NotificationHelperLaunchesChrome, ChromeLaunchTest) {
  // There isn't a way to directly correlate the notification_helper.exe server
  // to this test. So we need to hunt for the server.
  base::Process notification_helper_process =
      FindProcess(installer::kNotificationHelperExe);
  ASSERT_FALSE(notification_helper_process.IsValid());

  Microsoft::WRL::ComPtr<INotificationActivationCallback>
      notification_activator;
  ASSERT_HRESULT_SUCCEEDED(::CoCreateInstance(
      install_static::GetToastActivatorClsid(), nullptr, CLSCTX_LOCAL_SERVER,
      IID_PPV_ARGS(&notification_activator)));
  ASSERT_TRUE(notification_activator);

  // The notification_helper server is now invoked upon the request of creating
  // the object instance. The server module now holds a reference of the
  // instance object, the notification_helper.exe process is alive waiting for
  // that reference to be released.
  notification_helper_process = FindProcess(installer::kNotificationHelperExe);
  ASSERT_TRUE(notification_helper_process.IsValid());

  // This relies on |notification_helper_process| outliving |filter| to ensure
  // that its pid isn't reused.
  ChildProcessFilter filter(notification_helper_process.Pid());
  int child_chrome_process_count = 0;
  base::Process notification_helper_crashpad;
  {
    base::NamedProcessIterator iter(installer::kChromeExe, &filter);
    while (const auto* entry = iter.NextProcessEntry()) {
      ++child_chrome_process_count;
      notification_helper_crashpad =
          OpenProcessIfChildOf(entry->pid(), notification_helper_process);
    }
  }
  // The notification_helper process has launched a child chrome process as its
  // crashpad handler.
  ASSERT_EQ(child_chrome_process_count, 1);
  ASSERT_TRUE(notification_helper_crashpad.IsValid());

  // Launch chrome.exe with the launch id from notification_helper.
  ASSERT_HRESULT_SUCCEEDED(
      notification_activator->Activate(L"", kLaunchId, nullptr, 0));

  // Now the notification_helper process has another immediate child process, in
  // addition to the crashpad child process as mentioned above. Note that
  // notification_helper has more than two descendant chrome processes. Kill all
  // notification_helper's child processes except for the crashpad child process
  // while counting.
  child_chrome_process_count = 0;
  {
    base::NamedProcessIterator iter(installer::kChromeExe, &filter);
    while (const auto* entry = iter.NextProcessEntry()) {
      if (entry->pid() == notification_helper_crashpad.Pid())
        continue;
      base::Process process =
          OpenProcessIfChildOf(entry->pid(), notification_helper_process);
      ASSERT_TRUE(process.IsValid());
      KillProcessTree(std::move(process));
      ++child_chrome_process_count;
    }
  }
  ASSERT_EQ(child_chrome_process_count, 1);

  // The crashpad process should be the only living child process of
  // notification_helper.
  child_chrome_process_count = 0;
  {
    base::NamedProcessIterator iter(installer::kChromeExe, &filter);
    while (iter.NextProcessEntry())
      ++child_chrome_process_count;
  }
  ASSERT_EQ(child_chrome_process_count, 1);

  // Release the instance object. Now that the last (and the only) instance
  // object of the module is released, the event living in the server
  // process is signaled, which allows the notification_helper process and its
  // crashpad child process to exit.
  notification_activator.Reset();
  ASSERT_TRUE(notification_helper_process.WaitForExitWithTimeout(
      TestTimeouts::action_timeout(), nullptr));
  ASSERT_TRUE(notification_helper_crashpad.WaitForExitWithTimeout(
      TestTimeouts::action_timeout(), nullptr));
}