Commit fea665d9 authored by Nicholas Hollingum's avatar Nicholas Hollingum Committed by Chromium LUCI CQ

borealis: Add a utility to shutdown when all windows are closed

We extend the BorealisWindowManager to track the lifetime of individual
windows, apps (as collections of windows) and the session (as
collections of apps.

This new observer was kept separate from the already-present anonymous
observer as the two have limited overlap, and are not used
simultaneously by any clients (currently).

We also implement a trivial watcher in the BorealisContext which joins
the new session-observing behaviour with the ShutdownMonitor. This
watcher will only be active while the context exists so we will not
accidentally issue shutdown requests while the vm is not running.

Bug: b/172006764, b/172979315
Change-Id: Ie58c87acbb14eebe41152986f6b86032a5d8c759
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2576876
Commit-Queue: Nic Hollingum <hollingum@google.com>
Reviewed-by: default avatarDaniel Ng <danielng@google.com>
Cr-Commit-Position: refs/heads/master@{#835006}
parent 600c612a
......@@ -3,13 +3,52 @@
// found in the LICENSE file.
#include "chrome/browser/chromeos/borealis/borealis_context.h"
#include "base/memory/ptr_util.h"
#include "base/scoped_observation.h"
#include "chrome/browser/chromeos/borealis/borealis_service.h"
#include "chrome/browser/chromeos/borealis/borealis_shutdown_monitor.h"
#include "chrome/browser/chromeos/borealis/borealis_window_manager.h"
namespace borealis {
class BorealisLifetimeObserver
: public BorealisWindowManager::AppWindowLifetimeObserver {
public:
explicit BorealisLifetimeObserver(Profile* profile)
: profile_(profile), observation_{this} {
observation_.Observe(
&BorealisService::GetForProfile(profile_)->WindowManager());
}
// BorealisWindowManager::AppWindowLifetimeObserver overrides.
void OnSessionStarted() override {
BorealisService::GetForProfile(profile_)
->ShutdownMonitor()
.CancelDelayedShutdown();
}
void OnSessionFinished() override {
BorealisService::GetForProfile(profile_)
->ShutdownMonitor()
.ShutdownWithDelay();
}
void OnWindowManagerDeleted(BorealisWindowManager* window_manager) override {
DCHECK(observation_.IsObservingSource(window_manager));
observation_.Reset();
}
private:
Profile* const profile_;
base::ScopedObservation<BorealisWindowManager,
BorealisWindowManager::AppWindowLifetimeObserver>
observation_;
};
BorealisContext::~BorealisContext() = default;
BorealisContext::BorealisContext(Profile* profile) : profile_(profile) {}
BorealisContext::BorealisContext(Profile* profile)
: profile_(profile),
lifetime_observer_(std::make_unique<BorealisLifetimeObserver>(profile)) {}
std::unique_ptr<BorealisContext>
BorealisContext::CreateBorealisContextForTesting(Profile* profile) {
......
......@@ -14,6 +14,8 @@ class Profile;
namespace borealis {
class BorealisLifetimeObserver;
// An object to track information about the state of the Borealis VM.
// BorealisContext objects should only be created by the Borealis Context
// Manager, which is why the constructor is private.
......@@ -48,11 +50,13 @@ class BorealisContext {
explicit BorealisContext(Profile* profile);
Profile* const profile_;
bool borealis_running_ = false;
std::string vm_name_;
std::string container_name_;
std::string root_path_;
base::FilePath disk_path_;
// This instance listens for the session to finish and issues an automatic
// shutdown when it does.
std::unique_ptr<BorealisLifetimeObserver> lifetime_observer_;
};
} // namespace borealis
......
......@@ -33,7 +33,21 @@ const std::string* GetWindowId(aura::Window* window) {
return exo::GetShellStartupId(window);
}
std::string WindowToAnonymousAppId(aura::Window* window) {
std::string WindowToAppId(Profile* profile, aura::Window* window) {
// TODO(b/173977876): When we have better ways of associating apps with
// windows we will implement them. Until then, the mapping is identical to
// Crostini's so just spoof the relevant information and use theirs.
std::string pretend_crostini_id(*GetWindowId(window));
base::ReplaceFirstSubstringAfterOffset(
&pretend_crostini_id, 0, kBorealisWindowPrefix, "org.chromium.termina.");
std::string crostini_equivalent_id =
crostini::GetCrostiniShelfAppId(profile, &pretend_crostini_id, nullptr);
// If crostini thinks this app is registered, then it actually is registered
// for borealis.
if (!crostini::IsUnmatchedCrostiniShelfAppId(crostini_equivalent_id))
return crostini_equivalent_id;
return kBorealisAnonymousPrefix + *GetWindowId(window);
}
......@@ -43,6 +57,11 @@ std::string AnonymousIdentifierToName(const std::string& anon_id) {
sizeof(kBorealisWindowPrefix) - 1);
}
bool IsAnonymousAppId(const std::string& app_id) {
return base::StartsWith(app_id, kBorealisAnonymousPrefix,
base::CompareCase::SENSITIVE);
}
} // namespace
namespace borealis {
......@@ -59,75 +78,95 @@ BorealisWindowManager::BorealisWindowManager(Profile* profile)
: profile_(profile) {}
BorealisWindowManager::~BorealisWindowManager() {
for (auto& id_to_windows : anon_ids_to_windows_) {
for (aura::Window* window : id_to_windows.second) {
window->RemoveObserver(this);
}
for (auto& observer : observers_) {
observer.OnAnonymousAppRemoved(id_to_windows.first);
}
for (auto& observer : anon_observers_) {
observer.OnWindowManagerDeleted(this);
}
for (auto& observer : observers_) {
for (auto& observer : lifetime_observers_) {
observer.OnWindowManagerDeleted(this);
}
DCHECK(!observers_.might_have_observers());
DCHECK(!anon_observers_.might_have_observers());
DCHECK(!lifetime_observers_.might_have_observers());
}
void BorealisWindowManager::AddObserver(AnonymousAppObserver* observer) {
observers_.AddObserver(observer);
anon_observers_.AddObserver(observer);
}
void BorealisWindowManager::RemoveObserver(AnonymousAppObserver* observer) {
observers_.RemoveObserver(observer);
anon_observers_.RemoveObserver(observer);
}
void BorealisWindowManager::AddObserver(AppWindowLifetimeObserver* observer) {
lifetime_observers_.AddObserver(observer);
}
void BorealisWindowManager::RemoveObserver(
AppWindowLifetimeObserver* observer) {
lifetime_observers_.RemoveObserver(observer);
}
std::string BorealisWindowManager::GetShelfAppId(aura::Window* window) {
if (!IsBorealisWindow(window))
return {};
// TODO(b/173977876): When we have better ways of associating apps with
// windows we will implement them. Until then, the mapping is identical to
// Crostini's so just spoof the relevant information and use theirs.
std::string pretend_crostini_id(*GetWindowId(window));
base::ReplaceFirstSubstringAfterOffset(
&pretend_crostini_id, 0, kBorealisWindowPrefix, "org.chromium.termina.");
std::string crostini_equivalent_id =
crostini::GetCrostiniShelfAppId(profile_, &pretend_crostini_id, nullptr);
// If crostini thinks this app is registered, then it actually is registered
// for borealis.
if (!crostini::IsUnmatchedCrostiniShelfAppId(crostini_equivalent_id))
return crostini_equivalent_id;
std::string app_id = WindowToAppId(profile_, window);
HandleWindow(window, app_id);
// The app has no registration, it is anonymous.
std::string anon_id = WindowToAnonymousAppId(window);
if (!anon_ids_to_windows_.contains(anon_id)) {
std::string anon_name = AnonymousIdentifierToName(anon_id);
for (auto& observer : observers_)
observer.OnAnonymousAppAdded(anon_id, anon_name);
}
// Add the window to the tracking set, and if it wasn't already there, add an
// observer.
if (anon_ids_to_windows_[anon_id].emplace(window).second) {
window->AddObserver(this);
}
return anon_id;
return app_id;
}
void BorealisWindowManager::OnWindowDestroying(aura::Window* window) {
std::string anon_id = WindowToAnonymousAppId(window);
base::flat_map<std::string, base::flat_set<aura::Window*>>::iterator iter =
anon_ids_to_windows_.find(anon_id);
std::string app_id = WindowToAppId(profile_, window);
for (auto& observer : lifetime_observers_) {
observer.OnWindowFinished(app_id, window);
}
DCHECK(iter != anon_ids_to_windows_.end());
base::flat_map<std::string, base::flat_set<aura::Window*>>::iterator iter =
ids_to_windows_.find(app_id);
DCHECK(iter != ids_to_windows_.end());
DCHECK(iter->second.contains(window));
iter->second.erase(window);
if (!iter->second.empty())
return;
for (auto& observer : observers_)
observer.OnAnonymousAppRemoved(anon_id);
anon_ids_to_windows_.erase(iter);
if (IsAnonymousAppId(app_id)) {
for (auto& observer : anon_observers_)
observer.OnAnonymousAppRemoved(app_id);
}
for (auto& observer : lifetime_observers_)
observer.OnAppFinished(app_id);
ids_to_windows_.erase(iter);
if (!ids_to_windows_.empty())
return;
for (auto& observer : lifetime_observers_)
observer.OnSessionFinished();
}
void BorealisWindowManager::HandleWindow(aura::Window* window,
const std::string& app_id) {
// If this is the first window, the session has started.
if (ids_to_windows_.empty()) {
for (auto& observer : lifetime_observers_)
observer.OnSessionStarted();
}
// If this is the given app_id's first window, the app has started
if (ids_to_windows_[app_id].empty()) {
for (auto& observer : lifetime_observers_)
observer.OnAppStarted(app_id);
if (IsAnonymousAppId(app_id)) {
std::string anon_name = AnonymousIdentifierToName(app_id);
for (auto& observer : anon_observers_)
observer.OnAnonymousAppAdded(app_id, anon_name);
}
}
// If this window was not already in the set, observe it and notify our
// observers about it.
if (ids_to_windows_[app_id].emplace(window).second) {
window->AddObserver(this);
for (auto& observer : lifetime_observers_)
observer.OnWindowStarted(app_id, window);
}
}
} // namespace borealis
......@@ -21,6 +21,10 @@ class Window;
namespace borealis {
// The borealis window manager keeps track of the association of windows to
// borealis apps. This includes determining which windows belong to a borealis
// app, what the lifetime of the app is relative to its windows, and the
// presence of borealis windows with an unknown app (see go/anonymous-apps).
class BorealisWindowManager : public aura::WindowObserver {
public:
// Returns true if this window belongs to a borealis VM (based on its app_id
......@@ -47,6 +51,48 @@ class BorealisWindowManager : public aura::WindowObserver {
BorealisWindowManager* window_manager) = 0;
};
// An observer for tracking window/app lifetimes. The key concepts are:
// - "Session", which refers to all borealis windows.
// - "App", which refers to the subset of windows belonging to a single
// identified app.
// - "Window", which refers to single windows.
// These concepts are nested, all apps belong to one session, and each window
// belongs to a single app.
class AppWindowLifetimeObserver : public base::CheckedObserver {
public:
// Called when the first UI element of any borealis app becomes visible.
virtual void OnSessionStarted() {}
// Called when the last UI element of any borealis app disappears. This
// implies that there are no more borealis windows until the next
// OnSessionStarted() is called.
virtual void OnSessionFinished() {}
// Called when the first window for an app with this |app_id| becomes
// visible.
virtual void OnAppStarted(const std::string& app_id) {}
// Called when the last window for |app_id|'s app goes away, implying the
// app has no visible windows until OnAppStarted() is called again.
virtual void OnAppFinished(const std::string& app_id) {}
// Called when a window associated with |app_id|'s app comes into existence.
// Note that this has nothing to do with the visible state of |window|,
// only that it exists in memory.
virtual void OnWindowStarted(const std::string& app_id,
aura::Window* window) {}
// Called when |window|, associated with |app_id|'s app, is about to be
// closed.
virtual void OnWindowFinished(const std::string& app_id,
aura::Window* window) {}
// Called when the window manager is being deleted. Observers should
// unregister themselves from it.
virtual void OnWindowManagerDeleted(
BorealisWindowManager* window_manager) = 0;
};
explicit BorealisWindowManager(Profile* profile);
~BorealisWindowManager() override;
......@@ -54,16 +100,27 @@ class BorealisWindowManager : public aura::WindowObserver {
void AddObserver(AnonymousAppObserver* observer);
void RemoveObserver(AnonymousAppObserver* observer);
void AddObserver(AppWindowLifetimeObserver* observer);
void RemoveObserver(AppWindowLifetimeObserver* observer);
// Returns the application ID for the given window, or "" if the window does
// not belong to borealis. If the window does belong to borealis, this call
// will also cause the manager to track the window.
//
// TODO(b/175152663): Refactor this into two methods so that it is clear which
// one has side-effects.
std::string GetShelfAppId(aura::Window* window);
private:
// aura::WindowObserver overrides.
void OnWindowDestroying(aura::Window* window) override;
void HandleWindow(aura::Window* window, const std::string& app_id);
Profile* const profile_;
base::flat_map<std::string, base::flat_set<aura::Window*>>
anon_ids_to_windows_;
base::ObserverList<AnonymousAppObserver> observers_;
base::flat_map<std::string, base::flat_set<aura::Window*>> ids_to_windows_;
base::ObserverList<AnonymousAppObserver> anon_observers_;
base::ObserverList<AppWindowLifetimeObserver> lifetime_observers_;
};
} // namespace borealis
......
......@@ -15,6 +15,8 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/window.h"
using ::testing::_;
namespace borealis {
namespace {
......@@ -31,6 +33,30 @@ class MockAnonObserver
MOCK_METHOD(void, OnWindowManagerDeleted, (BorealisWindowManager*), ());
};
class MockLifetimeObserver
: public borealis::BorealisWindowManager::AppWindowLifetimeObserver {
public:
MOCK_METHOD(void, OnSessionStarted, (), ());
MOCK_METHOD(void, OnSessionFinished, (), ());
MOCK_METHOD(void, OnAppStarted, (const std::string& app_id), ());
MOCK_METHOD(void, OnAppFinished, (const std::string& app_id), ());
MOCK_METHOD(void,
OnWindowStarted,
(const std::string& app_id, aura::Window*),
());
MOCK_METHOD(void,
OnWindowFinished,
(const std::string& app_id, aura::Window*),
());
MOCK_METHOD(void, OnWindowManagerDeleted, (BorealisWindowManager*), ());
};
class BorealisWindowManagerTest : public testing::Test {
protected:
Profile* profile() { return &profile_; }
......@@ -60,23 +86,28 @@ TEST_F(BorealisWindowManagerTest, BorealisWindowHasAnId) {
EXPECT_NE(window_manager.GetShelfAppId(window.get()), "");
}
TEST_F(BorealisWindowManagerTest, ObserverNotifiedOnManagerShutdown) {
testing::StrictMock<MockAnonObserver> observer;
TEST_F(BorealisWindowManagerTest, ObserversNotifiedOnManagerShutdown) {
testing::StrictMock<MockAnonObserver> anon_observer;
testing::StrictMock<MockLifetimeObserver> life_observer;
BorealisWindowManager window_manager(profile());
window_manager.AddObserver(&observer);
window_manager.AddObserver(&anon_observer);
window_manager.AddObserver(&life_observer);
EXPECT_CALL(observer, OnWindowManagerDeleted(&window_manager))
.WillOnce(testing::Invoke([&observer](BorealisWindowManager* wm) {
wm->RemoveObserver(&observer);
EXPECT_CALL(anon_observer, OnWindowManagerDeleted(&window_manager))
.WillOnce(testing::Invoke([&anon_observer](BorealisWindowManager* wm) {
wm->RemoveObserver(&anon_observer);
}));
EXPECT_CALL(life_observer, OnWindowManagerDeleted(&window_manager))
.WillOnce(testing::Invoke([&life_observer](BorealisWindowManager* wm) {
wm->RemoveObserver(&life_observer);
}));
}
TEST_F(BorealisWindowManagerTest, ObserverCalledForAnonymousApp) {
testing::StrictMock<MockAnonObserver> observer;
EXPECT_CALL(
observer,
OnAnonymousAppAdded(testing::ContainsRegex("anonymous_app"), testing::_));
EXPECT_CALL(observer,
OnAnonymousAppAdded(testing::ContainsRegex("anonymous_app"), _));
BorealisWindowManager window_manager(profile());
window_manager.AddObserver(&observer);
......@@ -91,7 +122,55 @@ TEST_F(BorealisWindowManagerTest, ObserverCalledForAnonymousApp) {
window_manager.RemoveObserver(&observer);
}
TEST_F(BorealisWindowManagerTest, HandlesMultipleWindows) {
TEST_F(BorealisWindowManagerTest, LifetimeObserverTracksWindows) {
testing::StrictMock<MockLifetimeObserver> observer;
BorealisWindowManager window_manager(profile());
window_manager.AddObserver(&observer);
// This object forces all EXPECT_CALLs to occur in the order they are
// declared.
testing::InSequence sequence;
// A new window will start everything.
EXPECT_CALL(observer, OnSessionStarted());
EXPECT_CALL(observer, OnAppStarted(_));
EXPECT_CALL(observer, OnWindowStarted(_, _));
std::unique_ptr<aura::Window> first_foo =
MakeWindow("org.chromium.borealis.foo");
window_manager.GetShelfAppId(first_foo.get());
// A window for the same app only starts that window.
EXPECT_CALL(observer, OnWindowStarted(_, _));
std::unique_ptr<aura::Window> second_foo =
MakeWindow("org.chromium.borealis.foo");
window_manager.GetShelfAppId(second_foo.get());
// Whereas a new app starts both the app and the window.
EXPECT_CALL(observer, OnAppStarted(_));
EXPECT_CALL(observer, OnWindowStarted(_, _));
std::unique_ptr<aura::Window> only_bar =
MakeWindow("org.chromium.borealis.bar");
window_manager.GetShelfAppId(only_bar.get());
// Deleting an app window while one still exists does not end the app.
EXPECT_CALL(observer, OnWindowFinished(_, _));
first_foo.reset();
// But deleting them all does finish the app.
EXPECT_CALL(observer, OnWindowFinished(_, _));
EXPECT_CALL(observer, OnAppFinished(_));
second_foo.reset();
// And deleting all the windows finishes the session.
EXPECT_CALL(observer, OnWindowFinished(_, _));
EXPECT_CALL(observer, OnAppFinished(_));
EXPECT_CALL(observer, OnSessionFinished());
only_bar.reset();
window_manager.RemoveObserver(&observer);
}
TEST_F(BorealisWindowManagerTest, HandlesMultipleAnonymousWindows) {
testing::StrictMock<MockAnonObserver> observer;
BorealisWindowManager window_manager(profile());
......@@ -99,7 +178,7 @@ TEST_F(BorealisWindowManagerTest, HandlesMultipleWindows) {
// We add an anonymous window for the same app twice, but we should only see
// one observer call.
EXPECT_CALL(observer, OnAnonymousAppAdded(testing::_, testing::_)).Times(1);
EXPECT_CALL(observer, OnAnonymousAppAdded(_, _)).Times(1);
std::unique_ptr<aura::Window> window1 =
MakeWindow("org.chromium.borealis.anonymous_app");
......@@ -110,13 +189,13 @@ TEST_F(BorealisWindowManagerTest, HandlesMultipleWindows) {
// We only expect to see the app removed after the last window closes.
window1.reset();
EXPECT_CALL(observer, OnAnonymousAppRemoved(testing::_)).Times(1);
EXPECT_CALL(observer, OnAnonymousAppRemoved(_)).Times(1);
window2.reset();
window_manager.RemoveObserver(&observer);
}
TEST_F(BorealisWindowManagerTest, ObserverNotCalledForKnownApp) {
TEST_F(BorealisWindowManagerTest, AnonymousObserverNotCalledForKnownApp) {
// Generate a fake app.
vm_tools::apps::ApplicationList list;
list.set_vm_name("vm");
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment