OXYGENHUB
# d69f9ef6bbac retro-remote-debug-controller / core / retro_control.c
18.8 KiB · C · last changed Contract 0.3.0: memory write (POST /mem) + audio drain (GET /audio) 3 months ago source blame history raw
961b3c61 Andy McCall 1
/* retro_control.c — portable emulator control server. See retro_control.h
2
 * and ../SPEC.md. POSIX (Linux/macOS); on Windows the entry points are no-ops.
3
 *
4
 * One accept thread, one request in flight at a time. State reads are marshalled
5
 * to the emulator thread via retro_control_service() so they observe a
6
 * consistent machine; /step is driven cooperatively by retro_control_on_frame().
7
 */
8
#include "retro_control.h"
9
10
#include <stdio.h>
11
#include <stdlib.h>
12
#include <string.h>
13
14
#if defined(_WIN32)
15
/* No server on Windows for now — stub out so the fork still builds. */
16
int  retro_control_start(int port, const retro_control_backend_t *b) { (void)port; (void)b; return -1; }
17
void retro_control_service(void) {}
18
int  retro_control_running(void) { return 1; }
19
void retro_control_on_frame(void) {}
20
void retro_control_stop(void) {}
21
#else
22
23
#include <pthread.h>
24
#include <unistd.h>
25
#include <errno.h>
26
#include <signal.h>
27
#include <sys/socket.h>
28
#include <netinet/in.h>
29
#include <arpa/inet.h>
30
31
#ifndef MSG_NOSIGNAL
32
#define MSG_NOSIGNAL 0
33
#endif
34
35
#define MEM_CAP (1u << 20)   /* /mem hard cap: 1 MiB per request */
36
37
static const retro_control_backend_t *g_be = NULL;
38
static int       g_listen_fd = -1;
39
static pthread_t g_thread;
40
41
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
42
static pthread_cond_t  g_cv   = PTHREAD_COND_INITIALIZER;
43
44
/* run control: -1 free-run, 0 halted, >0 run this many frames then halt */
45
static long g_frames_to_run = -1;
46
static int  g_step_waiting  = 0;
47
674b8447 Andy McCall ↶ 48
/* single-slot marshalled request. */
49
typedef enum { REQ_NONE = 0, REQ_MEM, REQ_REGS, REQ_SHOT, REQ_KEY, REQ_RESET,
50
               REQ_WRITE, REQ_AUDIO } req_t;
961b3c61 Andy McCall 51
static volatile req_t g_req = REQ_NONE;
52
static uint32_t g_req_addr, g_req_len;
53
static int32_t  g_req_bank;
674b8447 Andy McCall ↶ 54
static const uint8_t *g_write_data;   /* POST /mem body (HTTP-thread owned) */
689a9ccb Andy McCall ↶ 55
static int      g_key_is_text;   /* 1 => g_key_value is a char code point */
56
static uint32_t g_key_value;
57
static int      g_key_action;    /* retro_key_action_t */
674b8447 Andy McCall ↶ 58
static char     g_resp_extra[64];     /* extra response header line, or "" */
961b3c61 Andy McCall 59
60
static uint8_t    *g_resp_body  = NULL;
61
static size_t      g_resp_len   = 0;
62
static int         g_resp_status = 200;
63
static const char *g_resp_ctype = "application/octet-stream";
64
static int         g_resp_ready = 0;
65
66
/* ---- emulator-thread side ------------------------------------------------ */
67
68
static void build_ppm(void)
69
{
70
    retro_framebuffer_t fb;
71
    memset(&fb, 0, sizeof fb);
72
    g_be->get_framebuffer(&fb);
73
    int w = fb.width, h = fb.height;
74
    int bpp = (fb.fmt == RETRO_PIX_RGB888) ? 3 : 4;
75
76
    char hdr[64];
77
    int hl = snprintf(hdr, sizeof hdr, "P6\n%d %d\n255\n", w, h);
78
    size_t total = (size_t)hl + (size_t)3 * w * h;
79
    uint8_t *out = (uint8_t *)malloc(total ? total : 1);
80
    if (!out) { g_resp_body = NULL; g_resp_len = 0; g_resp_status = 500; return; }
81
82
    memcpy(out, hdr, hl);
83
    uint8_t *p = out + hl;
84
    const uint8_t *s = fb.pixels;
85
    for (int i = 0; i < w * h; i++) {
86
        const uint8_t *px = s + (size_t)i * bpp;
87
        if (fb.fmt == RETRO_PIX_BGRA8888) {
88
            p[0] = px[2]; p[1] = px[1]; p[2] = px[0];
89
        } else {                       /* RGB888 / RGBA8888 */
90
            p[0] = px[0]; p[1] = px[1]; p[2] = px[2];
91
        }
92
        p += 3;
93
    }
94
    g_resp_body = out;
95
    g_resp_len = total;
96
    g_resp_status = 200;
97
    g_resp_ctype = "image/x-portable-pixmap";
98
}
99
674b8447 Andy McCall ↶ 100
#define AUDIO_MAX_SAMPLES (1u << 18)   /* drain cap: 256K int16 (~2.7 s stereo @48.8k) */
101
102
static void le16(uint8_t *p, uint16_t v) { p[0] = v; p[1] = v >> 8; }
103
static void le32(uint8_t *p, uint32_t v) { p[0] = v; p[1] = v >> 8; p[2] = v >> 16; p[3] = v >> 24; }
104
105
/* Drain the backend's audio ring and wrap it as a canonical PCM WAV. */
106
static void build_wav(void)
107
{
108
    static int16_t samp[AUDIO_MAX_SAMPLES];
109
    int rate = 48000, channels = 1;
110
    uint32_t dropped = 0;
111
    uint32_t n = g_be->capture_audio(samp, AUDIO_MAX_SAMPLES, &rate, &channels, &dropped);
112
    if (channels < 1) channels = 1;
113
    if (rate < 1) rate = 1;
114
115
    uint32_t data_bytes = n * 2;               /* int16 samples */
116
    uint32_t total = 44 + data_bytes;
117
    uint8_t *out = (uint8_t *)malloc(total);
118
    if (!out) { g_resp_body = NULL; g_resp_len = 0; g_resp_status = 500; return; }
119
120
    uint32_t byte_rate = (uint32_t)rate * channels * 2;
121
    memcpy(out, "RIFF", 4);       le32(out + 4, 36 + data_bytes);
122
    memcpy(out + 8, "WAVE", 4);
123
    memcpy(out + 12, "fmt ", 4);  le32(out + 16, 16);
124
    le16(out + 20, 1);            le16(out + 22, (uint16_t)channels);
125
    le32(out + 24, (uint32_t)rate);
126
    le32(out + 28, byte_rate);    le16(out + 32, (uint16_t)(channels * 2));
127
    le16(out + 34, 16);
128
    memcpy(out + 36, "data", 4);  le32(out + 40, data_bytes);
129
    memcpy(out + 44, samp, data_bytes);
130
131
    g_resp_body = out; g_resp_len = total; g_resp_status = 200;
132
    g_resp_ctype = "audio/wav";
133
    snprintf(g_resp_extra, sizeof g_resp_extra,
134
             "X-Rrdc-Audio-Dropped: %u\r\n", (unsigned)dropped);
135
}
136
961b3c61 Andy McCall 137
void retro_control_service(void)
138
{
139
    if (!g_be || g_req == REQ_NONE) return;   /* cheap unlocked fast-path */
140
141
    pthread_mutex_lock(&g_lock);
674b8447 Andy McCall ↶ 142
    g_resp_extra[0] = 0;
961b3c61 Andy McCall 143
    switch (g_req) {
144
    case REQ_MEM: {
145
        uint32_t len = g_req_len > MEM_CAP ? MEM_CAP : g_req_len;
146
        uint8_t *buf = (uint8_t *)malloc(len ? len : 1);
147
        uint32_t got = 0;
148
        if (buf && g_be->read_mem) got = g_be->read_mem(g_req_addr, g_req_bank, len, buf, len);
149
        g_resp_body = buf; g_resp_len = got;
150
        g_resp_status = g_be->read_mem ? 200 : 501;
151
        g_resp_ctype = "application/octet-stream";
152
        break;
153
    }
154
    case REQ_REGS: {
155
        char *buf = (char *)malloc(1024);
156
        if (buf && g_be->get_regs_json) { g_be->get_regs_json(buf, 1024); }
157
        else if (buf) { buf[0] = 0; }
158
        g_resp_body = (uint8_t *)buf; g_resp_len = buf ? strlen(buf) : 0;
159
        g_resp_status = g_be->get_regs_json ? 200 : 501;
160
        g_resp_ctype = "application/json";
161
        break;
162
    }
163
    case REQ_SHOT:
164
        if (g_be->get_framebuffer) build_ppm();
165
        else { g_resp_body = NULL; g_resp_len = 0; g_resp_status = 501; }
166
        break;
689a9ccb Andy McCall ↶ 167
    case REQ_KEY: {
168
        const char *json;
169
        if (!g_be->inject_key) {
170
            json = "{\"error\":\"not implemented\"}"; g_resp_status = 501;
171
        } else if (g_be->inject_key(g_key_is_text, g_key_value, g_key_action)) {
172
            json = "{\"injected\":true}"; g_resp_status = 200;
173
        } else {
174
            json = "{\"error\":\"unmapped key\"}"; g_resp_status = 400;
175
        }
176
        size_t l = strlen(json);
177
        uint8_t *b = (uint8_t *)malloc(l + 1);
178
        if (b) memcpy(b, json, l + 1);
179
        g_resp_body = b; g_resp_len = b ? l : 0;
180
        g_resp_ctype = "application/json";
181
        break;
182
    }
183
    case REQ_RESET: {
184
        const char *json;
185
        if (g_be->reset) { g_be->reset(); json = "{\"reset\":true}"; g_resp_status = 200; }
186
        else { json = "{\"error\":\"not implemented\"}"; g_resp_status = 501; }
187
        size_t l = strlen(json);
188
        uint8_t *b = (uint8_t *)malloc(l + 1);
189
        if (b) memcpy(b, json, l + 1);
190
        g_resp_body = b; g_resp_len = b ? l : 0;
674b8447 Andy McCall ↶ 191
        g_resp_ctype = "application/json";
192
        break;
193
    }
194
    case REQ_WRITE: {
195
        const char *json; char *b;
196
        if (!g_be->write_mem) {
197
            json = "{\"error\":\"not implemented\"}"; g_resp_status = 501;
198
            size_t l = strlen(json); b = (char *)malloc(l + 1);
199
            if (b) memcpy(b, json, l + 1);
200
            g_resp_len = b ? l : 0;
201
        } else {
202
            uint32_t wrote = g_be->write_mem(g_req_addr, g_req_bank, g_req_len, g_write_data);
203
            b = (char *)malloc(48);
204
            g_resp_len = b ? (size_t)snprintf(b, 48, "{\"written\":%u}", (unsigned)wrote) : 0;
205
            g_resp_status = 200;
206
        }
207
        g_resp_body = (uint8_t *)b;
689a9ccb Andy McCall ↶ 208
        g_resp_ctype = "application/json";
209
        break;
210
    }
674b8447 Andy McCall ↶ 211
    case REQ_AUDIO:
212
        if (g_be->capture_audio) build_wav();
213
        else {
214
            const char *json = "{\"error\":\"not implemented\"}";
215
            size_t l = strlen(json); uint8_t *b = (uint8_t *)malloc(l + 1);
216
            if (b) memcpy(b, json, l + 1);
217
            g_resp_body = b; g_resp_len = b ? l : 0; g_resp_status = 501;
218
            g_resp_ctype = "application/json";
219
        }
220
        break;
961b3c61 Andy McCall 221
    default: break;
222
    }
223
    g_req = REQ_NONE;
224
    g_resp_ready = 1;
225
    pthread_cond_broadcast(&g_cv);
226
    pthread_mutex_unlock(&g_lock);
227
}
228
229
int retro_control_running(void)
230
{
231
    if (!g_be) return 1;
232
    pthread_mutex_lock(&g_lock);
233
    long f = g_frames_to_run;
234
    pthread_mutex_unlock(&g_lock);
235
    return f != 0;
236
}
237
238
void retro_control_on_frame(void)
239
{
240
    if (!g_be) return;
241
    pthread_mutex_lock(&g_lock);
242
    if (g_frames_to_run > 0) {
243
        g_frames_to_run--;
244
        if (g_frames_to_run == 0 && g_step_waiting) {
245
            g_step_waiting = 0;
246
            g_resp_ready = 1;
247
            pthread_cond_broadcast(&g_cv);
248
        }
249
    }
250
    pthread_mutex_unlock(&g_lock);
251
}
252
253
/* ---- server-thread side -------------------------------------------------- */
254
255
static void write_all(int fd, const void *buf, size_t len)
256
{
257
    const char *p = (const char *)buf;
258
    while (len) {
259
        ssize_t w = send(fd, p, len, MSG_NOSIGNAL);
260
        if (w <= 0) return;
261
        p += w; len -= (size_t)w;
262
    }
263
}
264
674b8447 Andy McCall ↶ 265
/* `extra`, if non-NULL, is a complete header line ("Name: value\r\n"). */
961b3c61 Andy McCall 266
static void send_http(int fd, int status, const char *ctype,
674b8447 Andy McCall ↶ 267
                      const uint8_t *body, size_t len, const char *extra)
961b3c61 Andy McCall 268
{
269
    const char *reason =
270
        status == 200 ? "OK" :
271
        status == 400 ? "Bad Request" :
272
        status == 404 ? "Not Found" :
273
        status == 405 ? "Method Not Allowed" :
274
        status == 501 ? "Not Implemented" : "Error";
674b8447 Andy McCall ↶ 275
    char hdr[320];
961b3c61 Andy McCall 276
    int hl = snprintf(hdr, sizeof hdr,
277
        "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\n"
674b8447 Andy McCall ↶ 278
        "Connection: close\r\nAccess-Control-Allow-Origin: *\r\n%s\r\n",
279
        status, reason, ctype, len, extra ? extra : "");
961b3c61 Andy McCall 280
    write_all(fd, hdr, hl);
281
    if (body && len) write_all(fd, body, len);
282
}
283
284
static void send_json(int fd, int status, const char *json)
285
{
674b8447 Andy McCall ↶ 286
    send_http(fd, status, "application/json", (const uint8_t *)json, strlen(json), NULL);
961b3c61 Andy McCall 287
}
288
289
/* value of query key, decimal or 0x-hex (strtol base 0); def if absent. */
290
static long query_long(const char *q, const char *key, long def)
291
{
292
    size_t kl = strlen(key);
293
    const char *p = q;
294
    while (p && *p) {
295
        if (!strncmp(p, key, kl) && p[kl] == '=')
296
            return strtol(p + kl + 1, NULL, 0);
297
        p = strchr(p, '&');
298
        if (p) p++;
299
    }
300
    return def;
301
}
302
689a9ccb Andy McCall ↶ 303
/* copy the value of query key into out (NUL-terminated, truncated to cap).
304
 * Returns 1 if the key was present, 0 otherwise. */
305
static int query_str(const char *q, const char *key, char *out, size_t cap)
306
{
307
    size_t kl = strlen(key);
308
    const char *p = q;
309
    while (p && *p) {
310
        if (!strncmp(p, key, kl) && p[kl] == '=') {
311
            const char *v = p + kl + 1;
312
            const char *e = strchr(v, '&');
313
            size_t n = e ? (size_t)(e - v) : strlen(v);
314
            if (n >= cap) n = cap - 1;
315
            memcpy(out, v, n);
316
            out[n] = 0;
317
            return 1;
318
        }
319
        p = strchr(p, '&');
320
        if (p) p++;
321
    }
322
    if (cap) out[0] = 0;
323
    return 0;
324
}
325
961b3c61 Andy McCall 326
/* marshal a read to the emulator thread, wait, send the response. */
327
static void do_marshalled(int fd, req_t r)
328
{
329
    pthread_mutex_lock(&g_lock);
330
    g_resp_ready = 0; g_resp_body = NULL; g_resp_len = 0;
331
    g_req = r;
332
    while (!g_resp_ready) pthread_cond_wait(&g_cv, &g_lock);
333
    uint8_t *body = g_resp_body; size_t len = g_resp_len;
334
    int st = g_resp_status; const char *ct = g_resp_ctype;
674b8447 Andy McCall ↶ 335
    char extra[64]; memcpy(extra, g_resp_extra, sizeof extra);
961b3c61 Andy McCall 336
    g_resp_body = NULL;
337
    pthread_mutex_unlock(&g_lock);
338
674b8447 Andy McCall ↶ 339
    send_http(fd, st, ct, body, len, extra[0] ? extra : NULL);
961b3c61 Andy McCall 340
    free(body);
341
}
342
343
static void do_status(int fd)
344
{
345
    pthread_mutex_lock(&g_lock);
346
    long f = g_frames_to_run;
347
    pthread_mutex_unlock(&g_lock);
348
    unsigned long long fc = g_be->get_frame_count ? g_be->get_frame_count() : 0;
674b8447 Andy McCall ↶ 349
    /* Advertise the highest level whose defining callbacks are all present. */
350
    const char *contract =
351
        (g_be->inject_key && g_be->write_mem && g_be->capture_audio) ? "0.3.0" :
352
        g_be->inject_key ? "0.2.0" : "0.1.0";
961b3c61 Andy McCall 353
    char buf[320];
354
    snprintf(buf, sizeof buf,
355
        "{\"contract\":\"%s\",\"emulator\":\"%s\",\"platform\":\"%s\","
356
        "\"frame\":%llu,\"paused\":%s,\"running\":true}",
689a9ccb Andy McCall ↶ 357
        contract,
961b3c61 Andy McCall 358
        g_be->emulator ? g_be->emulator : "",
359
        g_be->platform ? g_be->platform : "",
360
        fc, (f == 0) ? "true" : "false");
361
    send_json(fd, 200, buf);
362
}
363
364
static void do_step(int fd, const char *query)
365
{
366
    long n = query_long(query, "frames", 1);
367
    if (n < 1) n = 1;
368
    pthread_mutex_lock(&g_lock);
369
    g_frames_to_run = n;
370
    g_step_waiting = 1;
371
    g_resp_ready = 0;
372
    while (!g_resp_ready) pthread_cond_wait(&g_cv, &g_lock);
373
    pthread_mutex_unlock(&g_lock);
374
375
    unsigned long long fc = g_be->get_frame_count ? g_be->get_frame_count() : 0;
376
    char buf[64];
377
    snprintf(buf, sizeof buf, "{\"frame\":%llu}", fc);
378
    send_json(fd, 200, buf);
379
}
380
381
static void do_setrun(int fd, long v, int paused)
382
{
383
    pthread_mutex_lock(&g_lock);
384
    g_frames_to_run = v;
385
    pthread_mutex_unlock(&g_lock);
386
    send_json(fd, 200, paused ? "{\"paused\":true}" : "{\"paused\":false}");
387
}
674b8447 Andy McCall ↶ 388
389
/* Read a request body (POST /mem). `req`/`n` = what handle_conn already recv'd
390
 * (headers + maybe some body); reads the rest per Content-Length. Returns a
391
 * malloc'd buffer of *out_len bytes (caller frees), capped at MEM_CAP. */
392
static uint8_t *read_body(int fd, char *req, size_t n, size_t *out_len)
393
{
394
    *out_len = 0;
395
    char *hdr_end = strstr(req, "\r\n\r\n");
396
    if (!hdr_end) return NULL;
397
    char *body_start = hdr_end + 4;
398
399
    long clen = 0;
400
    char *cl = strstr(req, "Content-Length:");
401
    if (!cl) cl = strstr(req, "content-length:");
402
    if (cl) clen = strtol(cl + 15, NULL, 10);
403
    if (clen < 0) clen = 0;
404
    if (clen > (long)MEM_CAP) clen = (long)MEM_CAP;
961b3c61 Andy McCall 405
674b8447 Andy McCall ↶ 406
    uint8_t *body = (uint8_t *)malloc((size_t)clen ? (size_t)clen : 1);
407
    if (!body) return NULL;
408
409
    size_t have = (size_t)(req + n - body_start);
410
    if (have > (size_t)clen) have = (size_t)clen;
411
    memcpy(body, body_start, have);
412
    size_t got = have;
413
    while (got < (size_t)clen) {
414
        ssize_t r = recv(fd, body + got, (size_t)clen - got, 0);
415
        if (r <= 0) break;
416
        got += (size_t)r;
417
    }
418
    *out_len = got;
419
    return body;
420
}
421
961b3c61 Andy McCall 422
static void handle_conn(int fd)
423
{
424
    char req[2048];
425
    size_t n = 0;
426
    while (n < sizeof(req) - 1) {
427
        ssize_t r = recv(fd, req + n, sizeof(req) - 1 - n, 0);
428
        if (r <= 0) break;
429
        n += (size_t)r;
430
        if (strstr(req, "\r\n\r\n")) break;
431
    }
432
    req[n] = 0;
433
434
    char method[8] = {0}, target[1024] = {0};
435
    if (sscanf(req, "%7s %1023s", method, target) != 2) {
436
        send_json(fd, 400, "{\"error\":\"bad request\"}");
437
        return;
438
    }
439
    char *q = strchr(target, '?');
440
    const char *query = "";
441
    if (q) { *q = 0; query = q + 1; }
442
    int is_post = !strcmp(method, "POST");
443
444
    if (!strcmp(target, "/status"))          { do_status(fd); return; }
445
    if (!strcmp(target, "/regs"))            { do_marshalled(fd, REQ_REGS); return; }
446
    if (!strcmp(target, "/screenshot"))      { do_marshalled(fd, REQ_SHOT); return; }
674b8447 Andy McCall ↶ 447
    if (!strcmp(target, "/audio"))           { do_marshalled(fd, REQ_AUDIO); return; }
961b3c61 Andy McCall 448
    if (!strcmp(target, "/mem")) {
449
        g_req_addr = (uint32_t)query_long(query, "addr", 0);
450
        g_req_bank = (int32_t)query_long(query, "bank", -1);
674b8447 Andy McCall ↶ 451
        if (is_post) {                        /* 0.3: write / poke */
452
            size_t blen = 0;
453
            uint8_t *body = read_body(fd, req, n, &blen);
454
            g_req_len = (uint32_t)blen;
455
            g_write_data = body;
456
            do_marshalled(fd, REQ_WRITE);
457
            free(body);
458
        } else {                              /* read */
459
            g_req_len = (uint32_t)query_long(query, "len", 0);
460
            do_marshalled(fd, REQ_MEM);
461
        }
961b3c61 Andy McCall 462
        return;
463
    }
464
    if (!strcmp(target, "/step")) {
465
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
466
        do_step(fd, query); return;
467
    }
468
    if (!strcmp(target, "/pause")) {
469
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
470
        do_setrun(fd, 0, 1); return;
471
    }
472
    if (!strcmp(target, "/resume")) {
473
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
474
        do_setrun(fd, -1, 0); return;
689a9ccb Andy McCall ↶ 475
    }
476
    if (!strcmp(target, "/key")) {
477
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
478
        char text[8];
479
        long code = query_long(query, "code", -1);
480
        long down = query_long(query, "down", -1);
481
        if (query_str(query, "text", text, sizeof text) && text[0]) {
482
            g_key_is_text = 1; g_key_value = (uint32_t)(unsigned char)text[0];
483
        } else if (code >= 0) {
484
            g_key_is_text = 0; g_key_value = (uint32_t)code;
485
        } else {
486
            send_json(fd, 400, "{\"error\":\"key needs text= or code=\"}"); return;
487
        }
488
        g_key_action = (down < 0) ? RETRO_KEY_TAP
489
                     : (down != 0) ? RETRO_KEY_DOWN : RETRO_KEY_UP;
490
        do_marshalled(fd, REQ_KEY);
491
        return;
492
    }
493
    if (!strcmp(target, "/reset")) {
494
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
495
        do_marshalled(fd, REQ_RESET);
496
        return;
961b3c61 Andy McCall 497
    }
498
    send_json(fd, 404, "{\"error\":\"not found\"}");
499
}
500
501
static void *server_main(void *arg)
502
{
503
    (void)arg;
504
    for (;;) {
505
        int fd = accept(g_listen_fd, NULL, NULL);
506
        if (fd < 0) {
507
            if (errno == EINTR) continue;
508
            break;
509
        }
510
        handle_conn(fd);
511
        close(fd);
512
    }
513
    return NULL;
514
}
515
516
int retro_control_start(int port, const retro_control_backend_t *backend)
517
{
518
    g_be = backend;
519
    signal(SIGPIPE, SIG_IGN);
520
521
    int fd = socket(AF_INET, SOCK_STREAM, 0);
522
    if (fd < 0) return -1;
523
    int one = 1;
524
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
525
526
    struct sockaddr_in addr;
527
    memset(&addr, 0, sizeof addr);
528
    addr.sin_family = AF_INET;
529
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);   /* 127.0.0.1 only */
530
    addr.sin_port = htons((uint16_t)port);
531
    if (bind(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
532
        fprintf(stderr, "[retro_control] bind 127.0.0.1:%d failed: %s\n",
533
                port, strerror(errno));
534
        close(fd);
535
        return -1;
536
    }
537
    if (listen(fd, 4) < 0) { close(fd); return -1; }
538
539
    g_listen_fd = fd;
540
    if (pthread_create(&g_thread, NULL, server_main, NULL) != 0) {
541
        close(fd); g_listen_fd = -1; return -1;
542
    }
543
    pthread_detach(g_thread);
544
    fprintf(stderr, "[retro_control] listening on 127.0.0.1:%d (contract %s)\n",
545
            port, RETRO_CONTROL_CONTRACT);
546
    return 0;
547
}
548
549
void retro_control_stop(void)
550
{
551
    if (g_listen_fd >= 0) { close(g_listen_fd); g_listen_fd = -1; }
552
}
553
554
#endif /* !_WIN32 */