/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-â
âvi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8                                :viâ
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¡
â Copyright 2022 Justine Alexandra Roberts Tunney                              â
â                                                                              â
â Permission to use, copy, modify, and/or distribute this software for         â
â any purpose with or without fee is hereby granted, provided that the         â
â above copyright notice and this permission notice appear in all copies.      â
â                                                                              â
â THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL                â
â WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED                â
â WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE             â
â AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL         â
â DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR        â
â PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER               â
â TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR             â
â PERFORMANCE OF THIS SOFTWARE.                                                â
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ*/
#include "libc/assert.h"
#include "libc/calls/blockcancel.internal.h"
#include "libc/calls/calls.h"
#include "libc/calls/struct/stat.h"
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/intrin/atomic.h"
#include "libc/intrin/nopl.internal.h"
#include "libc/intrin/strace.internal.h"
#include "libc/limits.h"
#include "libc/mem/mem.h"
#include "libc/runtime/runtime.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/at.h"
#include "libc/sysv/consts/map.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/prot.h"
#include "libc/sysv/errfuns.h"
#include "libc/thread/semaphore.h"
#include "libc/thread/thread.h"
#include "libc/thread/tls.h"

static struct Semaphores {
  pthread_once_t once;
  pthread_mutex_t lock;
  struct Semaphore {
    struct Semaphore *next;
    sem_t *sem;
    char *path;
    bool dead;
    int refs;
  } * list;
} g_semaphores;

static void sem_open_lock(void) {
  pthread_mutex_lock(&g_semaphores.lock);
}

static void sem_open_unlock(void) {
  pthread_mutex_unlock(&g_semaphores.lock);
}

static void sem_open_funlock(void) {
  pthread_mutex_init(&g_semaphores.lock, 0);
}

static void sem_open_setup(void) {
  sem_open_funlock();
  pthread_atfork(sem_open_lock, sem_open_unlock, sem_open_funlock);
}

static void sem_open_init(void) {
  pthread_once(&g_semaphores.once, sem_open_setup);
}

#ifdef _NOPL0
#define sem_open_lock()   _NOPL0("__threadcalls", sem_open_lock)
#define sem_open_unlock() _NOPL0("__threadcalls", sem_open_unlock)
#endif

static sem_t *sem_open_impl(const char *path, int oflag, unsigned mode,
                            unsigned value) {
  int fd;
  sem_t *sem;
  struct stat st;
  oflag |= O_RDWR | O_CLOEXEC;
  if ((fd = openat(AT_FDCWD, path, oflag, mode)) == -1) {
    return SEM_FAILED;
  }
  _npassert(!fstat(fd, &st));
  if (st.st_size < PAGESIZE && ftruncate(fd, PAGESIZE) == -1) {
    _npassert(!close(fd));
    return SEM_FAILED;
  }
  sem = mmap(0, PAGESIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  if (sem != MAP_FAILED) {
    atomic_store_explicit(&sem->sem_value, value, memory_order_relaxed);
    sem->sem_magic = SEM_MAGIC_NAMED;
    sem->sem_dev = st.st_dev;
    sem->sem_ino = st.st_ino;
    sem->sem_pshared = true;
  } else {
    sem = SEM_FAILED;
  }
  _npassert(!close(fd));
  return sem;
}

static struct Semaphore *sem_open_find(const char *path) {
  struct Semaphore *s;
  for (s = g_semaphores.list; s; s = s->next) {
    if (!strcmp(path, s->path)) {
      return s;
    }
  }
  return 0;
}

static struct Semaphore *sem_open_reopen(const char *path) {
  int e = errno;
  struct stat st;
  struct Semaphore *s;
  for (s = g_semaphores.list; s; s = s->next) {
    if (!s->dead &&  //
        !strcmp(path, s->path)) {
      if (!fstatat(AT_FDCWD, path, &st, 0) &&  //
          st.st_dev == s->sem->sem_dev &&      //
          st.st_ino == s->sem->sem_ino) {
        return s;
      } else {
        errno = e;
        s->dead = true;
      }
    }
  }
  return 0;
}

static struct Semaphore *sem_open_get(const sem_t *sem,
                                      struct Semaphore ***out_prev) {
  struct Semaphore *s, *t, **p;
  for (p = &g_semaphores.list, s = *p; s; p = &s->next, s = s->next) {
    if (s && sem == s->sem) {
      *out_prev = p;
      return s;
    }
  }
  return 0;
}

/**
 * Initializes and opens named semaphore.
 *
 * This function tracks open semaphore objects within a process. When a
 * process calls sem_open() multiple times with the same name, then the
 * same shared memory address will be returned, unless it was unlinked.
 *
 * @param name is arbitrary string that begins with a slash character
 * @param oflag can have any of:
 *     - `O_CREAT` to create the named semaphore if it doesn't exist,
 *       in which case two additional arguments must be supplied
 *     - `O_EXCL` to raise `EEXIST` if semaphore already exists
 * @param mode is octal mode bits, required if `oflag & O_CREAT`
 * @param value is initial of semaphore, required if `oflag & O_CREAT`
 * @return semaphore object which needs sem_close(), or SEM_FAILED w/ errno
 * @raise ENOSPC if file system is full when `name` would be `O_CREAT`ed
 * @raise EINVAL if `oflag` has bits other than `O_CREAT | O_EXCL`
 * @raise EINVAL if `value` is negative or exceeds `SEM_VALUE_MAX`
 * @raise EEXIST if `O_CREAT|O_EXCL` is used and semaphore exists
 * @raise EACCES if we didn't have permission to create semaphore
 * @raise EACCES if recreating open semaphore pending an unlink
 * @raise EMFILE if process `RLIMIT_NOFILE` has been reached
 * @raise ENFILE if system-wide file limit has been reached
 * @raise ENOMEM if we require more vespene gas
 * @raise EINTR if signal handler was called
 * @threadsafe
 */
sem_t *sem_open(const char *name, int oflag, ...) {
  sem_t *sem;
  va_list va;
  struct Semaphore *s;
  unsigned mode = 0, value = 0;
  char *path, pathbuf[PATH_MAX];
  if (oflag & ~(O_CREAT | O_EXCL)) {
    einval();
    return SEM_FAILED;
  }
  if (oflag & O_CREAT) {
    va_start(va, oflag);
    mode = va_arg(va, unsigned);
    value = va_arg(va, unsigned);
    va_end(va);
    if (value > SEM_VALUE_MAX) {
      einval();
      return SEM_FAILED;
    }
  }
  if (!(path = sem_path_np(name, pathbuf, sizeof(pathbuf)))) {
    return SEM_FAILED;
  }
  BLOCK_CANCELLATIONS;
  sem_open_init();
  sem_open_lock();
  if ((s = sem_open_reopen(path))) {
    if (s->sem->sem_lazydelete) {
      if (oflag & O_CREAT) {
        eacces();
      } else {
        enoent();
      }
      sem = SEM_FAILED;
    } else if (~oflag & O_EXCL) {
      sem = s->sem;
      atomic_fetch_add_explicit(&sem->sem_prefs, 1, memory_order_acq_rel);
      ++s->refs;
    } else {
      eexist();
      sem = SEM_FAILED;
    }
  } else if ((s = calloc(1, sizeof(struct Semaphore)))) {
    if ((s->path = strdup(path))) {
      if ((sem = sem_open_impl(path, oflag, mode, value)) != SEM_FAILED) {
        atomic_fetch_add_explicit(&sem->sem_prefs, 1, memory_order_acq_rel);
        s->next = g_semaphores.list;
        s->sem = sem;
        s->refs = 1;
        g_semaphores.list = s;
      } else {
        free(s->path);
        free(s);
      }
    } else {
      free(s);
      sem = SEM_FAILED;
    }
  } else {
    sem = SEM_FAILED;
  }
  sem_open_unlock();
  ALLOW_CANCELLATIONS;
  return sem;
}

/**
 * Closes named semaphore.
 *
 * Calling sem_close() on a semaphore not created by sem_open() has
 * undefined behavior. Using `sem` after calling sem_close() from either
 * the current process or forked processes sharing the same address is
 * also undefined behavior. If any threads in this process or forked
 * children are currently blocked on `sem` then calling sem_close() has
 * undefined behavior.
 *
 * @param sem was created with sem_open()
 * @return 0 on success, or -1 w/ errno
 */
int sem_close(sem_t *sem) {
  int rc, prefs;
  bool unmap, delete;
  struct Semaphore *s, **p;
  _npassert(sem->sem_magic == SEM_MAGIC_NAMED);
  sem_open_init();
  sem_open_lock();
  _npassert((s = sem_open_get(sem, &p)));
  prefs = atomic_fetch_add_explicit(&sem->sem_prefs, -1, memory_order_acq_rel);
  _npassert(s->refs > 0);
  if ((unmap = !--s->refs)) {
    _npassert(prefs > 0);
    delete = sem->sem_lazydelete && prefs == 1;
    *p = s->next;
  } else {
    _npassert(prefs > 1);
    delete = false;
  }
  sem_open_unlock();
  if (unmap) {
    _npassert(!munmap(sem, PAGESIZE));
  }
  if (delete) {
    rc = unlink(s->path);
  }
  if (unmap) {
    free(s->path);
    free(s);
  }
  return 0;
}

/**
 * Removes named semaphore.
 *
 * This causes the file resource to be deleted. If processes have this
 * semaphore currently opened, then on platforms like Windows deletion
 * may be postponed until the last process calls sem_close().
 *
 * @param name can be absolute path or should be component w/o slashes
 * @return 0 on success, or -1 w/ errno
 * @raise ACCESS if Windows is being fussy about deleting open files
 * @raise EPERM if pledge() is in play w/o `cpath` promise
 * @raise ENOENT if named semaphore doesn't exist
 * @raise EACCES if permission is denied
 * @raise ENAMETOOLONG if too long
 */
int sem_unlink(const char *name) {
  int rc, e = errno;
  struct Semaphore *s;
  char *path, pathbuf[PATH_MAX];
  if (!(path = sem_path_np(name, pathbuf, sizeof(pathbuf)))) return -1;
  if ((rc = unlink(path)) == -1 && IsWindows() && errno == EACCES) {
    sem_open_init();
    sem_open_lock();
    if ((s = sem_open_find(path))) {
      s->sem->sem_lazydelete = true;
      errno = e;
      rc = 0;
    }
    sem_open_unlock();
  }
  return rc;
}
