// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/base/network_config_watcher_mac.h"

#include <algorithm>

#include "base/compiler_specific.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/message_loop/message_pump_type.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/thread.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"

namespace net {

namespace {

// SCDynamicStore API does not exist on iOS.
#if !BUILDFLAG(IS_IOS)
const base::TimeDelta kRetryInterval = base::Seconds(1);
const int kMaxRetry = 5;

// Called back by OS.  Calls OnNetworkConfigChange().
void DynamicStoreCallback(SCDynamicStoreRef /* store */,
                          CFArrayRef changed_keys,
                          void* config_delegate) {
  NetworkConfigWatcherMac::Delegate* net_config_delegate =
      static_cast<NetworkConfigWatcherMac::Delegate*>(config_delegate);
  net_config_delegate->OnNetworkConfigChange(changed_keys);
}
#endif  // !BUILDFLAG(IS_IOS)

}  // namespace

class NetworkConfigWatcherMacThread : public base::Thread {
 public:
  explicit NetworkConfigWatcherMacThread(
      NetworkConfigWatcherMac::Delegate* delegate);
  NetworkConfigWatcherMacThread(const NetworkConfigWatcherMacThread&) = delete;
  NetworkConfigWatcherMacThread& operator=(
      const NetworkConfigWatcherMacThread&) = delete;
  ~NetworkConfigWatcherMacThread() override;

 protected:
  // base::Thread
  void Init() override;
  void CleanUp() override;

 private:
  // The SystemConfiguration calls in this function can lead to contention early
  // on, so we invoke this function later on in startup to keep it fast.
  void InitNotifications();

  // Returns whether initializing notifications has succeeded.
  bool InitNotificationsHelper();

  base::ScopedCFTypeRef<CFRunLoopSourceRef> run_loop_source_;
  const raw_ptr<NetworkConfigWatcherMac::Delegate> delegate_;
#if !BUILDFLAG(IS_IOS)
  int num_retry_ = 0;
#endif  // !BUILDFLAG(IS_IOS)
  base::WeakPtrFactory<NetworkConfigWatcherMacThread> weak_factory_;
};

NetworkConfigWatcherMacThread::NetworkConfigWatcherMacThread(
    NetworkConfigWatcherMac::Delegate* delegate)
    : base::Thread("NetworkConfigWatcher"),
      delegate_(delegate),
      weak_factory_(this) {}

NetworkConfigWatcherMacThread::~NetworkConfigWatcherMacThread() {
  // This is expected to be invoked during shutdown.
  base::ScopedAllowBaseSyncPrimitivesOutsideBlockingScope allow_thread_join;
  Stop();
}

void NetworkConfigWatcherMacThread::Init() {
  delegate_->Init();

  // TODO(willchan): Look to see if there's a better signal for when it's ok to
  // initialize this, rather than just delaying it by a fixed time.
  const base::TimeDelta kInitializationDelay = base::Seconds(1);
  task_runner()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&NetworkConfigWatcherMacThread::InitNotifications,
                     weak_factory_.GetWeakPtr()),
      kInitializationDelay);
}

void NetworkConfigWatcherMacThread::CleanUp() {
  if (!run_loop_source_.get())
    return;

  CFRunLoopRemoveSource(CFRunLoopGetCurrent(), run_loop_source_.get(),
                        kCFRunLoopCommonModes);
  run_loop_source_.reset();
}

void NetworkConfigWatcherMacThread::InitNotifications() {
  // If initialization fails, retry after a 1s delay.
  bool success = InitNotificationsHelper();

#if !BUILDFLAG(IS_IOS)
  if (!success && num_retry_ < kMaxRetry) {
    LOG(ERROR) << "Retrying SystemConfiguration registration in 1 second.";
    task_runner()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(&NetworkConfigWatcherMacThread::InitNotifications,
                       weak_factory_.GetWeakPtr()),
        kRetryInterval);
    num_retry_++;
    return;
  }

#else
  DCHECK(success);
#endif  // !BUILDFLAG(IS_IOS)
}

bool NetworkConfigWatcherMacThread::InitNotificationsHelper() {
#if !BUILDFLAG(IS_IOS)
  // SCDynamicStore API does not exist on iOS.
  // Add a run loop source for a dynamic store to the current run loop.
  SCDynamicStoreContext context = {
      0,          // Version 0.
      delegate_,  // User data.
      nullptr,    // This is not reference counted.  No retain function.
      nullptr,    // This is not reference counted.  No release function.
      nullptr,    // No description for this.
  };
  base::ScopedCFTypeRef<SCDynamicStoreRef> store(SCDynamicStoreCreate(
      nullptr, CFSTR("org.chromium"), DynamicStoreCallback, &context));
  if (!store) {
    int error = SCError();
    LOG(ERROR) << "SCDynamicStoreCreate failed with Error: " << error << " - "
               << SCErrorString(error);
    return false;
  }
  run_loop_source_.reset(
      SCDynamicStoreCreateRunLoopSource(nullptr, store.get(), 0));
  if (!run_loop_source_) {
    int error = SCError();
    LOG(ERROR) << "SCDynamicStoreCreateRunLoopSource failed with Error: "
               << error << " - " << SCErrorString(error);
    return false;
  }
  CFRunLoopAddSource(CFRunLoopGetCurrent(), run_loop_source_.get(),
                     kCFRunLoopCommonModes);
#endif  // !BUILDFLAG(IS_IOS)

  // Set up notifications for interface and IP address changes.
  delegate_->StartReachabilityNotifications();
#if !BUILDFLAG(IS_IOS)
  delegate_->SetDynamicStoreNotificationKeys(store.get());
#endif  // !BUILDFLAG(IS_IOS)
  return true;
}

NetworkConfigWatcherMac::NetworkConfigWatcherMac(Delegate* delegate)
    : notifier_thread_(
          std::make_unique<NetworkConfigWatcherMacThread>(delegate)) {
  // We create this notifier thread because the notification implementation
  // needs a thread with a CFRunLoop, and there's no guarantee that
  // CurrentThread::Get() meets that criterion.
  base::Thread::Options thread_options(base::MessagePumpType::UI, 0);
  notifier_thread_->StartWithOptions(std::move(thread_options));
}

NetworkConfigWatcherMac::~NetworkConfigWatcherMac() = default;

}  // namespace net
