123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692 |
- // Copyright (c) 2017 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/http/http_cache_writers.h"
- #include <algorithm>
- #include <utility>
- #include "base/auto_reset.h"
- #include "base/bind.h"
- #include "base/callback_helpers.h"
- #include "base/debug/crash_logging.h"
- #include "base/debug/dump_without_crashing.h"
- #include "base/logging.h"
- #include "base/strings/stringprintf.h"
- #include "base/threading/thread_task_runner_handle.h"
- #include "crypto/secure_hash.h"
- #include "crypto/sha2.h"
- #include "net/base/net_errors.h"
- #include "net/disk_cache/disk_cache.h"
- #include "net/http/http_cache_transaction.h"
- #include "net/http/http_response_info.h"
- #include "net/http/http_status_code.h"
- #include "net/http/partial_data.h"
- namespace net {
- namespace {
- base::debug::CrashKeyString* GetCacheKeyCrashKey() {
- static auto* crash_key = base::debug::AllocateCrashKeyString(
- "http_cache_key", base::debug::CrashKeySize::Size256);
- return crash_key;
- }
- base::debug::CrashKeyString* GetTransactionFlagsCrashKey() {
- static auto* crash_key = base::debug::AllocateCrashKeyString(
- "http_cache_transaction", base::debug::CrashKeySize::Size256);
- return crash_key;
- }
- bool IsValidResponseForWriter(bool is_partial,
- const HttpResponseInfo* response_info) {
- if (!response_info->headers.get())
- return false;
- // Return false if the response code sent by the server is garbled.
- // Both 200 and 304 are valid since concurrent writing is supported.
- if (!is_partial &&
- (response_info->headers->response_code() != net::HTTP_OK &&
- response_info->headers->response_code() != net::HTTP_NOT_MODIFIED)) {
- return false;
- }
- return true;
- }
- } // namespace
- HttpCache::Writers::TransactionInfo::TransactionInfo(PartialData* partial_data,
- const bool is_truncated,
- HttpResponseInfo info)
- : partial(partial_data), truncated(is_truncated), response_info(info) {}
- HttpCache::Writers::TransactionInfo::~TransactionInfo() = default;
- HttpCache::Writers::TransactionInfo::TransactionInfo(const TransactionInfo&) =
- default;
- HttpCache::Writers::Writers(HttpCache* cache, HttpCache::ActiveEntry* entry)
- : cache_(cache), entry_(entry) {
- DCHECK(cache_);
- DCHECK(entry_);
- }
- HttpCache::Writers::~Writers() = default;
- int HttpCache::Writers::Read(scoped_refptr<IOBuffer> buf,
- int buf_len,
- CompletionOnceCallback callback,
- Transaction* transaction) {
- DCHECK(buf);
- // TODO(https://crbug.com/1335423): Change to DCHECK_GT() or remove after bug
- // is fixed.
- CHECK_GT(buf_len, 0);
- DCHECK(!callback.is_null());
- DCHECK(transaction);
- // If another transaction invoked a Read which is currently ongoing, then
- // this transaction waits for the read to complete and gets its buffer filled
- // with the data returned from that read.
- if (next_state_ != State::NONE) {
- WaitingForRead read_info(buf, buf_len, std::move(callback));
- waiting_for_read_.insert(std::make_pair(transaction, std::move(read_info)));
- return ERR_IO_PENDING;
- }
- DCHECK_EQ(next_state_, State::NONE);
- DCHECK(callback_.is_null());
- DCHECK_EQ(nullptr, active_transaction_);
- DCHECK(HasTransaction(transaction));
- active_transaction_ = transaction;
- read_buf_ = std::move(buf);
- io_buf_len_ = buf_len;
- next_state_ = State::NETWORK_READ;
- int rv = DoLoop(OK);
- if (rv == ERR_IO_PENDING)
- callback_ = std::move(callback);
- return rv;
- }
- bool HttpCache::Writers::StopCaching(bool keep_entry) {
- // If this is the only transaction in Writers, then stopping will be
- // successful. If not, then we will not stop caching since there are
- // other consumers waiting to read from the cache.
- if (all_writers_.size() != 1)
- return false;
- network_read_only_ = true;
- if (!keep_entry) {
- should_keep_entry_ = false;
- cache_->WritersDoomEntryRestartTransactions(entry_);
- }
- return true;
- }
- void HttpCache::Writers::AddTransaction(
- Transaction* transaction,
- ParallelWritingPattern initial_writing_pattern,
- RequestPriority priority,
- const TransactionInfo& info) {
- DCHECK(transaction);
- ParallelWritingPattern writers_pattern;
- DCHECK(CanAddWriters(&writers_pattern));
- DCHECK_EQ(0u, all_writers_.count(transaction));
- // Set truncation related information.
- response_info_truncation_ = info.response_info;
- should_keep_entry_ =
- IsValidResponseForWriter(info.partial != nullptr, &(info.response_info));
- if (all_writers_.empty()) {
- DCHECK_EQ(PARALLEL_WRITING_NONE, parallel_writing_pattern_);
- parallel_writing_pattern_ = initial_writing_pattern;
- if (parallel_writing_pattern_ != PARALLEL_WRITING_JOIN)
- is_exclusive_ = true;
- } else {
- DCHECK_EQ(PARALLEL_WRITING_JOIN, parallel_writing_pattern_);
- }
- if (info.partial && !info.truncated) {
- DCHECK(!partial_do_not_truncate_);
- partial_do_not_truncate_ = true;
- }
- std::pair<Transaction*, TransactionInfo> writer(transaction, info);
- all_writers_.insert(writer);
- priority_ = std::max(priority, priority_);
- if (network_transaction_) {
- network_transaction_->SetPriority(priority_);
- }
- }
- void HttpCache::Writers::SetNetworkTransaction(
- Transaction* transaction,
- std::unique_ptr<HttpTransaction> network_transaction,
- std::unique_ptr<crypto::SecureHash> checksum) {
- DCHECK_EQ(1u, all_writers_.count(transaction));
- DCHECK(network_transaction);
- DCHECK(!network_transaction_);
- network_transaction_ = std::move(network_transaction);
- network_transaction_->SetPriority(priority_);
- DCHECK(!checksum_);
- checksum_ = std::move(checksum);
- }
- void HttpCache::Writers::ResetNetworkTransaction() {
- DCHECK(is_exclusive_);
- DCHECK_EQ(1u, all_writers_.size());
- DCHECK(all_writers_.begin()->second.partial);
- network_transaction_.reset();
- }
- void HttpCache::Writers::RemoveTransaction(Transaction* transaction,
- bool success) {
- EraseTransaction(transaction, OK);
- if (!all_writers_.empty())
- return;
- if (!success && ShouldTruncate())
- TruncateEntry();
- cache_->WritersDoneWritingToEntry(entry_, success, should_keep_entry_,
- TransactionSet());
- }
- void HttpCache::Writers::EraseTransaction(Transaction* transaction,
- int result) {
- // The transaction should be part of all_writers.
- auto it = all_writers_.find(transaction);
- DCHECK(it != all_writers_.end());
- EraseTransaction(it, result);
- }
- HttpCache::Writers::TransactionMap::iterator
- HttpCache::Writers::EraseTransaction(TransactionMap::iterator it, int result) {
- Transaction* transaction = it->first;
- transaction->WriterAboutToBeRemovedFromEntry(result);
- auto return_it = all_writers_.erase(it);
- if (all_writers_.empty() && next_state_ == State::NONE) {
- // This needs to be called to handle the edge case where even before Read is
- // invoked all transactions are removed. In that case the
- // network_transaction_ will still have a valid request info and so it
- // should be destroyed before its consumer is destroyed (request info
- // is a raw pointer owned by its consumer).
- network_transaction_.reset();
- } else {
- UpdatePriority();
- }
- if (active_transaction_ == transaction) {
- active_transaction_ = nullptr;
- } else {
- // If waiting for read, remove it from the map.
- waiting_for_read_.erase(transaction);
- }
- return return_it;
- }
- void HttpCache::Writers::UpdatePriority() {
- // Get the current highest priority.
- RequestPriority current_highest = MINIMUM_PRIORITY;
- for (auto& writer : all_writers_) {
- Transaction* transaction = writer.first;
- current_highest = std::max(transaction->priority(), current_highest);
- }
- if (priority_ != current_highest) {
- if (network_transaction_)
- network_transaction_->SetPriority(current_highest);
- priority_ = current_highest;
- }
- }
- void HttpCache::Writers::CloseConnectionOnDestruction() {
- if (network_transaction_)
- network_transaction_->CloseConnectionOnDestruction();
- }
- bool HttpCache::Writers::ContainsOnlyIdleWriters() const {
- return waiting_for_read_.empty() && !active_transaction_;
- }
- bool HttpCache::Writers::CanAddWriters(ParallelWritingPattern* reason) {
- *reason = parallel_writing_pattern_;
- if (all_writers_.empty())
- return true;
- return !is_exclusive_ && !network_read_only_;
- }
- void HttpCache::Writers::ProcessFailure(int error) {
- // Notify waiting_for_read_ of the failure. Tasks will be posted for all the
- // transactions.
- CompleteWaitingForReadTransactions(error);
- // Idle readers should fail when Read is invoked on them.
- RemoveIdleWriters(error);
- }
- void HttpCache::Writers::TruncateEntry() {
- DCHECK(ShouldTruncate());
- auto data = base::MakeRefCounted<PickledIOBuffer>();
- response_info_truncation_.Persist(data->pickle(),
- true /* skip_transient_headers*/,
- true /* response_truncated */);
- data->Done();
- io_buf_len_ = data->pickle()->size();
- entry_->disk_entry->WriteData(kResponseInfoIndex, 0, data.get(), io_buf_len_,
- base::DoNothing(), true);
- }
- bool HttpCache::Writers::ShouldTruncate() {
- // Don't set the flag for sparse entries or for entries that cannot be
- // resumed.
- if (!should_keep_entry_ || partial_do_not_truncate_)
- return false;
- // Check the response headers for strong validators.
- // Note that if this is a 206, content-length was already fixed after calling
- // PartialData::ResponseHeadersOK().
- if (response_info_truncation_.headers->GetContentLength() <= 0 ||
- response_info_truncation_.headers->HasHeaderValue("Accept-Ranges",
- "none") ||
- !response_info_truncation_.headers->HasStrongValidators()) {
- should_keep_entry_ = false;
- return false;
- }
- // Double check that there is something worth keeping.
- int current_size = entry_->disk_entry->GetDataSize(kResponseContentIndex);
- if (!current_size) {
- should_keep_entry_ = false;
- return false;
- }
- if (response_info_truncation_.headers->HasHeader("Content-Encoding")) {
- should_keep_entry_ = false;
- return false;
- }
- int64_t content_length =
- response_info_truncation_.headers->GetContentLength();
- if (content_length >= 0 && content_length <= current_size)
- return false;
- return true;
- }
- LoadState HttpCache::Writers::GetLoadState() const {
- if (network_transaction_)
- return network_transaction_->GetLoadState();
- return LOAD_STATE_IDLE;
- }
- HttpCache::Writers::WaitingForRead::WaitingForRead(
- scoped_refptr<IOBuffer> buf,
- int len,
- CompletionOnceCallback consumer_callback)
- : read_buf(std::move(buf)),
- read_buf_len(len),
- callback(std::move(consumer_callback)) {
- DCHECK(read_buf);
- DCHECK_GT(len, 0);
- DCHECK(!callback.is_null());
- }
- HttpCache::Writers::WaitingForRead::~WaitingForRead() = default;
- HttpCache::Writers::WaitingForRead::WaitingForRead(WaitingForRead&&) = default;
- int HttpCache::Writers::DoLoop(int result) {
- DCHECK_NE(State::UNSET, next_state_);
- DCHECK_NE(State::NONE, next_state_);
- int rv = result;
- do {
- State state = next_state_;
- next_state_ = State::UNSET;
- switch (state) {
- case State::NETWORK_READ:
- DCHECK_EQ(OK, rv);
- rv = DoNetworkRead();
- break;
- case State::NETWORK_READ_COMPLETE:
- rv = DoNetworkReadComplete(rv);
- break;
- case State::CACHE_WRITE_DATA:
- rv = DoCacheWriteData(rv);
- break;
- case State::CACHE_WRITE_DATA_COMPLETE:
- rv = DoCacheWriteDataComplete(rv);
- break;
- case State::MARK_SINGLE_KEYED_CACHE_ENTRY_UNUSABLE:
- // `rv` is bytes here.
- DCHECK_EQ(0, rv);
- rv = DoMarkSingleKeyedCacheEntryUnusable();
- break;
- case State::MARK_SINGLE_KEYED_CACHE_ENTRY_UNUSABLE_COMPLETE:
- rv = DoMarkSingleKeyedCacheEntryUnusableComplete(rv);
- break;
- case State::UNSET:
- NOTREACHED() << "bad state";
- rv = ERR_FAILED;
- break;
- case State::NONE:
- // Do Nothing.
- break;
- }
- } while (next_state_ != State::NONE && rv != ERR_IO_PENDING);
- if (next_state_ != State::NONE) {
- if (rv != ERR_IO_PENDING && !callback_.is_null()) {
- std::move(callback_).Run(rv);
- }
- return rv;
- }
- // Save the callback as |this| may be destroyed when |cache_callback_| is run.
- // Note that |callback_| is intentionally reset even if it is not run.
- CompletionOnceCallback callback = std::move(callback_);
- read_buf_ = nullptr;
- DCHECK(!all_writers_.empty() || cache_callback_);
- if (cache_callback_)
- std::move(cache_callback_).Run();
- // |this| may have been destroyed in the |cache_callback_|.
- if (rv != ERR_IO_PENDING && !callback.is_null())
- std::move(callback).Run(rv);
- return rv;
- }
- int HttpCache::Writers::DoNetworkRead() {
- DCHECK(network_transaction_);
- next_state_ = State::NETWORK_READ_COMPLETE;
- // TODO(https://crbug.com/778641): This is a partial mitigation and an attempt
- // to gather more info)
- if (!network_transaction_) {
- static bool reported = false;
- if (!reported) {
- reported = true;
- base::debug::ScopedCrashKeyString key_info(
- GetCacheKeyCrashKey(), active_transaction_
- ? active_transaction_->key()
- : "(no transaction)");
- base::debug::ScopedCrashKeyString flags_info(
- GetTransactionFlagsCrashKey(),
- active_transaction_
- ? base::StringPrintf(
- "mth=%s/m=%d/p=%d/t=%d/ex=%d/tc=%d/par=%d/pri=%d/nw=%zu",
- active_transaction_->method().c_str(),
- static_cast<int>(active_transaction_->mode()),
- static_cast<int>(active_transaction_->partial() != nullptr),
- static_cast<int>(active_transaction_->is_truncated()),
- static_cast<int>(IsExclusive()), GetTransactionsCount(),
- static_cast<int>(parallel_writing_pattern_),
- static_cast<int>(priority_), all_writers_.size())
- : "(no transaction)");
- base::debug::DumpWithoutCrashing();
- }
- return ERR_FAILED;
- }
- CompletionOnceCallback io_callback = base::BindOnce(
- &HttpCache::Writers::OnIOComplete, weak_factory_.GetWeakPtr());
- return network_transaction_->Read(read_buf_.get(), io_buf_len_,
- std::move(io_callback));
- }
- int HttpCache::Writers::DoNetworkReadComplete(int result) {
- if (result < 0) {
- next_state_ = State::NONE;
- OnNetworkReadFailure(result);
- return result;
- }
- next_state_ = State::CACHE_WRITE_DATA;
- return result;
- }
- void HttpCache::Writers::OnNetworkReadFailure(int result) {
- ProcessFailure(result);
- if (active_transaction_)
- EraseTransaction(active_transaction_, result);
- active_transaction_ = nullptr;
- if (ShouldTruncate())
- TruncateEntry();
- SetCacheCallback(false, TransactionSet());
- }
- int HttpCache::Writers::DoCacheWriteData(int num_bytes) {
- next_state_ = State::CACHE_WRITE_DATA_COMPLETE;
- write_len_ = num_bytes;
- if (!num_bytes || network_read_only_)
- return num_bytes;
- int current_size = entry_->disk_entry->GetDataSize(kResponseContentIndex);
- CompletionOnceCallback io_callback = base::BindOnce(
- &HttpCache::Writers::OnIOComplete, weak_factory_.GetWeakPtr());
- int rv = 0;
- PartialData* partial = nullptr;
- // The active transaction must be alive if this is a partial request, as
- // partial requests are exclusive and hence will always be the active
- // transaction.
- // TODO(shivanisha): When partial requests support parallel writing, this
- // assumption will not be true.
- if (active_transaction_)
- partial = all_writers_.find(active_transaction_)->second.partial;
- if (!partial) {
- rv = entry_->disk_entry->WriteData(kResponseContentIndex, current_size,
- read_buf_.get(), num_bytes,
- std::move(io_callback), true);
- } else {
- rv = partial->CacheWrite(entry_->GetEntry(), read_buf_.get(), num_bytes,
- std::move(io_callback));
- }
- return rv;
- }
- int HttpCache::Writers::DoCacheWriteDataComplete(int result) {
- DCHECK(!all_writers_.empty());
- next_state_ = State::NONE;
- if (checksum_) {
- if (write_len_ > 0) {
- checksum_->Update(read_buf_->data(), write_len_);
- } else {
- // The write to the cache may have failed if result < 0, but even in that
- // case we want to check whether the data we've read from the network is
- // valid or not.
- CHECK(active_transaction_);
- if (!active_transaction_->ResponseChecksumMatches(std::move(checksum_))) {
- next_state_ = State::MARK_SINGLE_KEYED_CACHE_ENTRY_UNUSABLE;
- }
- }
- }
- if (result != write_len_) {
- // Note that it is possible for cache write to fail if the size of the file
- // exceeds the per-file limit.
- OnCacheWriteFailure();
- // |active_transaction_| can continue reading from the network.
- result = write_len_;
- } else {
- OnDataReceived(result);
- }
- return result;
- }
- int HttpCache::Writers::DoMarkSingleKeyedCacheEntryUnusable() {
- // `response_info_truncation_` is not actually truncated.
- // TODO(ricea): Maybe change the name of the member?
- response_info_truncation_.single_keyed_cache_entry_unusable = true;
- next_state_ = State::MARK_SINGLE_KEYED_CACHE_ENTRY_UNUSABLE_COMPLETE;
- // Update cache metadata. This is a subset of what
- // HttpCache::Transaction::WriteResponseInfoToEntry does.
- auto data = base::MakeRefCounted<PickledIOBuffer>();
- response_info_truncation_.Persist(data->pickle(),
- /*skip_transient_headers=*/true,
- /*response_truncated=*/false);
- data->Done();
- io_buf_len_ = data->pickle()->size();
- CompletionOnceCallback io_callback = base::BindOnce(
- &HttpCache::Writers::OnIOComplete, weak_factory_.GetWeakPtr());
- return entry_->disk_entry->WriteData(kResponseInfoIndex, 0, data.get(),
- io_buf_len_, std::move(io_callback),
- true);
- }
- int HttpCache::Writers::DoMarkSingleKeyedCacheEntryUnusableComplete(
- int result) {
- next_state_ = State::NONE;
- if (result < 0) {
- OnCacheWriteFailure();
- }
- // DoLoop() wants the size of the data write, not the size of the metadata
- // write.
- return write_len_;
- }
- void HttpCache::Writers::OnDataReceived(int result) {
- DCHECK(!all_writers_.empty());
- auto it = all_writers_.find(active_transaction_);
- bool is_partial =
- active_transaction_ != nullptr && it->second.partial != nullptr;
- // Partial transaction will process the result, return from here.
- // This is done because partial requests handling require an awareness of both
- // headers and body state machines as they might have to go to the headers
- // phase for the next range, so it cannot be completely handled here.
- if (is_partial) {
- active_transaction_ = nullptr;
- return;
- }
- if (result == 0) {
- // Check if the response is actually completed or if not, attempt to mark
- // the entry as truncated in OnNetworkReadFailure.
- int current_size = entry_->disk_entry->GetDataSize(kResponseContentIndex);
- DCHECK(network_transaction_);
- const HttpResponseInfo* response_info =
- network_transaction_->GetResponseInfo();
- int64_t content_length = response_info->headers->GetContentLength();
- if (content_length >= 0 && content_length > current_size) {
- OnNetworkReadFailure(result);
- return;
- }
- if (active_transaction_)
- EraseTransaction(active_transaction_, result);
- active_transaction_ = nullptr;
- CompleteWaitingForReadTransactions(write_len_);
- // Invoke entry processing.
- DCHECK(ContainsOnlyIdleWriters());
- TransactionSet make_readers;
- for (auto& writer : all_writers_)
- make_readers.insert(writer.first);
- all_writers_.clear();
- SetCacheCallback(true, make_readers);
- return;
- }
- // Notify waiting_for_read_. Tasks will be posted for all the
- // transactions.
- CompleteWaitingForReadTransactions(write_len_);
- active_transaction_ = nullptr;
- }
- void HttpCache::Writers::OnCacheWriteFailure() {
- DLOG(ERROR) << "failed to write response data to cache";
- ProcessFailure(ERR_CACHE_WRITE_FAILURE);
- // Now writers will only be reading from the network.
- network_read_only_ = true;
- active_transaction_ = nullptr;
- should_keep_entry_ = false;
- if (all_writers_.empty()) {
- SetCacheCallback(false, TransactionSet());
- } else {
- cache_->WritersDoomEntryRestartTransactions(entry_);
- }
- }
- void HttpCache::Writers::CompleteWaitingForReadTransactions(int result) {
- for (auto it = waiting_for_read_.begin(); it != waiting_for_read_.end();) {
- Transaction* transaction = it->first;
- int callback_result = result;
- if (result >= 0) { // success
- // Save the data in the waiting transaction's read buffer.
- it->second.write_len = std::min(it->second.read_buf_len, result);
- memcpy(it->second.read_buf->data(), read_buf_->data(),
- it->second.write_len);
- callback_result = it->second.write_len;
- }
- // Post task to notify transaction.
- base::ThreadTaskRunnerHandle::Get()->PostTask(
- FROM_HERE,
- base::BindOnce(std::move(it->second.callback), callback_result));
- it = waiting_for_read_.erase(it);
- // If its response completion or failure, this transaction needs to be
- // removed from writers.
- if (result <= 0)
- EraseTransaction(transaction, result);
- }
- }
- void HttpCache::Writers::RemoveIdleWriters(int result) {
- // Since this is only for idle transactions, waiting_for_read_
- // should be empty.
- DCHECK(waiting_for_read_.empty());
- for (auto it = all_writers_.begin(); it != all_writers_.end();) {
- Transaction* transaction = it->first;
- if (transaction == active_transaction_) {
- it++;
- continue;
- }
- it = EraseTransaction(it, result);
- }
- }
- void HttpCache::Writers::SetCacheCallback(bool success,
- const TransactionSet& make_readers) {
- DCHECK(!cache_callback_);
- cache_callback_ = base::BindOnce(&HttpCache::WritersDoneWritingToEntry,
- cache_->GetWeakPtr(), entry_, success,
- should_keep_entry_, make_readers);
- }
- void HttpCache::Writers::OnIOComplete(int result) {
- DoLoop(result);
- }
- } // namespace net
|