/* usbsoak.c - pass-2 usb_device.c state-machine soak (runs as root in guest)
 *
 * Hammers the ugen entry points that drive sys/bus/u4b/usb_device.c:
 *   USB_SET_ALTINTERFACE -> usbd_set_alt_interface_index -> usb_fifo_free_wrap
 *                           -> usb_config_parse(iface, alt)  [zeroing loop]
 *   USB_SET_CONFIG       -> usbd_start_set_config -> usbd_set_config_index
 *                           (usb_unconfigure / usb_config_parse ALLOC+INIT /
 *                            usb_cdev_create churn on the explore proc)
 *   USB_GET_FULL_DESC    -> ugen_get_cdesc (cdesc cache + kfree churn)
 *   USB_REQUEST          -> control transfers (usbd_do_request_flags)
 *   USB_IFACE_DRIVER_DETACH -> usb_detach_device
 * concurrent on two devices; expects lots of EINVAL/EBUSY/ENXIO, NEVER a panic.
 */
#include <sys/ioctl.h>
#include <sys/fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <pthread.h>

#include <bus/u4b/usb.h>
#include <bus/u4b/usb_ioctl.h>

#ifndef RUN_SECONDS
#define RUN_SECONDS 90
#endif

static volatile int stop_flag;
static unsigned long ops, errs, ok_alt, ok_cfg;
static unsigned int seed;

static double now_sec(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec + ts.tv_nsec / 1e9;
}

static void do_alt(int fd) {
    struct usb_alt_interface ai;
    uint8_t idx[8] = {0, 0, 1, 2, 31, 255, (uint8_t)(seed >> 9), (uint8_t)(seed >> 17)};
    uint8_t alt[8] = {0, 0, 1, 2, 255, (uint8_t)(seed >> 5), (uint8_t)(seed >> 13), 0};
    int i = rand_r(&seed) % 8, j = rand_r(&seed) % 8;
    memset(&ai, 0, sizeof(ai));
    ai.uai_interface_index = idx[i];
    ai.uai_alt_index = alt[j];
    if (ioctl(fd, USB_SET_ALTINTERFACE, &ai) == 0) ok_alt++;
}

static void do_cfg(int fd) {
    int v[6] = {0, 0, 1, 2, 255, rand_r(&seed) & 0xff};
    int i = rand_r(&seed) % 6;
    if (ioctl(fd, USB_SET_CONFIG, &v[i]) == 0) ok_cfg++;
}

static void do_fulldesc(int fd) {
    static char buf[4200];
    struct usb_gen_descriptor gd;
    memset(&gd, 0, sizeof(gd));
    gd.ugd_data = buf;
    gd.ugd_maxlen = 64 + (rand_r(&seed) % 4096);
    gd.ugd_config_index = rand_r(&seed) % 4;
    gd.ugd_string_index = rand_r(&seed) % 4;
    ioctl(fd, USB_GET_FULL_DESC, &gd);
}

static void do_request(int fd) {
    static char buf[256];
    struct usb_ctl_request req;
    memset(&req, 0, sizeof(req));
    req.ucr_request.bmRequestType = 0x80;          /* device IN */
    req.ucr_request.bRequest = 0x06;               /* GET_DESCRIPTOR */
    req.ucr_request.wValue[0] = rand_r(&seed) % 4; /* index */
    req.ucr_request.wValue[1] = rand_r(&seed) % 4; /* type: device/config/string */
    req.ucr_request.wIndex[0] = 0x09; req.ucr_request.wIndex[1] = 0;
    req.ucr_request.wLength[0] = 0x20; req.ucr_request.wLength[1] = 0;
    req.ucr_data = buf;
    req.ucr_flags = 0;
    ioctl(fd, USB_DO_REQUEST, &req);
}

static void do_detach(int fd) {
    int v = rand_r(&seed) % 3;
    ioctl(fd, USB_IFACE_DRIVER_DETACH, &v);
}

static void *worker(void *arg) {
    const char *dev = arg;
    unsigned int lseed = seed ^ (unsigned)(size_t)arg;
    double t_end = now_sec() + RUN_SECONDS;
    int fd = open(dev, O_RDWR | O_NONBLOCK);
    if (fd < 0) { printf("open %s: %s\n", dev, strerror(errno)); return NULL; }
    while (now_sec() < t_end && !stop_flag) {
        int r = rand_r(&lseed) % 100;
        if (r < 35) do_alt(fd);
        else if (r < 55) do_cfg(fd);
        else if (r < 75) do_fulldesc(fd);
        else if (r < 95) do_request(fd);
        else do_detach(fd);
        ops++;
        if ((rand_r(&lseed) % 64) == 0)
            printf("tick ops=%lu alt_ok=%lu cfg_ok=%lu\n", ops, ok_alt, ok_cfg);
        usleep(200 + (rand_r(&lseed) % 1800));
    }
    close(fd);
    return NULL;
}

struct reopen_arg { const char *dev; };

static void *reopener(void *arg) {
    struct reopen_arg *ra = arg;
    double t_end = now_sec() + RUN_SECONDS;
    while (now_sec() < t_end && !stop_flag) {
        int fd = open(ra->dev, O_RDONLY | O_NONBLOCK);
        if (fd >= 0) {
            struct usb_gen_descriptor gd;
            static char buf[512];
            memset(&gd, 0, sizeof(gd));
            gd.ugd_data = buf;
            gd.ugd_maxlen = 64;
            ioctl(fd, USB_GET_FULL_DESC, &gd);
            usleep(1000 + (rand_r(&seed) % 5000));
            close(fd);
        }
        usleep(3000);
    }
    return NULL;
}

int main(int argc, char **argv) {
    pthread_t t1, t2, t3;
    seed = argc > 1 ? atoi(argv[1]) : 1234;
    printf("soak start seed=%u devs: /dev/usb/1.2.0 /dev/usb/1.3.0\n", seed);
    pthread_create(&t1, NULL, worker, "/dev/usb/1.2.0");
    pthread_create(&t2, NULL, worker, "/dev/usb/1.3.0");
    pthread_create(&t3, NULL, reopener, &(struct reopen_arg){"/dev/usb/1.3.1"});
    pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL);
    printf("soak done: ops=%lu alt_ok=%lu cfg_ok=%lu (errors expected, panic = FAIL)\n",
           ops, ok_alt, ok_cfg);
    return 0;
}
