Commit 5d998e71 authored by Kyle Williams's avatar Kyle Williams Committed by Chromium LUCI CQ

chromebox_for_meetings: Create Device Info Service

Create an API to allow devices on the Chromebox For Meetings platform to query relevant device system and policy information.

Bug:b:174847382
Test:'autoninja -C out/Default/ chrome/test:unit_tests
      chromeos:chromeos_unittests && xvfb-run ./out/Default/unit_tests
       --single-process-tests --gtest_filter=CfmDeviceInfoServiceTest.*'

Change-Id: I954ac3ff4368792e9e147a856ac3cd8c07242fd3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2570651Reviewed-by: default avatarKyle Horimoto <khorimoto@chromium.org>
Reviewed-by: default avatarDominick Ng <dominickn@chromium.org>
Commit-Queue: Kyle Williams <kdgwill@chromium.org>
Cr-Commit-Position: refs/heads/master@{#835992}
parent ecbace0f
......@@ -3104,6 +3104,8 @@ source_set("chromeos") {
"chromebox_for_meetings/browser/cfm_browser_service.h",
"chromebox_for_meetings/cfm_chrome_services.cc",
"chromebox_for_meetings/cfm_chrome_services.h",
"chromebox_for_meetings/device_info/device_info_service.cc",
"chromebox_for_meetings/device_info/device_info_service.h",
"chromebox_for_meetings/diagnostics/diagnostics_service.cc",
"chromebox_for_meetings/diagnostics/diagnostics_service.h",
"chromebox_for_meetings/logger/cfm_logger_service.cc",
......@@ -4129,6 +4131,7 @@ source_set("unit_tests") {
if (is_cfm) {
sources += [
"chromebox_for_meetings/browser/cfm_browser_service_unittest.cc",
"chromebox_for_meetings/device_info/device_info_service_unittest.cc",
"chromebox_for_meetings/diagnostics/diagnostics_service_unittest.cc",
"chromebox_for_meetings/logger/cfm_logger_service_unittest.cc",
"chromebox_for_meetings/service_adaptor_unittest.cc",
......
......@@ -5,6 +5,7 @@
#include "chrome/browser/chromeos/chromebox_for_meetings/cfm_chrome_services.h"
#include "chrome/browser/chromeos/chromebox_for_meetings/browser/cfm_browser_service.h"
#include "chrome/browser/chromeos/chromebox_for_meetings/device_info/device_info_service.h"
#include "chrome/browser/chromeos/chromebox_for_meetings/diagnostics/diagnostics_service.h"
#include "chrome/browser/chromeos/chromebox_for_meetings/logger/cfm_logger_service.h"
#include "chromeos/components/chromebox_for_meetings/features/features.h"
......@@ -20,6 +21,7 @@ void InitializeCfmServices() {
CfmBrowserService::Initialize();
CfmLoggerService::Initialize();
DeviceInfoService::Initialize();
DiagnosticsService::Initialize();
}
......@@ -29,6 +31,7 @@ void ShutdownCfmServices() {
}
DiagnosticsService::Shutdown();
DeviceInfoService::Shutdown();
CfmLoggerService::Shutdown();
CfmBrowserService::Shutdown();
}
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/chromebox_for_meetings/device_info/device_info_service.h"
#include <cstdint>
#include "base/bind.h"
#include "base/macros.h"
#include "base/optional.h"
#include "base/strings/string_number_conversions.h"
#include "base/system/sys_info.h"
#include "base/time/time.h"
#include "chrome/browser/chromeos/settings/device_settings_service.h"
#include "chromeos/dbus/chromebox_for_meetings/cfm_hotline_client.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
namespace chromeos {
namespace cfm {
namespace {
constexpr char kRootPartition[] = "/";
constexpr char kStatefulPartition[] = "/mnt/stateful_partition";
constexpr char kReleaseVersion[] = "CHROMEOS_RELEASE_VERSION";
constexpr char kReleasBuildType[] = "CHROMEOS_RELEASE_BUILD_TYPE";
constexpr char kReleaseTrack[] = "CHROMEOS_RELEASE_TRACK";
constexpr char kReleaseChromeMilestone[] = "CHROMEOS_RELEASE_CHROME_MILESTONE";
static DeviceInfoService* g_info_service = nullptr;
} // namespace
// static
void DeviceInfoService::Initialize() {
CHECK(!g_info_service);
g_info_service = new DeviceInfoService();
}
// static
void DeviceInfoService::Shutdown() {
CHECK(g_info_service);
delete g_info_service;
g_info_service = nullptr;
}
// static
DeviceInfoService* DeviceInfoService::Get() {
CHECK(g_info_service)
<< "DeviceInfoService::Get() called before Initialize()";
return g_info_service;
}
// static
bool DeviceInfoService::IsInitialized() {
return g_info_service;
}
bool DeviceInfoService::ServiceRequestReceived(
const std::string& interface_name) {
if (interface_name != mojom::MeetDevicesInfo::Name_) {
return false;
}
service_adaptor_.BindServiceAdaptor();
return true;
}
void DeviceInfoService::OnBindService(
mojo::ScopedMessagePipeHandle receiver_pipe) {
receivers_.Add(this, mojo::PendingReceiver<mojom::MeetDevicesInfo>(
std::move(receiver_pipe)));
}
void DeviceInfoService::OnAdaptorConnect(bool success) {
if (!success) {
LOG(ERROR) << "mojom::DeviceInfo Service Adaptor connection failed.";
return;
}
VLOG(3) << "mojom::DeviceInfo Service Adaptor is connected.";
CHECK(chromeos::DeviceSettingsService::IsInitialized());
chromeos::DeviceSettingsService::Get()->AddObserver(this);
}
void DeviceInfoService::OnAdaptorDisconnect() {
LOG(WARNING) << "mojom::DeviceInfo Service Adaptor has been disconnected";
Reset();
}
void DeviceInfoService::DeviceSettingsUpdated() {
// Post to primary task runner
task_runner_->PostTask(FROM_HERE,
base::BindOnce(&DeviceInfoService::UpdatePolicyInfo,
weak_ptr_factory_.GetWeakPtr()));
}
void DeviceInfoService::OnDeviceSettingsServiceShutdown() {
// Post to primary task runner
task_runner_->PostTask(FROM_HERE,
base::BindOnce(&DeviceInfoService::Reset,
weak_ptr_factory_.GetWeakPtr()));
}
void DeviceInfoService::AddDeviceSettingsObserver(
::mojo::PendingRemote<mojom::PolicyInfoObserver> observer) {
mojo::Remote<mojom::PolicyInfoObserver> info_observer(std::move(observer));
if (!current_policy_info_.is_null()) {
info_observer->OnPolicyInfoChange(current_policy_info_->Clone());
}
policy_remotes_.Add(std::move(info_observer));
}
void DeviceInfoService::UpdatePolicyInfo() {
auto* device_settings = chromeos::DeviceSettingsService::Get();
if (!device_settings || !device_settings->policy_data()) {
return;
}
auto* policy_data = device_settings->policy_data();
auto policy_info = mojom::PolicyInfo::New();
if (policy_data->has_timestamp()) {
policy_info->timestamp_ms = policy_data->timestamp();
}
if (policy_data->has_device_id()) {
policy_info->device_id = policy_data->device_id();
}
if (policy_data->has_service_account_identity()) {
policy_info->service_account_email_address =
policy_data->service_account_identity();
}
if (policy_data->has_gaia_id()) {
base::StringToInt64(policy_data->gaia_id(),
&policy_info->service_account_gaia_id);
}
if (current_policy_info_.Equals(policy_info)) {
return;
}
current_policy_info_ = std::move(policy_info);
for (auto& remote : policy_remotes_) {
remote->OnPolicyInfoChange(current_policy_info_->Clone());
}
}
void DeviceInfoService::GetPolicyInfo(GetPolicyInfoCallback callback) {
if (!current_policy_info_.is_null()) {
std::move(callback).Run(current_policy_info_->Clone());
} else {
std::move(callback).Run(nullptr);
}
}
void DeviceInfoService::GetSysInfo(GetSysInfoCallback callback) {
auto root = base::FilePath(kRootPartition);
auto stateful = base::FilePath(kStatefulPartition);
auto sys_info = mojom::SysInfo::New();
sys_info->uptime_ms = base::SysInfo::Uptime().InMilliseconds();
sys_info->model_name = base::SysInfo::HardwareModelName();
sys_info->num_proc = base::SysInfo::NumberOfProcessors();
sys_info->total_memory_bytes = base::SysInfo::AmountOfPhysicalMemory();
sys_info->available_memory_bytes =
base::SysInfo::AmountOfAvailablePhysicalMemory();
sys_info->kernel_version = base::SysInfo::KernelVersion();
sys_info->root_total_disk_space_bytes =
base::SysInfo::AmountOfTotalDiskSpace(root);
sys_info->root_free_disk_space_bytes =
base::SysInfo::AmountOfFreeDiskSpace(root);
sys_info->user_total_disk_space_bytes =
base::SysInfo::AmountOfTotalDiskSpace(stateful);
sys_info->user_free_disk_space_bytes =
base::SysInfo::AmountOfFreeDiskSpace(stateful);
std::string value;
if (base::SysInfo::GetLsbReleaseValue(kReleaseVersion, &value)) {
sys_info->release_version = std::move(value);
}
if (base::SysInfo::GetLsbReleaseValue(kReleasBuildType, &value)) {
sys_info->release_build_type = std::move(value);
}
if (base::SysInfo::GetLsbReleaseValue(kReleaseTrack, &value)) {
sys_info->release_track = std::move(value);
}
if (base::SysInfo::GetLsbReleaseValue(kReleaseChromeMilestone, &value)) {
sys_info->release_milestone = std::move(value);
}
std::move(callback).Run(std::move(sys_info));
}
// Private methods
DeviceInfoService::DeviceInfoService()
: service_adaptor_(mojom::MeetDevicesInfo::Name_, this),
task_runner_(base::SequencedTaskRunnerHandle::Get()) {
CfmHotlineClient::Get()->AddObserver(this);
current_policy_info_.reset();
}
DeviceInfoService::~DeviceInfoService() {
CfmHotlineClient::Get()->RemoveObserver(this);
Reset();
}
void DeviceInfoService::Reset() {
receivers_.Clear();
policy_remotes_.Clear();
chromeos::DeviceSettingsService::Get()->RemoveObserver(this);
}
} // namespace cfm
} // namespace chromeos
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_CHROMEOS_CHROMEBOX_FOR_MEETINGS_DEVICE_INFO_DEVICE_INFO_SERVICE_H_
#define CHROME_BROWSER_CHROMEOS_CHROMEBOX_FOR_MEETINGS_DEVICE_INFO_DEVICE_INFO_SERVICE_H_
#include "base/memory/weak_ptr.h"
#include "base/sequenced_task_runner.h"
#include "chrome/browser/chromeos/chromebox_for_meetings/service_adaptor.h"
#include "chrome/browser/chromeos/settings/device_settings_service.h"
#include "chromeos/dbus/chromebox_for_meetings/cfm_observer.h"
#include "chromeos/services/chromebox_for_meetings/public/mojom/meet_devices_info.mojom.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote_set.h"
namespace chromeos {
namespace cfm {
// Implementation of the DeviceInfo Service.
// Allowing query to relevant device information
class DeviceInfoService : public CfmObserver,
public ServiceAdaptor::Delegate,
public mojom::MeetDevicesInfo,
public DeviceSettingsService::Observer {
public:
DeviceInfoService(const DeviceInfoService&) = delete;
DeviceInfoService& operator=(const DeviceInfoService&) = delete;
// Manage singleton instance.
static void Initialize();
static void Shutdown();
static DeviceInfoService* Get();
static bool IsInitialized();
protected:
// mojom::CfmObserver implementation
bool ServiceRequestReceived(const std::string& interface_name) override;
// mojom::ServiceAdaptorDelegate implementation
void OnBindService(mojo::ScopedMessagePipeHandle receiver_pipe) override;
void OnAdaptorConnect(bool success) override;
void OnAdaptorDisconnect() override;
// DeviceSettingsService::Observer impl
void DeviceSettingsUpdated() override;
void OnDeviceSettingsServiceShutdown() override;
// ::mojom::DeviceInfo implementation
void AddDeviceSettingsObserver(
::mojo::PendingRemote<mojom::PolicyInfoObserver> observer) override;
void GetPolicyInfo(GetPolicyInfoCallback callback) override;
void GetSysInfo(GetSysInfoCallback callback) override;
// Query data policy information and notify observers if there is a change.
void UpdatePolicyInfo();
private:
DeviceInfoService();
~DeviceInfoService() override;
// Cleanup the service on mojom connection loss.
// Called after the DeviceInfo Service is no longer discoverable.
void Reset();
ServiceAdaptor service_adaptor_;
mojo::ReceiverSet<mojom::MeetDevicesInfo> receivers_;
mojo::RemoteSet<mojom::PolicyInfoObserver> policy_remotes_;
mojom::PolicyInfoPtr current_policy_info_;
scoped_refptr<base::SequencedTaskRunner> task_runner_;
// Note: This should remain the last member so it'll be destroyed and
// invalidate its weak pointers before any other members are destroyed.
base::WeakPtrFactory<DeviceInfoService> weak_ptr_factory_{this};
};
} // namespace cfm
} // namespace chromeos
#endif // CHROME_BROWSER_CHROMEOS_CHROMEBOX_FOR_MEETINGS_DEVICE_INFO_DEVICE_INFO_SERVICE_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/chromebox_for_meetings/device_info/device_info_service.h"
#include <memory>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/macros.h"
#include "base/optional.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/bind.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "chrome/browser/chromeos/policy/device_policy_builder.h"
#include "chrome/browser/chromeos/settings/device_settings_service.h"
#include "chrome/browser/chromeos/settings/device_settings_test_helper.h"
#include "chromeos/dbus/chromebox_for_meetings/fake_cfm_hotline_client.h"
#include "chromeos/dbus/session_manager/fake_session_manager_client.h"
#include "chromeos/services/chromebox_for_meetings/public/cpp/fake_service_connection.h"
#include "chromeos/services/chromebox_for_meetings/public/cpp/fake_service_context.h"
#include "chromeos/services/chromebox_for_meetings/public/cpp/service_connection.h"
#include "chromeos/services/chromebox_for_meetings/public/mojom/cfm_service_manager.mojom.h"
#include "chromeos/services/chromebox_for_meetings/public/mojom/meet_devices_info.mojom.h"
#include "components/ownership/mock_owner_key_util.h"
#include "content/public/test/test_utils.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace chromeos {
namespace cfm {
namespace {
class CfmDeviceInfoServiceTest : public ::testing::Test {
public:
CfmDeviceInfoServiceTest() = default;
CfmDeviceInfoServiceTest(const CfmDeviceInfoServiceTest&) = delete;
CfmDeviceInfoServiceTest& operator=(const CfmDeviceInfoServiceTest&) = delete;
void SetUp() override {
scoped_refptr<ownership::MockOwnerKeyUtil> owner_key_util_(
new ownership::MockOwnerKeyUtil());
owner_key_util_->SetPublicKeyFromPrivateKey(
*device_policy_.GetSigningKey());
chromeos::DeviceSettingsService::Get()->SetSessionManager(
&session_manager_client_, owner_key_util_);
CfmHotlineClient::InitializeFake();
ServiceConnection::UseFakeServiceConnectionForTesting(
&fake_service_connection_);
DeviceInfoService::Initialize();
}
void TearDown() override {
DeviceInfoService::Shutdown();
CfmHotlineClient::Shutdown();
chromeos::DeviceSettingsService::Get()->UnsetSessionManager();
}
FakeCfmHotlineClient* GetClient() {
return static_cast<FakeCfmHotlineClient*>(CfmHotlineClient::Get());
}
void UpdatePolicyInfo(int64_t timestamp,
std::string device_id,
std::string service_account_id,
int64_t gaia_id) {
device_policy_.policy_data().set_timestamp(timestamp);
device_policy_.policy_data().set_device_id(device_id);
device_policy_.policy_data().set_service_account_identity(
service_account_id);
device_policy_.policy_data().set_gaia_id(base::NumberToString(gaia_id));
device_policy_.Build();
session_manager_client_.set_device_policy(device_policy_.GetBlob());
chromeos::DeviceSettingsService::Get()->Load();
content::RunAllTasksUntilIdle();
}
// Returns a mojo::Remote for the mojom::MeetDevicesInfo by faking the
// way the cfm mojom binder daemon would request it through chrome.
const mojo::Remote<mojom::MeetDevicesInfo>& GetDeviceInfoRemote() {
if (device_info_remote_.is_bound()) {
return device_info_remote_;
}
// if there is no valid remote create one
auto* interface_name = mojom::MeetDevicesInfo::Name_;
base::RunLoop run_loop;
// Fake out CfmServiceContext
fake_service_connection_.SetCallback(base::BindLambdaForTesting(
[&](mojo::PendingReceiver<mojom::CfmServiceContext> pending_receiver,
bool success) {
ASSERT_TRUE(success);
context_receiver_set_.Add(&context_, std::move(pending_receiver));
}));
context_.SetFakeProvideAdaptorCallback(base::BindLambdaForTesting(
[&](const std::string& service_id,
mojo::PendingRemote<mojom::CfmServiceAdaptor>
pending_adaptor_remote,
mojom::CfmServiceContext::ProvideAdaptorCallback callback) {
ASSERT_EQ(interface_name, service_id);
adaptor_remote_.Bind(std::move(pending_adaptor_remote));
std::move(callback).Run(true);
}));
EXPECT_TRUE(GetClient()->FakeEmitSignal(interface_name));
run_loop.RunUntilIdle();
EXPECT_TRUE(adaptor_remote_.is_connected());
adaptor_remote_->OnBindService(
device_info_remote_.BindNewPipeAndPassReceiver().PassPipe());
EXPECT_TRUE(device_info_remote_.is_connected());
return device_info_remote_;
}
protected:
FakeCfmServiceContext context_;
mojo::Remote<mojom::MeetDevicesInfo> device_info_remote_;
mojo::ReceiverSet<mojom::CfmServiceContext> context_receiver_set_;
mojo::Remote<mojom::CfmServiceAdaptor> adaptor_remote_;
chromeos::ScopedTestDeviceSettingsService scoped_device_settings_service_;
chromeos::FakeSessionManagerClient session_manager_client_;
FakeServiceConnectionImpl fake_service_connection_;
policy::DevicePolicyBuilder device_policy_;
// Require a full task environment for testing device policy
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
};
// This test ensures that the DiagnosticsInfoService is discoverable by its
// mojom name by sending a signal received by CfmHotlineClient.
TEST_F(CfmDeviceInfoServiceTest, InfoServiceAvailable) {
ASSERT_TRUE(GetClient()->FakeEmitSignal(mojom::MeetDevicesInfo::Name_));
}
// This test ensures that the CfmDeviceInfoService correctly registers itself
// for discovery by the cfm mojom binder daemon and correctly returns a
// working mojom remote.
TEST_F(CfmDeviceInfoServiceTest, GetDeviceInfoRemote) {
ASSERT_TRUE(GetDeviceInfoRemote().is_connected());
}
TEST_F(CfmDeviceInfoServiceTest, GetDeviceInfoService) {
const auto& details_remote = GetDeviceInfoRemote();
ASSERT_TRUE(details_remote.is_connected());
}
TEST_F(CfmDeviceInfoServiceTest, TestPolicyInfo) {
base::RunLoop run_loop;
const auto& details_remote = GetDeviceInfoRemote();
run_loop.RunUntilIdle();
int64_t timestamp = 10;
std::string device_id = "device_id";
std::string service_account_id = "service_account_id";
int64_t gaia_id = 20;
UpdatePolicyInfo(timestamp, device_id, service_account_id, gaia_id);
base::RunLoop mojo_loop;
details_remote->GetPolicyInfo(
base::BindLambdaForTesting([&](mojom::PolicyInfoPtr policy_ptr) {
ASSERT_EQ(timestamp, policy_ptr->timestamp_ms);
ASSERT_EQ(device_id, policy_ptr->device_id);
ASSERT_EQ(service_account_id,
policy_ptr->service_account_email_address);
ASSERT_EQ(gaia_id, policy_ptr->service_account_gaia_id);
mojo_loop.Quit();
}));
mojo_loop.Run();
}
TEST_F(CfmDeviceInfoServiceTest, TestSysInfo) {
base::RunLoop run_loop;
const auto& details_remote = GetDeviceInfoRemote();
run_loop.RunUntilIdle();
base::RunLoop mojo_loop;
details_remote->GetSysInfo(
base::BindLambdaForTesting([&](mojom::SysInfoPtr info_ptr) {
ASSERT_GE(info_ptr->num_proc, 1);
mojo_loop.Quit();
}));
mojo_loop.Run();
}
} // namespace
} // namespace cfm
} // namespace chromeos
......@@ -9,6 +9,7 @@ mojom("mojom") {
"cfm_browser.mojom",
"cfm_service_manager.mojom",
"meet_devices_diagnostics.mojom",
"meet_devices_info.mojom",
"meet_devices_logger.mojom",
]
public_deps = [ "//mojo/public/mojom/base" ]
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
module chromeos.cfm.mojom;
// Interface defined by chromium specifically for the ChromeboxForMeetings
// platform to query relevant device system and policy information.
interface MeetDevicesInfo {
// Adds a DevicePolicyObserver for policy info change events.
// Returns unique id which can be used to remove the observer
// Note: Initial update using available policy if applicable upon registration
AddDeviceSettingsObserver@0(pending_remote<PolicyInfoObserver> observer);
// Request device policy information
// Note: will return nullptr on failure to query policy information
GetPolicyInfo@1()=>(PolicyInfo info);
// Request device system information
GetSysInfo@2()=>(SysInfo info);
};
// Inteface for an observer to monitor device policy changes
interface PolicyInfoObserver {
// Called whenever a devices policy change event is recieved.
OnPolicyInfoChange@0(PolicyInfo info);
};
// Device policy information
struct PolicyInfo {
// PolicyData::Timestamp
// Timestamp in milliseconds (javatime)
// From DMServer when server issued response (Java time)
int64 timestamp_ms@0;
// PolicyData::DeviceID
string? device_id@1;
// PolicyData::ServiceAccountIdentity
string? service_account_email_address@2;
// PolicyData::GaiaId
int64 service_account_gaia_id@3;
};
// Basic system information
struct SysInfo {
// Current system uptime
// Timestamp in milliseconds (javatime)
int64 uptime_ms@0;
// The name of the hardware
string model_name@1;
// Number of processors/cores
uint64 num_proc@2;
// Number of bytes of physical memory
uint64 total_memory_bytes@3;
// Current amount of available memory
uint64 available_memory_bytes@4;
// Returns the kernel version
string kernel_version@5;
// Total root partition disk space
// Note Return 0 on failure
uint64 root_total_disk_space_bytes@6;
// Available root partition disk space
// Note Return 0 on failure
uint64 root_free_disk_space_bytes@7;
// Total stateful partition disk space
// Note Return 0 on failure
uint64 user_total_disk_space_bytes@8;
// Available stateful partition disk space
// Note Return 0 on failure
uint64 user_free_disk_space_bytes@9;
// /etc/lsb-release: CHROMEOS_RELEASE_VERSION
string? release_version@10;
// /etc/lsb-release: CHROMEOS_RELEASE_BUILD_TYPE
string? release_build_type@11;
// /etc/lsb-release: CHROMEOS_RELEASE_TRACK
string? release_track@12;
// /etc/lsb-release: CHROMEOS_RELEASE_CHROME_MILESTONE
string? release_milestone@13;
};
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