diff --git a/include/multipass/mount_handler.h b/include/multipass/mount_handler.h index 56167d6a499..11e387dbb8e 100644 --- a/include/multipass/mount_handler.h +++ b/include/multipass/mount_handler.h @@ -67,7 +67,7 @@ class MountHandler : private DisabledCopyMove active = false; } - const VMMount& get_mount_spec() const + const VMMount& get_mount_spec() const noexcept { return mount_spec; } diff --git a/include/multipass/snapshot.h b/include/multipass/snapshot.h index b0abb949c67..dc0f10eee0d 100644 --- a/include/multipass/snapshot.h +++ b/include/multipass/snapshot.h @@ -35,13 +35,13 @@ struct VMMount; class Snapshot : private DisabledCopyMove { -public: // TODO@snapshots drop any accessors that we end up not needing +public: virtual ~Snapshot() = default; - virtual int get_index() const = 0; + virtual int get_index() const noexcept = 0; virtual std::string get_name() const = 0; virtual std::string get_comment() const = 0; - virtual QDateTime get_creation_timestamp() const = 0; + virtual QDateTime get_creation_timestamp() const noexcept = 0; virtual int get_num_cores() const noexcept = 0; virtual MemorySize get_mem_size() const noexcept = 0; virtual MemorySize get_disk_space() const noexcept = 0; diff --git a/include/multipass/virtual_machine.h b/include/multipass/virtual_machine.h index 8dc9cde7e6e..c3cc177790d 100644 --- a/include/multipass/virtual_machine.h +++ b/include/multipass/virtual_machine.h @@ -88,7 +88,7 @@ class VirtualMachine : private DisabledCopyMove const VMMount& mount) = 0; using SnapshotVista = std::vector>; // using vista to avoid confusion with C++ views - virtual SnapshotVista view_snapshots() const noexcept = 0; + virtual SnapshotVista view_snapshots() const = 0; virtual int get_num_snapshots() const noexcept = 0; virtual std::shared_ptr get_snapshot(const std::string& name) const = 0; @@ -99,7 +99,8 @@ class VirtualMachine : private DisabledCopyMove virtual std::shared_ptr take_snapshot(const VMSpecs& specs, const std::string& snapshot_name, const std::string& comment) = 0; - virtual void rename_snapshot(const std::string& old_name, const std::string& new_name) = 0; // TODO@snapshots remove + virtual void rename_snapshot(const std::string& old_name, + const std::string& new_name) = 0; // only VM can avoid repeated names virtual void delete_snapshot(const std::string& name) = 0; virtual void restore_snapshot(const std::string& name, VMSpecs& specs) = 0; virtual void load_snapshots() = 0; diff --git a/include/multipass/vm_mount.h b/include/multipass/vm_mount.h index 3f1e911b24f..b3f6b37eff4 100644 --- a/include/multipass/vm_mount.h +++ b/include/multipass/vm_mount.h @@ -20,6 +20,8 @@ #include +#include + #include namespace multipass @@ -32,6 +34,12 @@ struct VMMount Native = 1 }; + VMMount() = default; + VMMount(const QJsonObject& json); + VMMount(const std::string& sourcePath, id_mappings gidMappings, id_mappings uidMappings, MountType mountType); + + QJsonObject serialize() const; + std::string source_path; id_mappings gid_mappings; id_mappings uid_mappings; diff --git a/src/daemon/vm_specs.h b/include/multipass/vm_specs.h similarity index 59% rename from src/daemon/vm_specs.h rename to include/multipass/vm_specs.h index af638990a36..42af152bed6 100644 --- a/src/daemon/vm_specs.h +++ b/include/multipass/vm_specs.h @@ -18,10 +18,10 @@ #ifndef MULTIPASS_VM_SPECS_H #define MULTIPASS_VM_SPECS_H -#include -#include -#include -#include +#include "memory_size.h" +#include "network_interface.h" +#include "virtual_machine.h" +#include "vm_mount.h" #include #include @@ -48,10 +48,25 @@ struct VMSpecs inline bool operator==(const VMSpecs& a, const VMSpecs& b) { - return std::tie(a.num_cores, a.mem_size, a.disk_space, a.default_mac_address, a.extra_interfaces, a.ssh_username, - a.state, a.mounts, a.deleted, a.metadata) == - std::tie(b.num_cores, b.mem_size, b.disk_space, b.default_mac_address, b.extra_interfaces, b.ssh_username, - b.state, b.mounts, b.deleted, b.metadata); + return std::tie(a.num_cores, + a.mem_size, + a.disk_space, + a.default_mac_address, + a.extra_interfaces, + a.ssh_username, + a.state, + a.mounts, + a.deleted, + a.metadata) == std::tie(b.num_cores, + b.mem_size, + b.disk_space, + b.default_mac_address, + b.extra_interfaces, + b.ssh_username, + b.state, + b.mounts, + b.deleted, + b.metadata); } inline bool operator!=(const VMSpecs& a, const VMSpecs& b) // TODO drop in C++20 diff --git a/src/client/cli/formatter/table_formatter.cpp b/src/client/cli/formatter/table_formatter.cpp index 9756d9f5f38..06656fcbf71 100644 --- a/src/client/cli/formatter/table_formatter.cpp +++ b/src/client/cli/formatter/table_formatter.cpp @@ -103,7 +103,8 @@ std::string generate_snapshot_details(const mp::DetailedInfoItem& item) fmt::format_to(std::back_inserter(buf), "{}\n", *child); } - // TODO@snapshots split and align string if it extends onto several lines + /* TODO split and align string if it extends onto several lines; but actually better implement generic word-wrapping + for all output, taking both terminal width and current indentation level into account */ fmt::format_to(std::back_inserter(buf), "{:<16}{}\n", "Comment:", diff --git a/src/daemon/daemon.cpp b/src/daemon/daemon.cpp index 6f9063fea16..baec99d77b8 100644 --- a/src/daemon/daemon.cpp +++ b/src/daemon/daemon.cpp @@ -325,30 +325,8 @@ std::unordered_map load_db(const mp::Path& data_path, for (QJsonValueRef entry : record["mounts"].toArray()) { - mp::id_mappings uid_mappings; - mp::id_mappings gid_mappings; - - auto target_path = entry.toObject()["target_path"].toString().toStdString(); - auto source_path = entry.toObject()["source_path"].toString().toStdString(); - - for (QJsonValueRef uid_entry : entry.toObject()["uid_mappings"].toArray()) - { - uid_mappings.push_back( - {uid_entry.toObject()["host_uid"].toInt(), uid_entry.toObject()["instance_uid"].toInt()}); - } - - for (QJsonValueRef gid_entry : entry.toObject()["gid_mappings"].toArray()) - { - gid_mappings.push_back( - {gid_entry.toObject()["host_gid"].toInt(), gid_entry.toObject()["instance_gid"].toInt()}); - } - - uid_mappings = mp::unique_id_mappings(uid_mappings); - gid_mappings = mp::unique_id_mappings(gid_mappings); - auto mount_type = mp::VMMount::MountType(entry.toObject()["mount_type"].toInt()); - - mp::VMMount mount{source_path, gid_mappings, uid_mappings, mount_type}; - mounts[target_path] = mount; + const auto& json = entry.toObject(); + mounts[json["target_path"].toString().toStdString()] = mp::VMMount{json}; } reconstructed_records[key] = {num_cores, @@ -400,37 +378,8 @@ QJsonObject vm_spec_to_json(const mp::VMSpecs& specs) QJsonArray json_mounts; for (const auto& mount : specs.mounts) { - QJsonObject entry; - entry.insert("source_path", QString::fromStdString(mount.second.source_path)); + auto entry = mount.second.serialize(); entry.insert("target_path", QString::fromStdString(mount.first)); - - QJsonArray uid_mappings; - - for (const auto& map : mount.second.uid_mappings) - { - QJsonObject map_entry; - map_entry.insert("host_uid", map.first); - map_entry.insert("instance_uid", map.second); - - uid_mappings.append(map_entry); - } - - entry.insert("uid_mappings", uid_mappings); - - QJsonArray gid_mappings; - - for (const auto& map : mount.second.gid_mappings) - { - QJsonObject map_entry; - map_entry.insert("host_gid", map.first); - map_entry.insert("instance_gid", map.second); - - gid_mappings.append(map_entry); - } - - entry.insert("gid_mappings", gid_mappings); - - entry.insert("mount_type", static_cast(mount.second.mount_type)); json_mounts.append(entry); } diff --git a/src/daemon/daemon.h b/src/daemon/daemon.h index 4e915a93df9..790b1c87fae 100644 --- a/src/daemon/daemon.h +++ b/src/daemon/daemon.h @@ -20,13 +20,12 @@ #include "daemon_config.h" #include "daemon_rpc.h" -#include "multipass/virtual_machine.h" -#include "vm_specs.h" #include #include #include #include +#include #include #include diff --git a/src/daemon/instance_settings_handler.h b/src/daemon/instance_settings_handler.h index 9a17253e1c5..d2236be533f 100644 --- a/src/daemon/instance_settings_handler.h +++ b/src/daemon/instance_settings_handler.h @@ -18,11 +18,10 @@ #ifndef MULTIPASS_INSTANCE_SETTINGS_HANDLER_H #define MULTIPASS_INSTANCE_SETTINGS_HANDLER_H -#include "vm_specs.h" - #include #include #include +#include #include diff --git a/src/daemon/snapshot_settings_handler.cpp b/src/daemon/snapshot_settings_handler.cpp index 3451a37fc66..0af41669a76 100644 --- a/src/daemon/snapshot_settings_handler.cpp +++ b/src/daemon/snapshot_settings_handler.cpp @@ -80,7 +80,7 @@ mp::SnapshotSettingsException::SnapshotSettingsException(const std::string& deta mp::SnapshotSettingsHandler::SnapshotSettingsHandler( std::unordered_map& operative_instances, const std::unordered_map& deleted_instances, - const std::unordered_set& preparing_instances) + const std::unordered_set& preparing_instances) noexcept : operative_instances{operative_instances}, deleted_instances{deleted_instances}, preparing_instances{preparing_instances} diff --git a/src/daemon/snapshot_settings_handler.h b/src/daemon/snapshot_settings_handler.h index 5eecad76633..c613bbc6cad 100644 --- a/src/daemon/snapshot_settings_handler.h +++ b/src/daemon/snapshot_settings_handler.h @@ -35,7 +35,7 @@ class SnapshotSettingsHandler : public SettingsHandler public: SnapshotSettingsHandler(std::unordered_map& operative_instances, const std::unordered_map& deleted_instances, - const std::unordered_set& preparing_instances); + const std::unordered_set& preparing_instances) noexcept; std::set keys() const override; QString get(const QString& key) const override; diff --git a/src/platform/backends/libvirt/libvirt_virtual_machine.cpp b/src/platform/backends/libvirt/libvirt_virtual_machine.cpp index f811d7d3c46..afe9b3cc6e7 100644 --- a/src/platform/backends/libvirt/libvirt_virtual_machine.cpp +++ b/src/platform/backends/libvirt/libvirt_virtual_machine.cpp @@ -552,10 +552,10 @@ auto mp::LibVirtVirtualMachine::make_specific_snapshot(const std::string& /*snap std::shared_ptr /*parent*/) -> std::shared_ptr { - throw NotImplementedOnThisBackendException{"Snapshots"}; // TODO@snapshots + throw NotImplementedOnThisBackendException{"Snapshots"}; } auto mp::LibVirtVirtualMachine::make_specific_snapshot(const QString& /*filename*/) -> std::shared_ptr { - throw NotImplementedOnThisBackendException{"Snapshots"}; // TODO@snapshots + throw NotImplementedOnThisBackendException{"Snapshots"}; } diff --git a/src/platform/backends/lxd/lxd_virtual_machine.cpp b/src/platform/backends/lxd/lxd_virtual_machine.cpp index 5017b0e479e..09a340347b6 100644 --- a/src/platform/backends/lxd/lxd_virtual_machine.cpp +++ b/src/platform/backends/lxd/lxd_virtual_machine.cpp @@ -492,10 +492,10 @@ auto mp::LXDVirtualMachine::make_specific_snapshot(const std::string& snapshot_n const VMSpecs& specs, std::shared_ptr parent) -> std::shared_ptr { - throw NotImplementedOnThisBackendException{"Snapshots"}; // TODO@snapshots + throw NotImplementedOnThisBackendException{"Snapshots"}; } std::shared_ptr mp::LXDVirtualMachine::make_specific_snapshot(const QString& /*filename*/) { - throw NotImplementedOnThisBackendException{"Snapshots"}; // TODO@snapshots + throw NotImplementedOnThisBackendException{"Snapshots"}; } diff --git a/src/platform/backends/shared/base_snapshot.cpp b/src/platform/backends/shared/base_snapshot.cpp index f898b27920e..c5b963894c7 100644 --- a/src/platform/backends/shared/base_snapshot.cpp +++ b/src/platform/backends/shared/base_snapshot.cpp @@ -16,16 +16,16 @@ */ #include "base_snapshot.h" -#include "daemon/vm_specs.h" // TODO@snapshots move this +#include "multipass/virtual_machine.h" #include -#include // TODO@snapshots may be able to drop after extracting JSON utilities #include #include +#include #include -#include // TODO@snapshots may be able to drop after extracting JSON utilities +#include #include #include @@ -68,35 +68,13 @@ QJsonObject read_snapshot_json(const QString& filename) return json["snapshot"].toObject(); } -std::unordered_map load_mounts(const QJsonArray& json) +std::unordered_map load_mounts(const QJsonArray& mounts_json) { std::unordered_map mounts; - for (const auto& entry : json) + for (const auto& entry : mounts_json) { - mp::id_mappings uid_mappings; - mp::id_mappings gid_mappings; - - auto target_path = entry.toObject()["target_path"].toString().toStdString(); - auto source_path = entry.toObject()["source_path"].toString().toStdString(); - - for (const QJsonValueRef uid_entry : entry.toObject()["uid_mappings"].toArray()) - { - uid_mappings.push_back( - {uid_entry.toObject()["host_uid"].toInt(), uid_entry.toObject()["instance_uid"].toInt()}); - } - - for (const QJsonValueRef gid_entry : entry.toObject()["gid_mappings"].toArray()) - { - gid_mappings.push_back( - {gid_entry.toObject()["host_gid"].toInt(), gid_entry.toObject()["instance_gid"].toInt()}); - } - - uid_mappings = mp::unique_id_mappings(uid_mappings); - gid_mappings = mp::unique_id_mappings(gid_mappings); - auto mount_type = mp::VMMount::MountType(entry.toObject()["mount_type"].toInt()); - - mp::VMMount mount{source_path, gid_mappings, uid_mappings, mount_type}; - mounts[target_path] = std::move(mount); + const auto& json = entry.toObject(); + mounts[json["target_path"].toString().toStdString()] = mp::VMMount{json}; } return mounts; @@ -146,9 +124,13 @@ mp::BaseSnapshot::BaseSnapshot(const std::string& name, // NOLINT(modernize-p captured{captured} { assert(index > 0 && "snapshot indices need to start at 1"); + using St = VirtualMachine::State; + if (state != St::off && state != St::stopped) + throw std::runtime_error{fmt::format("Unsupported VM state in snapshot: {}", static_cast(state))}; + if (index < 1) + throw std::runtime_error{fmt::format("Snapshot index not positive: {}", index)}; if (index > max_snapshots) - throw std::runtime_error{fmt::format("Maximum number of snapshots exceeded: {}", max_snapshots)}; - + throw std::runtime_error{fmt::format("Maximum number of snapshots exceeded: {}", index)}; if (name.empty()) throw std::runtime_error{"Snapshot names cannot be empty"}; if (num_cores < 1) @@ -224,37 +206,8 @@ QJsonObject mp::BaseSnapshot::serialize() const QJsonArray json_mounts; for (const auto& mount : mounts) { - QJsonObject entry; - entry.insert("source_path", QString::fromStdString(mount.second.source_path)); + auto entry = mount.second.serialize(); entry.insert("target_path", QString::fromStdString(mount.first)); - - QJsonArray uid_mappings; - - for (const auto& map : mount.second.uid_mappings) - { - QJsonObject map_entry; - map_entry.insert("host_uid", map.first); - map_entry.insert("instance_uid", map.second); - - uid_mappings.append(map_entry); - } - - entry.insert("uid_mappings", uid_mappings); - - QJsonArray gid_mappings; - - for (const auto& map : mount.second.gid_mappings) - { - QJsonObject map_entry; - map_entry.insert("host_gid", map.first); - map_entry.insert("instance_gid", map.second); - - gid_mappings.append(map_entry); - } - - entry.insert("gid_mappings", gid_mappings); - - entry.insert("mount_type", static_cast(mount.second.mount_type)); json_mounts.append(entry); } diff --git a/src/platform/backends/shared/base_snapshot.h b/src/platform/backends/shared/base_snapshot.h index f67c9250682..a07afcb0c9c 100644 --- a/src/platform/backends/shared/base_snapshot.h +++ b/src/platform/backends/shared/base_snapshot.h @@ -42,11 +42,10 @@ class BaseSnapshot : public Snapshot const VirtualMachine& vm); BaseSnapshot(const QString& filename, VirtualMachine& vm); - // TODO@snapshots tag as noexcept those that can be - int get_index() const override; + int get_index() const noexcept override; std::string get_name() const override; std::string get_comment() const override; - QDateTime get_creation_timestamp() const override; + QDateTime get_creation_timestamp() const noexcept override; int get_num_cores() const noexcept override; MemorySize get_mem_size() const noexcept override; MemorySize get_disk_space() const noexcept override; @@ -130,12 +129,12 @@ inline std::string multipass::BaseSnapshot::get_comment() const return comment; } -inline int multipass::BaseSnapshot::get_index() const +inline int multipass::BaseSnapshot::get_index() const noexcept { return index; } -inline QDateTime multipass::BaseSnapshot::get_creation_timestamp() const +inline QDateTime multipass::BaseSnapshot::get_creation_timestamp() const noexcept { return creation_timestamp; } diff --git a/src/platform/backends/shared/base_virtual_machine.cpp b/src/platform/backends/shared/base_virtual_machine.cpp index 6717ef045e4..79edcbf7fd3 100644 --- a/src/platform/backends/shared/base_virtual_machine.cpp +++ b/src/platform/backends/shared/base_virtual_machine.cpp @@ -16,7 +16,6 @@ */ #include "base_virtual_machine.h" -#include "daemon/vm_specs.h" // TODO@snapshots move this #include #include @@ -26,6 +25,7 @@ #include #include #include +#include #include @@ -121,7 +121,7 @@ std::vector BaseVirtualMachine::get_all_ipv4(const SSHKeyProvider& return all_ipv4; } -auto BaseVirtualMachine::view_snapshots() const noexcept -> SnapshotVista +auto BaseVirtualMachine::view_snapshots() const -> SnapshotVista { SnapshotVista ret; @@ -522,8 +522,6 @@ void BaseVirtualMachine::restore_snapshot(const std::string& name, VMSpecs& spec assert_vm_stopped(state); // precondition auto snapshot = get_snapshot(name); - - // TODO@snapshots convert into runtime_error (persisted info could have been tampered with) assert(snapshot->get_state() == St::off || snapshot->get_state() == St::stopped); snapshot->apply(); diff --git a/src/platform/backends/shared/base_virtual_machine.h b/src/platform/backends/shared/base_virtual_machine.h index 20c6dc8c166..8ab079cda86 100644 --- a/src/platform/backends/shared/base_virtual_machine.h +++ b/src/platform/backends/shared/base_virtual_machine.h @@ -50,7 +50,7 @@ class BaseVirtualMachine : public VirtualMachine throw NotImplementedOnThisBackendException("native mounts"); }; - SnapshotVista view_snapshots() const noexcept override; + SnapshotVista view_snapshots() const override; int get_num_snapshots() const noexcept override; std::shared_ptr get_snapshot(const std::string& name) const override; diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index ced4a8824b3..cd5d0e4d2a2 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -21,7 +21,8 @@ function(add_target TARGET_NAME) standard_paths.cpp timer.cpp utils.cpp - vm_image_vault_utils.cpp) + vm_image_vault_utils.cpp + vm_mount.cpp) target_link_libraries(${TARGET_NAME} cert diff --git a/src/utils/vm_mount.cpp b/src/utils/vm_mount.cpp new file mode 100644 index 00000000000..b798eed92b1 --- /dev/null +++ b/src/utils/vm_mount.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include + +namespace mp = multipass; + +namespace +{ +mp::VMMount parse_json(const QJsonObject& json) +{ + mp::id_mappings uid_mappings; + mp::id_mappings gid_mappings; + auto source_path = json["source_path"].toString().toStdString(); + + for (const QJsonValueRef uid_entry : json["uid_mappings"].toArray()) + { + uid_mappings.push_back( + {uid_entry.toObject()["host_uid"].toInt(), uid_entry.toObject()["instance_uid"].toInt()}); + } + + for (const QJsonValueRef gid_entry : json["gid_mappings"].toArray()) + { + gid_mappings.push_back( + {gid_entry.toObject()["host_gid"].toInt(), gid_entry.toObject()["instance_gid"].toInt()}); + } + + uid_mappings = mp::unique_id_mappings(uid_mappings); + gid_mappings = mp::unique_id_mappings(gid_mappings); + auto mount_type = mp::VMMount::MountType(json["mount_type"].toInt()); + + return mp::VMMount{std::move(source_path), std::move(gid_mappings), std::move(uid_mappings), mount_type}; +} +} // namespace + +mp::VMMount::VMMount(const std::string& sourcePath, + id_mappings gidMappings, + id_mappings uidMappings, + MountType mountType) + : source_path(sourcePath), + gid_mappings(std::move(gidMappings)), + uid_mappings(std::move(uidMappings)), + mount_type(mountType) +{ +} + +mp::VMMount::VMMount(const QJsonObject& json) : VMMount{parse_json(json)} // delegate on copy ctor +{ +} + +QJsonObject mp::VMMount::serialize() const +{ + QJsonObject ret; + ret.insert("source_path", QString::fromStdString(source_path)); + + QJsonArray uid_mappings_json; + + for (const auto& map : uid_mappings) + { + QJsonObject map_entry; + map_entry.insert("host_uid", map.first); + map_entry.insert("instance_uid", map.second); + + uid_mappings_json.append(map_entry); + } + + ret.insert("uid_mappings", uid_mappings_json); + + QJsonArray gid_mappings_json; + + for (const auto& map : gid_mappings) + { + QJsonObject map_entry; + map_entry.insert("host_gid", map.first); + map_entry.insert("instance_gid", map.second); + + gid_mappings_json.append(map_entry); + } + + ret.insert("gid_mappings", gid_mappings_json); + + ret.insert("mount_type", static_cast(mount_type)); + return ret; +} diff --git a/tests/json_test_utils.h b/tests/json_test_utils.h index 1c8df6cf4e5..7bd74e73efa 100644 --- a/tests/json_test_utils.h +++ b/tests/json_test_utils.h @@ -21,8 +21,7 @@ #include "temp_dir.h" #include - -#include +#include #include #include diff --git a/tests/stub_snapshot.h b/tests/stub_snapshot.h index ced617d8cff..f384903ffd3 100644 --- a/tests/stub_snapshot.h +++ b/tests/stub_snapshot.h @@ -57,7 +57,7 @@ struct StubSnapshot : public Snapshot return nullptr; } - int get_index() const override + int get_index() const noexcept override { return 0; } diff --git a/tests/stub_virtual_machine.h b/tests/stub_virtual_machine.h index c64a55ec73e..f70ccabd7e3 100644 --- a/tests/stub_virtual_machine.h +++ b/tests/stub_virtual_machine.h @@ -126,7 +126,7 @@ struct StubVirtualMachine final : public multipass::VirtualMachine return std::make_unique(); } - SnapshotVista view_snapshots() const noexcept override + SnapshotVista view_snapshots() const override { return {}; } diff --git a/tests/test_instance_settings_handler.cpp b/tests/test_instance_settings_handler.cpp index d5954bbe397..12c9eeeeafe 100644 --- a/tests/test_instance_settings_handler.cpp +++ b/tests/test_instance_settings_handler.cpp @@ -20,9 +20,9 @@ #include #include +#include #include -#include #include