// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/ash/components/osauth/impl/auth_hub_mode_lifecycle.h"
#include <memory>
#include <ostream>
#include <utility>
#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/time/time.h"
#include "chromeos/ash/components/osauth/impl/auth_hub_common.h"
#include "chromeos/ash/components/osauth/public/auth_factor_engine.h"
#include "chromeos/ash/components/osauth/public/auth_factor_engine_factory.h"
#include "chromeos/ash/components/osauth/public/auth_parts.h"
#include "chromeos/ash/components/osauth/public/common_types.h"
#include "chromeos/ash/components/osauth/public/string_utils.h"
namespace ash {
namespace {
#if !defined(NDEBUG)
constexpr base::TimeDelta kWatchdogTimeout = base::Seconds(5);
#else
constexpr base::TimeDelta kWatchdogTimeout = base::Seconds(10);
#endif
enum class EngineStatus {
kStarting,
kStarted,
kFailed,
kShuttingDown,
kStopped
};
std::ostream& operator<<(std::ostream& out, EngineStatus status) {
switch (status) {
#define PRINT(s) \
case EngineStatus::k##s: \
return out << #s;
PRINT(Starting)
PRINT(Started)
PRINT(Failed)
PRINT(ShuttingDown)
PRINT(Stopped)
#undef PRINT
}
}
} // namespace
struct AuthHubModeLifecycle::EngineState {
std::unique_ptr<AuthFactorEngine> engine;
EngineStatus status;
};
AuthHubModeLifecycle::AuthHubModeLifecycle(AuthHubModeLifecycle::Owner* owner)
: owner_(owner) {}
AuthHubModeLifecycle::~AuthHubModeLifecycle() = default;
AuthHubModeLifecycle::Owner::~Owner() = default;
void AuthHubModeLifecycle::SwitchToMode(AuthHubMode target) {
switch (stage_) {
case Stage::kUninitialized:
CHECK_EQ(mode_, AuthHubMode::kNone);
if (target == AuthHubMode::kNone) {
owner_->OnModeShutdown();
return;
}
target_mode_ = target;
initializing_for_mode_ = target_mode_;
InitializeEnginesForMode();
break;
case Stage::kStarted:
CHECK_NE(mode_, AuthHubMode::kNone);
if (target != mode_) {
target_mode_ = target;
stage_ = Stage::kShuttingDownServices;
ShutDownEngines();
} else {
LOG(WARNING) << "Multiple initialization to " << mode_;
}
return;
case Stage::kStartingServices:
// Set up new target mode, but do not set initializing_for_mode_,
// it would trigger re-initialization.
target_mode_ = target;
break;
case Stage::kShuttingDownServices:
// Just update the target mode.
target_mode_ = target;
return;
}
}
void AuthHubModeLifecycle::InitializeEnginesForMode() {
CHECK(engines_.empty());
CHECK_NE(target_mode_, AuthHubMode::kNone);
for (const auto& factory : AuthParts::Get()->GetEngineFactories()) {
auto engine = factory->CreateEngine(target_mode_);
if (engine) {
AshAuthFactor factor = factory->GetFactor();
engines_[factor].engine = std::move(engine);
engines_[factor].status = EngineStatus::kStarting;
}
}
stage_ = Stage::kStartingServices;
watchdog_.Stop();
watchdog_.Start(
FROM_HERE, kWatchdogTimeout,
base::BindOnce(&AuthHubModeLifecycle::OnInitializationWatchdog,
weak_factory_.GetWeakPtr()));
// TODO(b/277929602): metrics on initialization time.
for (const auto& engine_state : engines_) {
engine_state.second.engine->InitializeCommon(
base::BindOnce(&AuthHubModeLifecycle::OnAuthEngineInitialized,
weak_factory_.GetWeakPtr()));
}
}
void AuthHubModeLifecycle::OnAuthEngineInitialized(AshAuthFactor factor) {
CHECK_EQ(stage_, Stage::kStartingServices);
engines_[factor].status = EngineStatus::kStarted;
CheckInitializationStatus();
}
void AuthHubModeLifecycle::OnInitializationWatchdog() {
CHECK_EQ(stage_, Stage::kStartingServices);
LOG(ERROR) << "Initialization watchdog triggered";
// Invalidate all initialization callbacks:
weak_factory_.InvalidateWeakPtrs();
for (auto& engine_state : engines_) {
if (engine_state.second.status == EngineStatus::kStarting) {
engine_state.second.status = EngineStatus::kFailed;
LOG(ERROR) << "Factor " << engine_state.first
<< " did not initialize in time";
engine_state.second.engine->InitializationTimedOut();
}
}
CheckInitializationStatus();
}
void AuthHubModeLifecycle::CheckInitializationStatus() {
bool all_initialized = true;
for (const auto& engine_state : engines_) {
switch (engine_state.second.status) {
case EngineStatus::kStarting:
all_initialized = false;
break;
case EngineStatus::kStarted:
case EngineStatus::kFailed:
break;
case EngineStatus::kShuttingDown:
case EngineStatus::kStopped:
LOG(FATAL) << "Engine " << engine_state.first << " is in invalid state "
<< engine_state.second.status;
}
}
if (all_initialized) {
watchdog_.Stop();
if (target_mode_ != initializing_for_mode_) {
// Trigger shutdown immediately; after shutdown is completed,
// `CheckShutdownStatus` will trigger re-initialization to target_mode_.
stage_ = Stage::kShuttingDownServices;
ShutDownEngines();
return;
}
initializing_for_mode_ = AuthHubMode::kNone;
mode_ = target_mode_;
stage_ = Stage::kStarted;
owner_->OnReadyForMode(mode_, GetAvailableEngines());
}
}
void AuthHubModeLifecycle::ShutDownEngines() {
CHECK_EQ(stage_, Stage::kShuttingDownServices);
for (auto& engine_state : engines_) {
engine_state.second.status = EngineStatus::kShuttingDown;
}
watchdog_.Stop();
watchdog_.Start(FROM_HERE, kWatchdogTimeout,
base::BindOnce(&AuthHubModeLifecycle::OnShutdownWatchdog,
weak_factory_.GetWeakPtr()));
// TODO(b/277929602): metrics on shutdown time.
for (const auto& engine_state : engines_) {
engine_state.second.engine->ShutdownCommon(
base::BindOnce(&AuthHubModeLifecycle::OnAuthEngineShutdown,
weak_factory_.GetWeakPtr()));
}
}
void AuthHubModeLifecycle::OnAuthEngineShutdown(AshAuthFactor factor) {
CHECK_EQ(stage_, Stage::kShuttingDownServices);
engines_[factor].status = EngineStatus::kStopped;
CheckShutdownStatus();
}
void AuthHubModeLifecycle::OnShutdownWatchdog() {
CHECK_EQ(stage_, Stage::kShuttingDownServices);
LOG(ERROR) << "Shutdown watchdog triggered";
// Invalidate all remaining shutdown callbacks:
weak_factory_.InvalidateWeakPtrs();
for (auto& engine_state : engines_) {
if (engine_state.second.status == EngineStatus::kShuttingDown) {
engine_state.second.status = EngineStatus::kFailed;
LOG(ERROR) << "Factor " << engine_state.first
<< " did not shut down in time";
engine_state.second.engine->ShutdownTimedOut();
}
}
CheckShutdownStatus();
}
void AuthHubModeLifecycle::CheckShutdownStatus() {
CHECK_EQ(stage_, Stage::kShuttingDownServices);
bool all_stopped = true;
for (const auto& engine_state : engines_) {
switch (engine_state.second.status) {
case EngineStatus::kShuttingDown:
all_stopped = false;
break;
case EngineStatus::kStopped:
case EngineStatus::kFailed:
break;
case EngineStatus::kStarting:
case EngineStatus::kStarted:
LOG(FATAL) << "Engine " << engine_state.first << " is in invalid state "
<< engine_state.second.status;
}
}
if (all_stopped) {
watchdog_.Stop();
// Let owner release references to engines.
if (mode_ != AuthHubMode::kNone) {
owner_->OnExitedMode(mode_);
}
engines_.clear();
mode_ = AuthHubMode::kNone;
stage_ = Stage::kUninitialized;
if (target_mode_ != AuthHubMode::kNone) {
initializing_for_mode_ = target_mode_;
InitializeEnginesForMode();
} else {
owner_->OnModeShutdown();
}
}
}
bool AuthHubModeLifecycle::IsReady() {
return stage_ == Stage::kStarted;
}
AuthHubMode AuthHubModeLifecycle::GetCurrentMode() const {
if (stage_ != Stage::kStarted) {
return AuthHubMode::kNone;
}
return mode_;
}
AuthEnginesMap AuthHubModeLifecycle::GetAvailableEngines() {
CHECK_EQ(stage_, Stage::kStarted);
AuthEnginesMap result;
for (const auto& engine_state : engines_) {
if (engine_state.second.status == EngineStatus::kStarted) {
result[engine_state.first] = engine_state.second.engine.get();
}
}
return result;
}
} // namespace ash