2350 lines
87 KiB
C++
2350 lines
87 KiB
C++
//
|
|
// Created by xtkuang on 2025/6/6.
|
|
//
|
|
|
|
#include "../include/grpc_system_service.h"
|
|
|
|
#include <algorithm>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "common/base/logging/logger.h"
|
|
#include "devices/agv/abstract_agv.h"
|
|
#include "devices/arm/robot_arm.h"
|
|
#include "devices/biohead/abstract_biohead.h"
|
|
#include "devices/camera/abstract_camera.h"
|
|
#include "devices/dexhand/abstract_dexhand.h"
|
|
#include "devices/gripper/abstract_gripper.h"
|
|
#include "devices/microphone/abstract_microphone.h"
|
|
#include "devices/speaker/abstract_speaker.h"
|
|
#include "manager/control_authority_manager/include/control_authority_manager.h"
|
|
#include "manager/media_source_manager/include/device_media_source_adapter.h"
|
|
#include "manager/task_manager/include/task_manager.h"
|
|
#include "service/grpc/action/include/action_queue_executor.h"
|
|
#include "service/grpc/server/include/camera_operational_activity_registry.h"
|
|
#include "service/grpc/server/include/camera_ptz_activity_registry.h"
|
|
#include "service/grpc/server/include/grpc_recovery_audit.h"
|
|
#include "service/grpc/server/include/grpc_safety_proto.h"
|
|
#include "service/grpc/server/include/grpc_safety_participants.h"
|
|
#include "service/grpc/server/include/grpc_security.h"
|
|
#include "service/grpc/server/include/media_activity_coordinator.h"
|
|
#include "service/grpc/server/include/motor_activity_coordinator.h"
|
|
#include "service/grpc/stop_all/include/stop_all_admission_gate.h"
|
|
#include "service/grpc/stop_all/include/stop_operation_dispatcher.h"
|
|
|
|
using namespace cmvr::device;
|
|
using namespace cmvr::service;
|
|
|
|
namespace {
|
|
|
|
class ScopedControlBarrierSet final {
|
|
public:
|
|
~ScopedControlBarrierSet()
|
|
{
|
|
auto& authority =
|
|
cmvr::control::ControlAuthorityManager::instance();
|
|
for (const auto& barrier : barriers_) {
|
|
if (barrier.release_on_destroy) {
|
|
authority.release(barrier.token);
|
|
} else {
|
|
(void)authority.retireSafetyHolder(barrier.token);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool acquire(const std::string& device_id, std::string& detail)
|
|
{
|
|
cmvr::control::ControlLeaseToken ignored;
|
|
return acquire(device_id, detail, ignored);
|
|
}
|
|
|
|
bool acquire(
|
|
const std::string& device_id,
|
|
std::string& detail,
|
|
cmvr::control::ControlLeaseToken& token)
|
|
{
|
|
static std::atomic<std::uint64_t> sequence{0};
|
|
const std::string owner =
|
|
"grpc-system:stop-all:" +
|
|
std::to_string(
|
|
sequence.fetch_add(1U, std::memory_order_relaxed) + 1U);
|
|
const auto result =
|
|
cmvr::control::ControlAuthorityManager::instance()
|
|
.preemptAcquire(
|
|
device_id,
|
|
owner,
|
|
std::chrono::duration_cast<
|
|
cmvr::control::ControlAuthorityManager::Duration>(
|
|
std::chrono::hours(24)));
|
|
if (!result.acquired) {
|
|
detail = result.detail;
|
|
return false;
|
|
}
|
|
// StopAll barriers default to fail-closed. If retaining the token in
|
|
// this local vector throws, retire the manager-side holder so a later
|
|
// successful StopAll can recover it without reopening control now.
|
|
try {
|
|
barriers_.push_back({result.token, false});
|
|
} catch (...) {
|
|
(void)cmvr::control::ControlAuthorityManager::instance()
|
|
.retireSafetyHolder(result.token);
|
|
throw;
|
|
}
|
|
token = result.token;
|
|
return true;
|
|
}
|
|
|
|
bool waitForPreemptedRelease(
|
|
const std::string& device_id,
|
|
const std::chrono::milliseconds timeout)
|
|
{
|
|
const auto* barrier = find_(device_id);
|
|
return barrier &&
|
|
cmvr::control::ControlAuthorityManager::instance()
|
|
.waitForPreemptedRelease(barrier->token, timeout);
|
|
}
|
|
|
|
bool recoverRetiredSafetyHolders(const std::string& device_id)
|
|
{
|
|
const auto* barrier = find_(device_id);
|
|
return barrier &&
|
|
cmvr::control::ControlAuthorityManager::instance()
|
|
.recoverRetiredSafetyHolders(barrier->token);
|
|
}
|
|
|
|
void quarantine(const std::string& device_id)
|
|
{
|
|
auto& authority =
|
|
cmvr::control::ControlAuthorityManager::instance();
|
|
for (auto& barrier : barriers_) {
|
|
if (barrier.token.resource_id == device_id) {
|
|
(void)authority.retireSafetyHolder(barrier.token);
|
|
barrier.release_on_destroy = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void quarantineAll()
|
|
{
|
|
auto& authority =
|
|
cmvr::control::ControlAuthorityManager::instance();
|
|
for (auto& barrier : barriers_) {
|
|
(void)authority.retireSafetyHolder(barrier.token);
|
|
barrier.release_on_destroy = false;
|
|
}
|
|
}
|
|
|
|
void confirmSafeToReleaseAll()
|
|
{
|
|
for (auto& barrier : barriers_) {
|
|
barrier.release_on_destroy = true;
|
|
}
|
|
}
|
|
|
|
bool empty() const noexcept { return barriers_.empty(); }
|
|
|
|
void recoverRetiredSafetyHoldersAll()
|
|
{
|
|
for (const auto& barrier : barriers_) {
|
|
if (!recoverRetiredSafetyHolders(
|
|
barrier.token.resource_id)) {
|
|
throw std::runtime_error(
|
|
"StopAll could not recover an earlier failed safety "
|
|
"barrier: " + barrier.token.resource_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
void releaseAll() noexcept
|
|
{
|
|
auto& authority =
|
|
cmvr::control::ControlAuthorityManager::instance();
|
|
for (const auto& barrier : barriers_) {
|
|
authority.release(barrier.token);
|
|
}
|
|
barriers_.clear();
|
|
}
|
|
|
|
private:
|
|
struct Barrier {
|
|
cmvr::control::ControlLeaseToken token;
|
|
bool release_on_destroy{false};
|
|
};
|
|
|
|
const Barrier* find_(const std::string& device_id) const noexcept
|
|
{
|
|
for (const auto& barrier : barriers_) {
|
|
if (barrier.token.resource_id == device_id) {
|
|
return &barrier;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::vector<Barrier> barriers_;
|
|
};
|
|
|
|
class ScopedMotorStopAll final {
|
|
public:
|
|
ScopedMotorStopAll()
|
|
: coordinator_(globalMotorActivityCoordinator()),
|
|
ticket_(coordinator_.beginStopAll(true))
|
|
{
|
|
}
|
|
|
|
~ScopedMotorStopAll()
|
|
{
|
|
if (ticket_.valid() && !completed_) {
|
|
(void)coordinator_.finishStopAll(ticket_, false);
|
|
}
|
|
}
|
|
|
|
bool valid() const noexcept { return ticket_.valid(); }
|
|
|
|
bool stopAndWait(
|
|
const std::chrono::milliseconds timeout,
|
|
std::string* error)
|
|
{
|
|
return ticket_.valid() &&
|
|
coordinator_.stopAndWait(ticket_, timeout, error);
|
|
}
|
|
|
|
bool requestStop(std::string* error)
|
|
{
|
|
return ticket_.valid() && coordinator_.requestStop(ticket_, error);
|
|
}
|
|
|
|
bool collectStopOperations(
|
|
std::vector<cmvr::service::DeferredStopOperation>& operations,
|
|
std::string* error)
|
|
{
|
|
return ticket_.valid() &&
|
|
coordinator_.collectStopOperations(ticket_, operations, error);
|
|
}
|
|
|
|
bool waitForStopped(
|
|
const std::chrono::milliseconds timeout,
|
|
std::string* error)
|
|
{
|
|
return ticket_.valid() &&
|
|
coordinator_.waitForStopped(ticket_, timeout, error);
|
|
}
|
|
|
|
bool complete(const bool all_motors_stopped)
|
|
{
|
|
if (!ticket_.valid() || completed_) {
|
|
return false;
|
|
}
|
|
const bool completed = coordinator_.finishStopAll(
|
|
ticket_, all_motors_stopped);
|
|
completed_ = completed;
|
|
return completed;
|
|
}
|
|
|
|
private:
|
|
MotorActivityCoordinator& coordinator_;
|
|
MotorActivityCoordinator::StopAllTicket ticket_;
|
|
bool completed_{false};
|
|
};
|
|
|
|
class ScopedActionQueueStopAll final {
|
|
public:
|
|
explicit ScopedActionQueueStopAll(
|
|
cmvr::service::ActionQueueExecutor& executor)
|
|
: executor_(executor), ticket_(executor_.beginStopAll(true))
|
|
{
|
|
}
|
|
|
|
~ScopedActionQueueStopAll()
|
|
{
|
|
if (ticket_.valid() && !completed_) {
|
|
(void)executor_.finishStopAll(ticket_, false);
|
|
}
|
|
}
|
|
|
|
bool valid() const noexcept { return ticket_.valid(); }
|
|
|
|
bool complete(const bool all_devices_stop_confirmed)
|
|
{
|
|
if (!ticket_.valid() || completed_) {
|
|
return false;
|
|
}
|
|
const bool completed = executor_.finishStopAll(
|
|
ticket_, all_devices_stop_confirmed);
|
|
completed_ = completed;
|
|
return completed;
|
|
}
|
|
|
|
private:
|
|
cmvr::service::ActionQueueExecutor& executor_;
|
|
cmvr::service::ActionQueueExecutor::StopAllTicket ticket_;
|
|
bool completed_{false};
|
|
};
|
|
|
|
class ScopedMediaStopAll final {
|
|
public:
|
|
ScopedMediaStopAll()
|
|
: coordinator_(cmvr::service::globalMediaActivityCoordinator()),
|
|
ticket_(coordinator_.beginStopAll(true))
|
|
{
|
|
}
|
|
|
|
~ScopedMediaStopAll()
|
|
{
|
|
if (ticket_.valid() && !completed_) {
|
|
(void)coordinator_.finishStopAll(ticket_, false);
|
|
}
|
|
}
|
|
|
|
bool valid() const noexcept { return ticket_.valid(); }
|
|
|
|
bool waitForStopped(const std::chrono::milliseconds timeout)
|
|
{
|
|
return ticket_.valid() &&
|
|
coordinator_.waitForStopped(ticket_, timeout);
|
|
}
|
|
|
|
bool requestCancellation()
|
|
{
|
|
return ticket_.valid() && coordinator_.requestCancellation(ticket_);
|
|
}
|
|
|
|
bool collectCancellationOperations(
|
|
std::vector<cmvr::service::DeferredStopOperation>& operations,
|
|
std::string* error)
|
|
{
|
|
return ticket_.valid() &&
|
|
coordinator_.collectCancellationOperations(
|
|
ticket_, operations, error);
|
|
}
|
|
|
|
bool complete(const bool all_media_stopped)
|
|
{
|
|
if (!ticket_.valid() || completed_) {
|
|
return false;
|
|
}
|
|
const bool completed =
|
|
coordinator_.finishStopAll(ticket_, all_media_stopped);
|
|
completed_ = completed;
|
|
return completed;
|
|
}
|
|
|
|
private:
|
|
cmvr::service::MediaActivityCoordinator& coordinator_;
|
|
cmvr::service::MediaActivityCoordinator::StopAllTicket ticket_;
|
|
bool completed_{false};
|
|
};
|
|
|
|
class ScopedAdmissionStopAll final {
|
|
public:
|
|
ScopedAdmissionStopAll()
|
|
: gate_(globalStopAllAdmissionGate()),
|
|
ticket_(gate_.beginStopAll())
|
|
{
|
|
}
|
|
|
|
~ScopedAdmissionStopAll()
|
|
{
|
|
if (ticket_.valid() && !completed_) {
|
|
(void)gate_.finishStopAll(ticket_, false);
|
|
}
|
|
}
|
|
|
|
bool valid() const noexcept { return ticket_.valid(); }
|
|
|
|
bool complete(const bool all_domains_stop_confirmed)
|
|
{
|
|
if (!ticket_.valid() || completed_) {
|
|
return false;
|
|
}
|
|
const auto result = gate_.finishStopAllDetailed(
|
|
ticket_, all_domains_stop_confirmed);
|
|
completed_ = result.ticket_consumed;
|
|
return result.admission_reopened;
|
|
}
|
|
|
|
private:
|
|
StopAllAdmissionGate& gate_;
|
|
StopAllAdmissionGate::StopAllTicket ticket_;
|
|
bool completed_{false};
|
|
};
|
|
|
|
std::uint64_t unixTimeMs() noexcept
|
|
{
|
|
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::system_clock::now().time_since_epoch());
|
|
return elapsed.count() > 0
|
|
? static_cast<std::uint64_t>(elapsed.count())
|
|
: 0U;
|
|
}
|
|
|
|
std::chrono::milliseconds remainingStopBudget(
|
|
const std::chrono::steady_clock::time_point deadline) noexcept
|
|
{
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (now >= deadline) {
|
|
return std::chrono::milliseconds::zero();
|
|
}
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
deadline - now);
|
|
}
|
|
|
|
std::timed_mutex& processStopAllMutex()
|
|
{
|
|
// SystemService is expected to be unique, but keeping the mutex at process
|
|
// scope also protects test, reload, and accidental multi-instance paths
|
|
// from issuing overlapping whole-device stop rounds.
|
|
static std::timed_mutex mutex;
|
|
return mutex;
|
|
}
|
|
|
|
using StopDispatcher = cmvr::service::StopOperationDispatcher;
|
|
using StopHandle = StopDispatcher::Handle;
|
|
using StopOutcome = StopDispatcher::OperationResult;
|
|
|
|
struct ProcessStopDispatcherRegistry final {
|
|
std::mutex mutex;
|
|
std::condition_variable available;
|
|
std::weak_ptr<StopDispatcher> dispatcher;
|
|
bool destroying{false};
|
|
};
|
|
|
|
ProcessStopDispatcherRegistry& processStopDispatcherRegistry()
|
|
{
|
|
static ProcessStopDispatcherRegistry registry;
|
|
return registry;
|
|
}
|
|
|
|
void acquireProcessStopDispatcher(
|
|
std::shared_ptr<StopDispatcher>& owner)
|
|
{
|
|
auto& registry = processStopDispatcherRegistry();
|
|
std::unique_lock lock(registry.mutex);
|
|
registry.available.wait(
|
|
lock, [®istry] { return !registry.destroying; });
|
|
if (auto existing = registry.dispatcher.lock()) {
|
|
owner = std::move(existing);
|
|
return;
|
|
}
|
|
|
|
owner = std::make_shared<StopDispatcher>();
|
|
registry.dispatcher = owner;
|
|
}
|
|
|
|
void releaseProcessStopDispatcher(
|
|
std::shared_ptr<StopDispatcher>& owner) noexcept
|
|
{
|
|
if (!owner) {
|
|
return;
|
|
}
|
|
|
|
auto& registry = processStopDispatcherRegistry();
|
|
std::unique_lock lock(registry.mutex);
|
|
if (owner.use_count() > 1) {
|
|
owner.reset();
|
|
return;
|
|
}
|
|
|
|
// Do not admit a replacement while the last dispatcher is joining a
|
|
// worker which may still be inside a device driver.
|
|
registry.destroying = true;
|
|
registry.dispatcher.reset();
|
|
registry.available.notify_all();
|
|
auto last_owner = std::move(owner);
|
|
lock.unlock();
|
|
last_owner.reset();
|
|
lock.lock();
|
|
registry.destroying = false;
|
|
lock.unlock();
|
|
registry.available.notify_all();
|
|
}
|
|
|
|
class StopDeadlineDetail final {
|
|
public:
|
|
void publishInitial(std::string detail)
|
|
{
|
|
std::lock_guard lock(mutex_);
|
|
detail_ = std::move(detail);
|
|
}
|
|
|
|
void publishUntil(
|
|
const StopDispatcher::Deadline deadline,
|
|
std::string detail)
|
|
{
|
|
std::lock_guard lock(mutex_);
|
|
if (StopDispatcher::Clock::now() < deadline) {
|
|
detail_ = std::move(detail);
|
|
}
|
|
}
|
|
|
|
std::string snapshot() const
|
|
{
|
|
std::lock_guard lock(mutex_);
|
|
return detail_;
|
|
}
|
|
|
|
private:
|
|
mutable std::mutex mutex_;
|
|
std::string detail_;
|
|
};
|
|
|
|
struct StopHandleEntry final {
|
|
std::string device_id;
|
|
StopHandle handle;
|
|
bool control_resource{false};
|
|
bool require_success{true};
|
|
};
|
|
|
|
std::string stopResourceKey(const std::string& device_id)
|
|
{
|
|
return "device:" + device_id;
|
|
}
|
|
|
|
void appendFailure(
|
|
std::vector<std::string>& failures,
|
|
const std::string& subject,
|
|
const std::string& detail)
|
|
{
|
|
failures.push_back(
|
|
subject + ": " +
|
|
(detail.empty() ? "operational stop was not confirmed" : detail));
|
|
}
|
|
|
|
bool waitForStopOperations(
|
|
const std::vector<StopHandleEntry>& operations,
|
|
const StopDispatcher::Deadline deadline,
|
|
ScopedControlBarrierSet& control_barriers,
|
|
std::vector<std::string>& failures,
|
|
std::unordered_set<std::string>* completed_devices = nullptr)
|
|
{
|
|
bool all_succeeded = true;
|
|
for (const auto& operation : operations) {
|
|
const auto outcome = operation.handle.waitUntil(deadline);
|
|
if (outcome.completed && completed_devices) {
|
|
completed_devices->emplace(operation.device_id);
|
|
}
|
|
if (outcome.completed && outcome.result) {
|
|
continue;
|
|
}
|
|
if (outcome.completed && !operation.require_success) {
|
|
CMVR_LOG(WARNING)
|
|
<< "[gRPCSystemServiceImpl] (StopAll): initial stop was not "
|
|
"confirmed and will be retried, id="
|
|
<< operation.device_id << ", detail=" << outcome.detail;
|
|
continue;
|
|
}
|
|
|
|
all_succeeded = false;
|
|
if (operation.control_resource) {
|
|
control_barriers.quarantine(operation.device_id);
|
|
}
|
|
appendFailure(
|
|
failures,
|
|
operation.device_id,
|
|
outcome.detail.empty()
|
|
? "stop operation did not complete before the deadline"
|
|
: outcome.detail);
|
|
}
|
|
return all_succeeded;
|
|
}
|
|
|
|
StopOutcome stopArm(
|
|
const std::shared_ptr<cmvr::device::RobotArm>& arm,
|
|
const bool final_confirmation)
|
|
{
|
|
const auto result = arm->stopMotion();
|
|
if (!result.ok()) {
|
|
return {
|
|
false,
|
|
std::string(final_confirmation ? "final " : "initial ") +
|
|
"RobotArm stop was not confirmed" +
|
|
(result.message.empty() ? "" : ": " + result.message)};
|
|
}
|
|
if (final_confirmation) {
|
|
try {
|
|
if (arm->busy()) {
|
|
return {false, "RobotArm remained busy after final stop"};
|
|
}
|
|
} catch (const std::exception& error) {
|
|
return {
|
|
false,
|
|
std::string("RobotArm idle confirmation threw: ") +
|
|
error.what()};
|
|
} catch (...) {
|
|
return {
|
|
false,
|
|
"RobotArm idle confirmation threw an unknown exception"};
|
|
}
|
|
}
|
|
return {true, {}};
|
|
}
|
|
|
|
bool agvStopResultAccepted(const cmvr::device::AgvResult& result) noexcept
|
|
{
|
|
return result.ok() ||
|
|
result.code == cmvr::device::AgvErrorCode::UnsupportedCommand;
|
|
}
|
|
|
|
StopOutcome stopAgv(
|
|
const std::shared_ptr<cmvr::device::AbstractAGV>& agv,
|
|
const bool final_confirmation)
|
|
{
|
|
const auto cancel = agv->cancelNavigation();
|
|
const auto velocity = agv->stopVelocityControl();
|
|
const auto mapping = agv->stopMapping();
|
|
const auto stopped = agv->confirmMotionStopped();
|
|
|
|
if (final_confirmation &&
|
|
(!agvStopResultAccepted(cancel) ||
|
|
!agvStopResultAccepted(velocity) ||
|
|
!agvStopResultAccepted(mapping) || !stopped.ok())) {
|
|
return {false, "final AGV operational stop was not confirmed"};
|
|
}
|
|
if (!final_confirmation && !stopped.ok()) {
|
|
return {
|
|
false,
|
|
"initial AGV stopped state was not confirmed" +
|
|
(stopped.message.empty() ? "" : ": " + stopped.message)};
|
|
}
|
|
return {true, {}};
|
|
}
|
|
|
|
StopOutcome stopDexHand(
|
|
const std::shared_ptr<cmvr::device::AbstractDexHand>& hand,
|
|
const bool final_confirmation)
|
|
{
|
|
if (!hand->stopOperationalActivity()) {
|
|
return {
|
|
false,
|
|
std::string(final_confirmation ? "final " : "initial ") +
|
|
"DexHand operational stop was not confirmed"};
|
|
}
|
|
return {true, {}};
|
|
}
|
|
|
|
template <typename Operation>
|
|
StopOutcome invokeStopOperation(
|
|
Operation&& operation,
|
|
const std::string& description) noexcept
|
|
{
|
|
try {
|
|
return std::forward<Operation>(operation)();
|
|
} catch (const std::exception& error) {
|
|
return {
|
|
false,
|
|
description + " threw: " + error.what()};
|
|
} catch (...) {
|
|
return {
|
|
false,
|
|
description + " threw an unknown exception"};
|
|
}
|
|
}
|
|
|
|
template <typename InitialStop, typename FinalStop>
|
|
StopOutcome stopControlWithFence(
|
|
const cmvr::control::ControlLeaseToken& barrier,
|
|
const StopDispatcher::Deadline deadline,
|
|
InitialStop&& initial_stop,
|
|
FinalStop&& final_stop,
|
|
const std::string& description,
|
|
const std::shared_ptr<StopDeadlineDetail>& deadline_detail)
|
|
{
|
|
deadline_detail->publishUntil(
|
|
deadline,
|
|
"initial " + description +
|
|
" stop did not complete before the StopAll deadline");
|
|
const auto initial = invokeStopOperation(
|
|
std::forward<InitialStop>(initial_stop),
|
|
"initial " + description + " stop");
|
|
if (!initial.success) {
|
|
CMVR_LOG(WARNING)
|
|
<< "[gRPCSystemServiceImpl] (StopAll): " << initial.detail;
|
|
}
|
|
|
|
if (!barrier.valid()) {
|
|
return {
|
|
false,
|
|
description +
|
|
" safety barrier was unavailable after the initial stop"};
|
|
}
|
|
|
|
const std::string handler_timeout_detail =
|
|
"timed out waiting for the preempted " + description +
|
|
" control handler to exit";
|
|
deadline_detail->publishUntil(deadline, handler_timeout_detail);
|
|
const bool handler_drained =
|
|
cmvr::control::ControlAuthorityManager::instance()
|
|
.waitForPreemptedRelease(
|
|
barrier, remainingStopBudget(deadline));
|
|
if (handler_drained) {
|
|
deadline_detail->publishUntil(
|
|
deadline,
|
|
"final " + description +
|
|
" stop did not complete before the StopAll deadline");
|
|
}
|
|
// This final typed stop remains mandatory even when the handler wait used
|
|
// the entire RPC budget. The dispatcher owns this worker past the RPC
|
|
// deadline, closing the race where an old handler resumes after the first
|
|
// stop while the safety barrier remains fail-closed.
|
|
const auto final = invokeStopOperation(
|
|
std::forward<FinalStop>(final_stop),
|
|
"final " + description + " stop");
|
|
if (!handler_drained) {
|
|
std::string detail = handler_timeout_detail;
|
|
if (!final.success) {
|
|
detail += "; " +
|
|
(final.detail.empty()
|
|
? "final " + description +
|
|
" stop was not confirmed"
|
|
: final.detail);
|
|
}
|
|
return {false, std::move(detail)};
|
|
}
|
|
return final;
|
|
}
|
|
|
|
StopOutcome stopCameraActivities(
|
|
const std::string& device_id,
|
|
const std::shared_ptr<cmvr::device::AbstractCamera>& camera)
|
|
{
|
|
std::vector<std::string> failures;
|
|
bool stopped = cmvr::media::globalMediaSourceManager()
|
|
.stopSourcesForDevice(device_id, &failures);
|
|
if (!globalCameraPtzActivityRegistry().stopActivitiesForDevice(
|
|
device_id, &failures)) {
|
|
stopped = false;
|
|
}
|
|
|
|
try {
|
|
cmvr::device::CameraState state{};
|
|
if (camera) {
|
|
camera->getState(state);
|
|
}
|
|
if (camera && state.is_recording) {
|
|
camera->stopRecording();
|
|
camera->getState(state);
|
|
if (state.is_recording) {
|
|
stopped = false;
|
|
failures.push_back(
|
|
device_id + ": camera recording did not stop");
|
|
}
|
|
}
|
|
} catch (const std::exception& error) {
|
|
stopped = false;
|
|
failures.push_back(
|
|
device_id + ": camera recording stop threw: " + error.what());
|
|
} catch (...) {
|
|
stopped = false;
|
|
failures.push_back(
|
|
device_id +
|
|
": camera recording stop threw an unknown exception");
|
|
}
|
|
|
|
if (!globalCameraOperationalActivityRegistry().stopActivitiesForDevice(
|
|
device_id, camera, &failures)) {
|
|
stopped = false;
|
|
}
|
|
return {
|
|
stopped,
|
|
failures.empty()
|
|
? std::string{}
|
|
: "camera activities were not fully stopped: " +
|
|
failures.front()};
|
|
}
|
|
|
|
StopOutcome stopMicrophone(
|
|
const std::string& device_id,
|
|
const std::shared_ptr<cmvr::device::AbstractMicrophone>& microphone)
|
|
{
|
|
std::vector<std::string> failures;
|
|
bool stopped = cmvr::media::globalMediaSourceManager()
|
|
.stopSourcesForDevice(device_id, &failures);
|
|
try {
|
|
cmvr::device::MicrophoneState state{};
|
|
microphone->getState(state);
|
|
if (state.is_recording) {
|
|
microphone->stopRecording();
|
|
microphone->getState(state);
|
|
if (state.is_recording) {
|
|
stopped = false;
|
|
failures.push_back(
|
|
device_id + ": microphone recording did not stop");
|
|
}
|
|
}
|
|
} catch (const std::exception& error) {
|
|
stopped = false;
|
|
failures.push_back(
|
|
device_id + ": microphone recording stop threw: " +
|
|
error.what());
|
|
} catch (...) {
|
|
stopped = false;
|
|
failures.push_back(
|
|
device_id +
|
|
": microphone recording stop threw an unknown exception");
|
|
}
|
|
return {
|
|
stopped,
|
|
failures.empty()
|
|
? std::string{}
|
|
: "microphone activities were not fully stopped: " +
|
|
failures.front()};
|
|
}
|
|
|
|
StopOutcome stopSpeaker(
|
|
const std::shared_ptr<cmvr::device::AbstractSpeaker>& speaker)
|
|
{
|
|
return speaker->stopPlayback()
|
|
? StopOutcome{true, {}}
|
|
: StopOutcome{false, "speaker playback did not stop"};
|
|
}
|
|
|
|
StopOutcome stopTrackedMediaActivities(const std::string& device_id)
|
|
{
|
|
std::vector<std::string> failures;
|
|
bool stopped = cmvr::media::globalMediaSourceManager()
|
|
.stopSourcesForDevice(device_id, &failures);
|
|
if (!globalCameraPtzActivityRegistry().stopActivitiesForDevice(
|
|
device_id, &failures)) {
|
|
stopped = false;
|
|
}
|
|
if (!globalCameraOperationalActivityRegistry().stopActivitiesForDevice(
|
|
device_id, &failures)) {
|
|
stopped = false;
|
|
}
|
|
return {
|
|
stopped,
|
|
failures.empty()
|
|
? std::string{}
|
|
: "tracked media activities were not fully stopped: " +
|
|
failures.front()};
|
|
}
|
|
|
|
void mergeStopOutcome(
|
|
const StopOutcome& outcome,
|
|
bool& all_stopped,
|
|
std::vector<std::string>& failures)
|
|
{
|
|
if (outcome.success) {
|
|
return;
|
|
}
|
|
all_stopped = false;
|
|
failures.push_back(
|
|
outcome.detail.empty()
|
|
? "operational stop was not confirmed"
|
|
: outcome.detail);
|
|
}
|
|
|
|
template <typename DeviceType>
|
|
StopOutcome stopOperationalDevice(
|
|
const std::shared_ptr<DeviceType>& device,
|
|
const char* description);
|
|
|
|
StopOutcome stopOtherActivities(
|
|
const std::string& device_id,
|
|
const std::shared_ptr<cmvr::device::AbstractCamera>& camera,
|
|
const std::shared_ptr<cmvr::device::AbstractMicrophone>& microphone,
|
|
const std::shared_ptr<cmvr::device::AbstractSpeaker>& speaker,
|
|
const std::shared_ptr<cmvr::device::AbstractBiohead>& head,
|
|
const std::shared_ptr<cmvr::device::AbstractGripper>& gripper,
|
|
const bool tracked_media)
|
|
{
|
|
bool all_stopped = true;
|
|
bool media_stopped_by_typed_device = false;
|
|
std::vector<std::string> failures;
|
|
|
|
if (camera) {
|
|
mergeStopOutcome(
|
|
invokeStopOperation(
|
|
[&] { return stopCameraActivities(device_id, camera); },
|
|
"camera activity stop"),
|
|
all_stopped, failures);
|
|
media_stopped_by_typed_device = true;
|
|
}
|
|
if (microphone) {
|
|
mergeStopOutcome(
|
|
invokeStopOperation(
|
|
[&] { return stopMicrophone(device_id, microphone); },
|
|
"microphone activity stop"),
|
|
all_stopped, failures);
|
|
media_stopped_by_typed_device = true;
|
|
}
|
|
if (tracked_media && !media_stopped_by_typed_device) {
|
|
mergeStopOutcome(
|
|
invokeStopOperation(
|
|
[&] { return stopTrackedMediaActivities(device_id); },
|
|
"tracked media activity stop"),
|
|
all_stopped, failures);
|
|
}
|
|
if (speaker) {
|
|
mergeStopOutcome(
|
|
invokeStopOperation(
|
|
[&] { return stopSpeaker(speaker); },
|
|
"speaker activity stop"),
|
|
all_stopped, failures);
|
|
}
|
|
if (head) {
|
|
mergeStopOutcome(
|
|
invokeStopOperation(
|
|
[&] { return stopOperationalDevice(head, "BioHead"); },
|
|
"BioHead activity stop"),
|
|
all_stopped, failures);
|
|
}
|
|
if (gripper) {
|
|
mergeStopOutcome(
|
|
invokeStopOperation(
|
|
[&] { return stopOperationalDevice(gripper, "gripper"); },
|
|
"gripper activity stop"),
|
|
all_stopped, failures);
|
|
}
|
|
|
|
return {
|
|
all_stopped,
|
|
failures.empty() ? std::string{} : failures.front()};
|
|
}
|
|
|
|
StopOutcome stopTaskActivity(
|
|
const std::shared_ptr<cmvr::task::Task>& task)
|
|
{
|
|
if (!task) {
|
|
return {false, "task activity target was null"};
|
|
}
|
|
return task->stopActivity()
|
|
? StopOutcome{true, {}}
|
|
: StopOutcome{
|
|
false,
|
|
"task " + task->id() +
|
|
" operational stop was not confirmed"};
|
|
}
|
|
|
|
void submitDeferredOperations(
|
|
StopDispatcher& dispatcher,
|
|
std::vector<cmvr::service::DeferredStopOperation> operations,
|
|
std::vector<StopHandleEntry>& handles)
|
|
{
|
|
handles.reserve(handles.size() + operations.size());
|
|
for (auto& operation : operations) {
|
|
const auto subject = operation.resource_key;
|
|
auto callback = std::move(operation.operation);
|
|
auto handle = dispatcher.submit(
|
|
operation.resource_key,
|
|
[callback = std::move(callback)]() mutable {
|
|
const auto result = callback();
|
|
return StopOutcome{result.success, result.detail};
|
|
});
|
|
handles.push_back(
|
|
{subject, std::move(handle), false, true});
|
|
}
|
|
}
|
|
|
|
template <typename DeviceType>
|
|
StopOutcome stopOperationalDevice(
|
|
const std::shared_ptr<DeviceType>& device,
|
|
const char* description)
|
|
{
|
|
return device->stopOperationalActivity()
|
|
? StopOutcome{true, {}}
|
|
: StopOutcome{
|
|
false,
|
|
std::string(description) +
|
|
" operational stop was not confirmed"};
|
|
}
|
|
|
|
cmvr::api::SystemDeviceType toApiDeviceType(
|
|
const cmvr::device::DeviceKind kind) noexcept
|
|
{
|
|
switch (kind) {
|
|
case cmvr::device::DeviceKind::AGV:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_AGV;
|
|
case cmvr::device::DeviceKind::Arm:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_ARM;
|
|
case cmvr::device::DeviceKind::Battery:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_BATTERY;
|
|
case cmvr::device::DeviceKind::BioHead:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_BIO_HEAD;
|
|
case cmvr::device::DeviceKind::Camera:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_CAMERA;
|
|
case cmvr::device::DeviceKind::CanBus:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_CAN_BUS;
|
|
case cmvr::device::DeviceKind::DexHand:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_DEX_HAND;
|
|
case cmvr::device::DeviceKind::Gripper:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_GRIPPER;
|
|
case cmvr::device::DeviceKind::Microphone:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_MICROPHONE;
|
|
case cmvr::device::DeviceKind::Motor:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_MOTOR;
|
|
case cmvr::device::DeviceKind::MotorSystem:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_MOTOR_SYSTEM;
|
|
case cmvr::device::DeviceKind::MujocoViewer:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_MUJOCO_VIEWER;
|
|
case cmvr::device::DeviceKind::MujocoWorld:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_MUJOCO_WORLD;
|
|
case cmvr::device::DeviceKind::Robot:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_ROBOT;
|
|
case cmvr::device::DeviceKind::Speaker:
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_SPEAKER;
|
|
case cmvr::device::DeviceKind::Unknown:
|
|
break;
|
|
}
|
|
return cmvr::api::SYSTEM_DEVICE_TYPE_UNSPECIFIED;
|
|
}
|
|
|
|
cmvr::api::SystemDeviceState toApiDeviceState(
|
|
const cmvr::device::ManagedDeviceState state) noexcept
|
|
{
|
|
switch (state) {
|
|
case cmvr::device::ManagedDeviceState::Disabled:
|
|
return cmvr::api::SYSTEM_DEVICE_STATE_DISABLED;
|
|
case cmvr::device::ManagedDeviceState::Initializing:
|
|
return cmvr::api::SYSTEM_DEVICE_STATE_INITIALIZING;
|
|
case cmvr::device::ManagedDeviceState::Registered:
|
|
return cmvr::api::SYSTEM_DEVICE_STATE_REGISTERED;
|
|
case cmvr::device::ManagedDeviceState::Ready:
|
|
return cmvr::api::SYSTEM_DEVICE_STATE_READY;
|
|
case cmvr::device::ManagedDeviceState::Running:
|
|
return cmvr::api::SYSTEM_DEVICE_STATE_RUNNING;
|
|
case cmvr::device::ManagedDeviceState::Stopped:
|
|
return cmvr::api::SYSTEM_DEVICE_STATE_STOPPED;
|
|
case cmvr::device::ManagedDeviceState::Error:
|
|
return cmvr::api::SYSTEM_DEVICE_STATE_ERROR;
|
|
case cmvr::device::ManagedDeviceState::Unknown:
|
|
break;
|
|
}
|
|
return cmvr::api::SYSTEM_DEVICE_STATE_UNSPECIFIED;
|
|
}
|
|
|
|
cmvr::api::SystemDeviceHealth toApiDeviceHealth(
|
|
const cmvr::device::DeviceHealthState state) noexcept
|
|
{
|
|
switch (state) {
|
|
case cmvr::device::DeviceHealthState::Healthy:
|
|
return cmvr::api::SYSTEM_DEVICE_HEALTH_HEALTHY;
|
|
case cmvr::device::DeviceHealthState::Degraded:
|
|
return cmvr::api::SYSTEM_DEVICE_HEALTH_DEGRADED;
|
|
case cmvr::device::DeviceHealthState::Fault:
|
|
return cmvr::api::SYSTEM_DEVICE_HEALTH_FAULT;
|
|
case cmvr::device::DeviceHealthState::Unknown:
|
|
break;
|
|
}
|
|
return cmvr::api::SYSTEM_DEVICE_HEALTH_UNSPECIFIED;
|
|
}
|
|
|
|
struct ParsedSafetyScope final {
|
|
bool valid{false};
|
|
bool all_devices{false};
|
|
std::vector<std::string> device_ids;
|
|
std::string error;
|
|
};
|
|
|
|
ParsedSafetyScope parseSafetyScope(
|
|
const cmvr::api::SafetyScope& scope,
|
|
const bool default_to_all)
|
|
{
|
|
ParsedSafetyScope result;
|
|
switch (scope.target_case()) {
|
|
case cmvr::api::SafetyScope::kAllDevices:
|
|
if (!scope.all_devices()) {
|
|
result.error = "all_devices must be explicitly true";
|
|
return result;
|
|
}
|
|
result.valid = true;
|
|
result.all_devices = true;
|
|
return result;
|
|
case cmvr::api::SafetyScope::kDevices: {
|
|
if (scope.devices().device_ids().empty()) {
|
|
result.error = "device scope must contain at least one device ID";
|
|
return result;
|
|
}
|
|
std::unordered_set<std::string> unique;
|
|
result.device_ids.reserve(scope.devices().device_ids_size());
|
|
for (const auto& id : scope.devices().device_ids()) {
|
|
if (id.empty() || !unique.insert(id).second) {
|
|
result.error =
|
|
"device scope IDs must be non-empty and unique";
|
|
return result;
|
|
}
|
|
result.device_ids.push_back(id);
|
|
}
|
|
result.valid = true;
|
|
return result;
|
|
}
|
|
case cmvr::api::SafetyScope::TARGET_NOT_SET:
|
|
if (default_to_all) {
|
|
result.valid = true;
|
|
result.all_devices = true;
|
|
} else {
|
|
result.error = "an explicit recovery scope is required";
|
|
}
|
|
return result;
|
|
}
|
|
result.error = "invalid safety scope";
|
|
return result;
|
|
}
|
|
|
|
void setSafetyHeaderFailure(
|
|
cmvr::api::CommandHeader_Feedback* header,
|
|
const cmvr::safety::SafetyReason reason,
|
|
const std::string& detail)
|
|
{
|
|
header->set_success(false);
|
|
header->set_reason_code(toApiSafetyReason(reason));
|
|
header->set_error_message(detail);
|
|
header->set_execution_state(
|
|
cmvr::api::COMMAND_EXECUTION_STATE_REJECTED_BEFORE_DISPATCH);
|
|
setCurrentTimestamp(header->mutable_timestamp());
|
|
}
|
|
|
|
bool recoveryCompletedAsRequested(
|
|
const cmvr::safety::RecoveryResultCode result) noexcept
|
|
{
|
|
return result == cmvr::safety::RecoveryResultCode::Recovered ||
|
|
result ==
|
|
cmvr::safety::RecoveryResultCode::VerifiedButStillBlocked ||
|
|
result == cmvr::safety::RecoveryResultCode::NothingToRecover;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
gRPCSystemServiceImpl::gRPCSystemServiceImpl()
|
|
: gRPCSystemServiceImpl(
|
|
std::chrono::seconds(15), makeDefaultGrpcSecurityGateway())
|
|
{
|
|
}
|
|
|
|
gRPCSystemServiceImpl::gRPCSystemServiceImpl(
|
|
const std::chrono::milliseconds stop_timeout)
|
|
: gRPCSystemServiceImpl(stop_timeout, makeDefaultGrpcSecurityGateway())
|
|
{
|
|
}
|
|
|
|
gRPCSystemServiceImpl::gRPCSystemServiceImpl(
|
|
std::shared_ptr<GrpcSecurityGateway> security_gateway)
|
|
: gRPCSystemServiceImpl(
|
|
std::chrono::seconds(15), std::move(security_gateway))
|
|
{
|
|
}
|
|
|
|
gRPCSystemServiceImpl::gRPCSystemServiceImpl(
|
|
const std::chrono::milliseconds stop_timeout,
|
|
std::shared_ptr<GrpcSecurityGateway> security_gateway)
|
|
: gRPCSystemServiceImpl(
|
|
stop_timeout, std::move(security_gateway), nullptr)
|
|
{
|
|
}
|
|
|
|
gRPCSystemServiceImpl::gRPCSystemServiceImpl(
|
|
const std::chrono::milliseconds stop_timeout,
|
|
std::shared_ptr<GrpcSecurityGateway> security_gateway,
|
|
std::shared_ptr<RecoveryAuditSink> recovery_audit_sink)
|
|
: dmgr_(DeviceManager::getInstance()),
|
|
stop_timeout_(
|
|
stop_timeout > std::chrono::milliseconds::zero()
|
|
? stop_timeout
|
|
: std::chrono::seconds(15)),
|
|
security_gateway_(
|
|
security_gateway ? std::move(security_gateway)
|
|
: makeDefaultGrpcSecurityGateway()),
|
|
recovery_audit_sink_(std::move(recovery_audit_sink)),
|
|
action_queue_(std::make_shared<ActionQueueExecutor>(dmgr_))
|
|
{
|
|
// Acquire only after ActionQueue construction succeeds. This ensures an
|
|
// exception cannot release the last dispatcher outside the registry.
|
|
acquireProcessStopDispatcher(stop_dispatcher_);
|
|
try {
|
|
safety_participant_registration_ = registerGrpcSafetyParticipants(
|
|
dmgr_.safetyManager(), action_queue_, stop_dispatcher_);
|
|
} catch (...) {
|
|
releaseProcessStopDispatcher(stop_dispatcher_);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
gRPCSystemServiceImpl::~gRPCSystemServiceImpl()
|
|
{
|
|
// The dispatcher intentionally outlives ActionQueue, then joins any
|
|
// deadline-overrunning stop workers before the last service disappears.
|
|
safety_participant_registration_.reset();
|
|
action_queue_.reset();
|
|
releaseProcessStopDispatcher(stop_dispatcher_);
|
|
}
|
|
|
|
bool gRPCSystemServiceImpl::waitForStopDispatcherDestructionForTesting(
|
|
const std::chrono::milliseconds timeout)
|
|
{
|
|
auto& registry = processStopDispatcherRegistry();
|
|
std::unique_lock lock(registry.mutex);
|
|
return registry.available.wait_for(
|
|
lock,
|
|
timeout,
|
|
[®istry] { return registry.destroying; });
|
|
}
|
|
|
|
void gRPCSystemServiceImpl::prepareForShutdown()
|
|
{
|
|
// Do not wait for a business StopAll round here. StopAll may be blocked in
|
|
// a device backend, while server shutdown must still invalidate queued and
|
|
// active ActionQueue work promptly. ActionQueueExecutor owns the state lock
|
|
// and generation fence needed to make this transition race-safe; an
|
|
// in-flight StopAll ticket then becomes stale and fails closed.
|
|
(void)action_queue_->disableForShutdown();
|
|
}
|
|
|
|
grpc::Status gRPCSystemServiceImpl::GetSystemInfo(grpc::ServerContext* context,
|
|
const api::GetSystemInfoCommand_Request* request, api::GetSystemInfoCommand_Feedback* response)
|
|
{
|
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
|
security_gateway_, context,
|
|
"/cmvr.api.SystemService/GetSystemInfo");
|
|
try {
|
|
response->set_version(dmgr_.version());
|
|
response->set_system_name(dmgr_.name());
|
|
response->set_action_service_instance_id(
|
|
action_queue_->instanceId());
|
|
const auto& security = security_gateway_->config();
|
|
response->set_grpc_transport_security(toString(security.transport));
|
|
response->set_grpc_authentication(toString(security.authentication));
|
|
response->set_grpc_recovery_exposure(
|
|
toString(security.recovery_exposure));
|
|
response->set_grpc_insecure_non_loopback(
|
|
security.insecure_non_loopback);
|
|
const auto safety = dmgr_.safetyManager().snapshot();
|
|
response->set_control_service_instance_id(
|
|
safety.service_instance_id);
|
|
response->set_safety_enforcement_mode(
|
|
cmvr::safety::toString(safety.enforcement_mode));
|
|
response->set_safety_schema_version(1);
|
|
response->mutable_header()->set_success(true);
|
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
|
CMVR_LOG(DEBUG) << "[gRPCSystemServiceImpl] (GetSystemInfo): success, name="
|
|
<< response->system_name() << ", version=" << response->version();
|
|
return grpc::Status::OK;
|
|
}
|
|
catch (std::exception& e) {
|
|
response->mutable_header()->set_success(false);
|
|
response->mutable_header()->set_error_message(e.what());
|
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
|
return grpc::Status::OK;
|
|
}
|
|
}
|
|
|
|
grpc::Status gRPCSystemServiceImpl::GetSystemStatus(grpc::ServerContext* context,
|
|
const api::GetSystemStatusCommand_Request* request, api::GetSystemStatusCommand_Feedback* response)
|
|
{
|
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
|
security_gateway_, context,
|
|
"/cmvr.api.SystemService/GetSystemStatus");
|
|
try {
|
|
std::list<std::pair<std::string, std::string>> dev_list;
|
|
dmgr_.getDeviceList(dev_list);
|
|
for (auto &pair: dev_list) {
|
|
auto* dev = response->add_device_list();
|
|
dev->set_device_id(pair.first);
|
|
if (pair.second == "AGV") {
|
|
dev->set_device_type(api::DeviceType::AGV);
|
|
}
|
|
else if (pair.second == "Battery") {
|
|
dev->set_device_type(api::DeviceType::Battery);
|
|
}
|
|
else if (pair.second == "Camera") {
|
|
dev->set_device_type(api::DeviceType::Camera);
|
|
}
|
|
else if (pair.second == "DexHand") {
|
|
dev->set_device_type(api::DeviceType::DexHand);
|
|
}
|
|
else if (pair.second == "Gripper") {
|
|
dev->set_device_type(api::DeviceType::Gripper);
|
|
}
|
|
else if (pair.second == "Microphone") {
|
|
dev->set_device_type(api::DeviceType::Microphone);
|
|
}
|
|
else if (pair.second == "Robot") {
|
|
dev->set_device_type(api::DeviceType::Robot);
|
|
}
|
|
else if (pair.second == "Speaker") {
|
|
dev->set_device_type(api::DeviceType::Speaker);
|
|
}
|
|
else if (pair.second == "Unknown") {
|
|
dev->set_device_type(api::DeviceType::Unknown);
|
|
}
|
|
}
|
|
response->mutable_header()->set_success(true);
|
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
|
CMVR_LOG(DEBUG) << "[gRPCSystemServiceImpl] (GetSystemStatus): success, devices="
|
|
<< response->device_list_size();
|
|
return grpc::Status::OK;
|
|
}
|
|
catch (const std::exception &e) {
|
|
response->mutable_header()->set_success(false);
|
|
response->mutable_header()->set_error_message(e.what());
|
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
|
return grpc::Status::OK;
|
|
}
|
|
}
|
|
|
|
grpc::Status gRPCSystemServiceImpl::GetDeviceList(
|
|
grpc::ServerContext* context,
|
|
const api::GetDeviceListCommand_Request* request,
|
|
api::GetDeviceListCommand_Feedback* response)
|
|
{
|
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
|
security_gateway_, context,
|
|
"/cmvr.api.SystemService/GetDeviceList");
|
|
(void)request;
|
|
try {
|
|
const auto snapshot = dmgr_.snapshot();
|
|
response->set_manager_name(snapshot.name);
|
|
response->set_manager_version(snapshot.version);
|
|
response->set_manager_description(snapshot.description);
|
|
response->set_sampled_at_unix_ms(unixTimeMs());
|
|
|
|
for (const auto& source : snapshot.devices) {
|
|
// The public inventory contains enabled entries only. Keep enabled
|
|
// devices visible even when their lifecycle or health is in error.
|
|
if (!source.enabled) {
|
|
continue;
|
|
}
|
|
|
|
auto* destination = response->add_device_list();
|
|
destination->set_device_id(source.id);
|
|
destination->set_device_type(toApiDeviceType(source.kind));
|
|
destination->set_type_name(source.type_name);
|
|
destination->set_enabled(true);
|
|
destination->set_manager_state(toApiDeviceState(source.state));
|
|
destination->set_health(toApiDeviceHealth(source.health.state));
|
|
destination->set_has_error(source.abnormal);
|
|
destination->set_error_message(source.error_message);
|
|
destination->set_status_updated_at_unix_ms(
|
|
source.status_updated_at_unix_ms);
|
|
}
|
|
|
|
response->mutable_header()->set_success(true);
|
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
|
CMVR_LOG(DEBUG) << "[gRPCSystemServiceImpl] (GetDeviceList): success, devices="
|
|
<< response->device_list_size();
|
|
return grpc::Status::OK;
|
|
}
|
|
catch (const std::exception& e) {
|
|
response->Clear();
|
|
response->mutable_header()->set_success(false);
|
|
response->mutable_header()->set_error_message(e.what());
|
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
|
return grpc::Status::OK;
|
|
}
|
|
}
|
|
|
|
grpc::Status gRPCSystemServiceImpl::GetSafetyState(
|
|
grpc::ServerContext* context,
|
|
const cmvr::api::GetSafetyStateCommand_Request* request,
|
|
cmvr::api::GetSafetyStateCommand_Feedback* response)
|
|
{
|
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
|
security_gateway_, context,
|
|
"/cmvr.api.SystemService/GetSafetyState");
|
|
if (!request || !response) {
|
|
return grpc::Status(
|
|
grpc::StatusCode::INVALID_ARGUMENT,
|
|
"GetSafetyState request and response are required");
|
|
}
|
|
|
|
const auto scope = parseSafetyScope(request->scope(), true);
|
|
if (!scope.valid) {
|
|
setSafetyHeaderFailure(
|
|
response->mutable_header(),
|
|
cmvr::safety::SafetyReason::InvalidArgument,
|
|
scope.error);
|
|
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, scope.error);
|
|
}
|
|
|
|
const auto snapshot = dmgr_.safetyManager().snapshot();
|
|
response->set_system_state(
|
|
toApiSystemAdmissionState(snapshot.system_state));
|
|
response->set_safety_epoch(snapshot.safety_epoch);
|
|
response->set_control_service_instance_id(snapshot.service_instance_id);
|
|
response->set_enforcement_mode(
|
|
cmvr::safety::toString(snapshot.enforcement_mode));
|
|
response->set_active_operation_id(snapshot.active_operation_id);
|
|
response->set_active_operation_phase(snapshot.active_operation_phase);
|
|
response->set_sampled_at_unix_ms(unixTimeMs());
|
|
|
|
std::unordered_set<std::string> requested_ids(
|
|
scope.device_ids.begin(), scope.device_ids.end());
|
|
for (const auto& device : snapshot.devices) {
|
|
if (!scope.all_devices &&
|
|
requested_ids.erase(device.descriptor.device_id) == 0U) {
|
|
continue;
|
|
}
|
|
populateDeviceSafetyState(device, *response->add_devices());
|
|
}
|
|
for (const auto& participant : snapshot.participants) {
|
|
populateSafetyParticipantState(
|
|
participant, *response->add_participants());
|
|
}
|
|
if (!requested_ids.empty()) {
|
|
const auto detail =
|
|
"safety device is not registered: " + *requested_ids.begin();
|
|
response->Clear();
|
|
setSafetyHeaderFailure(
|
|
response->mutable_header(),
|
|
cmvr::safety::SafetyReason::DeviceNotFound,
|
|
detail);
|
|
return grpc::Status(grpc::StatusCode::NOT_FOUND, detail);
|
|
}
|
|
|
|
response->mutable_header()->set_success(true);
|
|
response->mutable_header()->set_reason_code(
|
|
cmvr::api::COMMAND_REASON_CODE_NONE);
|
|
response->mutable_header()->set_service_instance_id(
|
|
snapshot.service_instance_id);
|
|
response->mutable_header()->set_safety_epoch(snapshot.safety_epoch);
|
|
response->mutable_header()->set_execution_state(
|
|
cmvr::api::COMMAND_EXECUTION_STATE_COMPLETED);
|
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
|
return grpc::Status::OK;
|
|
}
|
|
|
|
grpc::Status gRPCSystemServiceImpl::RecoverSafetyState(
|
|
grpc::ServerContext* context,
|
|
const cmvr::api::RecoverSafetyStateCommand_Request* request,
|
|
cmvr::api::RecoverSafetyStateCommand_Feedback* response)
|
|
{
|
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
|
security_gateway_, context,
|
|
"/cmvr.api.SystemService/RecoverSafetyState");
|
|
if (!request || !response) {
|
|
return grpc::Status(
|
|
grpc::StatusCode::INVALID_ARGUMENT,
|
|
"RecoverSafetyState request and response are required");
|
|
}
|
|
|
|
const auto scope = parseSafetyScope(request->scope(), false);
|
|
if (!scope.valid || request->recovery_id().empty() ||
|
|
request->reason().empty() || request->expected_safety_epoch() == 0 ||
|
|
request->mode() ==
|
|
cmvr::api::RecoverSafetyStateCommand::MODE_UNSPECIFIED) {
|
|
std::string detail = scope.valid
|
|
? "recovery_id, reason, expected_safety_epoch, and mode are required"
|
|
: scope.error;
|
|
setSafetyHeaderFailure(
|
|
response->mutable_header(),
|
|
request->reason().empty()
|
|
? cmvr::safety::SafetyReason::RecoveryReasonRequired
|
|
: cmvr::safety::SafetyReason::InvalidArgument,
|
|
detail);
|
|
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, detail);
|
|
}
|
|
if (!recovery_audit_sink_) {
|
|
const std::string detail =
|
|
"persistent recovery audit is not configured";
|
|
setSafetyHeaderFailure(
|
|
response->mutable_header(),
|
|
cmvr::safety::SafetyReason::RecoveryAuditFailed,
|
|
detail);
|
|
return grpc::Status(
|
|
grpc::StatusCode::FAILED_PRECONDITION,
|
|
"RECOVERY_AUDIT_FAILED");
|
|
}
|
|
|
|
const bool verify_only = request->mode() ==
|
|
cmvr::api::RecoverSafetyStateCommand::VERIFY_ONLY;
|
|
RecoveryAuditRecord audit;
|
|
audit.occurred_at_unix_ms = unixTimeMs();
|
|
audit.stage = "accepted";
|
|
audit.correlation_id = cmvr_grpc_call_guard.context().correlation_id;
|
|
audit.principal_id = cmvr_grpc_call_guard.context().principal.id;
|
|
audit.peer = cmvr_grpc_call_guard.context().peer;
|
|
audit.recovery_id = request->recovery_id();
|
|
audit.reason = request->reason();
|
|
audit.mode = verify_only ? "verify_only" : "clear_software_latch";
|
|
audit.all_devices = scope.all_devices;
|
|
audit.device_ids = scope.device_ids;
|
|
audit.expected_safety_epoch = request->expected_safety_epoch();
|
|
audit.result = "pending";
|
|
std::string audit_error;
|
|
if (!recovery_audit_sink_->append(audit, &audit_error)) {
|
|
const auto detail = audit_error.empty()
|
|
? std::string("persistent recovery audit write failed")
|
|
: audit_error;
|
|
setSafetyHeaderFailure(
|
|
response->mutable_header(),
|
|
cmvr::safety::SafetyReason::RecoveryAuditFailed,
|
|
detail);
|
|
return grpc::Status(
|
|
grpc::StatusCode::FAILED_PRECONDITION,
|
|
"RECOVERY_AUDIT_FAILED");
|
|
}
|
|
|
|
auto deadline = cmvr_grpc_call_guard.context().deadline;
|
|
const auto configured_deadline =
|
|
cmvr::safety::SafetyClock::now() +
|
|
dmgr_.safetyManager().config().recovery_timeout;
|
|
if (deadline == cmvr::safety::SafetyClock::time_point::max() ||
|
|
configured_deadline < deadline) {
|
|
deadline = configured_deadline;
|
|
}
|
|
if (request->timeout_ms() != 0) {
|
|
deadline = std::min(
|
|
deadline,
|
|
cmvr::safety::SafetyClock::now() +
|
|
std::chrono::milliseconds(request->timeout_ms()));
|
|
}
|
|
|
|
cmvr::safety::RecoveryRequest coordinator_request;
|
|
coordinator_request.recovery_id = request->recovery_id();
|
|
coordinator_request.device_ids = scope.device_ids;
|
|
coordinator_request.all_devices = scope.all_devices;
|
|
coordinator_request.expected_safety_epoch =
|
|
request->expected_safety_epoch();
|
|
coordinator_request.verify_only = verify_only;
|
|
coordinator_request.reason = request->reason();
|
|
coordinator_request.deadline = deadline;
|
|
if (!verify_only) {
|
|
auto commit_audit = audit;
|
|
commit_audit.stage = "clear_commit";
|
|
commit_audit.result = "authorized";
|
|
const auto sink = recovery_audit_sink_;
|
|
coordinator_request.authorize_clear =
|
|
[sink, commit_audit = std::move(commit_audit)]() mutable {
|
|
commit_audit.occurred_at_unix_ms = unixTimeMs();
|
|
std::string error;
|
|
const bool persisted = sink->append(commit_audit, &error);
|
|
if (!persisted) {
|
|
CMVR_LOG(ERROR)
|
|
<< "[gRPCSystemServiceImpl] Recovery clear audit "
|
|
"failed: "
|
|
<< error;
|
|
}
|
|
return persisted;
|
|
};
|
|
}
|
|
|
|
const auto result =
|
|
dmgr_.safetyManager().recover(coordinator_request);
|
|
response->set_recovery_id(result.recovery_id);
|
|
response->set_result(toApiRecoveryResult(result.result));
|
|
response->set_previous_safety_epoch(result.previous_safety_epoch);
|
|
response->set_current_safety_epoch(result.current_safety_epoch);
|
|
response->set_system_state(
|
|
toApiSystemAdmissionState(result.system_state));
|
|
for (const auto& target : result.targets) {
|
|
populateSafetyTargetResult(target, *response->add_targets());
|
|
}
|
|
|
|
const bool success = recoveryCompletedAsRequested(result.result);
|
|
auto* header = response->mutable_header();
|
|
header->set_success(success);
|
|
header->set_command_id(request->recovery_id());
|
|
header->set_service_instance_id(
|
|
dmgr_.safetyManager().serviceInstanceId());
|
|
header->set_safety_epoch(result.current_safety_epoch);
|
|
header->set_execution_state(
|
|
success ? cmvr::api::COMMAND_EXECUTION_STATE_COMPLETED
|
|
: cmvr::api::COMMAND_EXECUTION_STATE_FAILED);
|
|
if (success) {
|
|
header->set_reason_code(cmvr::api::COMMAND_REASON_CODE_NONE);
|
|
} else if (!result.targets.empty()) {
|
|
header->set_reason_code(
|
|
toApiSafetyReason(result.targets.front().reason));
|
|
header->set_error_message(result.targets.front().detail);
|
|
} else {
|
|
header->set_reason_code(
|
|
cmvr::api::COMMAND_REASON_CODE_INTERNAL_ERROR);
|
|
header->set_error_message("recovery failed without a target result");
|
|
}
|
|
setCurrentTimestamp(header->mutable_timestamp());
|
|
|
|
audit.occurred_at_unix_ms = unixTimeMs();
|
|
audit.stage = "completed";
|
|
audit.previous_safety_epoch = result.previous_safety_epoch;
|
|
audit.current_safety_epoch = result.current_safety_epoch;
|
|
audit.result = cmvr::safety::toString(result.result);
|
|
if (!recovery_audit_sink_->append(audit, &audit_error)) {
|
|
CMVR_LOG(ERROR)
|
|
<< "[gRPCSystemServiceImpl] Recovery completion audit failed: "
|
|
<< audit_error;
|
|
}
|
|
return grpc::Status::OK;
|
|
}
|
|
|
|
grpc::Status gRPCSystemServiceImpl::RestoreOperationalState(
|
|
grpc::ServerContext* context,
|
|
const cmvr::api::RestoreOperationalStateCommand_Request* request,
|
|
cmvr::api::RestoreOperationalStateCommand_Feedback* response)
|
|
{
|
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
|
security_gateway_, context,
|
|
"/cmvr.api.SystemService/RestoreOperationalState");
|
|
if (!request || !response) {
|
|
return grpc::Status(
|
|
grpc::StatusCode::INVALID_ARGUMENT,
|
|
"RestoreOperationalState request and response are required");
|
|
}
|
|
if (request->recovery_id().empty() || request->reason().empty()) {
|
|
const std::string detail =
|
|
"recovery_id and reason are required";
|
|
setSafetyHeaderFailure(
|
|
response->mutable_header(),
|
|
request->reason().empty()
|
|
? cmvr::safety::SafetyReason::RecoveryReasonRequired
|
|
: cmvr::safety::SafetyReason::InvalidArgument,
|
|
detail);
|
|
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, detail);
|
|
}
|
|
if (!recovery_audit_sink_) {
|
|
const std::string detail =
|
|
"persistent recovery audit is not configured";
|
|
setSafetyHeaderFailure(
|
|
response->mutable_header(),
|
|
cmvr::safety::SafetyReason::RecoveryAuditFailed,
|
|
detail);
|
|
return grpc::Status(
|
|
grpc::StatusCode::FAILED_PRECONDITION,
|
|
"RECOVERY_AUDIT_FAILED");
|
|
}
|
|
|
|
const auto accepted_snapshot = dmgr_.safetyManager().snapshot();
|
|
RecoveryAuditRecord audit;
|
|
audit.occurred_at_unix_ms = unixTimeMs();
|
|
audit.stage = "accepted";
|
|
audit.correlation_id = cmvr_grpc_call_guard.context().correlation_id;
|
|
audit.principal_id = cmvr_grpc_call_guard.context().principal.id;
|
|
audit.peer = cmvr_grpc_call_guard.context().peer;
|
|
audit.recovery_id = request->recovery_id();
|
|
audit.reason = request->reason();
|
|
audit.mode = "restore_operational_state";
|
|
audit.all_devices = true;
|
|
audit.expected_safety_epoch = accepted_snapshot.safety_epoch;
|
|
audit.result = "pending";
|
|
std::string audit_error;
|
|
if (!recovery_audit_sink_->append(audit, &audit_error)) {
|
|
const auto detail = audit_error.empty()
|
|
? std::string("persistent recovery audit write failed")
|
|
: audit_error;
|
|
setSafetyHeaderFailure(
|
|
response->mutable_header(),
|
|
cmvr::safety::SafetyReason::RecoveryAuditFailed,
|
|
detail);
|
|
return grpc::Status(
|
|
grpc::StatusCode::FAILED_PRECONDITION,
|
|
"RECOVERY_AUDIT_FAILED");
|
|
}
|
|
|
|
auto deadline = cmvr_grpc_call_guard.context().deadline;
|
|
const auto configured_deadline =
|
|
cmvr::safety::SafetyClock::now() +
|
|
dmgr_.safetyManager().config().recovery_timeout;
|
|
if (deadline == cmvr::safety::SafetyClock::time_point::max() ||
|
|
configured_deadline < deadline) {
|
|
deadline = configured_deadline;
|
|
}
|
|
if (request->timeout_ms() != 0) {
|
|
deadline = std::min(
|
|
deadline,
|
|
cmvr::safety::SafetyClock::now() +
|
|
std::chrono::milliseconds(request->timeout_ms()));
|
|
}
|
|
|
|
auto commit_audit = audit;
|
|
commit_audit.stage = "restore_commit";
|
|
commit_audit.result = "authorized";
|
|
const auto sink = recovery_audit_sink_;
|
|
|
|
cmvr::safety::RecoveryRequest coordinator_request;
|
|
coordinator_request.recovery_id = request->recovery_id();
|
|
coordinator_request.all_devices = true;
|
|
coordinator_request.expected_safety_epoch =
|
|
accepted_snapshot.safety_epoch;
|
|
coordinator_request.verify_only = false;
|
|
coordinator_request.restore_operational_state = true;
|
|
coordinator_request.reason = request->reason();
|
|
coordinator_request.deadline = deadline;
|
|
coordinator_request.authorize_clear =
|
|
[sink, commit_audit = std::move(commit_audit)]() mutable {
|
|
commit_audit.occurred_at_unix_ms = unixTimeMs();
|
|
std::string error;
|
|
const bool persisted = sink->append(commit_audit, &error);
|
|
if (!persisted) {
|
|
CMVR_LOG(ERROR)
|
|
<< "[gRPCSystemServiceImpl] Operational restore audit "
|
|
"failed: "
|
|
<< error;
|
|
}
|
|
return persisted;
|
|
};
|
|
|
|
const auto result =
|
|
dmgr_.safetyManager().recover(coordinator_request);
|
|
response->set_recovery_id(result.recovery_id);
|
|
response->set_result(toApiRecoveryResult(result.result));
|
|
response->set_previous_safety_epoch(result.previous_safety_epoch);
|
|
response->set_current_safety_epoch(result.current_safety_epoch);
|
|
response->set_system_state(
|
|
toApiSystemAdmissionState(result.system_state));
|
|
for (const auto& target : result.targets) {
|
|
populateSafetyTargetResult(target, *response->add_targets());
|
|
}
|
|
|
|
const bool success = recoveryCompletedAsRequested(result.result);
|
|
auto* header = response->mutable_header();
|
|
header->set_success(success);
|
|
header->set_command_id(request->recovery_id());
|
|
header->set_service_instance_id(
|
|
dmgr_.safetyManager().serviceInstanceId());
|
|
header->set_safety_epoch(result.current_safety_epoch);
|
|
header->set_execution_state(
|
|
success ? cmvr::api::COMMAND_EXECUTION_STATE_COMPLETED
|
|
: cmvr::api::COMMAND_EXECUTION_STATE_FAILED);
|
|
if (success) {
|
|
header->set_reason_code(cmvr::api::COMMAND_REASON_CODE_NONE);
|
|
} else {
|
|
const auto failed = std::find_if(
|
|
result.targets.begin(), result.targets.end(),
|
|
[](const auto& target) { return !target.success; });
|
|
if (failed != result.targets.end()) {
|
|
header->set_reason_code(toApiSafetyReason(failed->reason));
|
|
header->set_error_message(failed->detail);
|
|
} else {
|
|
header->set_reason_code(
|
|
cmvr::api::COMMAND_REASON_CODE_INTERNAL_ERROR);
|
|
header->set_error_message(
|
|
"operational restore failed without a target result");
|
|
}
|
|
}
|
|
setCurrentTimestamp(header->mutable_timestamp());
|
|
|
|
audit.occurred_at_unix_ms = unixTimeMs();
|
|
audit.stage = "completed";
|
|
audit.previous_safety_epoch = result.previous_safety_epoch;
|
|
audit.current_safety_epoch = result.current_safety_epoch;
|
|
audit.result = cmvr::safety::toString(result.result);
|
|
if (!recovery_audit_sink_->append(audit, &audit_error)) {
|
|
CMVR_LOG(ERROR)
|
|
<< "[gRPCSystemServiceImpl] Operational restore completion audit "
|
|
"failed: "
|
|
<< audit_error;
|
|
}
|
|
return grpc::Status::OK;
|
|
}
|
|
|
|
grpc::Status gRPCSystemServiceImpl::UpdateParams(grpc::ServerContext* context, const cmvr::api::UpdateParamsCommand_Request* request, cmvr::api::UpdateParamsCommand_Feedback* response)
|
|
{
|
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
|
security_gateway_, context,
|
|
"/cmvr.api.SystemService/UpdateParams");
|
|
response->mutable_header()->set_success(false);
|
|
response->mutable_header()->set_error_message("UpdateParams is no longer supported. Use typed device commands or reload configuration.");
|
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
|
return grpc::Status::OK;
|
|
}
|
|
|
|
grpc::Status gRPCSystemServiceImpl::StopAll(grpc::ServerContext* context,
|
|
const cmvr::api::StopAllCommand_Request* request, cmvr::api::StopAllCommand_Feedback* response)
|
|
{
|
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
|
security_gateway_, context, "/cmvr.api.SystemService/StopAll");
|
|
if (!request || !response) {
|
|
return grpc::Status(
|
|
grpc::StatusCode::INVALID_ARGUMENT,
|
|
"StopAll request and response are required");
|
|
}
|
|
if (dmgr_.safetyManager().config().enforcement_mode !=
|
|
cmvr::safety::EnforcementMode::Legacy) {
|
|
auto deadline = cmvr_grpc_call_guard.context().deadline;
|
|
const auto configured_deadline =
|
|
cmvr::safety::SafetyClock::now() + stop_timeout_;
|
|
if (deadline == cmvr::safety::SafetyClock::time_point::max() ||
|
|
configured_deadline < deadline) {
|
|
deadline = configured_deadline;
|
|
}
|
|
if (request->timeout_ms() != 0) {
|
|
deadline = std::min(
|
|
deadline,
|
|
cmvr::safety::SafetyClock::now() +
|
|
std::chrono::milliseconds(request->timeout_ms()));
|
|
}
|
|
|
|
std::string operation_id = request->operation_id();
|
|
if (operation_id.empty() && request->has_header()) {
|
|
operation_id = request->header().command_id();
|
|
}
|
|
const auto result = dmgr_.safetyManager().stopAll(
|
|
std::move(operation_id), deadline);
|
|
response->set_operation_id(result.operation_id);
|
|
response->set_previous_safety_epoch(
|
|
result.previous_safety_epoch);
|
|
response->set_current_safety_epoch(
|
|
result.current_safety_epoch);
|
|
response->set_system_state(
|
|
toApiSystemAdmissionState(result.system_state));
|
|
for (const auto& target : result.targets) {
|
|
populateSafetyTargetResult(target, *response->add_targets());
|
|
}
|
|
|
|
auto* header = response->mutable_header();
|
|
header->set_success(result.success);
|
|
header->set_command_id(result.operation_id);
|
|
header->set_service_instance_id(
|
|
dmgr_.safetyManager().serviceInstanceId());
|
|
header->set_safety_epoch(result.current_safety_epoch);
|
|
header->set_execution_state(
|
|
result.success
|
|
? cmvr::api::COMMAND_EXECUTION_STATE_COMPLETED
|
|
: cmvr::api::COMMAND_EXECUTION_STATE_FAILED);
|
|
if (result.success) {
|
|
header->set_reason_code(cmvr::api::COMMAND_REASON_CODE_NONE);
|
|
} else if (!result.targets.empty()) {
|
|
const auto failed = std::find_if(
|
|
result.targets.begin(), result.targets.end(),
|
|
[](const auto& target) { return !target.success; });
|
|
if (failed != result.targets.end()) {
|
|
header->set_reason_code(toApiSafetyReason(failed->reason));
|
|
header->set_error_message(failed->detail);
|
|
}
|
|
} else {
|
|
header->set_reason_code(
|
|
cmvr::api::COMMAND_REASON_CODE_STOP_UNCONFIRMED);
|
|
header->set_error_message(
|
|
"StopAll did not produce a participant result");
|
|
}
|
|
setCurrentTimestamp(header->mutable_timestamp());
|
|
return grpc::Status::OK;
|
|
}
|
|
(void)request;
|
|
const auto stop_deadline =
|
|
std::chrono::steady_clock::now() + stop_timeout_;
|
|
|
|
std::unique_lock<std::timed_mutex> stop_all_lock(
|
|
processStopAllMutex(), std::defer_lock);
|
|
while (!stop_all_lock.try_lock_for(std::min(
|
|
std::chrono::milliseconds(50),
|
|
remainingStopBudget(stop_deadline)))) {
|
|
if (context && context->IsCancelled()) {
|
|
return grpc::Status(
|
|
grpc::StatusCode::CANCELLED,
|
|
"StopAll was cancelled while waiting for another StopAll round");
|
|
}
|
|
if (remainingStopBudget(stop_deadline) ==
|
|
std::chrono::milliseconds::zero()) {
|
|
response->mutable_header()->set_success(false);
|
|
response->mutable_header()->set_error_message(
|
|
"StopAll timed out waiting for another StopAll round");
|
|
setCurrentTimestamp(
|
|
response->mutable_header()->mutable_timestamp());
|
|
return grpc::Status::OK;
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Close every admission domain before resolving devices or issuing
|
|
// stops. The process remains alive; only new operational work pauses.
|
|
ScopedAdmissionStopAll admission_stop;
|
|
ScopedMediaStopAll media_stop;
|
|
ScopedMotorStopAll motor_stop;
|
|
ScopedActionQueueStopAll action_stop(*action_queue_);
|
|
if (!admission_stop.valid()) {
|
|
throw std::runtime_error(
|
|
"StopAll could not establish the system admission barrier");
|
|
}
|
|
if (!media_stop.valid()) {
|
|
throw std::runtime_error(
|
|
"StopAll could not establish the media activity barrier");
|
|
}
|
|
if (!action_stop.valid()) {
|
|
throw std::runtime_error(
|
|
"StopAll cannot start because the service is shutting down");
|
|
}
|
|
if (!motor_stop.valid()) {
|
|
throw std::runtime_error(
|
|
"StopAll could not establish the motor activity barrier");
|
|
}
|
|
|
|
// This snapshot is intentionally metadata-only. A wedged health query
|
|
// must not prevent physical stop requests from being dispatched.
|
|
const auto inventory = dmgr_.inventorySnapshot();
|
|
ScopedControlBarrierSet control_barriers;
|
|
std::vector<std::string> unconfirmed_devices;
|
|
|
|
struct ControlTarget final {
|
|
std::string id;
|
|
cmvr::device::DeviceKind kind{
|
|
cmvr::device::DeviceKind::Unknown};
|
|
std::shared_ptr<cmvr::device::RobotArm> arm;
|
|
std::shared_ptr<cmvr::device::AbstractAGV> agv;
|
|
std::shared_ptr<cmvr::device::AbstractDexHand> hand;
|
|
cmvr::control::ControlLeaseToken barrier;
|
|
};
|
|
std::vector<ControlTarget> control_targets;
|
|
control_targets.reserve(inventory.size());
|
|
|
|
struct OtherTarget final {
|
|
std::string id;
|
|
std::shared_ptr<cmvr::device::AbstractCamera> camera;
|
|
std::shared_ptr<cmvr::device::AbstractMicrophone> microphone;
|
|
std::shared_ptr<cmvr::device::AbstractSpeaker> speaker;
|
|
std::shared_ptr<cmvr::device::AbstractBiohead> head;
|
|
std::shared_ptr<cmvr::device::AbstractGripper> gripper;
|
|
bool tracked_media{false};
|
|
};
|
|
std::unordered_map<std::string, OtherTarget> other_targets;
|
|
other_targets.reserve(inventory.size());
|
|
|
|
for (const auto& device : inventory) {
|
|
const bool is_control =
|
|
device.kind == cmvr::device::DeviceKind::Arm ||
|
|
device.kind == cmvr::device::DeviceKind::AGV ||
|
|
device.kind == cmvr::device::DeviceKind::DexHand;
|
|
if (!is_control) {
|
|
OtherTarget target;
|
|
target.id = device.id;
|
|
switch (device.kind) {
|
|
case cmvr::device::DeviceKind::Camera:
|
|
target.camera = std::dynamic_pointer_cast<
|
|
cmvr::device::AbstractCamera>(device.device);
|
|
break;
|
|
case cmvr::device::DeviceKind::Microphone:
|
|
target.microphone = std::dynamic_pointer_cast<
|
|
cmvr::device::AbstractMicrophone>(device.device);
|
|
break;
|
|
case cmvr::device::DeviceKind::Speaker:
|
|
target.speaker = std::dynamic_pointer_cast<
|
|
cmvr::device::AbstractSpeaker>(device.device);
|
|
break;
|
|
case cmvr::device::DeviceKind::BioHead:
|
|
target.head = std::dynamic_pointer_cast<
|
|
cmvr::device::AbstractBiohead>(device.device);
|
|
break;
|
|
case cmvr::device::DeviceKind::Gripper:
|
|
target.gripper = std::dynamic_pointer_cast<
|
|
cmvr::device::AbstractGripper>(device.device);
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
if (!target.camera && !target.microphone &&
|
|
!target.speaker && !target.head && !target.gripper) {
|
|
appendFailure(
|
|
unconfirmed_devices, device.id,
|
|
"inventory type did not resolve to its typed device");
|
|
continue;
|
|
}
|
|
other_targets.emplace(device.id, std::move(target));
|
|
continue;
|
|
}
|
|
|
|
ControlTarget target;
|
|
target.id = device.id;
|
|
target.kind = device.kind;
|
|
if (device.kind == cmvr::device::DeviceKind::Arm) {
|
|
target.arm = std::dynamic_pointer_cast<
|
|
cmvr::device::RobotArm>(device.device);
|
|
} else if (device.kind == cmvr::device::DeviceKind::AGV) {
|
|
target.agv = std::dynamic_pointer_cast<
|
|
cmvr::device::AbstractAGV>(device.device);
|
|
} else {
|
|
target.hand = std::dynamic_pointer_cast<
|
|
cmvr::device::AbstractDexHand>(device.device);
|
|
}
|
|
if (!target.arm && !target.agv && !target.hand) {
|
|
appendFailure(
|
|
unconfirmed_devices, device.id,
|
|
"inventory type did not resolve to the typed control device");
|
|
continue;
|
|
}
|
|
|
|
std::string detail;
|
|
bool acquired = false;
|
|
try {
|
|
acquired = control_barriers.acquire(
|
|
device.id, detail, target.barrier);
|
|
} catch (const std::exception& error) {
|
|
detail = error.what();
|
|
} catch (...) {
|
|
detail = "unknown exception";
|
|
}
|
|
if (!acquired) {
|
|
appendFailure(
|
|
unconfirmed_devices, device.id,
|
|
"could not establish the device safety barrier" +
|
|
(detail.empty() ? "" : ": " + detail));
|
|
continue;
|
|
}
|
|
control_targets.push_back(std::move(target));
|
|
}
|
|
|
|
std::unordered_set<std::string> tracked_media_ids;
|
|
const auto merge_tracked_ids = [&tracked_media_ids](
|
|
const std::vector<std::string>& ids) {
|
|
tracked_media_ids.insert(ids.begin(), ids.end());
|
|
};
|
|
merge_tracked_ids(
|
|
cmvr::media::globalMediaSourceManager().trackedSourceIds());
|
|
merge_tracked_ids(
|
|
globalCameraPtzActivityRegistry().trackedDeviceIds());
|
|
merge_tracked_ids(
|
|
globalCameraOperationalActivityRegistry().trackedDeviceIds());
|
|
for (const auto& id : tracked_media_ids) {
|
|
auto [target, inserted] = other_targets.try_emplace(id);
|
|
if (inserted) {
|
|
target->second.id = id;
|
|
}
|
|
target->second.tracked_media = true;
|
|
}
|
|
|
|
// Every independent stop is submitted before waiting for any result.
|
|
// A blocked backend therefore cannot delay peer motion or media stops.
|
|
std::vector<StopHandleEntry> control_stops;
|
|
control_stops.reserve(control_targets.size());
|
|
for (const auto& target : control_targets) {
|
|
StopHandle handle;
|
|
auto deadline_detail = std::make_shared<StopDeadlineDetail>();
|
|
if (target.arm) {
|
|
const auto arm = target.arm;
|
|
const auto barrier = target.barrier;
|
|
deadline_detail->publishInitial(
|
|
"initial RobotArm stop did not complete before the "
|
|
"StopAll deadline");
|
|
handle = stop_dispatcher_->submit(
|
|
"control:" + stopResourceKey(target.id),
|
|
[arm, barrier, stop_deadline, deadline_detail] {
|
|
return stopControlWithFence(
|
|
barrier, stop_deadline,
|
|
[arm] { return stopArm(arm, false); },
|
|
[arm] { return stopArm(arm, true); },
|
|
"RobotArm", deadline_detail);
|
|
},
|
|
[deadline_detail] {
|
|
return deadline_detail->snapshot();
|
|
});
|
|
} else if (target.agv) {
|
|
const auto agv = target.agv;
|
|
const auto barrier = target.barrier;
|
|
deadline_detail->publishInitial(
|
|
"initial AGV stop did not complete before the StopAll "
|
|
"deadline");
|
|
handle = stop_dispatcher_->submit(
|
|
"control:" + stopResourceKey(target.id),
|
|
[agv, barrier, stop_deadline, deadline_detail] {
|
|
return stopControlWithFence(
|
|
barrier, stop_deadline,
|
|
[agv] { return stopAgv(agv, false); },
|
|
[agv] { return stopAgv(agv, true); },
|
|
"AGV", deadline_detail);
|
|
},
|
|
[deadline_detail] {
|
|
return deadline_detail->snapshot();
|
|
});
|
|
} else {
|
|
const auto hand = target.hand;
|
|
const auto barrier = target.barrier;
|
|
deadline_detail->publishInitial(
|
|
"initial DexHand stop did not complete before the "
|
|
"StopAll deadline");
|
|
handle = stop_dispatcher_->submit(
|
|
"control:" + stopResourceKey(target.id),
|
|
[hand, barrier, stop_deadline, deadline_detail] {
|
|
return stopControlWithFence(
|
|
barrier, stop_deadline,
|
|
[hand] { return stopDexHand(hand, false); },
|
|
[hand] { return stopDexHand(hand, true); },
|
|
"DexHand", deadline_detail);
|
|
},
|
|
[deadline_detail] {
|
|
return deadline_detail->snapshot();
|
|
});
|
|
}
|
|
control_stops.push_back(
|
|
{target.id, std::move(handle), true, true});
|
|
}
|
|
|
|
std::vector<StopHandleEntry> motor_operations;
|
|
std::vector<cmvr::service::DeferredStopOperation>
|
|
deferred_motor_operations;
|
|
std::string motor_collection_error;
|
|
if (!motor_stop.collectStopOperations(
|
|
deferred_motor_operations, &motor_collection_error)) {
|
|
appendFailure(
|
|
unconfirmed_devices, "motors",
|
|
motor_collection_error.empty()
|
|
? "could not collect registered motor stop operations"
|
|
: motor_collection_error);
|
|
} else {
|
|
submitDeferredOperations(
|
|
*stop_dispatcher_, std::move(deferred_motor_operations),
|
|
motor_operations);
|
|
}
|
|
|
|
std::vector<StopHandleEntry> media_cancellations;
|
|
std::vector<cmvr::service::DeferredStopOperation>
|
|
deferred_media_cancellations;
|
|
std::string media_collection_error;
|
|
if (!media_stop.collectCancellationOperations(
|
|
deferred_media_cancellations, &media_collection_error)) {
|
|
appendFailure(
|
|
unconfirmed_devices, "media",
|
|
media_collection_error.empty()
|
|
? "could not collect active media cancellations"
|
|
: media_collection_error);
|
|
} else {
|
|
submitDeferredOperations(
|
|
*stop_dispatcher_, std::move(deferred_media_cancellations),
|
|
media_cancellations);
|
|
}
|
|
|
|
std::vector<StopHandleEntry> task_stops;
|
|
const auto task_targets =
|
|
cmvr::task::TaskManager::activitySnapshotIfInitialized();
|
|
task_stops.reserve(task_targets.size());
|
|
for (const auto& task : task_targets) {
|
|
const auto task_id = task ? task->id() : std::string{"unknown"};
|
|
auto handle = stop_dispatcher_->submit(
|
|
"task:" + task_id,
|
|
[task] {
|
|
return invokeStopOperation(
|
|
[task] { return stopTaskActivity(task); },
|
|
"task activity stop");
|
|
});
|
|
task_stops.push_back(
|
|
{task_id, std::move(handle), false, true});
|
|
}
|
|
|
|
std::vector<StopHandleEntry> other_stops;
|
|
other_stops.reserve(other_targets.size());
|
|
for (const auto& [id, target] : other_targets) {
|
|
const auto camera = target.camera;
|
|
const auto microphone = target.microphone;
|
|
const auto speaker = target.speaker;
|
|
const auto head = target.head;
|
|
const auto gripper = target.gripper;
|
|
const bool tracked_media = target.tracked_media;
|
|
auto handle = stop_dispatcher_->submit(
|
|
"activity:" + stopResourceKey(id),
|
|
[id, camera, microphone, speaker, head, gripper,
|
|
tracked_media] {
|
|
return stopOtherActivities(
|
|
id, camera, microphone, speaker, head, gripper,
|
|
tracked_media);
|
|
});
|
|
other_stops.push_back(
|
|
{id, std::move(handle), false, true});
|
|
}
|
|
|
|
std::unordered_set<std::string> completed_control_stops;
|
|
(void)waitForStopOperations(
|
|
control_stops, stop_deadline, control_barriers,
|
|
unconfirmed_devices, &completed_control_stops);
|
|
(void)waitForStopOperations(
|
|
motor_operations, stop_deadline, control_barriers,
|
|
unconfirmed_devices);
|
|
(void)waitForStopOperations(
|
|
media_cancellations, stop_deadline, control_barriers,
|
|
unconfirmed_devices);
|
|
(void)waitForStopOperations(
|
|
task_stops, stop_deadline, control_barriers,
|
|
unconfirmed_devices);
|
|
std::unordered_set<std::string> completed_other_stops;
|
|
(void)waitForStopOperations(
|
|
other_stops, stop_deadline, control_barriers,
|
|
unconfirmed_devices, &completed_other_stops);
|
|
|
|
std::string motor_error;
|
|
const bool motors_stopped = motor_stop.waitForStopped(
|
|
remainingStopBudget(stop_deadline), &motor_error);
|
|
if (!motors_stopped) {
|
|
unconfirmed_devices.push_back(
|
|
"motors: " + (motor_error.empty()
|
|
? "operational stop was not confirmed"
|
|
: motor_error));
|
|
}
|
|
|
|
const bool media_stopped = media_stop.waitForStopped(
|
|
remainingStopBudget(stop_deadline));
|
|
if (!media_stopped) {
|
|
unconfirmed_devices.push_back(
|
|
"media: timed out waiting for active RPCs to stop");
|
|
} else {
|
|
// The first device stop runs in parallel with media cancellation so
|
|
// motion stops are never delayed by a blocked media handler. A
|
|
// handler already inside runIfCurrent(), however, can finish a
|
|
// start/resume after that first stop. Once every old media session
|
|
// has drained, repeat the typed operational stops while all
|
|
// admission gates are still closed. Do not retry a device whose
|
|
// first stop is still running; StopOperationDispatcher would only
|
|
// join that old job and a concurrent driver stop would be unsafe.
|
|
std::vector<StopHandleEntry> final_media_stops;
|
|
final_media_stops.reserve(completed_other_stops.size());
|
|
for (const auto& [id, target] : other_targets) {
|
|
const bool needs_final_media_stop =
|
|
target.camera || target.microphone || target.speaker ||
|
|
target.tracked_media;
|
|
if (!needs_final_media_stop ||
|
|
completed_other_stops.count(id) == 0U) {
|
|
continue;
|
|
}
|
|
|
|
const auto camera = target.camera;
|
|
const auto microphone = target.microphone;
|
|
const auto speaker = target.speaker;
|
|
const bool tracked_media = target.tracked_media;
|
|
auto handle = stop_dispatcher_->submit(
|
|
"activity:" + stopResourceKey(id),
|
|
[id, camera, microphone, speaker, tracked_media] {
|
|
return stopOtherActivities(
|
|
id, camera, microphone, speaker, nullptr, nullptr,
|
|
tracked_media);
|
|
});
|
|
final_media_stops.push_back(
|
|
{id, std::move(handle), false, true});
|
|
}
|
|
|
|
// DexHand sensor RPCs also belong to the media coordinator and can
|
|
// race their resumeOperationalActivity() with the initial control
|
|
// stop. Their typed safety barrier is still held here, so a final
|
|
// operational stop cannot admit a new hand command.
|
|
for (const auto& target : control_targets) {
|
|
if (!target.hand ||
|
|
completed_control_stops.count(target.id) == 0U) {
|
|
continue;
|
|
}
|
|
const auto hand = target.hand;
|
|
auto handle = stop_dispatcher_->submit(
|
|
"final-media-control:" + stopResourceKey(target.id),
|
|
[hand] {
|
|
return invokeStopOperation(
|
|
[hand] { return stopDexHand(hand, true); },
|
|
"final DexHand media activity stop");
|
|
});
|
|
final_media_stops.push_back(
|
|
{target.id, std::move(handle), true, true});
|
|
}
|
|
(void)waitForStopOperations(
|
|
final_media_stops, stop_deadline, control_barriers,
|
|
unconfirmed_devices);
|
|
}
|
|
|
|
if (!action_queue_->waitForIdle(
|
|
remainingStopBudget(stop_deadline))) {
|
|
unconfirmed_devices.push_back(
|
|
"ActionQueue: timed out waiting for the execution queue to "
|
|
"become idle");
|
|
}
|
|
if (!unconfirmed_devices.empty()) {
|
|
control_barriers.quarantineAll();
|
|
throw std::runtime_error(
|
|
"StopAll could not confirm that every device stopped; "
|
|
"control remains paused and affected resources remain "
|
|
"quarantined: " +
|
|
unconfirmed_devices.front());
|
|
}
|
|
control_barriers.recoverRetiredSafetyHoldersAll();
|
|
if (!action_stop.complete(true)) {
|
|
throw std::runtime_error(
|
|
"StopAll stopped all devices but could not safely resume "
|
|
"ActionQueue admission");
|
|
}
|
|
if (!media_stop.complete(true)) {
|
|
throw std::runtime_error(
|
|
"StopAll stopped all media but could not safely resume "
|
|
"media activity admission");
|
|
}
|
|
if (!motor_stop.complete(true)) {
|
|
throw std::runtime_error(
|
|
"StopAll stopped all motors but could not safely resume "
|
|
"motor command admission");
|
|
}
|
|
// Release the current round's typed control barriers while the global
|
|
// gate is still closed. Retired barriers from earlier failed rounds
|
|
// were recovered above; unrelated active safety holders are preserved.
|
|
control_barriers.releaseAll();
|
|
if (!admission_stop.complete(true)) {
|
|
throw std::runtime_error(
|
|
"StopAll stopped all activities but could not safely resume "
|
|
"system admission");
|
|
}
|
|
response->mutable_header()->set_success(true);
|
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
|
CMVR_LOG(DEBUG) << "[gRPCSystemServiceImpl] (StopAll): success";
|
|
return grpc::Status::OK;
|
|
}
|
|
catch (std::exception& e) {
|
|
response->mutable_header()->set_success(false);
|
|
response->mutable_header()->set_error_message(e.what());
|
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
|
return grpc::Status::OK;
|
|
}
|
|
catch (...) {
|
|
response->mutable_header()->set_success(false);
|
|
response->mutable_header()->set_error_message(
|
|
"StopAll failed with an unknown exception");
|
|
setCurrentTimestamp(response->mutable_header()->mutable_timestamp());
|
|
return grpc::Status::OK;
|
|
}
|
|
}
|
|
|
|
grpc::Status gRPCSystemServiceImpl::ExecuteActionQueue(
|
|
grpc::ServerContext* context,
|
|
const cmvr::api::ActionQueueCommand_Request* request,
|
|
cmvr::api::ActionQueueCommand_Feedback* response)
|
|
{
|
|
CMVR_GRPC_REQUIRE_REGISTERED_CALL(
|
|
security_gateway_, context,
|
|
"/cmvr.api.SystemService/ExecuteActionQueue");
|
|
if (!request || !response) {
|
|
return grpc::Status(
|
|
grpc::StatusCode::INVALID_ARGUMENT,
|
|
"ActionQueue request and response are required");
|
|
}
|
|
try {
|
|
cmvr::safety::CommandActor actor;
|
|
actor.principal_id =
|
|
cmvr_grpc_call_guard.context().principal.id;
|
|
actor.authenticated =
|
|
cmvr_grpc_call_guard.context().principal.authenticated;
|
|
for (const auto role :
|
|
cmvr_grpc_call_guard.context().principal.roles) {
|
|
actor.roles.emplace_back(cmvr::service::toString(role));
|
|
}
|
|
// The callback is consumed only on this synchronous handler stack. It
|
|
// is never retained by the worker-owned action Record, so returning the
|
|
// RPC cannot leave a dangling ServerContext reference.
|
|
const auto wait_result = action_queue_->submitAndWait(
|
|
*request,
|
|
*response,
|
|
[context]() {
|
|
return context && context->IsCancelled();
|
|
},
|
|
std::move(actor));
|
|
if (wait_result ==
|
|
ActionQueueExecutor::WaitResult::CanceledBeforeAdmission) {
|
|
return grpc::Status(
|
|
grpc::StatusCode::CANCELLED,
|
|
"ActionQueue RPC was canceled before admission");
|
|
}
|
|
if (wait_result ==
|
|
ActionQueueExecutor::WaitResult::CanceledAfterAdmission) {
|
|
return grpc::Status(
|
|
grpc::StatusCode::CANCELLED,
|
|
"ActionQueue RPC waiter was canceled after admission; the edge action continues and its result can be retrieved with the same action_id");
|
|
}
|
|
return grpc::Status::OK;
|
|
} catch (const std::exception& error) {
|
|
response->Clear();
|
|
response->set_action_id(request->action_id());
|
|
response->set_service_instance_id(action_queue_->instanceId());
|
|
response->set_result(api::ACTION_RESULT_CODE_FAILED);
|
|
response->mutable_header()->set_success(false);
|
|
response->mutable_header()->set_error_message(error.what());
|
|
setCurrentTimestamp(
|
|
response->mutable_header()->mutable_timestamp());
|
|
return grpc::Status::OK;
|
|
}
|
|
}
|