diff --git a/CMakeLists.txt b/CMakeLists.txt index a15d782c..6518f703 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,6 +147,7 @@ if (WIN32 OR APPLE) add_subdirectory(standalone) if (WIN32) + add_subdirectory(explorer/src) add_subdirectory(injector/src) if (MP_BUILD_VSPACKAGE) @@ -154,6 +155,7 @@ if (WIN32 OR APPLE) add_subdirectory(micro-profiler) endif() + add_subdirectory(explorer/tests) add_subdirectory(injector/tests) endif () endif () diff --git a/explorer/process.h b/explorer/process.h new file mode 100644 index 00000000..4ed652d5 --- /dev/null +++ b/explorer/process.h @@ -0,0 +1,58 @@ +// Copyright (c) 2011-2022 by Artem A. Gevorkyan (gevorkyan.org) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#pragma once + +#include +#include +#include +#include +#include + +namespace micro_profiler +{ + struct process_info + { + enum architectures { x86, x64, }; + typedef std::int64_t process_time_t; + + id_t pid, parent_pid; + std::string path; + architectures architecture; + process_time_t started_at; + std::shared_ptr handle; + + unsigned int cycle; + }; + + class process_explorer : public sdb::table + { + public: + process_explorer(mt::milliseconds update_interval, scheduler::queue &apartment_queue); + + private: + void update(); + + private: + scheduler::private_queue _apartment; + mt::milliseconds _update_interval; + unsigned int _cycle; + }; +} diff --git a/explorer/src/CMakeLists.txt b/explorer/src/CMakeLists.txt new file mode 100644 index 00000000..45ff8459 --- /dev/null +++ b/explorer/src/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 2.8) + +set(EXPLORER_SOURCES + process_win32.cpp +) + +add_library(explorer STATIC ${EXPLORER_SOURCES}) diff --git a/explorer/src/explorer.lib.vcxproj b/explorer/src/explorer.lib.vcxproj new file mode 100644 index 00000000..d014a290 --- /dev/null +++ b/explorer/src/explorer.lib.vcxproj @@ -0,0 +1,52 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + + + + + + + {7F768E1B-51FB-46E9-88C6-43E9F136710A} + + + + StaticLibrary + + + + + + + + + %(AdditionalIncludeDirectories) + false + + + true + + + psapi.lib + + + + \ No newline at end of file diff --git a/explorer/src/explorer.lib.vcxproj.filters b/explorer/src/explorer.lib.vcxproj.filters new file mode 100644 index 00000000..092b037a --- /dev/null +++ b/explorer/src/explorer.lib.vcxproj.filters @@ -0,0 +1,16 @@ + + + + + {d322ec2c-728b-4052-96d0-b9e94732dc6b} + + + + + + + + src + + + \ No newline at end of file diff --git a/explorer/src/process_win32.cpp b/explorer/src/process_win32.cpp new file mode 100644 index 00000000..8ab7c86b --- /dev/null +++ b/explorer/src/process_win32.cpp @@ -0,0 +1,92 @@ +// Copyright (c) 2011-2022 by Artem A. Gevorkyan (gevorkyan.org) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include + +#include +#include +#include +#include + +using namespace std; + +namespace micro_profiler +{ + namespace keyer + { + struct pid + { + id_t operator ()(const process_info &record) const { return record.pid; } + + template + void operator ()(const IndexT &, process_info &record, id_t key) const + { record.pid = key; } + }; + } + + + process_explorer::process_explorer(mt::milliseconds update_interval, scheduler::queue &apartment_queue) + : _apartment(apartment_queue), _update_interval(update_interval), _cycle(0) + { update(); } + + void process_explorer::update() + { + auto &idx = sdb::unique_index(*this, keyer::pid()); + shared_ptr snapshot(::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0), &::CloseHandle); + PROCESSENTRY32W entry = { sizeof(PROCESSENTRY32W), }; + + _cycle++; + for (auto lister = &::Process32FirstW; + lister(snapshot.get(), &entry); + lister = &::Process32NextW, entry.szExeFile[0] = 0) + { + auto rec = idx[entry.th32ProcessID]; + auto &p = *rec; + + p.parent_pid = entry.th32ParentProcessID; + p.path = unicode(entry.szExeFile); + if (!p.handle) + { + if (auto handle = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, entry.th32ProcessID)) + { + p.handle.reset(handle, &::CloseHandle); + } + else + { + rec.remove(); + continue; + } + } + p.cycle = _cycle; + rec.commit(); + } + for (auto i = begin(); i != end(); ++i) + { + if (i->cycle != _cycle) + { + auto rec = modify(i); + + (*rec).handle.reset(); + rec.commit(); + } + } + _apartment.schedule([this] { update(); }, _update_interval); + } +} diff --git a/explorer/tests/CMakeLists.txt b/explorer/tests/CMakeLists.txt new file mode 100644 index 00000000..844244d9 --- /dev/null +++ b/explorer/tests/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 2.8) + +set(MP_OUTDIR $) + +include(test) + +set(EXPLORER_TEST_SOURCES + ProcessExplorerTests.cpp +) + +add_custom_command(OUTPUT copy-guineas.x + COMMAND ${CMAKE_COMMAND} -E copy ${MP_OUTDIR}/guinea_runner.exe ${MP_OUTDIR}/guinea_runner2.exe + COMMAND ${CMAKE_COMMAND} -E copy ${MP_OUTDIR}/guinea_runner.exe ${MP_OUTDIR}/guinea_runner3.exe + COMMENT "Cloning guinea runners..." +) + +add_library(explorer.tests SHARED ${EXPLORER_TEST_SOURCES} copy-guineas.x) +target_link_libraries(explorer.tests explorer ipc scheduler logger mt test-helpers) +add_dependencies(explorer.tests guinea_runner) diff --git a/explorer/tests/ProcessExplorerTests.cpp b/explorer/tests/ProcessExplorerTests.cpp new file mode 100644 index 00000000..c5818993 --- /dev/null +++ b/explorer/tests/ProcessExplorerTests.cpp @@ -0,0 +1,243 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include + + #define getpid _getpid + +#else + #include + #include + #include + +#endif + +using namespace std; + +namespace micro_profiler +{ + namespace tests + { + namespace + { + void make_connection(const_byte_range payload) + { + struct dummy : ipc::channel + { + virtual void disconnect() throw() { } + virtual void message(const_byte_range /*payload*/) { } + } dummy_channel; + + string controller_id(payload.begin(), payload.end()); + + ipc::connect_client(controller_id.c_str(), dummy_channel); + } + + template + size_t count(pair p) + { return distance(p.first, p.second); } + + struct pid + { + id_t operator ()(const process_info &info) const { return info.pid; } + }; + } + + begin_test_suite( ProcessExplorerTests ) + + string format_endpoint_id() + { + static mt::atomic port(6110); + + return ipc::sockets_endpoint_id(ipc::localhost, static_cast(port.fetch_add(1))); + } + + string controller_id; + shared_ptr< runner_controller<> > controller; + shared_ptr hcontroller; + mocks::queue queue; + + init( Initialize ) + { + controller_id = format_endpoint_id(); + + controller.reset(new runner_controller<>); + hcontroller = ipc::run_server(controller_id, controller); + } + + + test( RunningProcessesAreListedOnConstruction ) + { + // INIT + shared_ptr child1 = create_process("./guinea_runner", " \"" + controller_id + "\""); + controller->wait_connection(); + shared_ptr child2 = create_process("./guinea_runner", " \"" + controller_id + "\""); + controller->wait_connection(); + shared_ptr child3 = create_process("./guinea_runner2", " \"" + controller_id + "\""); + controller->wait_connection(); + shared_ptr child4 = create_process("./guinea_runner3", " \"" + controller_id + "\""); + controller->wait_connection(); + + // INIT / ACT + process_explorer e1(mt::milliseconds(17), queue); + auto &i1 = sdb::multi_index(e1, pid()); + + // ACT / ASSERT + assert_equal(1u, queue.tasks.size()); + assert_equal(mt::milliseconds(17), queue.tasks.back().second); + assert_is_true(1 <= count(i1.equal_range(child1->get_pid()))); + assert_is_true(1 <= count(i1.equal_range(child2->get_pid()))); + assert_is_true(1 <= count(i1.equal_range(child3->get_pid()))); + assert_is_true(1 <= count(i1.equal_range(child4->get_pid()))); + + // INIT + shared_ptr child5 = create_process("./guinea_runner", " \"" + controller_id + "\""); + controller->wait_connection(); + shared_ptr child6 = create_process("./guinea_runner2", " \"" + controller_id + "\""); + controller->wait_connection(); + + // INIT / ACT + process_explorer e2(mt::milliseconds(11), queue); + auto &i2 = sdb::multi_index(e2, pid()); + + // ACT / ASSERT + size_t n_child3; + + assert_equal(2u, queue.tasks.size()); + assert_equal(mt::milliseconds(11), queue.tasks.back().second); + assert_is_true(1 <= count(i2.equal_range(child1->get_pid()))); + assert_is_true(1 <= count(i2.equal_range(child2->get_pid()))); + assert_is_true(1 <= (n_child3 = count(i2.equal_range(child3->get_pid())))); + assert_is_true(1 <= count(i2.equal_range(child4->get_pid()))); + assert_is_true(1 <= count(i2.equal_range(child5->get_pid()))); + assert_is_true(1 <= count(i2.equal_range(child6->get_pid()))); + + // INIT + controller->sessions[2]->disconnect_client(); + child3->wait(); + + // INIT / ACT + process_explorer e3(mt::milliseconds(171), queue); + auto &i3 = sdb::multi_index(e3, pid()); + + // ACT / ASSERT + assert_equal(3u, queue.tasks.size()); + assert_equal(mt::milliseconds(171), queue.tasks.back().second); + assert_is_true(1 <= count(i3.equal_range(child1->get_pid()))); + assert_is_true(1 <= count(i3.equal_range(child2->get_pid()))); + assert_equal(n_child3 - 1, count(i3.equal_range(child3->get_pid()))); + assert_is_true(1 <= count(i3.equal_range(child4->get_pid()))); + assert_is_true(1 <= count(i3.equal_range(child5->get_pid()))); + assert_is_true(1 <= count(i3.equal_range(child6->get_pid()))); + } + + + test( NewProcessesAreReportedAsNewRecords ) + { + // INIT + process_explorer e(mt::milliseconds(1), queue); + auto &idx = sdb::multi_index(e, pid()); + + shared_ptr child1 = create_process("./guinea_runner", " \"" + controller_id + "\""); + controller->wait_connection(); + shared_ptr child2 = create_process("./guinea_runner", " \"" + controller_id + "\""); + controller->wait_connection(); + shared_ptr child3 = create_process("./guinea_runner2", " \"" + controller_id + "\""); + controller->wait_connection(); + shared_ptr child4 = create_process("./guinea_runner3", " \"" + controller_id + "\""); + controller->wait_connection(); + + // ACT + queue.run_one(); + + // ASSERT + size_t n_children[5] = { 0 }; + assert_equal(1u, queue.tasks.size()); + assert_equal(mt::milliseconds(1), queue.tasks.back().second); + assert_is_true(1u <= (n_children[0] = count(idx.equal_range(child1->get_pid())))); + assert_is_true(1u <= (n_children[1] = count(idx.equal_range(child2->get_pid())))); + assert_is_true(1u <= (n_children[2] = count(idx.equal_range(child3->get_pid())))); + assert_is_true(1u <= (n_children[3] = count(idx.equal_range(child4->get_pid())))); + + // INIT + shared_ptr child5 = create_process("./guinea_runner3", " \"" + controller_id + "\""); + controller->wait_connection(); + + // ACT + queue.run_one(); + + // ASSERT + assert_equal(1u, queue.tasks.size()); + assert_equal(mt::milliseconds(1), queue.tasks.back().second); + assert_equal(n_children[0], count(idx.equal_range(child1->get_pid()))); + assert_equal(n_children[1], count(idx.equal_range(child2->get_pid()))); + assert_equal(n_children[2], count(idx.equal_range(child3->get_pid()))); + assert_equal(n_children[3], count(idx.equal_range(child4->get_pid()))); + assert_is_true(1u <= count(idx.equal_range(child5->get_pid()))); + } + + + test( ProcessInfoFieldsAreFilledOutAccordingly ) + { + // INIT + shared_ptr child1 = create_process("./guinea_runner", " \"" + controller_id + "\""); + controller->wait_connection(); + shared_ptr child2 = create_process("./guinea_runner2", " \"" + controller_id + "\""); + controller->wait_connection(); + + // INIT / ACT + process_explorer e(mt::milliseconds(1), queue); + auto &idx = sdb::unique_index(e); + + // ACT / ASSERT + auto p1 = idx.find(child1->get_pid()); + assert_not_null(p1); + assert_equal("guinea_runner.exe", (string)*p1->path); + assert_equal((unsigned)getpid(), p1->parent_pid); + assert_not_null(p1->handle); + + auto p2 = idx.find(child2->get_pid()); + assert_not_null(p2); + assert_equal("guinea_runner2.exe", (string)*p2->path); + assert_equal((unsigned)getpid(), p2->parent_pid); + assert_not_null(p2->handle); + + assert_not_equal(p2->handle, p1->handle); + + // INIT + controller->sessions[1]->disconnect_client(); + child2->wait(); + + // ACT / ASSERT + assert_not_null(p2->handle); + + // ACT + queue.run_one(); + + // ACT / ASSERT + assert_null(p2->handle); + + // INIT + controller->sessions[0]->disconnect_client(); + child1->wait(); + queue.run_one(); + + // ACT / ASSERT + assert_null(p1->handle); + } + end_test_suite + } +} diff --git a/explorer/tests/explorer.tests.vcxproj b/explorer/tests/explorer.tests.vcxproj new file mode 100644 index 00000000..e6c9120a --- /dev/null +++ b/explorer/tests/explorer.tests.vcxproj @@ -0,0 +1,60 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {4B88C6DA-6F89-499D-A0C2-8174871CB6A8} + + + + DynamicLibrary + + + + + + + + false + + + + + + + {69508827-452f-479e-a28f-af300c5c1633} + + + {2ecfc1ae-8829-4a91-9b6e-2befc569acf7} + + + {f1eb4266-766c-4087-95f4-387a955b12aa} + + + {3d321437-3220-4baf-aa87-a5d6297bbe82} + + + {d319214f-4c16-406a-9ad5-70d1b4f4aa4e} + + + {7f768e1b-51fb-46e9-88c6-43e9f136710a} + + + + \ No newline at end of file diff --git a/explorer/tests/explorer.tests.vcxproj.user b/explorer/tests/explorer.tests.vcxproj.user new file mode 100644 index 00000000..066b82dd --- /dev/null +++ b/explorer/tests/explorer.tests.vcxproj.user @@ -0,0 +1,9 @@ + + + + $(TestRunnerExe) + $(TargetPath) + $(TargetDir) + WindowsLocalDebugger + + diff --git a/host/client.h b/host/client.h new file mode 100644 index 00000000..d7d74df3 --- /dev/null +++ b/host/client.h @@ -0,0 +1,44 @@ +// Copyright (c) 2011-2022 by Artem A. Gevorkyan (gevorkyan.org) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#pragma once + +#include +#include + +namespace micro_profiler +{ + namespace host + { + // host::client is a universal-architecture facility to load an module image and execute its functions + // via ipc::channel. + class client + { + public: + enum bitness { _32bit, _64bit, }; + + public: + client(scheduler::queue &apartment_queue); + + void load_server(std::shared_ptr &request, const std::string &server_image_path, bitness image_bitness, + ipc::channel &inbound, std::function complete); + }; + } +} diff --git a/host/src/CMakeLists.txt b/host/src/CMakeLists.txt new file mode 100644 index 00000000..6ab94a8c --- /dev/null +++ b/host/src/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 2.8) + +set(INJECTOR_SOURCES + process_win32.cpp +) + +add_library(injector STATIC ${INJECTOR_SOURCES}) + +target_link_libraries(injector psapi.lib) diff --git a/host/src/host.lib.vcxproj b/host/src/host.lib.vcxproj new file mode 100644 index 00000000..64ac4a98 --- /dev/null +++ b/host/src/host.lib.vcxproj @@ -0,0 +1,48 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + + + + {41873087-07A8-41CE-9212-121B97302117} + + + + StaticLibrary + + + + + + + + %(AdditionalIncludeDirectories) + false + + + true + + + psapi.lib + + + + \ No newline at end of file diff --git a/host/src/host.lib.vcxproj.filters b/host/src/host.lib.vcxproj.filters new file mode 100644 index 00000000..7992fc66 --- /dev/null +++ b/host/src/host.lib.vcxproj.filters @@ -0,0 +1,11 @@ + + + + + {d322ec2c-728b-4052-96d0-b9e94732dc6b} + + + + + + \ No newline at end of file diff --git a/micro-profiler.sln b/micro-profiler.sln index 9284ffc1..dd0e7e2f 100644 --- a/micro-profiler.sln +++ b/micro-profiler.sln @@ -153,6 +153,12 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "viewer", "standalone\viewer EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "savant_db.tests", "sdb\tests\savant_db.tests.vcxproj", "{568C6092-C213-4E8D-B0C0-2BAE495182F6}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "host.lib", "host\src\host.lib.vcxproj", "{41873087-07A8-41CE-9212-121B97302117}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "explorer.lib", "explorer\src\explorer.lib.vcxproj", "{7F768E1B-51FB-46E9-88C6-43E9F136710A}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "explorer.tests", "explorer\tests\explorer.tests.vcxproj", "{4B88C6DA-6F89-499D-A0C2-8174871CB6A8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 @@ -579,49 +585,74 @@ Global {568C6092-C213-4E8D-B0C0-2BAE495182F6}.Release|Win32.Build.0 = Release|Win32 {568C6092-C213-4E8D-B0C0-2BAE495182F6}.Release|x64.ActiveCfg = Release|x64 {568C6092-C213-4E8D-B0C0-2BAE495182F6}.Release|x64.Build.0 = Release|x64 + {41873087-07A8-41CE-9212-121B97302117}.Debug|Win32.ActiveCfg = Debug|Win32 + {41873087-07A8-41CE-9212-121B97302117}.Debug|Win32.Build.0 = Debug|Win32 + {41873087-07A8-41CE-9212-121B97302117}.Debug|x64.ActiveCfg = Debug|x64 + {41873087-07A8-41CE-9212-121B97302117}.Debug|x64.Build.0 = Debug|x64 + {41873087-07A8-41CE-9212-121B97302117}.Release|Win32.ActiveCfg = Release|Win32 + {41873087-07A8-41CE-9212-121B97302117}.Release|Win32.Build.0 = Release|Win32 + {41873087-07A8-41CE-9212-121B97302117}.Release|x64.ActiveCfg = Release|x64 + {41873087-07A8-41CE-9212-121B97302117}.Release|x64.Build.0 = Release|x64 + {7F768E1B-51FB-46E9-88C6-43E9F136710A}.Debug|Win32.ActiveCfg = Debug|Win32 + {7F768E1B-51FB-46E9-88C6-43E9F136710A}.Debug|Win32.Build.0 = Debug|Win32 + {7F768E1B-51FB-46E9-88C6-43E9F136710A}.Debug|x64.ActiveCfg = Debug|x64 + {7F768E1B-51FB-46E9-88C6-43E9F136710A}.Debug|x64.Build.0 = Debug|x64 + {7F768E1B-51FB-46E9-88C6-43E9F136710A}.Release|Win32.ActiveCfg = Release|Win32 + {7F768E1B-51FB-46E9-88C6-43E9F136710A}.Release|Win32.Build.0 = Release|Win32 + {7F768E1B-51FB-46E9-88C6-43E9F136710A}.Release|x64.ActiveCfg = Release|x64 + {7F768E1B-51FB-46E9-88C6-43E9F136710A}.Release|x64.Build.0 = Release|x64 + {4B88C6DA-6F89-499D-A0C2-8174871CB6A8}.Debug|Win32.ActiveCfg = Debug|Win32 + {4B88C6DA-6F89-499D-A0C2-8174871CB6A8}.Debug|Win32.Build.0 = Debug|Win32 + {4B88C6DA-6F89-499D-A0C2-8174871CB6A8}.Debug|x64.ActiveCfg = Debug|x64 + {4B88C6DA-6F89-499D-A0C2-8174871CB6A8}.Debug|x64.Build.0 = Debug|x64 + {4B88C6DA-6F89-499D-A0C2-8174871CB6A8}.Release|Win32.ActiveCfg = Release|Win32 + {4B88C6DA-6F89-499D-A0C2-8174871CB6A8}.Release|Win32.Build.0 = Release|Win32 + {4B88C6DA-6F89-499D-A0C2-8174871CB6A8}.Release|x64.ActiveCfg = Release|x64 + {4B88C6DA-6F89-499D-A0C2-8174871CB6A8}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {AB34E6B5-083B-4463-9CF0-21B02F4C5D48} = {4CBC12AC-0837-43A8-B098-E47F096F91F0} - {685F6A5E-B74A-4ACF-873F-A5A6FB6AC8C9} = {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} + {EE4583A8-C1F3-4353-B504-4D739466FF63} = {4CBC12AC-0837-43A8-B098-E47F096F91F0} {2423F23A-B689-F3F7-A864-A1729EC337C9} = {FA650D5D-355E-46E0-9D03-5DA8E3AA6C80} {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} = {FA650D5D-355E-46E0-9D03-5DA8E3AA6C80} - {8A994BD7-54A9-4104-BA15-7D464E628591} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} - {0D7CDC51-E10F-4ECC-88BB-CE415D80599E} = {8A994BD7-54A9-4104-BA15-7D464E628591} - {63B63694-6773-47AE-8785-A25D8A5BAA22} = {8A994BD7-54A9-4104-BA15-7D464E628591} - {DB4F7DA2-1C0D-46F5-B20C-8801BD4A27F2} = {8A994BD7-54A9-4104-BA15-7D464E628591} - {EE4583A8-C1F3-4353-B504-4D739466FF63} = {4CBC12AC-0837-43A8-B098-E47F096F91F0} {F849E92C-248A-46B2-9EAB-F86578A21316} = {FA650D5D-355E-46E0-9D03-5DA8E3AA6C80} {4DB73ACE-2D47-4A6C-8213-2BE191DD921F} = {FA650D5D-355E-46E0-9D03-5DA8E3AA6C80} - {AD0BA725-9F6F-474E-ADB0-1F506B1CD394} = {D6AE81FB-2D24-47BD-B23A-6AF09C99B918} + {4B446510-FA62-AD02-51BA-9BDE9564FF26} = {FA650D5D-355E-46E0-9D03-5DA8E3AA6C80} + {120FA1C6-37AA-42CE-B922-346BF91EEC0F} = {FA650D5D-355E-46E0-9D03-5DA8E3AA6C80} + {78B079BD-9FC7-4B9E-B4A6-96DA0F00248B} = {FA650D5D-355E-46E0-9D03-5DA8E3AA6C80} + {685F6A5E-B74A-4ACF-873F-A5A6FB6AC8C9} = {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} {A180777F-1EE0-4E11-90CA-D81B01D45ED3} = {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} {CC4F834A-FA9B-40AD-BA2C-5BDFDF95BDFA} = {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} + {01F120A2-7E71-4C3A-B4A8-1F396DA7A0AD} = {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} + {9A6A2463-D5CE-47DD-B69F-E13B08F8E1EC} = {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} + {0AB5C43E-DEAD-4C33-9B56-702BAFC1EA37} = {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} + {60F5D59B-7647-25B1-BF47-8527687DA8AE} = {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} + {8A994BD7-54A9-4104-BA15-7D464E628591} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} {0FCF8B72-B1A4-48E8-A938-8631174ACFC7} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} {3B56B4A3-F4F6-44EC-A7F5-E9045A3C3394} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} {EA61B516-201C-4AB0-93C4-D43248712764} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} {81701143-86EC-4436-9CF7-697EE748BCE8} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} {1220DC3A-2E6D-492E-95B2-73968C209940} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} - {01F120A2-7E71-4C3A-B4A8-1F396DA7A0AD} = {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} {C051C262-E929-4A27-B8A6-1BC5345747B6} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} {8C499723-AB70-4401-9089-864A948532AF} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} - {02D60EA4-E979-4333-AF22-909B005BED04} = {8A994BD7-54A9-4104-BA15-7D464E628591} {9B322934-ADAB-4D13-965E-E27879370F77} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} - {CD421114-D215-4B1E-A884-55CD1858802D} = {8A994BD7-54A9-4104-BA15-7D464E628591} {58516232-A3AD-4D87-B469-C5CF828AE8F1} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} {97C8E773-9F28-4A36-B8B5-D28046B3FDDD} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} - {4B446510-FA62-AD02-51BA-9BDE9564FF26} = {FA650D5D-355E-46E0-9D03-5DA8E3AA6C80} - {120FA1C6-37AA-42CE-B922-346BF91EEC0F} = {FA650D5D-355E-46E0-9D03-5DA8E3AA6C80} - {78B079BD-9FC7-4B9E-B4A6-96DA0F00248B} = {FA650D5D-355E-46E0-9D03-5DA8E3AA6C80} - {9A6A2463-D5CE-47DD-B69F-E13B08F8E1EC} = {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} {A04A53AA-4991-4D8C-906C-428D15DB89D2} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} - {0AB5C43E-DEAD-4C33-9B56-702BAFC1EA37} = {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} - {60F5D59B-7647-25B1-BF47-8527687DA8AE} = {EC366B0C-68AA-4C2D-A15F-5CF9F4156B01} {EF72A11B-BC6E-4F5E-AFAB-34C87ED8ED7C} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} {E00E7A03-E0B0-4F9B-82A1-1448CC321BBD} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} {1655DC47-0291-4459-86D5-D8FF39212EA6} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} {568C6092-C213-4E8D-B0C0-2BAE495182F6} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} + {4B88C6DA-6F89-499D-A0C2-8174871CB6A8} = {DC591EF0-82F7-433C-B43D-1EED3A0F0E67} + {0D7CDC51-E10F-4ECC-88BB-CE415D80599E} = {8A994BD7-54A9-4104-BA15-7D464E628591} + {63B63694-6773-47AE-8785-A25D8A5BAA22} = {8A994BD7-54A9-4104-BA15-7D464E628591} + {DB4F7DA2-1C0D-46F5-B20C-8801BD4A27F2} = {8A994BD7-54A9-4104-BA15-7D464E628591} + {02D60EA4-E979-4333-AF22-909B005BED04} = {8A994BD7-54A9-4104-BA15-7D464E628591} + {CD421114-D215-4B1E-A884-55CD1858802D} = {8A994BD7-54A9-4104-BA15-7D464E628591} + {AD0BA725-9F6F-474E-ADB0-1F506B1CD394} = {D6AE81FB-2D24-47BD-B23A-6AF09C99B918} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CE8841C8-4ACC-4708-8C62-1C21574B3EBB}