Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/native-linux-uinput/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"files": [
"dist",
"src/helper/opencontroller-uinput-bridge.c",
"src/helper/uinput-open.h",
"README.md",
"LICENSE"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ describe("linux uinput adapter helpers", () => {

test("helper source advertises Linux force-feedback rumble", async () => {
const source = await readFile(linuxUinputHelperSourcePath, "utf8");
const openSource = await readFile(
new URL("../helper/uinput-open.h", import.meta.url),
"utf8",
);

expect(source).toContain("UI_SET_EVBIT, EV_FF");
expect(source).toContain("UI_SET_FFBIT, FF_RUMBLE");
Expand All @@ -111,7 +115,7 @@ describe("linux uinput adapter helpers", () => {
expect(source).toContain("UI_END_FF_UPLOAD");
expect(source).toContain("EV_UINPUT");
expect(source).toContain("opencontroller.bridge.feedback");
expect(source).toContain("O_RDWR | O_NONBLOCK");
expect(openSource).toContain("O_RDWR | O_NONBLOCK");
});

test("helper source advertises Linux player LED feedback", async () => {
Expand Down
46 changes: 46 additions & 0 deletions packages/native-linux-uinput/src/__tests__/uinput-open.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, test } from "bun:test";
import { execFileSync } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

describe("native uinput open retry helper", () => {
const nativeTest = process.platform === "linux" ? test : test.skip;

nativeTest("passes deterministic bounded retry cases", async () => {
const temporaryDirectory = await mkdtemp(
join(tmpdir(), "opencontroller-uinput-open-test-"),
);
const binaryPath = join(temporaryDirectory, "uinput-open-test");
const testSourcePath = fileURLToPath(
new URL("../helper/uinput-open-test.c", import.meta.url),
);
const helperDirectory = fileURLToPath(
new URL("../helper/", import.meta.url),
);

try {
execFileSync(
"cc",
[
"-std=c11",
"-D_DEFAULT_SOURCE",
"-Wall",
"-Wextra",
"-Werror",
"-Wno-unused-function",
"-I",
helperDirectory,
testSourcePath,
"-o",
binaryPath,
],
{ stdio: "pipe" },
);
execFileSync(binaryPath, [], { stdio: "pipe" });
} finally {
await rm(temporaryDirectory, { recursive: true, force: true });
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
#include "uinput-open.h"

#define OC_XINPUT_REPORT_BYTES 12
#define OC_HID_REPORT_BYTES 13
Expand Down Expand Up @@ -1015,6 +1016,7 @@ int main(int argc, char **argv) {
size_t line_length = 0;
int stdin_open = 1;
int fd;
unsigned int open_attempt_count = 0;
int created = 0;
struct oc_rumble_effect effects[OC_MAX_FF_EFFECTS];
char feedback_controller_id[OC_CONTROLLER_ID_MAX];
Expand Down Expand Up @@ -1071,10 +1073,14 @@ int main(int argc, char **argv) {
set_feedback_controller_id(feedback_controller_id,
sizeof(feedback_controller_id), controller_id);

fd = open(device_path, O_RDWR | O_NONBLOCK);
fd = oc_uinput_open_device(device_path, &open_attempt_count);
if (fd < 0) {
fprintf(stderr, "opencontroller-uinput: failed to open %s: %s\n",
device_path, strerror(errno));
fprintf(stderr,
"opencontroller-uinput: failed to open %s after %u attempt%s: %s; "
"check device and permission setup with "
"opencontroller-linux-uinput-doctor --check\n",
device_path, open_attempt_count,
open_attempt_count == 1 ? "" : "s", strerror(errno));
return 1;
}

Expand Down
101 changes: 101 additions & 0 deletions packages/native-linux-uinput/src/helper/uinput-open-test.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stddef.h>
#include <string.h>

#include "uinput-open.h"

struct open_result {
int fd;
int error;
};

static struct open_result results[OC_UINPUT_OPEN_MAX_ATTEMPTS];
static unsigned int result_count;
static unsigned int open_count;
static unsigned int wait_count;
static unsigned int waits[OC_UINPUT_OPEN_MAX_ATTEMPTS];

static int scripted_open(const char *path, int flags) {
struct open_result result;

assert(strcmp(path, "/test/uinput") == 0);
assert(flags == (O_RDWR | O_NONBLOCK));
assert(open_count < result_count);
result = results[open_count++];
if (result.fd < 0) {
errno = result.error;
}
return result.fd;
}

static void scripted_wait(unsigned int milliseconds) {
assert(wait_count < OC_UINPUT_OPEN_MAX_ATTEMPTS);
waits[wait_count++] = milliseconds;
errno = EINTR;
}

static void reset_script(const struct open_result *script, unsigned int count) {
memcpy(results, script, count * sizeof(script[0]));
result_count = count;
open_count = 0;
wait_count = 0;
}

static void test_success_after_transient_failures(void) {
const struct open_result script[] = {
{-1, ENOENT}, {-1, EACCES}, {42, 0},
};
unsigned int attempts = 0;

reset_script(script, sizeof(script) / sizeof(script[0]));
assert(oc_uinput_open_with_retry("/test/uinput", scripted_open,
scripted_wait, &attempts) == 42);
assert(attempts == 3);
assert(open_count == 3);
assert(wait_count == 2);
assert(waits[0] == 10);
assert(waits[1] == 20);
}

static void test_persistent_failure_stops_at_bound(void) {
const struct open_result script[] = {
{-1, EACCES}, {-1, EACCES}, {-1, EACCES}, {-1, EACCES}, {-1, EACCES},
};
unsigned int attempts = 0;

reset_script(script, sizeof(script) / sizeof(script[0]));
errno = 0;
assert(oc_uinput_open_with_retry("/test/uinput", scripted_open,
scripted_wait, &attempts) == -1);
assert(errno == EACCES);
assert(attempts == OC_UINPUT_OPEN_MAX_ATTEMPTS);
assert(open_count == OC_UINPUT_OPEN_MAX_ATTEMPTS);
assert(wait_count == OC_UINPUT_OPEN_MAX_ATTEMPTS - 1);
assert(waits[0] == 10);
assert(waits[1] == 20);
assert(waits[2] == 40);
assert(waits[3] == 40);
}

static void test_other_errors_are_not_retried(void) {
const struct open_result script[] = {{-1, EPERM}};
unsigned int attempts = 0;

reset_script(script, sizeof(script) / sizeof(script[0]));
errno = 0;
assert(oc_uinput_open_with_retry("/test/uinput", scripted_open,
scripted_wait, &attempts) == -1);
assert(errno == EPERM);
assert(attempts == 1);
assert(open_count == 1);
assert(wait_count == 0);
}

int main(void) {
test_success_after_transient_failures();
test_persistent_failure_stops_at_bound();
test_other_errors_are_not_retried();
return 0;
}
79 changes: 79 additions & 0 deletions packages/native-linux-uinput/src/helper/uinput-open.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#ifndef OPENCONTROLLER_UINPUT_OPEN_H
#define OPENCONTROLLER_UINPUT_OPEN_H

#include <errno.h>
#include <fcntl.h>
#include <time.h>

#define OC_UINPUT_OPEN_MAX_ATTEMPTS 5
#define OC_UINPUT_OPEN_INITIAL_BACKOFF_MS 10
#define OC_UINPUT_OPEN_MAX_BACKOFF_MS 40

typedef int (*oc_uinput_open_fn)(const char *path, int flags);
typedef void (*oc_uinput_wait_fn)(unsigned int milliseconds);

static unsigned int oc_uinput_open_backoff_ms(unsigned int failed_attempt) {
unsigned int delay = OC_UINPUT_OPEN_INITIAL_BACKOFF_MS;

while (failed_attempt > 1 && delay < OC_UINPUT_OPEN_MAX_BACKOFF_MS) {
delay *= 2;
failed_attempt--;
}

return delay > OC_UINPUT_OPEN_MAX_BACKOFF_MS
? OC_UINPUT_OPEN_MAX_BACKOFF_MS
: delay;
}

static int oc_uinput_open_with_retry(const char *path, oc_uinput_open_fn open_fn,
oc_uinput_wait_fn wait_fn,
unsigned int *attempt_count) {
unsigned int attempt;

for (attempt = 1; attempt <= OC_UINPUT_OPEN_MAX_ATTEMPTS; attempt++) {
int fd = open_fn(path, O_RDWR | O_NONBLOCK);
int open_error;

if (attempt_count != NULL) {
*attempt_count = attempt;
}

if (fd >= 0) {
return fd;
}

open_error = errno;
if ((open_error != ENOENT && open_error != EACCES) ||
attempt == OC_UINPUT_OPEN_MAX_ATTEMPTS) {
errno = open_error;
return -1;
}

wait_fn(oc_uinput_open_backoff_ms(attempt));
}

errno = EIO;
return -1;
}

static void oc_uinput_wait_for_backoff(unsigned int milliseconds) {
struct timespec delay;
struct timespec remaining;

delay.tv_sec = (time_t)(milliseconds / 1000);
delay.tv_nsec = (long)(milliseconds % 1000) * 1000000L;
while (nanosleep(&delay, &remaining) < 0 && errno == EINTR) {
delay = remaining;
}
}

static int oc_system_uinput_open(const char *path, int flags) {
return open(path, flags);
}

static int oc_uinput_open_device(const char *path, unsigned int *attempt_count) {
return oc_uinput_open_with_retry(path, oc_system_uinput_open,
oc_uinput_wait_for_backoff, attempt_count);
}

#endif
5 changes: 4 additions & 1 deletion scripts/check-npm-packages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ const workspaces = [
const sourceAllowlist = new Map([
[
"packages/native-linux-uinput",
new Set(["src/helper/opencontroller-uinput-bridge.c"]),
new Set([
"src/helper/opencontroller-uinput-bridge.c",
"src/helper/uinput-open.h",
]),
],
]);

Expand Down
Loading