/*
 * Copyright 2016-present Facebook, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package com.facebook.buck.cli;

import com.facebook.buck.log.Logger;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;

/**
 * Daemon killer that kills the daemon when domain socket used to communicate with it has been
 * deleted.
 */
class SocketLossKiller {
  private static final Logger LOG = Logger.get(SocketLossKiller.class);

  private final ScheduledExecutorService scheduledExecutorService;
  private final Path absolutePathToSocket;
  private final Runnable killTask;

  private AtomicBoolean isArmed = new AtomicBoolean();

  @GuardedBy("this")
  private @Nullable ScheduledFuture<?> checkSocketTask;

  public SocketLossKiller(
      ScheduledExecutorService scheduledExecutorService,
      Path absolutePathToSocket,
      Runnable killTask) {
    this.scheduledExecutorService = scheduledExecutorService;
    this.absolutePathToSocket = absolutePathToSocket;
    this.killTask = killTask;
  }

  /**
   * Start the service, recording the current socket file info.
   *
   * If the socket does not exist or can't be accessed, the kill task will be called.
   *
   * This is meant to be called after the server is started and socket created, such as in
   * {@code NailMain}. Repeated calls are no-ops.
   */
  public synchronized void arm() {
    if (isArmed.getAndSet(true)) {
      // Already armed.
      return;
    }
    try {
      BasicFileAttributes socketFileAttributes = readFileAttributes(absolutePathToSocket);
      checkSocketTask = scheduledExecutorService.scheduleAtFixedRate(
          () -> checkSocket(socketFileAttributes),
          0,
          5000,
          TimeUnit.MILLISECONDS);
    } catch (IOException e) {
      LOG.error(e, "Failed to read socket when arming SocketLossKiller.");
      cancelAndKill();
      return;
    }
  }

  private void checkSocket(BasicFileAttributes originalAttributes) {
    BasicFileAttributes newAttributes;
    try {
      newAttributes = readFileAttributes(absolutePathToSocket);
    } catch (NoSuchFileException | FileNotFoundException e) {
      // File no longer exists.
      cancelAndKill();
      return;
    } catch (IOException e) {
      // Some other error occurred.
      LOG.error(e, "Failed to check socket.");
      cancelAndKill();
      return;
    }
    synchronized (this) {
      // On platforms supporting file key, we make sure file keys are equal.
      // This may catch more cases than creation time if the socket is deleted and re-created
      // faster than can be distinguished by the timestamp resolution.
      //
      // Check modified time in addition to creation time because Windows lies about creation time
      // ("file system tunneling"). This feature is is still enabled in Windows 10.
      // See: https://support.microsoft.com/en-us/kb/172190
      if (newAttributes.fileKey() != null &&
          !newAttributes.fileKey().equals(originalAttributes.fileKey())) {
        cancelAndKill();
      } else if (
          !newAttributes.creationTime().equals(originalAttributes.creationTime()) ||
          !newAttributes.lastModifiedTime().equals(originalAttributes.lastModifiedTime())) {
        cancelAndKill();
      }
    }
  }

  private synchronized void cancelAndKill() {
    if (checkSocketTask != null) {
      checkSocketTask.cancel(false);
      checkSocketTask = null;
    }
    killTask.run();
  }

  private static BasicFileAttributes readFileAttributes(Path path) throws IOException {
    return Files.getFileAttributeView(path, BasicFileAttributeView.class).readAttributes();
  }
}
