blob: 9e2b57f189dd5b949519ca9966f4e308645bef3d [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/process/process.h"
#include "base/run_loop.h"
#include "services/shell/public/cpp/identity.h"
#include "services/shell/public/cpp/shell_test.h"
#include "services/shell/public/interfaces/shell.mojom.h"
#include "services/shell/tests/lifecycle/lifecycle_unittest.mojom.h"
#include "services/shell/tests/util.h"
namespace shell {
namespace {
const char kTestAppName[] = "mojo:lifecycle_unittest_app";
const char kTestParentName[] = "mojo:lifecycle_unittest_parent";
const char kTestExeName[] = "exe:lifecycle_unittest_exe";
const char kTestPackageName[] = "mojo:lifecycle_unittest_package";
const char kTestPackageAppNameA[] = "mojo:lifecycle_unittest_package_app_a";
const char kTestPackageAppNameB[] = "mojo:lifecycle_unittest_package_app_b";
const char kTestName[] = "mojo:lifecycle_unittest";
void QuitLoop(base::RunLoop* loop) {
loop->Quit();
}
void DecrementCountAndQuitWhenZero(base::RunLoop* loop, size_t* count) {
if (!--(*count))
loop->Quit();
}
struct Instance {
Instance() : id(mojom::kInvalidInstanceID), pid(0) {}
Instance(const Identity& identity, uint32_t id, uint32_t pid)
: identity(identity), id(id), pid(pid) {}
Identity identity;
uint32_t id;
uint32_t pid;
};
class InstanceState : public mojom::InstanceListener {
public:
InstanceState(mojom::InstanceListenerRequest request, base::RunLoop* loop)
: binding_(this, std::move(request)), loop_(loop) {}
~InstanceState() override {}
bool HasInstanceForName(const std::string& name) const {
return instances_.find(name) != instances_.end();
}
size_t GetNewInstanceCount() const {
return instances_.size() - initial_instances_.size();
}
void WaitForInstanceDestruction(base::RunLoop* loop) {
DCHECK(!destruction_loop_);
destruction_loop_ = loop;
// First of all check to see if we should be spinning this loop at all -
// the app(s) we're waiting on quitting may already have quit.
TryToQuitDestructionLoop();
}
private:
// mojom::InstanceListener:
void SetExistingInstances(
mojo::Array<mojom::InstanceInfoPtr> instances) override {
for (const auto& instance : instances) {
Instance i(instance->identity.To<Identity>(), instance->id,
instance->pid);
initial_instances_[i.identity.name()] = i;
instances_[i.identity.name()] = i;
}
loop_->Quit();
}
void InstanceCreated(mojom::InstanceInfoPtr instance) override {
instances_[instance->identity->name] =
Instance(instance->identity.To<Identity>(), instance->id,
instance->pid);
}
void InstanceDestroyed(uint32_t id) override {
for (auto it = instances_.begin(); it != instances_.end(); ++it) {
if (it->second.id == id) {
instances_.erase(it);
break;
}
}
TryToQuitDestructionLoop();
}
void InstancePIDAvailable(uint32_t id, uint32_t pid) override {
for (auto& instance : instances_) {
if (instance.second.id == id) {
instance.second.pid = pid;
break;
}
}
}
void TryToQuitDestructionLoop() {
if (!GetNewInstanceCount() && destruction_loop_) {
destruction_loop_->Quit();
destruction_loop_ = nullptr;
}
}
// All currently running instances.
std::map<std::string, Instance> instances_;
// The initial set of instances.
std::map<std::string, Instance> initial_instances_;
mojo::Binding<mojom::InstanceListener> binding_;
base::RunLoop* loop_;
// Set when the client wants to wait for this object to track the destruction
// of an instance before proceeding.
base::RunLoop* destruction_loop_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(InstanceState);
};
} // namespace
class LifecycleTest : public test::ShellTest {
public:
LifecycleTest() : ShellTest(kTestName) {}
~LifecycleTest() override {}
protected:
// test::ShellTest:
void SetUp() override {
test::ShellTest::SetUp();
InitPackage();
instances_ = TrackInstances();
}
void TearDown() override {
instances_.reset();
test::ShellTest::TearDown();
}
bool CanRunCrashTest() {
return !base::CommandLine::ForCurrentProcess()->HasSwitch("single-process");
}
void InitPackage() {
test::mojom::LifecycleControlPtr lifecycle = ConnectTo(kTestPackageName);
base::RunLoop loop;
lifecycle.set_connection_error_handler(base::Bind(&QuitLoop, &loop));
lifecycle->GracefulQuit();
loop.Run();
}
test::mojom::LifecycleControlPtr ConnectTo(const std::string& name) {
test::mojom::LifecycleControlPtr lifecycle;
connector()->ConnectToInterface(name, &lifecycle);
PingPong(lifecycle.get());
return lifecycle;
}
base::Process LaunchProcess() {
base::Process process;
test::LaunchAndConnectToProcess(
#if defined(OS_WIN)
"lifecycle_unittest_exe.exe",
#else
"lifecycle_unittest_exe",
#endif
Identity(kTestExeName, mojom::kInheritUserID),
connector(),
&process);
return process;
}
void PingPong(test::mojom::LifecycleControl* lifecycle) {
base::RunLoop loop;
lifecycle->Ping(base::Bind(&QuitLoop, &loop));
loop.Run();
}
InstanceState* instances() { return instances_.get(); }
void WaitForInstanceDestruction() {
base::RunLoop loop;
instances()->WaitForInstanceDestruction(&loop);
loop.Run();
}
private:
std::unique_ptr<InstanceState> TrackInstances() {
mojom::ShellPtr shell;
connector()->ConnectToInterface("mojo:shell", &shell);
mojom::InstanceListenerPtr listener;
base::RunLoop loop;
InstanceState* state = new InstanceState(GetProxy(&listener), &loop);
shell->AddInstanceListener(std::move(listener));
loop.Run();
return base::WrapUnique(state);
}
std::unique_ptr<InstanceState> instances_;
DISALLOW_COPY_AND_ASSIGN(LifecycleTest);
};
TEST_F(LifecycleTest, Standalone_GracefulQuit) {
test::mojom::LifecycleControlPtr lifecycle = ConnectTo(kTestAppName);
EXPECT_TRUE(instances()->HasInstanceForName(kTestAppName));
EXPECT_EQ(1u, instances()->GetNewInstanceCount());
base::RunLoop loop;
lifecycle.set_connection_error_handler(base::Bind(&QuitLoop, &loop));
lifecycle->GracefulQuit();
loop.Run();
WaitForInstanceDestruction();
EXPECT_FALSE(instances()->HasInstanceForName(kTestAppName));
EXPECT_EQ(0u, instances()->GetNewInstanceCount());
}
TEST_F(LifecycleTest, Standalone_Crash) {
if (!CanRunCrashTest()) {
LOG(INFO) << "Skipping Standalone_Crash test in --single-process mode.";
return;
}
test::mojom::LifecycleControlPtr lifecycle = ConnectTo(kTestAppName);
EXPECT_TRUE(instances()->HasInstanceForName(kTestAppName));
EXPECT_EQ(1u, instances()->GetNewInstanceCount());
base::RunLoop loop;
lifecycle.set_connection_error_handler(base::Bind(&QuitLoop, &loop));
lifecycle->Crash();
loop.Run();
WaitForInstanceDestruction();
EXPECT_FALSE(instances()->HasInstanceForName(kTestAppName));
EXPECT_EQ(0u, instances()->GetNewInstanceCount());
}
TEST_F(LifecycleTest, Standalone_CloseShellConnection) {
test::mojom::LifecycleControlPtr lifecycle = ConnectTo(kTestAppName);
EXPECT_TRUE(instances()->HasInstanceForName(kTestAppName));
EXPECT_EQ(1u, instances()->GetNewInstanceCount());
base::RunLoop loop;
lifecycle.set_connection_error_handler(base::Bind(&QuitLoop, &loop));
lifecycle->CloseShellConnection();
WaitForInstanceDestruction();
// |lifecycle| pipe should still be valid.
PingPong(lifecycle.get());
}
TEST_F(LifecycleTest, PackagedApp_GracefulQuit) {
test::mojom::LifecycleControlPtr lifecycle = ConnectTo(kTestPackageAppNameA);
// There should be two new instances - one for the app and one for the package
// that vended it.
EXPECT_TRUE(instances()->HasInstanceForName(kTestPackageAppNameA));
EXPECT_TRUE(instances()->HasInstanceForName(kTestPackageName));
EXPECT_EQ(2u, instances()->GetNewInstanceCount());
base::RunLoop loop;
lifecycle.set_connection_error_handler(base::Bind(&QuitLoop, &loop));
lifecycle->GracefulQuit();
loop.Run();
WaitForInstanceDestruction();
EXPECT_FALSE(instances()->HasInstanceForName(kTestPackageName));
EXPECT_FALSE(instances()->HasInstanceForName(kTestAppName));
EXPECT_EQ(0u, instances()->GetNewInstanceCount());
}
TEST_F(LifecycleTest, PackagedApp_Crash) {
if (!CanRunCrashTest()) {
LOG(INFO) << "Skipping Standalone_Crash test in --single-process mode.";
return;
}
test::mojom::LifecycleControlPtr lifecycle = ConnectTo(kTestPackageAppNameA);
// There should be two new instances - one for the app and one for the package
// that vended it.
EXPECT_TRUE(instances()->HasInstanceForName(kTestPackageAppNameA));
EXPECT_TRUE(instances()->HasInstanceForName(kTestPackageName));
EXPECT_EQ(2u, instances()->GetNewInstanceCount());
base::RunLoop loop;
lifecycle.set_connection_error_handler(base::Bind(&QuitLoop, &loop));
lifecycle->Crash();
loop.Run();
WaitForInstanceDestruction();
EXPECT_FALSE(instances()->HasInstanceForName(kTestPackageName));
EXPECT_FALSE(instances()->HasInstanceForName(kTestPackageAppNameA));
EXPECT_EQ(0u, instances()->GetNewInstanceCount());
}
// When a single package provides multiple apps out of one process, crashing one
// app crashes all.
TEST_F(LifecycleTest, PackagedApp_CrashCrashesOtherProvidedApp) {
if (!CanRunCrashTest()) {
LOG(INFO) << "Skipping Standalone_Crash test in --single-process mode.";
return;
}
test::mojom::LifecycleControlPtr lifecycle_a =
ConnectTo(kTestPackageAppNameA);
test::mojom::LifecycleControlPtr lifecycle_b =
ConnectTo(kTestPackageAppNameB);
test::mojom::LifecycleControlPtr lifecycle_package =
ConnectTo(kTestPackageName);
// There should be three instances, one for each packaged app and the package
// itself.
EXPECT_TRUE(instances()->HasInstanceForName(kTestPackageAppNameA));
EXPECT_TRUE(instances()->HasInstanceForName(kTestPackageAppNameB));
EXPECT_TRUE(instances()->HasInstanceForName(kTestPackageName));
size_t instance_count = instances()->GetNewInstanceCount();
EXPECT_EQ(3u, instance_count);
base::RunLoop loop;
lifecycle_a.set_connection_error_handler(
base::Bind(&DecrementCountAndQuitWhenZero, &loop, &instance_count));
lifecycle_b.set_connection_error_handler(
base::Bind(&DecrementCountAndQuitWhenZero, &loop, &instance_count));
lifecycle_package.set_connection_error_handler(
base::Bind(&DecrementCountAndQuitWhenZero, &loop, &instance_count));
// Now crash one of the packaged apps.
lifecycle_a->Crash();
loop.Run();
WaitForInstanceDestruction();
EXPECT_FALSE(instances()->HasInstanceForName(kTestPackageName));
EXPECT_FALSE(instances()->HasInstanceForName(kTestPackageAppNameA));
EXPECT_FALSE(instances()->HasInstanceForName(kTestPackageAppNameB));
EXPECT_EQ(0u, instances()->GetNewInstanceCount());
}
// When a single package provides multiple apps out of one process, crashing one
// app crashes all.
TEST_F(LifecycleTest, PackagedApp_GracefulQuitPackageQuitsAll) {
test::mojom::LifecycleControlPtr lifecycle_a =
ConnectTo(kTestPackageAppNameA);
test::mojom::LifecycleControlPtr lifecycle_b =
ConnectTo(kTestPackageAppNameB);
test::mojom::LifecycleControlPtr lifecycle_package =
ConnectTo(kTestPackageName);
// There should be three instances, one for each packaged app and the package
// itself.
EXPECT_TRUE(instances()->HasInstanceForName(kTestPackageAppNameA));
EXPECT_TRUE(instances()->HasInstanceForName(kTestPackageAppNameB));
EXPECT_TRUE(instances()->HasInstanceForName(kTestPackageName));
size_t instance_count = instances()->GetNewInstanceCount();
EXPECT_EQ(3u, instance_count);
base::RunLoop loop;
lifecycle_a.set_connection_error_handler(
base::Bind(&DecrementCountAndQuitWhenZero, &loop, &instance_count));
lifecycle_b.set_connection_error_handler(
base::Bind(&DecrementCountAndQuitWhenZero, &loop, &instance_count));
lifecycle_package.set_connection_error_handler(
base::Bind(&DecrementCountAndQuitWhenZero, &loop, &instance_count));
// Now quit the package. All the packaged apps should close.
lifecycle_package->GracefulQuit();
loop.Run();
WaitForInstanceDestruction();
EXPECT_FALSE(instances()->HasInstanceForName(kTestPackageName));
EXPECT_FALSE(instances()->HasInstanceForName(kTestPackageAppNameA));
EXPECT_FALSE(instances()->HasInstanceForName(kTestPackageAppNameB));
EXPECT_EQ(0u, instances()->GetNewInstanceCount());
}
TEST_F(LifecycleTest, Exe_GracefulQuit) {
base::Process process = LaunchProcess();
test::mojom::LifecycleControlPtr lifecycle = ConnectTo(kTestExeName);
EXPECT_TRUE(instances()->HasInstanceForName(kTestExeName));
EXPECT_EQ(1u, instances()->GetNewInstanceCount());
base::RunLoop loop;
lifecycle.set_connection_error_handler(base::Bind(&QuitLoop, &loop));
lifecycle->GracefulQuit();
loop.Run();
WaitForInstanceDestruction();
EXPECT_FALSE(instances()->HasInstanceForName(kTestExeName));
EXPECT_EQ(0u, instances()->GetNewInstanceCount());
process.Terminate(9, true);
}
TEST_F(LifecycleTest, Exe_TerminateProcess) {
base::Process process = LaunchProcess();
test::mojom::LifecycleControlPtr lifecycle = ConnectTo(kTestExeName);
EXPECT_TRUE(instances()->HasInstanceForName(kTestExeName));
EXPECT_EQ(1u, instances()->GetNewInstanceCount());
base::RunLoop loop;
lifecycle.set_connection_error_handler(base::Bind(&QuitLoop, &loop));
process.Terminate(9, true);
loop.Run();
WaitForInstanceDestruction();
EXPECT_FALSE(instances()->HasInstanceForName(kTestExeName));
EXPECT_EQ(0u, instances()->GetNewInstanceCount());
}
TEST_F(LifecycleTest, ShutdownTree) {
// Verifies that Instances are destroyed when their creator is.
std::unique_ptr<Connection> parent_connection =
connector()->Connect(kTestParentName);
test::mojom::ParentPtr parent;
parent_connection->GetInterface(&parent);
// This asks kTestParentName to open a connection to kTestAppName and blocks
// on a response from a Ping().
{
base::RunLoop loop;
parent->ConnectToChild(base::Bind(&QuitLoop, &loop));
loop.Run();
}
// Should now have two new instances (parent and child).
EXPECT_EQ(2u, instances()->GetNewInstanceCount());
EXPECT_TRUE(instances()->HasInstanceForName(kTestParentName));
EXPECT_TRUE(instances()->HasInstanceForName(kTestAppName));
parent->Quit();
// Quitting the parent should cascade-quit the child.
WaitForInstanceDestruction();
EXPECT_EQ(0u, instances()->GetNewInstanceCount());
EXPECT_FALSE(instances()->HasInstanceForName(kTestParentName));
EXPECT_FALSE(instances()->HasInstanceForName(kTestAppName));
}
} // namespace shell