C++程序  |  989行  |  24.3 KB

/*
 * Copyright 2007 The Android Open Source Project
 *
 * Simulator interactions.
 *
 * TODO: for multi-process we probably need to switch to a new process
 * group if we are the first process (could be runtime, could be gdb),
 * rather than wait for the simulator to tell us to switch.
 */
#include "Common.h"

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/un.h>
#include <signal.h>
#include <assert.h>

// fwd
static int connectToSim(void);
static void listenToSim(void);

/*
 * Env var to restrict who tries to talk to the front end.
 */
#define kWrapSimConnectedEnv    "WRAP_SIM_CONNECTED"


/*
 * Signal the main thread that we're ready to continue.
 */
static void signalMainThread(void)
{
    int cc;

    cc = pthread_mutex_lock(&gWrapSim.startLock);
    assert(cc == 0);

    gWrapSim.startReady = 1;

    cc = pthread_cond_signal(&gWrapSim.startCond);
    assert(cc == 0);

    cc = pthread_mutex_unlock(&gWrapSim.startLock);
    assert(cc == 0);
}


/*
 * Entry point for the sim management thread.
 *
 * Once we have established a connection to the simulator and are ready
 * for other threads to send messages, we signal the main thread.
 */
static void* simThreadEntry(void* arg)
{
    wsLog("--- sim manager thread started\n");

    /*
     * Establish a connection to the simulator front-end.  If we can't do
     * that, we have no access to input or output devices, and we might
     * as well give up.
     */
    if (connectToSim() != 0) {
        signalMainThread();
        return NULL;
    }

    /* success! */
    wsLog("--- sim manager thread ready\n");
    gWrapSim.simulatorInitFailed = 0;
    signalMainThread();

    listenToSim();

    wsLog("--- sim manager thread exiting\n");

    return NULL;
}

/*
 * If we think we're not yet connected to the sim, do so now.  We only
 * want to do this once per process *group*, so we control access with
 * an environment variable.
 */
int wsSimConnect(void)
{
    /*
     * If the environment variable hasn't been set, assume we're the first
     * to get here, and should attach to the simulator.  We set the env
     * var here whether or not we succeed in connecting to the sim.
     *
     * (For correctness we should wrap the getenv/setenv in a semaphore.)
     */
    if (getenv(kWrapSimConnectedEnv) == NULL) {
        pthread_attr_t threadAttr;
        pthread_t threadHandle;
        int cc;

        gWrapSim.simulatorInitFailed = 1;
        setenv(kWrapSimConnectedEnv, "1", 1);

        cc = pthread_mutex_lock(&gWrapSim.startLock);
        assert(cc == 0);

        pthread_attr_init(&threadAttr);
        pthread_attr_setdetachstate(&threadAttr, PTHREAD_CREATE_DETACHED);
        cc = pthread_create(&threadHandle, &threadAttr, simThreadEntry, NULL);
        if (cc != 0) {
            wsLog("Unable to create new thread: %s\n", strerror(errno));
            abort();
        }

        while (!gWrapSim.startReady) {
            cc = pthread_cond_wait(&gWrapSim.startCond, &gWrapSim.startLock);
            assert(cc == 0);
        }

        cc = pthread_mutex_unlock(&gWrapSim.startLock);
        assert(cc == 0);

        if (gWrapSim.simulatorInitFailed) {
            wsLog("Simulator initialization failed, bailing\n");

            /* this *should* be okay to do */
            fprintf(stderr, "Fatal error:"
                " unable to connect to sim front-end (not running?)\n");
            abort();
        }
    }

    wsLog("+++ continuing\n");
    return 0;
}


/*
 * ===========================================================================
 *      Message / MessageStream
 * ===========================================================================
 */

/*
 * This is a quick & dirty rewrite of the C++ Message and MessageStream
 * classes, ported to C, reduced in generality, with syscalls stubbed
 * where necessary.  I didn't fix the API to make it more sensible in C
 * (which lacks destructors), so some of this is a little fragile or
 * awkward.
 */

/* values for message type byte; must match android::Message constants */
typedef enum MessageType {
    kTypeUnknown = 0,
    kTypeRaw,           // chunk of raw data
    kTypeConfig,        // send a name=value pair to peer
    kTypeCommand,       // simple command w/arg
    kTypeCommandExt,    // slightly more complicated command
    kTypeLogBundle,     // multi-part log message
} MessageType;

/*
 * Reusable message object.
 */
typedef struct Message {
    MessageType     mType;
    unsigned char*  mData;
    int             mLength;
} Message;

/* magic init messages; must match android::MessageStream constants */
enum {
    kHelloMsg       = 0x4e303047,       // 'N00G'
    kHelloAckMsg    = 0x31455221,       // '1ER!'
};


/*
 * Clear out a Message.
 */
static void Message_clear(Message* msg)
{
    memset(msg, 0, sizeof(Message));
}

/*
 * Keep reading until we get all bytes or hit EOF/error.  "fd" is expected
 * to be in blocking mode.
 *
 * Returns 0 on success.
 */
static int readAll(int fd, void* buf, size_t count)
{
    ssize_t actual;
    ssize_t have;

    have = 0;
    while (have != (ssize_t) count) {
        actual = _ws_read(fd, ((char*) buf) + have, count - have);
        if (actual < 0) {
            if (errno == EINTR)
                continue;
            wsLog("read %d failed: %s\n", fd, strerror(errno));
        } else if (actual == 0) {
            wsLog("early EOF on %d\n", fd);
            return -1;
        } else {
            have += actual;
        }

        assert(have <= (ssize_t)count);
    }

    return 0;
}

#if 0
/*
 * Keep writing until we put all bytes or hit an error.  "fd" is expected
 * to be in blocking mode.
 *
 * Returns 0 on success.
 */
static int writeAll(int fd, const void* buf, size_t count)
{
    ssize_t actual;
    ssize_t have;

    have = 0;
    while (have != count) {
        actual = _ws_write(fd, ((const char*) buf) + have, count - have);
        if (actual < 0) {
            if (errno == EINTR)
                continue;
            wsLog("write %d failed: %s\n", fd, strerror(errno));
        } else if (actual == 0) {
            wsLog("wrote zero on %d\n", fd);
            return -1;
        } else {
            have += actual;
        }

        assert(have <= count);
    }

    return 0;
}
#endif

/*
 * Read a message from the specified file descriptor.
 *
 * The caller must Message_release(&msg).
 *
 * We guarantee 32-bit alignment for msg->mData.
 */
static int Message_read(Message* msg, int fd)
{
    unsigned char header[4];

    readAll(fd, header, 4);

    msg->mType = (MessageType) header[2];
    msg->mLength = header[0] | header[1] << 8;
    msg->mLength -= 2;   // we already read two of them in the header

    if (msg->mLength > 0) {
        int actual;

        /* Linux malloc guarantees at least 32-bit alignment */
        msg->mData = (unsigned char*) malloc(msg->mLength);
        if (msg->mData == NULL) {
            wsLog("alloc %d failed\n", msg->mLength);
            return -1;
        }

        if (readAll(fd, msg->mData, msg->mLength) != 0) {
            wsLog("failed reading message body (wanted %d)\n", msg->mLength);
            return -1;
        }
    } else {
        msg->mData = NULL;
    }

    return 0;
}

/*
 * Write a message to the specified file descriptor.
 *
 * The caller must Message_release(&msg).
 */
static int Message_write(Message* msg, int fd)
{
    struct iovec writeVec[2];
    unsigned char header[4];
    int len, numVecs;
    ssize_t actual;

    len = msg->mLength + 2;
    header[0] = len & 0xff;
    header[1] = (len >> 8) & 0xff;
    header[2] = msg->mType;
    header[3] = 0;
    writeVec[0].iov_base = header;
    writeVec[0].iov_len = 4;
    numVecs = 1;

    if (msg->mLength > 0) {
        assert(msg->mData != NULL);
        writeVec[1].iov_base = msg->mData;
        writeVec[1].iov_len = msg->mLength;
        numVecs++;
    }

    /* write it all in one shot; not worrying about partial writes for now */
    actual = _ws_writev(fd, writeVec, numVecs);
    if (actual != len+2) {
        wsLog("failed writing message to fd %d: %d of %d %s\n",
            fd, actual, len+2, strerror(errno));
        return -1;
    }

    return 0;
}

/*
 * Release storage associated with a Message.
 */
static void Message_release(Message* msg)
{
    free(msg->mData);
    msg->mData = NULL;
}

/*
 * Extract a name/value pair from a message.
 */
static int getConfig(const Message* msg, const char** name, const char** val)
{
    if (msg->mLength < 2) {
        wsLog("message len (%d) is too short\n", msg->mLength);
        return -1;
    }
    const char* ptr = (const char*) msg->mData;

    *name = (const char*) ptr;
    *val = (const char*) (ptr + strlen((char*)ptr) +1);
    return 0;
}

/*
 * Extract a command from a message.
 */
static int getCommand(const Message* msg, int* pCmd, int* pArg)
{
    if (msg->mLength != sizeof(int) * 2) {
        wsLog("message len (%d) is wrong for cmd (%d)\n",
            msg->mLength, sizeof(int) * 2);
        return -1;
    }

    /* this assumes 32-bit alignment on mData */
    const int* ptr = (const int*) msg->mData;

    *pCmd = ptr[0];
    *pArg = ptr[1];

    return 0;
}

/*
 * Extract an extended command from a message.
 */
static int getCommandExt(const Message* msg, int* pCmd, int* pArg0,
    int* pArg1, int* pArg2)
{
    if (msg->mLength != sizeof(int) * 4) {
        wsLog("message len (%d) is wrong for cmd (%d)\n",
            msg->mLength, sizeof(int) * 4);
        return -1;
    }

    /* this assumes 32-bit alignment on mData */
    const int* ptr = (const int*) msg->mData;

    *pCmd = ptr[0];
    *pArg0 = ptr[1];
    *pArg1 = ptr[2];
    *pArg2 = ptr[3];

    return 0;
}

/*
 * Attach 8 bytes of data with "cmd" and "arg" to "msg".
 *
 * "msg->mData" will need to be freed by the caller.  (This approach made
 * more sense when C++ destructors were available, but it's just not worth
 * reworking it.)
 */
static int setCommand(Message* msg, int cmd, int arg)
{
    Message_clear(msg);

    msg->mLength = 8;
    msg->mData = malloc(msg->mLength);
    msg->mType = kTypeCommand;

    /* assumes 32-bit alignment on malloc blocks */
    int* pInt = (int*) msg->mData;
    pInt[0] = cmd;
    pInt[1] = arg;

    return 0;
}

/*
 * Construct the full path.  The caller must free() the return value.
 */
static char* makeFilename(const char* name)
{
    static const char* kBasePath = "/tmp/android-";
    char* fileName;

    assert(name != NULL && name[0] != '\0');

    fileName = (char*) malloc(strlen(kBasePath) + strlen(name) + 1);
    strcpy(fileName, kBasePath);
    strcat(fileName, name);

    return fileName;
}

/*
 * Attach to a SysV shared memory segment.
 */
static int attachToShmem(int key, int* pShmid, void** pAddr, long* pLength)
{
    int shmid;

    shmid = shmget(key, 0, 0);
    if (shmid == -1) {
        wsLog("ERROR: failed to find shmem key=%d\n", key);
        return -1;
    }

    void* addr = shmat(shmid, NULL, 0);
    if (addr == (void*) -1) {
        wsLog("ERROR: could not attach to key=%d shmid=%d\n", key, shmid);
        return -1;
    }

    struct shmid_ds shmids;
    int cc;

    cc = shmctl(shmid, IPC_STAT, &shmids);
    if (cc != 0) {
        wsLog("ERROR: could not IPC_STAT shmid=%d\n", shmid);
        return -1;
    }
    *pLength = shmids.shm_segsz;

    *pAddr = addr;
    *pShmid = shmid;
    return 0;
}

/*
 * Attach to a SysV semaphore.
 */
static int attachToSem(int key, int* pSemid)
{
    int semid;

    semid = semget(key, 0, 0);
    if (semid == -1) {
        wsLog("ERROR: failed to attach to semaphore key=%d\n", key);
        return -1;
    }

    *pSemid = semid;
    return 0;
}

/*
 * "Adjust" a semaphore.
 */
static int adjustSem(int semid, int adj)
{
    const int wait = 1;
    struct sembuf op;
    int cc;

    op.sem_num = 0;
    op.sem_op = adj;
    op.sem_flg = SEM_UNDO;
    if (!wait)
        op.sem_flg |= IPC_NOWAIT;

    cc = semop(semid, &op, 1);
    if (cc != 0) {
        if (wait || errno != EAGAIN) {
            wsLog("Warning:"
                " semaphore adjust by %d failed for semid=%d (errno=%d)\n",
                adj, semid, errno);
        }
        return -1;
    }

    return 0;
}

/*
 * Acquire the semaphore associated with a display.
 */
void wsLockDisplay(int displayIdx)
{
    assert(displayIdx >= 0 && displayIdx < gWrapSim.numDisplays);
    int semid = gWrapSim.display[displayIdx].semid;

    (void) adjustSem(semid, -1);
}

/*
 * Acquire the semaphore associated with a display.
 */
void wsUnlockDisplay(int displayIdx)
{
    assert(displayIdx >= 0 && displayIdx < gWrapSim.numDisplays);
    int semid = gWrapSim.display[displayIdx].semid;

    (void) adjustSem(semid, 1);
}

/*
 * Process the display config from the simulator
 *
 * Right now this is a blob of raw data that looks like this:
 *  +00 magic number
 *  +04 #of displays
 *  +08 display 0:
 *      +00 width
 *      +04 height
 *      +08 format
 *      +0c refresh rate
 *      +10 shmem key
 *  +1c display 1...
 */
static int handleDisplayConfig(const int* pData, int length)
{
    int numDisplays;

    if (length < 8) {
        wsLog("Bad display config: length is %d\n", length);
        return -1;
    }
    assert(*pData == kDisplayConfigMagic);

    /*
     * Pull out the #of displays.  If it looks valid, configure the runtime.
     */
    pData++;        // skip over magic
    numDisplays = *pData++;

    if (numDisplays <= 0 || numDisplays > kMaxDisplays) {
        wsLog("Bizarre display count %d\n", numDisplays);
        return -1;
    }
    if (length != 8 + numDisplays * kValuesPerDisplay * (int)sizeof(int)) {
        wsLog("Bad display config: length is %d (expected %d)\n",
            length, 8 + numDisplays * kValuesPerDisplay * (int)sizeof(int));
        return -1;
    }

    /*
     * Extract the config values.
     *
     * The runtime doesn't support multiple devices, so we don't either.
     */
    int i;
    for (i = 0; i < numDisplays; i++) {
        gWrapSim.display[i].width = pData[0];
        gWrapSim.display[i].height = pData[1];
        gWrapSim.display[i].shmemKey = pData[4];
        /* format/refresh no longer needed */

        void* addr;
        int shmid, semid;
        long length;
        if (attachToShmem(gWrapSim.display[i].shmemKey, &shmid, &addr,
                &length) != 0)
        {
            wsLog("Unable to connect to shared memory\n");
            return -1;
        }

        if (attachToSem(gWrapSim.display[i].shmemKey, &semid) != 0) {
            wsLog("Unable to attach to sempahore\n");
            return -1;
        }

        gWrapSim.display[i].shmid = shmid;
        gWrapSim.display[i].addr = addr;
        gWrapSim.display[i].length = length;
        gWrapSim.display[i].semid = semid;

        wsLog("Display %d: width=%d height=%d\n",
            i,
            gWrapSim.display[i].width,
            gWrapSim.display[i].height);
        wsLog("  shmem=0x%08x addr=%p len=%ld semid=%d\n",
            gWrapSim.display[i].shmemKey,
            gWrapSim.display[i].addr,
            gWrapSim.display[i].length,
            gWrapSim.display[i].semid);

        pData += kValuesPerDisplay;
    }
    gWrapSim.numDisplays = numDisplays;

    return 0;
}


/*
 * Initialize our connection to the simulator, which will be listening on
 * a UNIX domain socket.
 *
 * On success, this configures gWrapSim.simulatorFd and returns 0.
 */
static int openSimConnection(const char* name)
{
    int result = -1;
    char* fileName = NULL;
    int sock = -1;
    int cc;

    assert(gWrapSim.simulatorFd == -1);

    fileName = makeFilename(name);

    struct sockaddr_un addr;
    
    sock = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sock < 0) {
        wsLog("UNIX domain socket create failed (errno=%d)\n", errno);
        goto bail;
    }

    /* connect to socket; fails if file doesn't exist */
    strcpy(addr.sun_path, fileName);    // max 108 bytes
    addr.sun_family = AF_UNIX;
    cc = connect(sock, (struct sockaddr*) &addr, SUN_LEN(&addr));
    if (cc < 0) {
        // ENOENT means socket file doesn't exist
        // ECONNREFUSED means socket exists but nobody is listening
        wsLog("AF_UNIX connect failed for '%s': %s\n",
            fileName, strerror(errno));
        goto bail;
    }

    gWrapSim.simulatorFd = sock;
    sock = -1;

    result = 0;
    wsLog("+++ connected to '%s'\n", fileName);

bail:
    if (sock >= 0)
        _ws_close(sock);
    free(fileName);
    return result;
}

/*
 * Prepare communication with the front end.  We wait for a "hello" from
 * the other side, and respond in kind.
 */
static int prepSimConnection(void)
{
    /* NOTE: this is endian-specific; we're x86 Linux only, so no problem */
    static const unsigned int hello = kHelloMsg;
    static const unsigned int helloAck = kHelloAckMsg;
    Message msg;

    if (Message_read(&msg, gWrapSim.simulatorFd) != 0) {
        wsLog("hello read failed\n");
        return -1;
    }

    if (memcmp(msg.mData, &hello, 4) != 0) {
        wsLog("Got bad hello from peer\n");
        return -1;
    }

    Message_release(&msg);

    msg.mType = kTypeRaw;
    msg.mData = (unsigned char*) &helloAck;
    msg.mLength = 4;

    if (Message_write(&msg, gWrapSim.simulatorFd) != 0) {
        wsLog("hello ack write failed\n");
        return -1;
    }

    return 0;
}

/*
 * Get the sim front-end configuration.  We loop here until the sim claims
 * to be done with us.
 */
static int getSimConfig(void)
{
    Message msg;
    int joinNewGroup, grabTerminal, done;
    int result = -1;

    joinNewGroup = grabTerminal = done = 0;
    Message_clear(&msg);        // clear out msg->mData

    wsLog("+++ awaiting hardware configuration\n");
    while (!done) {
        if (Message_read(&msg, gWrapSim.simulatorFd) != 0) {
            wsLog("failed receiving config from parent\n");
            goto bail;
        }

        if (msg.mType == kTypeCommand) {
            int cmd, arg;

            if (getCommand(&msg, &cmd, &arg) != 0)
                goto bail;

            switch (cmd) {
            case kCommandGoAway:
                wsLog("Simulator front-end is busy\n");
                goto bail;
            case kCommandNewPGroup:
                joinNewGroup = 1;
                grabTerminal = (arg != 0);
                wsLog("Simulator wants us to be in a new pgrp (term=%d)\n",
                    grabTerminal);
                break;
            case kCommandConfigDone:
                done = 1;
                break;
            default:
                wsLog("Got unexpected command %d/%d\n", cmd, arg);
                break;
            }
        } else if (msg.mType == kTypeRaw) {
            /* assumes 32-bit alignment and identical byte ordering */
            int* pData = (int*) msg.mData;
            if (msg.mLength >= 4 && *pData == kDisplayConfigMagic) {
                if (handleDisplayConfig(pData, msg.mLength) != 0)
                    goto bail;
            }
        } else if (msg.mType == kTypeConfig) {
            const char* name = NULL;
            const char* val = NULL;
            getConfig(&msg, &name, &val);
            if(strcmp(name, "keycharmap") == 0) {
                free((void*)gWrapSim.keyMap);
                gWrapSim.keyMap = strdup(val);
            }
        } else {
            wsLog("Unexpected msg type %d during startup\n", msg.mType);
            goto bail;
        }

        /* clear out the data field if necessary */
        Message_release(&msg);
    }

    wsLog("Configuration received from simulator\n");

    if (joinNewGroup) {
        /* set pgid to pid */
        pid_t pgid = getpid();
        setpgid(0, pgid);

        /*
         * Put our pgrp in the foreground.
         * tcsetpgrp() from background process causes us to get a SIGTTOU,
         * which is mostly harmless but makes tcsetpgrp() fail with EINTR.
         */
        signal(SIGTTOU, SIG_IGN);
        if (grabTerminal) {
            if (tcsetpgrp(fileno(stdin), getpgrp()) != 0) {
                wsLog("tcsetpgrp(%d, %d) failed (errno=%d)\n",
                    fileno(stdin), getpgrp(), errno);
            }
            wsLog("Set pgrp %d as foreground\n", (int) getpgrp());
        }
    
        /* tell the sim where we're at */
        Message msg;
        setCommand(&msg, kCommandNewPGroupCreated, pgid);
        Message_write(&msg, gWrapSim.simulatorFd);
        Message_release(&msg);
    }

    result = 0;

bail:
    /* make sure the data was freed */
    Message_release(&msg);
    //wsLog("bailing, result=%d\n", result);
    return result;
}

/*
 * Connect to the simulator and exchange pleasantries.
 *
 * Returns 0 on success.
 */
static int connectToSim(void)
{
    if (openSimConnection(kAndroidPipeName) != 0)
        return -1;

    if (prepSimConnection() != 0)
        return -1;

    if (getSimConfig() != 0)
        return -1;

    wsLog("+++ sim is ready to go\n");

    return 0;
}

/*
 * Listen to the sim forever or until the front end shuts down, whichever
 * comes first.
 *
 * All we're really getting here are key events.
 */
static void listenToSim(void)
{
    wsLog("--- listening for input events from front end\n");

    while (1) {
        Message msg;

        Message_clear(&msg);
        if (Message_read(&msg, gWrapSim.simulatorFd) != 0) {
            wsLog("--- sim message read failed\n");
            return;
        }

        if (msg.mType == kTypeCommand) {
            int cmd, arg;

            if (getCommand(&msg, &cmd, &arg) != 0) {
                wsLog("bad command from sim?\n");
                continue;
            }

            switch (cmd) {
            case kCommandQuit:
                wsLog("--- sim sent us a QUIT message\n");
                return;
            case kCommandKeyDown:
                wsLog("KEY DOWN: %d\n", arg);
                wsSendSimKeyEvent(arg, 1);
                break;
            case kCommandKeyUp:
                wsLog("KEY UP: %d\n", arg);
                wsSendSimKeyEvent(arg, 0);
                break;
            default:
                wsLog("--- sim sent unrecognized command %d\n", cmd);
                break;
            }

            Message_release(&msg);
        } else if (msg.mType == kTypeCommandExt) {
            int cmd, arg0, arg1, arg2;

            if (getCommandExt(&msg, &cmd, &arg0, &arg1, &arg2) != 0) {
                wsLog("bad ext-command from sim?\n");
                continue;
            }

            switch (cmd) {
            case kCommandTouch:
                wsSendSimTouchEvent(arg0, arg1, arg2);
                break;
            }

            Message_release(&msg);
        } else {
            wsLog("--- sim sent non-command message, type=%d\n", msg.mType);
        }
    }

    assert(0);      // not reached
}


/*
 * Tell the simulator front-end that the display has been updated.
 */
void wsPostDisplayUpdate(int displayIdx)
{
    if (gWrapSim.simulatorFd < 0) {
        wsLog("Not posting display update -- sim not ready\n");
        return;
    }

    Message msg;

    setCommand(&msg, kCommandUpdateDisplay, displayIdx);
    Message_write(&msg, gWrapSim.simulatorFd);
    Message_release(&msg);
}

/*
 * Send a log message to the front-end.
 */
void wsPostLogMessage(int logPrio, const char* tag, const char* message)
{
    if (gWrapSim.simulatorFd < 0) {
        wsLog("Not posting log message -- sim not ready\n");
        return;
    }

    time_t when = time(NULL);
    int pid = (int) getpid();
    int tagLen, messageLen, totalLen;

    tagLen = strlen(tag) +1;
    messageLen = strlen(message) +1;
    totalLen = sizeof(int) * 3 + tagLen + messageLen;
    unsigned char outBuf[totalLen];
    unsigned char* cp = outBuf;

    /* See Message::set/getLogBundle() in simulator/MessageStream.cpp. */
    memcpy(cp, &when, sizeof(int));
    cp += sizeof(int);
    memcpy(cp, &logPrio, sizeof(int));
    cp += sizeof(int);
    memcpy(cp, &pid, sizeof(int));
    cp += sizeof(int);
    memcpy(cp, tag, tagLen);
    cp += tagLen;
    memcpy(cp, message, messageLen);
    cp += messageLen;

    assert(cp - outBuf == totalLen);

    Message msg;
    msg.mType = kTypeLogBundle;
    msg.mData = outBuf;
    msg.mLength = totalLen;
    Message_write(&msg, gWrapSim.simulatorFd);

    msg.mData = NULL;       // don't free
    Message_release(&msg);
}

/*
 * Turn the vibrating notification device on or off.
 */
void wsEnableVibration(int vibrateOn)
{
    if (gWrapSim.simulatorFd < 0) {
        wsLog("Not posting vibrator update -- sim not ready\n");
        return;
    }

    Message msg;

    //wsLog("+++ sending vibrate:%d\n", vibrateOn);

    setCommand(&msg, kCommandVibrate, vibrateOn);
    Message_write(&msg, gWrapSim.simulatorFd);
    Message_release(&msg);
}