Branch data Line data Source code
1 : : /*
2 : : * Copyright (C) 2026 by Thun Lu. All rights reserved.
3 : : * Author: Thun Lu <thun.lu@zohomail.cn>
4 : : * Repo: https://github.com/thun-res/vlink
5 : : * _ __ __ _ __
6 : : * | | / / / / (_) ____ / /__
7 : : * | | / / / / / / / __ \ / //_/
8 : : * | |/ / / /___ / / / / / / / ,<
9 : : * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10 : : *
11 : : * Licensed under the Apache License, Version 2.0 (the "License");
12 : : * you may not use this file except in compliance with the License.
13 : : * You may obtain a copy of the License at
14 : : *
15 : : * http://www.apache.org/licenses/LICENSE-2.0
16 : : *
17 : : * Unless required by applicable law or agreed to in writing, software
18 : : * distributed under the License is distributed on an "AS IS" BASIS,
19 : : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 : : * See the License for the specific language governing permissions and
21 : : * limitations under the License.
22 : : */
23 : :
24 : : #include "./base/plugin.h"
25 : :
26 : : #include <algorithm>
27 : : #include <atomic>
28 : : #include <cctype>
29 : : #include <deque>
30 : : #include <filesystem>
31 : : #include <memory>
32 : : #include <mutex>
33 : : #include <shared_mutex>
34 : : #include <stdexcept>
35 : : #include <string>
36 : : #include <string_view>
37 : : #include <unordered_map>
38 : : #include <utility>
39 : :
40 : : #if defined(_WIN32) || defined(_WIN64)
41 : : #ifndef WIN32_LEAN_AND_MEAN
42 : : #define WIN32_LEAN_AND_MEAN
43 : : #define VLINK_PLUGIN_UNDEFINE_LEAN_AND_MEAN
44 : : #endif
45 : : #ifndef NOMINMAX
46 : : #define NOMINMAX
47 : : #define VLINK_PLUGIN_UNDEFINE_NOMINMAX
48 : : #endif
49 : : #include <windows.h>
50 : : #ifdef VLINK_PLUGIN_UNDEFINE_LEAN_AND_MEAN
51 : : #undef WIN32_LEAN_AND_MEAN
52 : : #undef VLINK_PLUGIN_UNDEFINE_LEAN_AND_MEAN
53 : : #endif
54 : : #ifdef VLINK_PLUGIN_UNDEFINE_NOMINMAX
55 : : #undef NOMINMAX
56 : : #undef VLINK_PLUGIN_UNDEFINE_NOMINMAX
57 : : #endif
58 : : #else
59 : : #include <dlfcn.h>
60 : : #endif
61 : :
62 : : #include "./base/helpers.h"
63 : : #include "./base/logger.h"
64 : : #include "./base/utils.h"
65 : :
66 : : namespace vlink {
67 : :
68 : 33 : [[maybe_unused]] static std::string get_current_dir() {
69 : : try {
70 [ + - + - ]: 66 : return std::filesystem::current_path().string();
71 : : } catch (std::filesystem::filesystem_error&) { // LCOV_EXCL_LINE GCOVR_EXCL_LINE
72 : : return "."; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
73 : : } // LCOV_EXCL_LINE GCOVR_EXCL_LINE
74 : : }
75 : :
76 : 184 : [[maybe_unused]] static bool check_regular_file(const std::string& path) {
77 : : try {
78 [ + - + - ]: 184 : return std::filesystem::is_regular_file(path);
79 : : } catch (std::filesystem::filesystem_error&) { // LCOV_EXCL_LINE GCOVR_EXCL_LINE
80 : : return false; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
81 : : } // LCOV_EXCL_LINE GCOVR_EXCL_LINE
82 : : }
83 : :
84 : : // DynamicLibrary
85 : : class DynamicLibrary final {
86 : : public:
87 : : #if defined(_WIN32) || defined(_WIN64)
88 : : static constexpr const char* kFilenamePrefix = "";
89 : : static constexpr const char* kFilenameSuffix = ".dll";
90 : : using NativeHandle = HINSTANCE;
91 : : using NativeSymbol = FARPROC;
92 : : #elif defined(__APPLE__)
93 : : static constexpr const char* kFilenamePrefix = "lib";
94 : : static constexpr const char* kFilenameSuffix = ".dylib";
95 : : using NativeHandle = void*;
96 : : using NativeSymbol = void*;
97 : : #else
98 : : static constexpr const char* kFilenamePrefix = "lib";
99 : : static constexpr const char* kFilenameSuffix = ".so";
100 : : using NativeHandle = void*;
101 : : using NativeSymbol = void*;
102 : : #endif
103 : :
104 : : static_assert(std::is_pointer_v<NativeHandle>, "Expecting native handle to be a pointer.");
105 : : static_assert(std::is_pointer_v<NativeSymbol>, "Expecting native symbol to be a pointer.");
106 : :
107 : : class Exception : public std::runtime_error {
108 : : using std::runtime_error::runtime_error;
109 : : };
110 : :
111 : : class LoadError : public Exception {
112 : : using Exception::Exception;
113 : : };
114 : :
115 : : class SymbolError : public Exception {
116 : : using Exception::Exception;
117 : : };
118 : :
119 : 18 : explicit DynamicLibrary(const std::string& path) {
120 : 18 : handle_ = open_library(path.c_str());
121 : :
122 [ + + ]: 18 : if VUNLIKELY (!handle_) {
123 [ + - + - : 1 : throw LoadError("Could not load library \"" + path + "\"\n" + get_error_description());
+ - + - ]
124 : : }
125 : 17 : }
126 : :
127 : 17 : ~DynamicLibrary() {
128 [ + - ]: 17 : if VLIKELY (handle_) {
129 : 17 : close_library(handle_);
130 : : }
131 : 17 : }
132 : :
133 : : DynamicLibrary(DynamicLibrary&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; }
134 : :
135 : : DynamicLibrary& operator=(DynamicLibrary&& other) noexcept {
136 : : if VLIKELY (this != &other) {
137 : : std::swap(handle_, other.handle_);
138 : : }
139 : :
140 : : return *this;
141 : : }
142 : :
143 : : template <typename SignatureT>
144 : 32 : SignatureT* get_function(const std::string& name) const {
145 [ - + ]: 32 : if VUNLIKELY (!handle_) {
146 [ # # ]: 0 : throw std::logic_error("The dynamic library handle is null. This object may have been moved from.");
147 : : }
148 : :
149 : 32 : auto* symbol = locate_symbol(handle_, name.c_str());
150 : :
151 [ + + ]: 32 : if VUNLIKELY (!symbol) {
152 [ + - + - : 1 : throw SymbolError("Could not get symbol \"" + name + "\"\n" + get_error_description());
+ - + - ]
153 : : }
154 : :
155 : : #if defined(__GNUC__) && __GNUC__ >= 8
156 : : #pragma GCC diagnostic push
157 : : #pragma GCC diagnostic ignored "-Wcast-function-type"
158 : : #endif
159 : :
160 : 31 : return reinterpret_cast<SignatureT*>(symbol);
161 : :
162 : : #if defined(__GNUC__) && __GNUC__ >= 8
163 : : #pragma GCC diagnostic pop
164 : : #endif
165 : : }
166 : :
167 : : NativeHandle native_handle() const noexcept { return handle_; }
168 : :
169 : : private:
170 : 18 : static NativeHandle open_library(const char* path) noexcept {
171 : : #if defined(_WIN32) || defined(_WIN64)
172 : : return ::LoadLibraryA(path);
173 : : #else
174 : 18 : return ::dlopen(path, RTLD_NOW | RTLD_LOCAL);
175 : : #endif
176 : : }
177 : :
178 : 32 : static NativeSymbol locate_symbol(NativeHandle lib, const char* name) noexcept {
179 : : #if defined(_WIN32) || defined(_WIN64)
180 : : return ::GetProcAddress(lib, name);
181 : : #else
182 : 32 : return ::dlsym(lib, name);
183 : : #endif
184 : : }
185 : :
186 : 17 : static void close_library(NativeHandle lib) noexcept {
187 : : #if defined(_WIN32) || defined(_WIN64)
188 : : ::FreeLibrary(lib);
189 : : #else
190 : 17 : ::dlclose(lib);
191 : : #endif
192 : 17 : }
193 : :
194 : 2 : static std::string get_error_description() noexcept {
195 : : #if defined(_WIN32) || defined(_WIN64)
196 : : static constexpr size_t kBufferSize = 512;
197 : : const auto error_code = ::GetLastError();
198 : :
199 : : if (!error_code) {
200 : : return "No error reported by GetLastError";
201 : : }
202 : :
203 : : char description[kBufferSize];
204 : :
205 : : const auto language = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
206 : : const DWORD length =
207 : : ::FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, error_code, language, description, kBufferSize, nullptr);
208 : :
209 : : return (length == 0) ? "Unknown error (FormatMessage failed)" : description;
210 : : #else
211 : 2 : const auto* description = ::dlerror();
212 : :
213 [ + - ]: 2 : return (description == nullptr) ? "No error reported by dlerror" : description;
214 : : #endif
215 : : }
216 : :
217 : : NativeHandle handle_{nullptr};
218 : :
219 : : VLINK_DISALLOW_COPY_AND_ASSIGN(DynamicLibrary)
220 : : };
221 : :
222 : : // PluginEntry
223 : : struct PluginEntry final {
224 : : std::unique_ptr<DynamicLibrary> loader;
225 : : std::string plugin_complex_id;
226 : : Logger::Level log_level{Logger::kTrace};
227 : : };
228 : :
229 : : // Plugin::Impl
230 : : struct Plugin::Impl final {
231 : : std::atomic<Logger::Level> log_level{Logger::kTrace};
232 : : std::shared_mutex mtx;
233 : : std::unordered_map<std::string, std::shared_ptr<PluginEntry>> plugin_map;
234 : : };
235 : :
236 : : // Plugin
237 : 198 : Plugin::Plugin() : impl_(std::make_unique<Impl>()) {}
238 : :
239 : 5 : void Plugin::set_log_level(Logger::Level level) { impl_->log_level.store(level, std::memory_order_release); }
240 : :
241 : 2 : Logger::Level Plugin::get_log_level() const { return impl_->log_level.load(std::memory_order_acquire); }
242 : :
243 : 198 : Plugin::~Plugin() = default;
244 : :
245 : 33 : std::deque<std::string> Plugin::default_search_path() {
246 [ + - ]: 33 : const std::string current_dir = get_current_dir();
247 [ + - + - ]: 66 : const std::string plugin_dir_env = Utils::get_env("VLINK_PLUGIN_DIR");
248 : 33 : const std::string local_app_dir = Utils::get_app_dir();
249 : :
250 : : std::deque<std::string> search_path{
251 : : current_dir,
252 : : local_app_dir,
253 : : local_app_dir + "/../lib64",
254 : : local_app_dir + "/../lib",
255 : : local_app_dir + "/lib64",
256 : : local_app_dir + "/lib",
257 : : "/lib64", // LCOV_EXCL_LINE GCOVR_EXCL_LINE
258 : : "/lib",
259 [ + - + - : 363 : };
+ - + - +
- + - + -
+ - + - ]
260 : :
261 : 33 : const auto plugin_dirs = Helpers::split_any(plugin_dir_env);
262 : :
263 [ + + ]: 33 : if (!plugin_dirs.empty()) {
264 [ + - ]: 1 : search_path.insert(search_path.begin(), plugin_dirs.begin(), plugin_dirs.end());
265 : : }
266 : :
267 : 66 : return search_path;
268 : 33 : }
269 : :
270 : 72 : void Plugin::clear() {
271 [ + - ]: 72 : std::lock_guard lock(impl_->mtx);
272 : 72 : impl_->plugin_map.clear();
273 : 72 : }
274 : :
275 : 39 : Plugin::Handle Plugin::load_and_create(const std::string& plugin_id, const std::string& lib_name,
276 : : uint16_t version_major, uint16_t version_minor, const std::string& dir_name,
277 : : const std::deque<std::string>& search_paths, const std::string& function_name,
278 : : std::shared_ptr<PluginEntry>* plugin_entry) {
279 : 39 : const auto log_level = impl_->log_level.load(std::memory_order_acquire);
280 : :
281 [ - + ]: 39 : if VUNLIKELY (plugin_id.empty()) {
282 : : if (log_level <= Logger::kError) { // LCOV_EXCL_LINE GCOVR_EXCL_LINE
283 : : VLOG_E("Plugin: Plugin id is empty."); // LCOV_EXCL_LINE GCOVR_EXCL_LINE
284 : : }
285 : :
286 : : return nullptr; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
287 : : }
288 : :
289 [ + + ]: 39 : if VUNLIKELY (lib_name.empty()) {
290 [ + - ]: 1 : if (log_level <= Logger::kError) {
291 [ + - + - ]: 2 : VLOG_E("Plugin: Lib name is empty.");
292 : : }
293 : :
294 : 1 : return nullptr;
295 : : }
296 : :
297 [ + - + - ]: 38 : std::string plugin_complex_id = lib_name + "@" + plugin_id;
298 : :
299 : : {
300 [ + - ]: 38 : std::unique_lock load_lock(impl_->mtx);
301 : :
302 [ + - + + ]: 38 : if VUNLIKELY (impl_->plugin_map.count(plugin_complex_id) != 0U) {
303 [ + - ]: 1 : if (log_level <= Logger::kError) {
304 [ + - + - ]: 2 : VLOG_E("Plugin: Already loaded (", plugin_complex_id, ").");
305 : : }
306 : :
307 : 1 : return nullptr;
308 : : }
309 [ + + ]: 38 : }
310 : :
311 : 37 : std::string plugin_path;
312 : :
313 : : {
314 : 37 : bool has_filename_suffix = vlink::Helpers::has_endwith(lib_name, DynamicLibrary::kFilenameSuffix);
315 : : #if defined(_WIN32) || defined(_WIN64)
316 : : const std::string_view filename_suffix = DynamicLibrary::kFilenameSuffix;
317 : :
318 : : if (!has_filename_suffix && lib_name.size() >= filename_suffix.size()) {
319 : : has_filename_suffix =
320 : : std::equal(filename_suffix.begin(), filename_suffix.end(), lib_name.end() - filename_suffix.size(),
321 : : [](unsigned char lhs, unsigned char rhs) { return std::tolower(lhs) == std::tolower(rhs); });
322 : : }
323 : : #endif
324 : 37 : const bool has_path_separator = lib_name.find_first_of("/\\") != std::string::npos;
325 [ + + - + ]: 37 : const bool path_like = has_path_separator || has_filename_suffix;
326 : :
327 [ + + ]: 37 : if (path_like) {
328 [ + - - + ]: 2 : if (check_regular_file(lib_name)) {
329 [ # # # # : 0 : plugin_path = has_path_separator ? lib_name : get_current_dir() + "/" + lib_name;
# # # # #
# # # # #
# # # # #
# ]
330 : : }
331 : : } else {
332 [ + - + - ]: 35 : std::string plugin_name = DynamicLibrary::kFilenamePrefix + lib_name + DynamicLibrary::kFilenameSuffix;
333 : 35 : std::string check_path;
334 : :
335 [ + + ]: 199 : for (const auto& path : search_paths) {
336 [ + - ]: 182 : check_path = path;
337 [ + - ]: 182 : check_path += '/';
338 : :
339 [ + + ]: 182 : if (!dir_name.empty()) {
340 [ + - ]: 1 : check_path += dir_name;
341 [ + - ]: 1 : check_path += '/';
342 : : }
343 : :
344 [ + - ]: 182 : check_path += plugin_name;
345 : :
346 [ + - + + ]: 182 : if (check_regular_file(check_path)) {
347 : 18 : plugin_path = std::move(check_path);
348 : 18 : break;
349 : : }
350 : : }
351 : 35 : }
352 : : }
353 : :
354 : 37 : Handle handle = nullptr;
355 : :
356 [ + + ]: 37 : if VUNLIKELY (plugin_path.empty()) {
357 [ + + ]: 19 : if (log_level <= Logger::kError) {
358 [ + - + - ]: 36 : VLOG_E("Plugin: Cannot find plugin (", plugin_complex_id, ").");
359 : : }
360 : :
361 : 19 : return handle;
362 : : }
363 : :
364 [ + + ]: 18 : if (log_level <= Logger::kTrace) {
365 [ + - + - ]: 32 : VLOG_T("Plugin: Loading plugin: ", plugin_path, ".");
366 : : }
367 : :
368 : : try {
369 [ + - ]: 18 : auto entry = std::make_shared<PluginEntry>();
370 [ + + ]: 18 : entry->loader = std::make_unique<DynamicLibrary>(std::move(plugin_path));
371 [ + - ]: 17 : entry->plugin_complex_id = plugin_complex_id;
372 : 17 : entry->log_level = log_level;
373 : :
374 : : auto create_function =
375 [ + + ]: 17 : entry->loader->get_function<Handle(const char*, const char*, uint16_t, uint16_t, uint8_t)>(function_name);
376 : :
377 [ - + ]: 16 : if VUNLIKELY (!create_function) {
378 : : // LCOV_EXCL_START GCOVR_EXCL_START
379 : : if (log_level <= Logger::kError) {
380 : : VLOG_E("Plugin: Cannot find symbol function to create (", plugin_complex_id, ").");
381 : : }
382 : :
383 : : return handle;
384 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
385 : : }
386 : :
387 [ + - ]: 16 : handle = create_function(lib_name.c_str(), plugin_id.c_str(), version_major, version_minor, log_level);
388 : :
389 [ + + ]: 16 : if VUNLIKELY (!handle) {
390 [ + - ]: 1 : if (log_level <= Logger::kError) {
391 [ + - + - ]: 2 : VLOG_E("Plugin: Failed to create handle (", plugin_complex_id, ").");
392 : : }
393 : :
394 : 1 : return handle;
395 : : }
396 : :
397 [ + + ]: 15 : if (log_level <= Logger::kTrace) {
398 [ + - + - ]: 26 : VLOG_T("Plugin: Loaded successfully (", plugin_complex_id, ").");
399 : : }
400 : :
401 : 15 : bool inserted = false;
402 : :
403 : : {
404 [ + - ]: 15 : std::unique_lock load_lock(impl_->mtx);
405 [ + - ]: 15 : auto [iter, emplaced] = impl_->plugin_map.emplace(plugin_complex_id, entry);
406 : : (void)iter;
407 : 15 : inserted = emplaced;
408 : 15 : }
409 : :
410 [ - + ]: 15 : if VUNLIKELY (!inserted) {
411 : : destroy(entry, handle); // LCOV_EXCL_LINE GCOVR_EXCL_LINE
412 : : return nullptr; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
413 : : }
414 : :
415 [ + - ]: 15 : if (plugin_entry) {
416 : 15 : *plugin_entry = entry;
417 : : }
418 : :
419 : 15 : return handle;
420 [ - + ]: 20 : } catch (const DynamicLibrary::Exception& e) {
421 [ + - ]: 2 : if (log_level <= Logger::kError) {
422 [ + - + - ]: 4 : VLOG_E("Plugin: Failed to load plugin (", plugin_complex_id, "): ", e.what(), ".");
423 : : }
424 : :
425 : 2 : return handle;
426 : 2 : }
427 : 38 : }
428 : :
429 : 4 : bool Plugin::unload(const std::string& plugin_complex_id) {
430 : 4 : const auto log_level = impl_->log_level.load(std::memory_order_acquire);
431 : :
432 [ - + ]: 4 : if VUNLIKELY (plugin_complex_id.empty()) {
433 : : if (log_level <= Logger::kError) { // LCOV_EXCL_LINE GCOVR_EXCL_LINE
434 : : VLOG_E("Plugin: Plugin id is empty."); // LCOV_EXCL_LINE GCOVR_EXCL_LINE
435 : : }
436 : :
437 : : return false; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
438 : : }
439 : :
440 : 4 : bool erased = false;
441 : :
442 : : {
443 [ + - ]: 4 : std::lock_guard lock(impl_->mtx);
444 [ + - ]: 4 : erased = impl_->plugin_map.erase(plugin_complex_id) != 0U;
445 : 4 : }
446 : :
447 [ + + ]: 4 : if VUNLIKELY (!erased) {
448 [ + + ]: 2 : if (log_level <= Logger::kError) {
449 [ + - + - ]: 2 : VLOG_E("Plugin: Not loaded (", plugin_complex_id, ").");
450 : : }
451 : :
452 : 2 : return false;
453 : : }
454 : :
455 : 2 : return true;
456 : : }
457 : :
458 : 6 : bool Plugin::has_loaded(const std::string& plugin_complex_id) {
459 [ + - ]: 6 : std::shared_lock lock(impl_->mtx);
460 [ + - ]: 12 : return impl_->plugin_map.count(plugin_complex_id) != 0;
461 : 6 : }
462 : :
463 : 15 : bool Plugin::destroy(std::shared_ptr<PluginEntry> plugin_entry, Handle handle, const std::string& function_name) {
464 [ + - - + : 15 : if VUNLIKELY (!plugin_entry || !plugin_entry->loader || !handle) {
+ - - + -
+ ]
465 : : return false; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
466 : : }
467 : :
468 : : try {
469 [ + - ]: 15 : auto destroy_function = plugin_entry->loader->get_function<bool(Handle)>(function_name);
470 : :
471 [ - + ]: 15 : if VUNLIKELY (!destroy_function) {
472 : : // LCOV_EXCL_START GCOVR_EXCL_START
473 : : if (plugin_entry->log_level <= Logger::kError) {
474 : : VLOG_E("Plugin: Cannot find symbol function to destroy (", plugin_entry->plugin_complex_id, ").");
475 : : }
476 : :
477 : : return false;
478 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
479 : : }
480 : :
481 [ + - - + ]: 15 : if VUNLIKELY (!destroy_function(handle)) {
482 : : // LCOV_EXCL_START GCOVR_EXCL_START
483 : : if (plugin_entry->log_level <= Logger::kError) {
484 : : VLOG_E("Plugin: Failed to destroy handle (", plugin_entry->plugin_complex_id, ").");
485 : : }
486 : :
487 : : return false;
488 : : }
489 : : } catch (const std::exception& e) {
490 : : if (plugin_entry->log_level <= Logger::kError) {
491 : : VLOG_E("Plugin: Failed to destroy handle (", plugin_entry->plugin_complex_id, "): ", e.what(), ".");
492 : : }
493 : :
494 : : return false;
495 : : } catch (...) {
496 : : if (plugin_entry->log_level <= Logger::kError) {
497 : : VLOG_E("Plugin: Failed to destroy handle (", plugin_entry->plugin_complex_id, "): non-std exception.");
498 : : }
499 : :
500 : : return false;
501 : : }
502 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
503 : :
504 : 15 : return true;
505 : : }
506 : :
507 : 24 : bool Plugin::process_plugin_internal(const std::string& lib_name, const std::string& local_plugin_id,
508 : : uint16_t local_version_major, uint16_t local_version_minor,
509 : : const std::string& target_plugin_id, uint16_t target_version_major,
510 : : uint16_t target_version_minor, uint8_t log_level) {
511 [ + + ]: 24 : if (log_level <= Logger::kInfo) {
512 [ + - + - ]: 30 : VLOG_I("Plugin: ", lib_name, "@", local_plugin_id, "#", local_version_major, ".", local_version_minor, ".");
513 : : }
514 : :
515 [ + + + + : 24 : if VUNLIKELY (target_plugin_id.empty() || target_plugin_id != local_plugin_id) {
+ + ]
516 [ + - ]: 3 : if (log_level <= Logger::kError) {
517 [ + - + - ]: 6 : VLOG_E("Plugin: Plugin id mismatch: expected '", local_plugin_id, "', got '", target_plugin_id, "'.");
518 : : }
519 : :
520 : 3 : return false;
521 : : }
522 : :
523 [ + + + + : 21 : if VUNLIKELY (target_version_major != local_version_major || target_version_minor > local_version_minor) {
+ + ]
524 [ + - ]: 2 : if (log_level <= Logger::kError) {
525 [ + - + - ]: 4 : VLOG_E("Plugin: Version mismatch: local ", local_version_major, ".", local_version_minor, ", required ",
526 : : target_version_major, ".", target_version_minor, ".");
527 : : }
528 : :
529 : 2 : return false;
530 : : }
531 : :
532 : 19 : return true;
533 : : }
534 : :
535 : : } // namespace vlink
|