/*
 * DF-5449 KLD harness -- production-machinery demonstration of the uvscom
 * (SUNTAC Slipper U / U-Cable, VID 0x05db) cfg-chain synchronous-request
 * stall. Same evidence class as DF-5367/5383/5421/5424/5425.
 *
 * WHAT IT PROVES (with the real in-kernel u4b usb_process machinery, the
 * same taskqueue ucom's cfg callbacks run on):
 *
 *   open(2) chain (uvscom.c):
 *     uvscom_cfg_open()  (uvscom.c:634-641) -> uvscom_cfg_read_status()
 *         (uvscom.c:714-734)  1 x READ_STATUS EP0 read  (1000 ms timeout)
 *     + core pause hz/10 after cfg_open    (usb_serial.c:777)
 *     ucom_cfg_line_state  (usb_serial.c:1356-1421) from
 *         ucom_open()'s ucom_modem(DTR|RTS) (usb_serial.c:874):
 *         uvscom_cfg_set_dtr (uvscom.c:470-482)  1 x LINE_CTL write
 *         uvscom_cfg_set_rts (uvscom.c:484-497)  1 x LINE_CTL write
 *     ucom_open()'s ucom_break(sc,0) (usb_serial.c:878):
 *         uvscom_cfg_set_break (uvscom.c:499-512) 1 x LINE_CTL write
 *   = 4 x 1000 ms + 100 ms ~= 4.1 s taskqueue occupancy per open.
 *
 *   Every request goes through ucom_cfg_do_request(...,1000)
 *   (uvscom.c:706-707, 727-728) whose usb_error_t is DISCARDED
 *   (DPRINTFN "... (ignored)", uvscom.c:708-711, 729-732), so a silent
 *   evil SUNTAC-clone (NAK every vendor data stage) makes each request eat
 *   its full 1000 ms timeout (usb_request.c:642-644 "WXFER" loop, 699-704)
 *   and the chain marches on anyway.
 *
 *   close(2) is SYNCHRONOUS against that queue:
 *     ucom_close() (usb_serial.c:939-978) ->
 *       ucom_shutdown() HUPCL -> uvscom_cfg_set_dtr(0): 1 write
 *         (usb_serial.c:694-696 -> uvscom.c:479-481)
 *       ucom_queue_command(ucom_cfg_close) -> usb_proc_mwait()
 *         (usb_serial.c:671-672, 955-957), uninterruptible, while
 *         uvscom_cfg_close() does 1 x UVSCOM_SHUTDOWN write
 *         (uvscom.c:643-651)
 *   => a close(2) right behind an open(2) blocks ~4100 + 2000 = ~6.1 s,
 *      SIGKILL-immune. Unplug mid-cycle stalls the USB hub event thread
 *      the same way (uvscom_detach -> ucom_detach -> usb_proc_drain,
 *      usb_serial.c:394) plus the per-device enum lock every request holds
 *      (usb_request.c:467).
 *
 *   (The tcsetattr chain, uvscom_cfg_param = 2 more writes (uvscom.c:581,
 *   614), is presently unreachable because of the DF-5447 pre_param
 *   inversion; phase B models the fixed driver where it aborts.)
 *
 * PHASE A (baseline): open chain (4 x 1000 ms + hz/10) then the closer
 *   (1 shutdown-DTR write + cfg_close 1 write) with usb_proc_mwait.
 *   EXPECT ~6100 ms.
 * PHASE B (fix mechanism from fix.diff: abort each cfg chain on its first
 *   failed request AND bound the cfg-path control timeout to 250 ms):
 *   cfg_open aborts after the failed READ_STATUS (250+100), the two
 *   line-state tasks abort on their first failed write (250 each), the
 *   closer's two writes likewise (2 x 250).
 *   EXPECT ~1350 ms.
 */
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/lock.h>
#include <sys/condvar.h>
#include <sys/time.h>
#include <sys/types.h>

#include <sys/bus.h>
#include <bus/u4b/usb.h>
#include <bus/u4b/usbdi.h>
#include <bus/u4b/usb_process.h>

#define	HX_OPEN_REQS	4	/* uvscom.c:640 + 481 + 496 + 511 */
#define	HX_CLOSE_REQS	2	/* uvscom.c:481 (HUPCL DTR) + 650 (SHUTDOWN) */
#define	HX_REQ_MS	1000	/* uvscom.c:707/728 production timeout */
#define	HX_REQ_MS_FIXED	250	/* fix.diff bounds the cfg-path timeout */

static struct lock hx_lock;		/* stands in for sc->sc_lock */
static struct usb_process hx_tq;	/* sc_super_ucom.sc_tq (usb_serial.c:322) */

struct hx_msg {
	struct usb_proc_msg hdr;
};
static struct hx_msg hx_open_msg[2];	/* ucom sc_open_task[2] */
static struct hx_msg hx_line_msg[2];	/* ucom sc_line_state_task[2]:
					 * [0] = DTR|RTS task (ucom_open:874)
					 * [1] = BREAK-clear task (ucom_open:878)
					 * NOTE: production reuses this same
					 * double-buffer for the closer's HUPCL
					 * drop, which can merge with a still-
					 * pending open task; the dedicated
					 * hx_hupcl buffer below models the
					 * unmerged (worst-case, longest) count */
static struct hx_msg hx_hupcl[2];	/* closer's HUPCL DTR-drop task */
static struct hx_close_msg {
	struct usb_proc_msg hdr;
} hx_close[2];				/* ucom sc_close_task[2] */

static int hx_fixed;			/* phase B: fix mechanism */
static int hx_req_ms;

static int64_t
hx_ms(struct timeval *t0, struct timeval *t1)
{
	return ((int64_t)(t1->tv_sec - t0->tv_sec) * 1000 +
	    (t1->tv_usec - t0->tv_usec) / 1000);
}

/*
 * One uvscom_cfg_write()/uvscom_cfg_read_status() against the silent evil
 * SUNTAC clone: the device never answers the vendor data stage, so the
 * requester sleeps in the "WXFER" loop until the timeout fires
 * (usb_request.c:642-644, 699-704) and the request returns
 * USB_ERR_TIMEOUT -- which production uvscom discards (void / "ignored").
 */
static int
hx_one_request(void)
{
	lksleep(&hx_lock, &hx_lock, 0, "WXFER", USB_MS_TO_TICKS(hx_req_ms));
	return (0);	/* production: even a timeout is ignored */
}

/* uvscom_cfg_open()'s chain: READ_STATUS + pause, then the two open-time
 * line-state tasks (DTR+RTS, then BREAK-clear), runs on sc_tq */
static void
hx_cfg_open(struct usb_proc_msg *pm)
{

	kprintf("HX5449: cfg-open task: enter, 1 x %d ms READ_STATUS + "
	    "hz/10 pause%s\n", hx_req_ms, hx_fixed ? " (fix: abort on error)"
	    : "");
	hx_one_request();			/* uvscom.c:640 */
	/* ucom_cfg_open()'s trailing pause, usb_serial.c:777 */
	usb_pause_mtx(&hx_lock, hz / 10);
	kprintf("HX5449: cfg-open task: leave\n");
}

static void
hx_cfg_line(struct usb_proc_msg *pm)
{

	kprintf("HX5449: line-state task: enter (%s)\n",
	    pm == (struct usb_proc_msg *)&hx_line_msg[0] ?
	    "DTR+RTS set: 2 x LINE_CTL (uvscom.c:481,496)" :
	    "BREAK clear: 1 x LINE_CTL (uvscom.c:511)");
	if (pm == (struct usb_proc_msg *)&hx_line_msg[0]) {
		hx_one_request();		/* uvscom_cfg_set_dtr */
		if (hx_fixed)
			return;			/* fix: abort skips set_rts */
		hx_one_request();		/* uvscom_cfg_set_rts */
	} else {
		hx_one_request();		/* uvscom_cfg_set_break */
	}
	kprintf("HX5449: line-state task: leave\n");
}

/* the closer's ucom_shutdown() HUPCL task: set_dtr(0): 1 LINE_CTL write */
static void
hx_cfg_hupcl(struct usb_proc_msg *pm)
{

	kprintf("HX5449: HUPCL line task: enter, 1 x %d ms LINE_CTL write "
	    "(uvscom.c:481)\n", hx_req_ms);
	hx_one_request();
	kprintf("HX5449: HUPCL line task: leave\n");
}

/* uvscom_cfg_close(): 1 x UVSCOM_SHUTDOWN write (uvscom.c:650) */
static void
hx_cfg_close(struct usb_proc_msg *pm)
{

	kprintf("HX5449: cfg-close task: enter, 1 x %d ms SHUTDOWN write\n",
	    hx_req_ms);
	hx_one_request();
	kprintf("HX5449: cfg-close task: leave\n");
}

/*
 * Exactly ucom_close()'s synchronous close (usb_serial.c:671-672 + 955-957):
 * first the HUPCL line-state task (set_dtr(0), 1 write), then queue
 * cfg_close behind everything and mwait uninterruptibly
 * (usb_process.c:385-393).
 */
static int64_t
hx_closer_thread(char tag)
{
	struct timeval t0, t1;

	/* 100 ms into the open chain, like a real close() racing the queue */
	tsleep(&hx_tq, 0, "hxlead", hz / 10);

	kprintf("HX5449[%c]: closer: HUPCL line task + queue cfg_close + "
	    "usb_proc_mwait() (uninterruptible, usb_serial.c:671-672) behind "
	    "the open chain\n", tag);
	microtime(&t0);
	lockmgr(&hx_lock, LK_EXCLUSIVE);
	if (!usb_proc_is_gone(&hx_tq)) {
		struct hx_msg *m;
		struct hx_close_msg *c;

		/* ucom_shutdown() -> ucom_modem(0, SER_DTR): 1 LINE_CTL write */
		m = usb_proc_msignal(&hx_tq, &hx_hupcl[0], &hx_hupcl[1]);
		m->hdr.pm_callback = &hx_cfg_hupcl;

		/* usb_serial.c:671-672 */
		c = usb_proc_msignal(&hx_tq, &hx_close[0], &hx_close[1]);
		c->hdr.pm_callback = &hx_cfg_close;
		usb_proc_mwait(&hx_tq, &hx_close[0], &hx_close[1]);
	}
	lockmgr(&hx_lock, LK_RELEASE);
	microtime(&t1);
	kprintf("HX5449[%c]: closer: unblocked  ELAPSED=%jd ms\n",
	    tag, (intmax_t)hx_ms(&t0, &t1));
	return (hx_ms(&t0, &t1));
}

/*
 * The open(2) path: ucom_open() queues ucom_cfg_open first
 * (usb_serial.c:865-867), then the line-state tasks (874-878).
 */
static void
hx_open(void)
{
	struct hx_msg *m;

	lockmgr(&hx_lock, LK_EXCLUSIVE);
	m = usb_proc_msignal(&hx_tq, &hx_open_msg[0], &hx_open_msg[1]);
	m->hdr.pm_callback = &hx_cfg_open;
	/* line-state tasks are double-buffered: DTR+RTS then BREAK */
	m = usb_proc_msignal(&hx_tq, &hx_line_msg[0], &hx_line_msg[1]);
	m->hdr.pm_callback = &hx_cfg_line;
	m = usb_proc_msignal(&hx_tq, &hx_line_msg[0], &hx_line_msg[1]);
	m->hdr.pm_callback = &hx_cfg_line;
	lockmgr(&hx_lock, LK_RELEASE);
}

static int
hx_phase(int phase)
{
	int error;

	memset(&hx_tq, 0, sizeof(hx_tq));
	error = usb_proc_create(&hx_tq, &hx_lock,
	    phase == 2 ? "hx5449b" : "hx5449a", USB_PRI_MED);
	if (error) {
		kprintf("HX5449: usb_proc_create failed: %d\n", error);
		return (error);
	}
	hx_fixed = (phase == 2);
	hx_req_ms = hx_fixed ? HX_REQ_MS_FIXED : HX_REQ_MS;

	/* the user opens the tty (queues the 4-request open chain) */
	hx_open();

	/* the closing thread (or the hub event thread on unplug) */
	hx_closer_thread(phase == 2 ? 'B' : 'A');

	usb_proc_free(&hx_tq);
	return (0);
}

static int
hx_modevent(module_t mod, int type, void *data)
{
	int error = 0;

	switch (type) {
	case MOD_LOAD:
		lockinit(&hx_lock, "hx5449", 0, LK_CANRECURSE);

		kprintf("HX5449: DF-5449 uvscom cfg-chain stall harness, "
		    "hz=%d, %d reqs/open-chain, %d reqs/close-chain, "
		    "%d ms/request\n",
		    hz, HX_OPEN_REQS, HX_CLOSE_REQS, hx_req_ms);

		hx_phase(1);		/* A: open+close ~6.1 s */
		hx_phase(2);		/* B: fix mechanism ~1.35 s */

		lockuninit(&hx_lock);
		kprintf("HX5449: harness complete\n");
		break;
	case MOD_UNLOAD:
		break;
	default:
		error = EOPNOTSUPP;
		break;
	}
	return (error);
}

static moduledata_t hx_mod = { "uvscomcfgstall", hx_modevent, 0 };
DECLARE_MODULE(uvscomcfgstall, hx_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
MODULE_DEPEND(uvscomcfgstall, usb, 1, 1, 1);
