File: service_unittest.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 (552 lines) | stat: -rw-r--r-- 20,932 bytes parent folder | download | duplicates (5)
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
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <objbase.h>

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

#include <map>
#include <memory>
#include <numeric>
#include <ostream>
#include <string>
#include <utility>
#include <vector>

#include "base/barrier_closure.h"
#include "base/base_paths.h"
#include "base/environment.h"
#include "base/files/file_path_watcher.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/process/process.h"
#include "base/process/process_handle.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/synchronization/lock.h"
#include "base/task/bind_post_task.h"
#include "base/task/single_thread_task_runner_thread_mode.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/threading/sequence_bound.h"
#include "base/threading/thread.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/types/expected.h"
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/windows_handle_util.h"
#include "chrome/common/env_vars.h"
#include "chrome/windows_services/service_program/test_service_idl.h"
#include "chrome/windows_services/service_program/test_support/service_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"
#include "third_party/crashpad/crashpad/client/crash_report_database.h"
#include "third_party/crashpad/crashpad/client/prune_crash_reports.h"

namespace {

// Collects log messages from test service processes on behalf of a test.
class LogCollector {
 public:
  explicit LogCollector(ServiceEnvironment& service_environment);
  LogCollector(const LogCollector&) = delete;
  LogCollector& operator=(const LogCollector&) = delete;
  ~LogCollector();

  // Emits all messages that have been collected to the test process's log
  // output in a single message.
  void EmitLogs();

 private:
  // A mapping from a test service's process id to a sequence of intercepted log
  // messages ordered by time of arrival. Due to aggressive PID reuse on
  // Windows, it is possible that messages from distinct instances will be
  // grouped under the same PID.
  using ServiceLogsMap = std::map<base::ProcessId, std::vector<std::string>>;

  // A ServiceEnvironment::LogMessageCallback.
  bool OnLogMessage(base::ProcessId process_id, std::string_view message);

  const raw_ref<ServiceEnvironment> service_environment_;
  base::Lock lock_;
  ServiceLogsMap service_logs_ GUARDED_BY(lock_);
};

LogCollector::LogCollector(ServiceEnvironment& service_environment)
    : service_environment_(service_environment) {
  // Unretained is safe here because the callback is cleared in the dtor.
  service_environment.SetLogMessageCallback(
      base::BindRepeating(&LogCollector::OnLogMessage, base::Unretained(this)));
}

LogCollector::~LogCollector() {
  service_environment_->SetLogMessageCallback({});
}

void LogCollector::EmitLogs() {
  ServiceLogsMap service_logs;
  {
    base::AutoLock lock(lock_);
    service_logs.swap(service_logs_);
  }
  for (const auto& [pid, messages] : service_logs) {
    LOG(ERROR) << "LOG MESSAGES FROM SERVICE PID " << pid << " "
               << testing::PrintToString(messages);
  }
}

bool LogCollector::OnLogMessage(base::ProcessId process_id,
                                std::string_view message) {
  base::AutoLock lock(lock_);
  service_logs_[process_id].emplace_back(message);
  return true;  // Suppress emission of the message by the LogGrabber.
}

}  // namespace

// A test harness that installs the test service at test suite setup time (i.e.,
// once for all tests that use this harness) and provides facilities for calling
// into the service.
class ServiceTest : public testing::Test {
 protected:
  static void SetUpTestSuite() {
    if (!::IsUserAnAdmin()) {
      GTEST_SKIP() << "Test requires admin rights";
    }
    service_environment_ = new ServiceEnvironment(
        L"Chromium Test Service", FILE_PATH_LITERAL("test_service.exe"),
        /*testing_switch=*/{}, __uuidof(TestService), __uuidof(ITestService));
    ASSERT_TRUE(service_environment_->is_valid());
  }

  static void TearDownTestSuite() {
    delete std::exchange(service_environment_, nullptr);
  }

  ServiceTest() = default;
  ~ServiceTest() override {
    if (HasFailure()) {  // Emit server logs in case of failure.
      log_collector_.EmitLogs();
    }
  }

  void SetUp() override { ASSERT_TRUE(com_initializer_.Succeeded()); }

  // Instantiates the test service, returning a reference to it in
  // `test_service`. Asserts in case of failure.
  static void CreateService(
      Microsoft::WRL::ComPtr<ITestService>& test_service) {
    Microsoft::WRL::ComPtr<IUnknown> unknown;
    ASSERT_HRESULT_SUCCEEDED(::CoCreateInstance(__uuidof(TestService), nullptr,
                                                CLSCTX_LOCAL_SERVER,
                                                IID_PPV_ARGS(&unknown)));

    ASSERT_HRESULT_SUCCEEDED(unknown.As(&test_service));
    unknown.Reset();

    ASSERT_HRESULT_SUCCEEDED(::CoSetProxyBlanket(
        test_service.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
        COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
        RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING));
  }

  // Returns a handle to `test_service`'s process in `process`. Asserts in case
  // of failure.
  static void GetServiceProcess(
      Microsoft::WRL::ComPtr<ITestService>& test_service,
      base::Process& process) {
    unsigned long handle_value = 0;
    ASSERT_HRESULT_SUCCEEDED(test_service->GetProcessHandle(&handle_value));
    HANDLE service_process_handle = base::win::Uint32ToHandle(handle_value);
    ASSERT_NE(service_process_handle, nullptr);
    ASSERT_NE(service_process_handle, INVALID_HANDLE_VALUE);
    process = base::Process(service_process_handle);
  }

 private:
  static ServiceEnvironment* service_environment_;

  // The main thread is an STA thread, so it must run a UI message loop.
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::MainThreadType::UI};
  base::win::ScopedCOMInitializer com_initializer_;
  LogCollector log_collector_{*service_environment_};
};

// static
ServiceEnvironment* ServiceTest::service_environment_ = nullptr;

namespace {

// Map a (creation time, pid) pair, identifying a single instance of the test
// service, to a sequence of (tick count, tid) pairs, identifying the client
// thread and time of each request to the service.
using ServiceMap = std::map<std::pair<base::Time, base::ProcessId>,
                            std::vector<std::pair<DWORD, DWORD>>>;

}  // namespace

namespace std {

// Teach Google Test how to print a ServiceMap.
void PrintTo(const ServiceMap& service_map, std::ostream* os) {
  *os << "(";
  bool first = true;
  for (const auto& [key, transactions] : service_map) {
    const auto& [creation_time, pid] = key;
    if (!first) {
      *os << ",";
    } else {
      first = false;
    }
    *os << "(" << creation_time << "," << pid
        << "):" << testing::PrintToString(transactions);
  }
  *os << ")";
}

}  // namespace std

// Tests that a service can handle two requests on the same object.
TEST_F(ServiceTest, TwoRequests) {
  base::Process service_process;
  base::Process service_process2;

  Microsoft::WRL::ComPtr<ITestService> test_service;
  ASSERT_NO_FATAL_FAILURE(CreateService(test_service));
  ASSERT_NO_FATAL_FAILURE(GetServiceProcess(test_service, service_process));
  ASSERT_NO_FATAL_FAILURE(GetServiceProcess(test_service, service_process2));
  test_service.Reset();

  ASSERT_EQ(service_process.Pid(), service_process2.Pid());
  service_process2.Close();
  int exit_code = 0;
  service_process.WaitForExit(&exit_code);
  ASSERT_EQ(exit_code, 0);
}

TEST_F(ServiceTest, IsRunningUnattended) {
  Microsoft::WRL::ComPtr<ITestService> test_service;
  ASSERT_NO_FATAL_FAILURE(CreateService(test_service));
  VARIANT_BOOL is_running_unattended = VARIANT_FALSE;
  ASSERT_HRESULT_SUCCEEDED(
      test_service->IsRunningUnattended(&is_running_unattended));
  ASSERT_EQ(is_running_unattended != VARIANT_FALSE,
            base::Environment::Create()->HasVar(env_vars::kHeadless));
}

// Tests that a service can handle rapid use that should result in some requests
// happening in the same instance of the service as a previous request, while
// some are handled in a separate instance of the service. This is a regression
// test against https://crbug.com/375097840.
TEST_F(ServiceTest, RapidReuse) {
  // Calculate the average time to start the service, get an answer from it, and
  // for it to terminate over five runs. Ignore the first run, as it is expected
  // to be slower.
  base::TimeDelta average_call_time;
  {
    constexpr int kTimingTries = 5;
    for (int i = 0; i < kTimingTries + 1; ++i) {
      base::Process service_process;

      base::ElapsedTimer elapsed_timer;
      Microsoft::WRL::ComPtr<ITestService> test_service;
      ASSERT_NO_FATAL_FAILURE(CreateService(test_service));
      ASSERT_NO_FATAL_FAILURE(GetServiceProcess(test_service, service_process));
      test_service.Reset();
      int exit_code = 0;
      ASSERT_TRUE(service_process.WaitForExit(&exit_code));
      if (i) {  // Ignore the first run.
        average_call_time = elapsed_timer.Elapsed();
      }
      ASSERT_EQ(exit_code, 0);
    }
    average_call_time /= kTimingTries;
  }

  // Now throw off a group of tasks that will race each other to repeatedly call
  // the service at random intervals around `average_call_time`.
  size_t succeeded_count = 0;  // The number of tasks that ran to completion.
  ServiceMap transactions;     // The requests processed by each service.

  // Adds a single task's success/failure and requests collection to the overall
  // stats, then runs a given `quit_closure`. This is run on the main thread
  // following each task's completion.
  auto result_accumulator = base::BindLambdaForTesting(
      [&succeeded_count, &transactions](base::RepeatingClosure quit_closure,
                                        bool succeeded,
                                        const ServiceMap& task_transactions) {
        if (succeeded) {
          ++succeeded_count;
        }
        // Merge this task's requests in with those from all other tasks that
        // have completed so far, sorting each process's by the time at which
        // the client made the request.
        for (const auto& [process, xactions] : task_transactions) {
          auto& combined = transactions[process];
          combined.insert(combined.end(), xactions.begin(), xactions.end());
          std::ranges::stable_sort(
              combined, [](auto& a, auto& b) { return a.first < b.first; });
        }
        std::move(quit_closure).Run();
      });

  // Issues requests to the service in a loop for five seconds; running
  // `on_result` with the results on completion. Asserts in case of failure.
  auto task = base::BindRepeating(
      [](base::TimeDelta average_call_time,
         base::OnceCallback<void(bool, const ServiceMap&)> on_result) {
        bool succeeded = false;
        ServiceMap task_transactions;
        absl::Cleanup return_results = [&on_result, &succeeded,
                                        &task_transactions] {
          std::move(on_result).Run(succeeded, task_transactions);
        };

        base::ProcessId last_pid = base::kNullProcessId;
        const DWORD tid = ::GetCurrentThreadId();
        for (base::ElapsedTimer timer; timer.Elapsed() < base::Seconds(5);) {
          base::Process service_process;
          Microsoft::WRL::ComPtr<ITestService> test_service;
          base::ElapsedTimer transaction_timer;
          const auto tick_count = ::GetTickCount();
          ASSERT_NO_FATAL_FAILURE(CreateService(test_service))
              << "Last service pid: " << last_pid
              << " at tick_count: " << tick_count << " from thread: " << tid;
          ASSERT_NO_FATAL_FAILURE(
              GetServiceProcess(test_service, service_process));
          // Drop the connection.
          test_service.Reset();
          base::ProcessId pid = service_process.Pid();
          ASSERT_NE(pid, base::kNullProcessId);
          base::Time creation_time = service_process.CreationTime();
          ASSERT_FALSE(creation_time.is_null());
          service_process.Close();
          task_transactions[std::make_pair(creation_time, pid)].emplace_back(
              tick_count, tid);
          last_pid = pid;

          // Wait a bit to give the service a chance to get closer to shutting
          // down.
          base::TimeDelta remaining =
              average_call_time - transaction_timer.Elapsed();
          if (remaining > base::TimeDelta()) {
            base::PlatformThread::Sleep(base::RandTimeDeltaUpTo(remaining));
          }
        }
        // If execution reached this point, all requests were handled.
        succeeded = true;
      });

  base::RunLoop run_loop;

  // Reduce to only one thread due to flaky CO_E_SERVER_EXEC_FAILURE; see
  // https://crbug.com/375097840.
  static constexpr int kTaskCount = 1;

  // Quit `run_loop` after all `kTaskCount` tasks have run this closure.
  base::RepeatingClosure quit_barrier =
      base::BarrierClosure(kTaskCount, run_loop.QuitClosure());

  // Issue the tasks.
  for (int i = 0; i < kTaskCount; ++i) {
    base::ThreadPool::CreateCOMSTATaskRunner(
        {}, base::SingleThreadTaskRunnerThreadMode::DEDICATED)
        ->PostTask(
            FROM_HERE,
            base::BindOnce(task, average_call_time,
                           base::BindPostTaskToCurrentDefault(base::BindOnce(
                               result_accumulator, quit_barrier))));
  }
  run_loop.Run();  // Wait for all tasks to return their results.

  if (HasFailure()) {
    LOG(ERROR) << succeeded_count << " out of " << kTaskCount
               << " tasks ran to completion";
    LOG(ERROR) << transactions.size() << " services handled a total of "
               << std::accumulate(
                      transactions.begin(), transactions.end(), size_t(0),
                      [](size_t acc, const ServiceMap::value_type& v) {
                        return acc + v.second.size();
                      })
               << " requests";
    LOG(ERROR) << "transactions: " << testing::PrintToString(transactions);
  }
}

// Crashpad delegates to ASAN's exception handler when `is_asan = true`, so no
// dumps are generated in the crashpad database.
#if !defined(ADDRESS_SANITIZER)

// A test fixture for validating crashpad integration.
class ServiceCrashTest : public ServiceTest {
 protected:
  void SetUp() override {
    ASSERT_TRUE(io_thread_.StartWithOptions({base::MessagePumpType::IO,
                                             /*size=*/0}));
  }

  void TearDown() override {
    database_watcher_.SynchronouslyResetForTest();
    DeleteCrashDatabase();
  }

  void CreateServiceForCrashTest(
      Microsoft::WRL::ComPtr<ITestService>& test_service) {
    CreateService(test_service);
    if (!test_service) {
      return;
    }
    if (crashpad_database_path_.empty()) {
      // This is the first connection to the service. Get the path to its
      // crashpad database from the service.
      base::win::ScopedBstr database_path;
      ASSERT_HRESULT_SUCCEEDED(
          test_service->GetCrashpadDatabasePath(database_path.Receive()));
      crashpad_database_path_ = base::FilePath(
          std::wstring_view(database_path.Get(), database_path.Length()));

      // Delete all existing crash reports that may be left over from past
      // executions.
      PruneOldReports();

      // Start watching the crash database for modifications.
      WatchForNewReports();
    }
  }

  // Waits for a new crash report to appear in the service's crash database.
  void WaitForDump() {
    base::RunLoop run_loop;
    on_dump_closure_ = run_loop.QuitClosure();
    run_loop.Run();
  }

 private:
  class PruneAllCondition : public crashpad::PruneCondition {
   public:
    // crashpad::PruneCondition:
    bool ShouldPruneReport(
        const crashpad::CrashReportDatabase::Report&) override {
      return true;
    }
    void ResetPruneConditionState() override {}
  };

  // Deletes all existing crash reports from the test service.
  void PruneOldReports() {
    if (auto database =
            crashpad::CrashReportDatabase::InitializeWithoutCreating(
                crashpad_database_path_);
        database) {
      PruneAllCondition all_condition;
      crashpad::PruneCrashReportDatabase(database.get(), &all_condition);

      // Make sure that the database is now empty of all reports.
      std::vector<crashpad::CrashReportDatabase::Report> reports;
      ASSERT_EQ(database->GetPendingReports(&reports),
                crashpad::CrashReportDatabase::kNoError);
      ASSERT_TRUE(reports.empty());
      ASSERT_EQ(database->GetCompletedReports(&reports),
                crashpad::CrashReportDatabase::kNoError);
      ASSERT_TRUE(reports.empty());
    }
  }

  // Starts monitoring for new crash reports to appear in the database.
  void WatchForNewReports() {
    base::RunLoop run_loop;
    database_watcher_.emplace(io_thread_.task_runner());

    // OnDatabaseChange will be called on the main test thread for any change to
    // the crash database's "metadata" file. This file is written to after a
    // dump is written to disk.
    database_watcher_.AsyncCall(&base::FilePathWatcher::WatchWithChangeInfo)
        .WithArgs(
            crashpad_database_path_.Append(FILE_PATH_LITERAL("metadata")),
            base::FilePathWatcher::WatchOptions{
                .type = base::FilePathWatcher::Type::kNonRecursive},
            base::BindPostTaskToCurrentDefault(base::BindRepeating(
                &ServiceCrashTest::OnDatabaseChange, base::Unretained(this))))
        .Then(base::BindOnce(
            [](base::OnceClosure quit_loop, bool succeeded) {
              std::move(quit_loop).Run();
              ASSERT_TRUE(succeeded);
            },
            run_loop.QuitClosure()));
    run_loop.Run();
  }

  // Deletes the entire crash database from the test service.
  void DeleteCrashDatabase() {
    base::DeletePathRecursively(crashpad_database_path_);
  }

  // Processes a change to the crash database on the main thread.
  void OnDatabaseChange(const base::FilePathWatcher::ChangeInfo& change_info,
                        const base::FilePath& path,
                        bool error) {
    ASSERT_FALSE(error);
    if (!on_dump_closure_) {
      return;  // Not presently waiting for a dump to appear.
    }
    std::optional<crashpad::CrashReportDatabase::Report> report;
    if (auto database =
            crashpad::CrashReportDatabase::InitializeWithoutCreating(
                crashpad_database_path_);
        database) {
      // Search the database for any report. The database is cleared during
      // startup, so any report must be generated by the service under test.
      std::vector<crashpad::CrashReportDatabase::Report> reports;
      ASSERT_EQ(database->GetPendingReports(&reports),
                crashpad::CrashReportDatabase::kNoError);
      if (reports.empty()) {
        ASSERT_EQ(database->GetCompletedReports(&reports),
                  crashpad::CrashReportDatabase::kNoError);
      }
      if (!reports.empty()) {
        report.emplace(std::move(reports.front()));
      }
    }
    if (report.has_value()) {
      std::move(on_dump_closure_).Run();
    }
  }

  // The path to the service's crashpad database.
  base::FilePath crashpad_database_path_;

  // A thread with an IO message loop for using a FilePathWatcher.
  base::Thread io_thread_{"IO Thread"};

  // A watcher to be notified when the service's crashpad database is modified.
  base::SequenceBound<base::FilePathWatcher> database_watcher_;

  // A callback that is run when a dump has been added to the service's crashpad
  // database.
  base::OnceClosure on_dump_closure_;
};

// Tests that a dump is produced if a crash happens during a COM call.
TEST_F(ServiceCrashTest, InduceCrash) {
  Microsoft::WRL::ComPtr<ITestService> test_service;
  ASSERT_NO_FATAL_FAILURE(CreateServiceForCrashTest(test_service));
  ASSERT_EQ(test_service->InduceCrash(), HRESULT_FROM_WIN32(RPC_S_CALL_FAILED));
  WaitForDump();
}

// Tests that a dump is produced if a crash happens in the background.
TEST_F(ServiceCrashTest, InduceCrashSoon) {
  Microsoft::WRL::ComPtr<ITestService> test_service;
  ASSERT_NO_FATAL_FAILURE(CreateServiceForCrashTest(test_service));
  ASSERT_HRESULT_SUCCEEDED(test_service->InduceCrashSoon());
  WaitForDump();
}

#endif  // !defined(ADDRESS_SANITIZER)