| // Copyright (c) 2012 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 "net/proxy_resolution/dhcp_pac_file_fetcher_win.h" |
| |
| #include <memory> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/containers/queue.h" |
| #include "base/memory/free_deleter.h" |
| #include "base/synchronization/lock.h" |
| #include "base/task/post_task.h" |
| #include "base/task_runner.h" |
| #include "base/threading/scoped_blocking_call.h" |
| #include "base/values.h" |
| #include "net/base/net_errors.h" |
| #include "net/log/net_log.h" |
| #include "net/proxy_resolution/dhcp_pac_file_adapter_fetcher_win.h" |
| |
| #include <winsock2.h> |
| #include <iphlpapi.h> |
| |
| namespace net { |
| |
| namespace { |
| |
| // Returns true if |adapter| should be considered when probing for WPAD via |
| // DHCP. |
| bool IsDhcpCapableAdapter(IP_ADAPTER_ADDRESSES* adapter) { |
| if (adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK) |
| return false; |
| if ((adapter->Flags & IP_ADAPTER_DHCP_ENABLED) == 0) |
| return false; |
| |
| // Don't probe interfaces which are not up and ready to pass packets. |
| // |
| // This is a speculative fix for https://crbug.com/770201, in case calling |
| // dhcpsvc!DhcpRequestParams on interfaces that aren't ready yet blocks for |
| // a long time. |
| // |
| // Since ProxyResolutionService restarts WPAD probes in response to other |
| // network level changes, this will likely get called again once the |
| // interface is up. |
| if (adapter->OperStatus != IfOperStatusUp) |
| return false; |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| // This struct contains logging information describing how |
| // GetCandidateAdapterNames() performed, for output to NetLog. |
| struct DhcpAdapterNamesLoggingInfo { |
| DhcpAdapterNamesLoggingInfo() = default; |
| ~DhcpAdapterNamesLoggingInfo() = default; |
| |
| // The error that iphlpapi!GetAdaptersAddresses returned. |
| ULONG error; |
| |
| // The adapters list that iphlpapi!GetAdaptersAddresses returned. |
| std::unique_ptr<IP_ADAPTER_ADDRESSES, base::FreeDeleter> adapters; |
| |
| // The time immediately before GetCandidateAdapterNames was posted to a worker |
| // thread from the origin thread. |
| base::TimeTicks origin_thread_start_time; |
| |
| // The time when GetCandidateAdapterNames began running on the worker thread. |
| base::TimeTicks worker_thread_start_time; |
| |
| // The time when GetCandidateAdapterNames completed running on the worker |
| // thread. |
| base::TimeTicks worker_thread_end_time; |
| |
| // The time when control returned to the origin thread |
| // (OnGetCandidateAdapterNamesDone) |
| base::TimeTicks origin_thread_end_time; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(DhcpAdapterNamesLoggingInfo); |
| }; |
| |
| namespace { |
| |
| // Maximum number of DHCP lookup tasks running concurrently. This is chosen |
| // based on the following UMA data: |
| // - When OnWaitTimer fires, ~99.8% of users have 6 or fewer network |
| // adapters enabled for DHCP in total. |
| // - At the same measurement point, ~99.7% of users have 3 or fewer pending |
| // DHCP adapter lookups. |
| // - There is however a very long and thin tail of users who have |
| // systems reporting up to 100+ adapters (this must be some very weird |
| // OS bug (?), probably the cause of http://crbug.com/240034). |
| // |
| // Th value is chosen such that DHCP lookup tasks don't prevent other tasks from |
| // running even on systems that report a huge number of network adapters, while |
| // giving a good chance of getting back results for any responsive adapters. |
| constexpr int kMaxConcurrentDhcpLookupTasks = 12; |
| |
| // How long to wait at maximum after we get results (a PAC file or |
| // knowledge that no PAC file is configured) from whichever network |
| // adapter finishes first. |
| constexpr base::TimeDelta kMaxWaitAfterFirstResult = |
| base::TimeDelta::FromMilliseconds(400); |
| |
| // A TaskRunner that never schedules more than |kMaxConcurrentDhcpLookupTasks| |
| // tasks concurrently. |
| class TaskRunnerWithCap : public base::TaskRunner { |
| public: |
| TaskRunnerWithCap() = default; |
| |
| bool PostDelayedTask(const base::Location& from_here, |
| base::OnceClosure task, |
| base::TimeDelta delay) override { |
| // Delayed tasks are not supported. |
| DCHECK(delay.is_zero()); |
| |
| // Wrap the task in a callback that runs |task|, then tries to schedule a |
| // task from |pending_tasks_|. |
| base::OnceClosure wrapped_task = |
| base::BindOnce(&TaskRunnerWithCap::RunTaskAndSchedulePendingTask, this, |
| std::move(task)); |
| |
| { |
| base::AutoLock auto_lock(lock_); |
| |
| // If |kMaxConcurrentDhcpLookupTasks| tasks are scheduled, move the task |
| // to |pending_tasks_|. |
| DCHECK_LE(num_scheduled_tasks_, kMaxConcurrentDhcpLookupTasks); |
| if (num_scheduled_tasks_ == kMaxConcurrentDhcpLookupTasks) { |
| pending_tasks_.emplace(from_here, std::move(wrapped_task)); |
| return true; |
| } |
| |
| // If less than |kMaxConcurrentDhcpLookupTasks| tasks are scheduled, |
| // increment |num_scheduled_tasks_| and schedule the task. |
| ++num_scheduled_tasks_; |
| } |
| |
| task_runner_->PostTask(from_here, std::move(wrapped_task)); |
| return true; |
| } |
| |
| bool RunsTasksInCurrentSequence() const override { |
| return task_runner_->RunsTasksInCurrentSequence(); |
| } |
| |
| private: |
| struct LocationAndTask { |
| LocationAndTask() = default; |
| LocationAndTask(const base::Location& from_here, base::OnceClosure task) |
| : from_here(from_here), task(std::move(task)) {} |
| base::Location from_here; |
| base::OnceClosure task; |
| }; |
| |
| ~TaskRunnerWithCap() override = default; |
| |
| void RunTaskAndSchedulePendingTask(base::OnceClosure task) { |
| // Run |task|. |
| std::move(task).Run(); |
| |
| // If |pending_tasks_| is non-empty, schedule a task from it. Otherwise, |
| // decrement |num_scheduled_tasks_|. |
| LocationAndTask task_to_schedule; |
| |
| { |
| base::AutoLock auto_lock(lock_); |
| |
| DCHECK_GT(num_scheduled_tasks_, 0); |
| if (pending_tasks_.empty()) { |
| --num_scheduled_tasks_; |
| return; |
| } |
| |
| task_to_schedule = std::move(pending_tasks_.front()); |
| pending_tasks_.pop(); |
| } |
| |
| DCHECK(task_to_schedule.task); |
| task_runner_->PostTask(task_to_schedule.from_here, |
| std::move(task_to_schedule.task)); |
| } |
| |
| const scoped_refptr<base::TaskRunner> task_runner_ = |
| base::CreateTaskRunnerWithTraits( |
| {base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN, |
| base::TaskPriority::USER_VISIBLE}); |
| |
| // Synchronizes access to members below. |
| base::Lock lock_; |
| |
| // Number of tasks that are currently scheduled. |
| int num_scheduled_tasks_ = 0; |
| |
| // Tasks that are waiting to be scheduled. |
| base::queue<LocationAndTask> pending_tasks_; |
| |
| DISALLOW_COPY_AND_ASSIGN(TaskRunnerWithCap); |
| }; |
| |
| // Helper to set an integer value into a base::DictionaryValue. Because of |
| // C++'s implicit narrowing casts to |int|, this can be called with int64_t and |
| // ULONG too. |
| void SetInt(base::StringPiece key, int value, base::DictionaryValue* dict) { |
| dict->SetKey(key, base::Value(value)); |
| } |
| |
| std::unique_ptr<base::Value> NetLogGetAdaptersDoneCallback( |
| DhcpAdapterNamesLoggingInfo* info, |
| NetLogCaptureMode /* capture_mode */) { |
| std::unique_ptr<base::DictionaryValue> result = |
| std::make_unique<base::DictionaryValue>(); |
| |
| // Add information on each of the adapters enumerated (including those that |
| // were subsequently skipped). |
| base::ListValue adapters_value; |
| for (IP_ADAPTER_ADDRESSES* adapter = info->adapters.get(); adapter; |
| adapter = adapter->Next) { |
| base::DictionaryValue adapter_value; |
| |
| adapter_value.SetKey("AdapterName", base::Value(adapter->AdapterName)); |
| SetInt("IfType", adapter->IfType, &adapter_value); |
| SetInt("Flags", adapter->Flags, &adapter_value); |
| SetInt("OperStatus", adapter->OperStatus, &adapter_value); |
| SetInt("TunnelType", adapter->TunnelType, &adapter_value); |
| |
| // "skipped" means the adapter was not ultimately chosen as a candidate for |
| // testing WPAD. |
| bool skipped = !IsDhcpCapableAdapter(adapter); |
| adapter_value.SetKey("skipped", base::Value(skipped)); |
| |
| adapters_value.GetList().push_back(std::move(adapter_value)); |
| } |
| result->SetKey("adapters", std::move(adapters_value)); |
| |
| SetInt("origin_to_worker_thread_hop_dt", |
| (info->worker_thread_start_time - info->origin_thread_start_time) |
| .InMilliseconds(), |
| result.get()); |
| SetInt("worker_to_origin_thread_hop_dt", |
| (info->origin_thread_end_time - info->worker_thread_end_time) |
| .InMilliseconds(), |
| result.get()); |
| SetInt("worker_dt", |
| (info->worker_thread_end_time - info->worker_thread_start_time) |
| .InMilliseconds(), |
| result.get()); |
| |
| if (info->error != ERROR_SUCCESS) |
| SetInt("error", info->error, result.get()); |
| |
| return result; |
| } |
| |
| std::unique_ptr<base::Value> NetLogFetcherDoneCallback( |
| int fetcher_index, |
| int net_error, |
| NetLogCaptureMode /* capture_mode */) { |
| std::unique_ptr<base::DictionaryValue> result = |
| std::make_unique<base::DictionaryValue>(); |
| |
| result->SetKey("fetcher_index", base::Value(fetcher_index)); |
| result->SetKey("net_error", base::Value(net_error)); |
| |
| return result; |
| } |
| |
| } // namespace |
| |
| DhcpPacFileFetcherWin::DhcpPacFileFetcherWin( |
| URLRequestContext* url_request_context) |
| : state_(STATE_START), |
| num_pending_fetchers_(0), |
| destination_string_(NULL), |
| url_request_context_(url_request_context), |
| task_runner_(base::MakeRefCounted<TaskRunnerWithCap>()) { |
| DCHECK(url_request_context_); |
| } |
| |
| DhcpPacFileFetcherWin::~DhcpPacFileFetcherWin() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| // Count as user-initiated if we are not yet in STATE_DONE. |
| Cancel(); |
| } |
| |
| int DhcpPacFileFetcherWin::Fetch( |
| base::string16* utf16_text, |
| CompletionOnceCallback callback, |
| const NetLogWithSource& net_log, |
| const NetworkTrafficAnnotationTag traffic_annotation) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (state_ != STATE_START && state_ != STATE_DONE) { |
| NOTREACHED(); |
| return ERR_UNEXPECTED; |
| } |
| |
| net_log_ = net_log; |
| |
| if (!url_request_context_) |
| return ERR_CONTEXT_SHUT_DOWN; |
| |
| state_ = STATE_WAIT_ADAPTERS; |
| callback_ = std::move(callback); |
| destination_string_ = utf16_text; |
| |
| net_log.BeginEvent(NetLogEventType::WPAD_DHCP_WIN_FETCH); |
| |
| // TODO(eroman): This event is not ended in the case of cancellation. |
| net_log.BeginEvent(NetLogEventType::WPAD_DHCP_WIN_GET_ADAPTERS); |
| |
| last_query_ = ImplCreateAdapterQuery(); |
| last_query_->logging_info()->origin_thread_start_time = |
| base::TimeTicks::Now(); |
| |
| task_runner_->PostTaskAndReply( |
| FROM_HERE, |
| base::Bind(&DhcpPacFileFetcherWin::AdapterQuery::GetCandidateAdapterNames, |
| last_query_.get()), |
| base::Bind(&DhcpPacFileFetcherWin::OnGetCandidateAdapterNamesDone, |
| AsWeakPtr(), last_query_, traffic_annotation)); |
| |
| return ERR_IO_PENDING; |
| } |
| |
| void DhcpPacFileFetcherWin::Cancel() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| CancelImpl(); |
| } |
| |
| void DhcpPacFileFetcherWin::OnShutdown() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| // Back up callback, if there is one, as CancelImpl() will destroy it. |
| net::CompletionOnceCallback callback = std::move(callback_); |
| |
| // Cancel current request, if there is one. |
| CancelImpl(); |
| |
| // Prevent future network requests. |
| url_request_context_ = nullptr; |
| |
| // Invoke callback with error, if present. |
| if (callback) |
| std::move(callback).Run(ERR_CONTEXT_SHUT_DOWN); |
| } |
| |
| void DhcpPacFileFetcherWin::CancelImpl() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| if (state_ != STATE_DONE) { |
| callback_.Reset(); |
| wait_timer_.Stop(); |
| state_ = STATE_DONE; |
| |
| for (FetcherVector::iterator it = fetchers_.begin(); |
| it != fetchers_.end(); |
| ++it) { |
| (*it)->Cancel(); |
| } |
| |
| fetchers_.clear(); |
| } |
| } |
| |
| void DhcpPacFileFetcherWin::OnGetCandidateAdapterNamesDone( |
| scoped_refptr<AdapterQuery> query, |
| const NetworkTrafficAnnotationTag traffic_annotation) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| // This can happen if this object is reused for multiple queries, |
| // and a previous query was cancelled before it completed. |
| if (query.get() != last_query_.get()) |
| return; |
| last_query_ = NULL; |
| |
| DhcpAdapterNamesLoggingInfo* logging_info = query->logging_info(); |
| logging_info->origin_thread_end_time = base::TimeTicks::Now(); |
| |
| net_log_.EndEvent(NetLogEventType::WPAD_DHCP_WIN_GET_ADAPTERS, |
| base::Bind(&NetLogGetAdaptersDoneCallback, |
| base::Unretained(logging_info))); |
| |
| // Enable unit tests to wait for this to happen; in production this function |
| // call is a no-op. |
| ImplOnGetCandidateAdapterNamesDone(); |
| |
| // We may have been cancelled. |
| if (state_ != STATE_WAIT_ADAPTERS) |
| return; |
| |
| state_ = STATE_NO_RESULTS; |
| |
| const std::set<std::string>& adapter_names = query->adapter_names(); |
| |
| if (adapter_names.empty()) { |
| TransitionToDone(); |
| return; |
| } |
| |
| for (const std::string& adapter_name : adapter_names) { |
| std::unique_ptr<DhcpPacFileAdapterFetcher> fetcher( |
| ImplCreateAdapterFetcher()); |
| size_t fetcher_index = fetchers_.size(); |
| fetcher->Fetch(adapter_name, |
| base::Bind(&DhcpPacFileFetcherWin::OnFetcherDone, |
| base::Unretained(this), fetcher_index), |
| traffic_annotation); |
| fetchers_.push_back(std::move(fetcher)); |
| } |
| num_pending_fetchers_ = fetchers_.size(); |
| } |
| |
| std::string DhcpPacFileFetcherWin::GetFetcherName() const { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| return "win"; |
| } |
| |
| const GURL& DhcpPacFileFetcherWin::GetPacURL() const { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| DCHECK_EQ(state_, STATE_DONE); |
| |
| return pac_url_; |
| } |
| |
| void DhcpPacFileFetcherWin::OnFetcherDone(size_t fetcher_index, |
| int result) { |
| DCHECK(state_ == STATE_NO_RESULTS || state_ == STATE_SOME_RESULTS); |
| |
| net_log_.AddEvent( |
| NetLogEventType::WPAD_DHCP_WIN_ON_FETCHER_DONE, |
| base::Bind(&NetLogFetcherDoneCallback, fetcher_index, result)); |
| |
| if (--num_pending_fetchers_ == 0) { |
| TransitionToDone(); |
| return; |
| } |
| |
| // If the only pending adapters are those less preferred than one |
| // with a valid PAC script, we do not need to wait any longer. |
| for (FetcherVector::iterator it = fetchers_.begin(); |
| it != fetchers_.end(); |
| ++it) { |
| bool did_finish = (*it)->DidFinish(); |
| int result = (*it)->GetResult(); |
| if (did_finish && result == OK) { |
| TransitionToDone(); |
| return; |
| } |
| if (!did_finish || result != ERR_PAC_NOT_IN_DHCP) { |
| break; |
| } |
| } |
| |
| // Once we have a single result, we set a maximum on how long to wait |
| // for the rest of the results. |
| if (state_ == STATE_NO_RESULTS) { |
| state_ = STATE_SOME_RESULTS; |
| net_log_.AddEvent(NetLogEventType::WPAD_DHCP_WIN_START_WAIT_TIMER); |
| wait_timer_.Start(FROM_HERE, |
| ImplGetMaxWait(), this, &DhcpPacFileFetcherWin::OnWaitTimer); |
| } |
| } |
| |
| void DhcpPacFileFetcherWin::OnWaitTimer() { |
| DCHECK_EQ(state_, STATE_SOME_RESULTS); |
| |
| net_log_.AddEvent(NetLogEventType::WPAD_DHCP_WIN_ON_WAIT_TIMER); |
| TransitionToDone(); |
| } |
| |
| void DhcpPacFileFetcherWin::TransitionToDone() { |
| DCHECK(state_ == STATE_NO_RESULTS || state_ == STATE_SOME_RESULTS); |
| |
| int used_fetcher_index = -1; |
| int result = ERR_PAC_NOT_IN_DHCP; // Default if no fetchers. |
| if (!fetchers_.empty()) { |
| // Scan twice for the result; once through the whole list for success, |
| // then if no success, return result for most preferred network adapter, |
| // preferring "real" network errors to the ERR_PAC_NOT_IN_DHCP error. |
| // Default to ERR_ABORTED if no fetcher completed. |
| result = ERR_ABORTED; |
| for (size_t i = 0; i < fetchers_.size(); ++i) { |
| const auto& fetcher = fetchers_[i]; |
| if (fetcher->DidFinish() && fetcher->GetResult() == OK) { |
| result = OK; |
| *destination_string_ = fetcher->GetPacScript(); |
| pac_url_ = fetcher->GetPacURL(); |
| used_fetcher_index = i; |
| break; |
| } |
| } |
| if (result != OK) { |
| destination_string_->clear(); |
| for (size_t i = 0; i < fetchers_.size(); ++i) { |
| const auto& fetcher = fetchers_[i]; |
| if (fetcher->DidFinish()) { |
| result = fetcher->GetResult(); |
| used_fetcher_index = i; |
| if (result != ERR_PAC_NOT_IN_DHCP) { |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| CompletionOnceCallback callback = std::move(callback_); |
| CancelImpl(); |
| DCHECK_EQ(state_, STATE_DONE); |
| DCHECK(fetchers_.empty()); |
| |
| net_log_.EndEvent( |
| NetLogEventType::WPAD_DHCP_WIN_FETCH, |
| base::Bind(&NetLogFetcherDoneCallback, used_fetcher_index, result)); |
| |
| // We may be deleted re-entrantly within this outcall. |
| std::move(callback).Run(result); |
| } |
| |
| int DhcpPacFileFetcherWin::num_pending_fetchers() const { |
| return num_pending_fetchers_; |
| } |
| |
| URLRequestContext* DhcpPacFileFetcherWin::url_request_context() const { |
| return url_request_context_; |
| } |
| |
| scoped_refptr<base::TaskRunner> DhcpPacFileFetcherWin::GetTaskRunner() { |
| return task_runner_; |
| } |
| |
| DhcpPacFileAdapterFetcher* DhcpPacFileFetcherWin::ImplCreateAdapterFetcher() { |
| return new DhcpPacFileAdapterFetcher(url_request_context_, task_runner_); |
| } |
| |
| DhcpPacFileFetcherWin::AdapterQuery* |
| DhcpPacFileFetcherWin::ImplCreateAdapterQuery() { |
| return new AdapterQuery(); |
| } |
| |
| base::TimeDelta DhcpPacFileFetcherWin::ImplGetMaxWait() { |
| return kMaxWaitAfterFirstResult; |
| } |
| |
| bool DhcpPacFileFetcherWin::GetCandidateAdapterNames( |
| std::set<std::string>* adapter_names, |
| DhcpAdapterNamesLoggingInfo* info) { |
| DCHECK(adapter_names); |
| adapter_names->clear(); |
| |
| // The GetAdaptersAddresses MSDN page recommends using a size of 15000 to |
| // avoid reallocation. |
| ULONG adapters_size = 15000; |
| std::unique_ptr<IP_ADAPTER_ADDRESSES, base::FreeDeleter> adapters; |
| ULONG error = ERROR_SUCCESS; |
| int num_tries = 0; |
| |
| do { |
| adapters.reset(static_cast<IP_ADAPTER_ADDRESSES*>(malloc(adapters_size))); |
| // Return only unicast addresses, and skip information we do not need. |
| base::ScopedBlockingCall scoped_blocking_call( |
| FROM_HERE, base::BlockingType::MAY_BLOCK); |
| error = GetAdaptersAddresses(AF_UNSPEC, |
| GAA_FLAG_SKIP_ANYCAST | |
| GAA_FLAG_SKIP_MULTICAST | |
| GAA_FLAG_SKIP_DNS_SERVER | |
| GAA_FLAG_SKIP_FRIENDLY_NAME, |
| NULL, |
| adapters.get(), |
| &adapters_size); |
| ++num_tries; |
| } while (error == ERROR_BUFFER_OVERFLOW && num_tries <= 3); |
| |
| if (info) |
| info->error = error; |
| |
| if (error == ERROR_NO_DATA) { |
| // There are no adapters that we care about. |
| return true; |
| } |
| |
| if (error != ERROR_SUCCESS) { |
| LOG(WARNING) << "Unexpected error retrieving WPAD configuration from DHCP."; |
| return false; |
| } |
| |
| IP_ADAPTER_ADDRESSES* adapter = NULL; |
| for (adapter = adapters.get(); adapter; adapter = adapter->Next) { |
| if (IsDhcpCapableAdapter(adapter)) { |
| DCHECK(adapter->AdapterName); |
| adapter_names->insert(adapter->AdapterName); |
| } |
| } |
| |
| // Transfer the buffer containing the adapters, so it can be used later for |
| // emitting NetLog parameters from the origin thread. |
| if (info) |
| info->adapters = std::move(adapters); |
| return true; |
| } |
| |
| DhcpPacFileFetcherWin::AdapterQuery::AdapterQuery() |
| : logging_info_(new DhcpAdapterNamesLoggingInfo()) {} |
| |
| void DhcpPacFileFetcherWin::AdapterQuery::GetCandidateAdapterNames() { |
| logging_info_->error = ERROR_NO_DATA; |
| logging_info_->adapters.reset(); |
| logging_info_->worker_thread_start_time = base::TimeTicks::Now(); |
| |
| ImplGetCandidateAdapterNames(&adapter_names_, logging_info_.get()); |
| |
| logging_info_->worker_thread_end_time = base::TimeTicks::Now(); |
| } |
| |
| const std::set<std::string>& |
| DhcpPacFileFetcherWin::AdapterQuery::adapter_names() const { |
| return adapter_names_; |
| } |
| |
| bool DhcpPacFileFetcherWin::AdapterQuery::ImplGetCandidateAdapterNames( |
| std::set<std::string>* adapter_names, |
| DhcpAdapterNamesLoggingInfo* info) { |
| return DhcpPacFileFetcherWin::GetCandidateAdapterNames(adapter_names, |
| info); |
| } |
| |
| DhcpPacFileFetcherWin::AdapterQuery::~AdapterQuery() {} |
| |
| } // namespace net |