/****************************************************************************

NAME
   shmexport.c - shared-memory export from the daemon

DESCRIPTION
   This is a very lightweight alternative to JSON-over-sockets.  Clients
won't be able to filter by device, and won't get device activation/deactivation
notifications.  But both client and daemon will avoid all the marshalling and
unmarshalling overhead.

PERMISSIONS
   This file is Copyright by the GPSD project
   SPDX-License-Identifier: BSD-2-clause

***************************************************************************/

#include "../include/gpsd_config.h"

#ifdef SHM_EXPORT_ENABLE

#include <errno.h>
#include <stddef.h>
#include <stdlib.h>              // for atexit()
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/time.h>

#include "../include/gpsd.h"
#include "../include/libgps.h"    // for SHM_PSEUDO_FD

static int shmid_for_atexit = 0;

// cleanup SHM on exit
static void shm_cleanup(void)
{
    if (0 == shmid_for_atexit) {
        // nothing to do
        return;
    }
    /* mark SHM to be destroyed on exit
     * Since we dropped root, this fails. */
    if (-1 == shmctl(shmid_for_atexit, IPC_RMID, NULL)) {
        /* let it fail silently
         * fprintf(stderr, "SHM: shmctl(%d) for IPC_RMID failed, %s(%d)\n",
         *        shmid_for_atexit, strerror(errno), errno);
         */
    }
}

/* initialize the shared-memory segment to be used for export
 *
 * Return: true = OK
 *         false: failed
 */
bool shm_acquire(struct gps_context_t *context)
{
    const char *shmkey_s = getenv("GPSD_SHM_KEY");;
    long shmkey = GPSD_SHM_KEY;
    if (NULL != shmkey_s) {
        long tmp = strtol(shmkey_s, NULL, 0);
        if (0 < tmp) {
            shmkey = tmp;
        }
    }

    int shmid = shmget((key_t)shmkey, sizeof(struct shmexport_t),
                       (int)(IPC_CREAT|0666));
    context->shmid = shmid;
    if (-1 == shmid) {
        GPSD_LOG(LOG_ERROR, &context->errout,
                 "SHM: shmget(0x%lx, %zd, 0666) SHM export failed: %s(%d)\n",
                 shmkey,
                 sizeof(struct shmexport_t),
                 strerror(errno), errno);
        return false;
    }

    GPSD_LOG(LOG_PROG, &context->errout,
             "SHM: shmget(0x%lx, %zd, 0666) for SHM export succeeded\n",
             shmkey,
             sizeof(struct shmexport_t));

    context->shmexport = (void *)shmat(shmid, 0, 0);
    if ((void *)-1 == context->shmexport) {
        GPSD_LOG(LOG_ERROR, &context->errout,
                 "SHM: shmat failed: %s(%d)\n", strerror(errno), errno);
        context->shmexport = NULL;
        // close shmid
        shm_release(context);
        return false;
    }

    /* Tried to do IPC_RMID, but in spite of what the man page says, it
     * destroyed the segment right away.  Also, pending IPC_RMID is Linux
     * only. So try an atexit(). */
    shmid_for_atexit = shmid;
    if (0 != atexit(shm_cleanup)) {
        GPSD_LOG(LOG_ERROR, &context->errout,
                 "SHM: atexit() failed: %s(%d)\n", strerror(errno), errno);
        exit(EXIT_FAILURE);
    }

    GPSD_LOG(LOG_PROG, &context->errout,
             "SHM: shmat() for SHM export succeeded, segment %d\n", shmid);
    return true;
}

// release the shared-memory segment used for export
void shm_release(struct gps_context_t *context)
{

    /* Mark shmid to go away when no longer used
     * Having it linger forever is bad, and when the size enlarges
     * it can no longer be opened
     */

    /* debug
    * GPSD_LOG(LOG_SHOUT, &context->errout,
    *          "SHM: shm_release() shmid %d shmexport %p\n",
    *          context->shmid, context->shmexport);
    */

    /* detach from segment, it we were last user, it should be deleted.
     * at least on those OS that IPC_RIMID works, which is none. */
    if (NULL != context->shmexport) {
        if (-1 == shmdt(context->shmexport)) {
            GPSD_LOG(LOG_WARN, &context->errout,
                     "SHM: shmdt() for shmid %d failed: %s(%d)\n",
                     context->shmid, strerror(errno), errno);
        }
    }

    context->shmid = -1;

}

// export an update to all listeners
void shm_update(struct gps_context_t *context, struct gps_data_t *gpsdata)
{
    if (NULL != context->shmexport) {
        static int tick;
        volatile struct shmexport_t *shared = \
                            (volatile struct shmexport_t *)context->shmexport;

        ++tick;
        /*
         * Following block of instructions must not be reordered, otherwise
         * havoc will ensue.
         *
         * This is a simple optimistic-concurrency technique.  We write
         * the second bookend first, then the data, then the first bookend.
         * Reader copies what it sees in normal order; that way, if we
         * start to write the segment during the read, the second bookend will
         * get clobbered first and the data can be detected as bad.
         *
         * Of course many architectures, like Intel, make no guarantees
         * about the actual memory read or write order into RAM, so this
         * is partly wishful thinking.  Thus the need for the memory_barriers()
         * to enforce the required order.
         */
        shared->bookend2 = tick;
        memory_barrier();
        shared->gpsdata = *gpsdata;
        memory_barrier();
        shared->gpsdata.gps_fd = (gps_fd_t)SHM_PSEUDO_FD;
        memory_barrier();
        shared->bookend1 = tick;
    }
}


#endif  // SHM_EXPORT_ENABLE

// vim: set expandtab shiftwidth=4
