@velox0/cerver 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/runtime/server.c CHANGED
@@ -1,7 +1,9 @@
1
1
  /*
2
- * server.c — Socket setup and event loop for the cerver runtime.
2
+ * server.c — Socket setup, thread pool, and event loop for the cerver runtime.
3
3
  *
4
4
  * Uses kqueue on macOS, epoll on Linux, with a select() fallback.
5
+ * Requests are dispatched to a fixed-size thread pool via a ring-buffer
6
+ * task queue protected by a mutex + condition variable.
5
7
  */
6
8
 
7
9
  #include "cerver.h"
@@ -17,6 +19,7 @@
17
19
  #include <sys/types.h>
18
20
  #include <netinet/in.h>
19
21
  #include <arpa/inet.h>
22
+ #include <pthread.h>
20
23
 
21
24
  /* Platform-specific event API */
22
25
  #if defined(__APPLE__) || defined(__FreeBSD__)
@@ -49,53 +52,122 @@ static int set_nonblocking(int fd) {
49
52
  }
50
53
 
51
54
  /* ------------------------------------------------------------------ */
52
- /* Handle a single connection: read, parse, dispatch, respond */
55
+ /* memmem fallback for systems that lack it */
53
56
  /* ------------------------------------------------------------------ */
54
57
 
55
- static int set_blocking(int fd) {
56
- int flags = fcntl(fd, F_GETFL, 0);
57
- if (flags < 0) return -1;
58
- return fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
58
+ #if !defined(__APPLE__) && !defined(__linux__) && !defined(_GNU_SOURCE)
59
+ static void *cerver_memmem(const void *hay, size_t haylen,
60
+ const void *needle, size_t nlen) {
61
+ if (nlen == 0) return (void *)hay;
62
+ if (nlen > haylen) return NULL;
63
+ const char *p = (const char *)hay;
64
+ const char *end = p + haylen - nlen;
65
+ for (; p <= end; p++) {
66
+ if (memcmp(p, needle, nlen) == 0) return (void *)p;
67
+ }
68
+ return NULL;
59
69
  }
70
+ #define memmem cerver_memmem
71
+ #endif
72
+
73
+ /* ------------------------------------------------------------------ */
74
+ /* Buffered read — accumulates until \r\n\r\n or limit reached */
75
+ /* ------------------------------------------------------------------ */
76
+
77
+ static char *read_full_request(int fd, size_t *out_len) {
78
+ size_t cap = CERVER_READ_BUF;
79
+ size_t len = 0;
80
+ char *buf = malloc(cap + 1); /* +1 for null terminator */
81
+ if (!buf) return NULL;
82
+
83
+ while (len < (size_t)CERVER_READ_BUF_MAX) {
84
+ ssize_t n = read(fd, buf + len, cap - len);
85
+ if (n <= 0) break;
86
+ len += (size_t)n;
87
+
88
+ /* Check for end of headers */
89
+ if (len >= 4 && memmem(buf, len, "\r\n\r\n", 4)) break;
90
+
91
+ /* Grow buffer if full */
92
+ if (len == cap) {
93
+ size_t newcap = cap * 2;
94
+ if (newcap > (size_t)CERVER_READ_BUF_MAX)
95
+ newcap = (size_t)CERVER_READ_BUF_MAX;
96
+ char *tmp = realloc(buf, newcap + 1);
97
+ if (!tmp) { free(buf); return NULL; }
98
+ buf = tmp;
99
+ cap = newcap;
100
+ }
101
+ }
102
+
103
+ if (len == 0) {
104
+ free(buf);
105
+ return NULL;
106
+ }
107
+
108
+ buf[len] = '\0';
109
+ *out_len = len;
110
+ return buf;
111
+ }
112
+
113
+ /* ------------------------------------------------------------------ */
114
+ /* Handle a single connection: read, parse, dispatch, respond */
115
+ /* ------------------------------------------------------------------ */
60
116
 
61
117
  static void handle_connection(cerver_server_t *srv, int client_fd) {
62
- /* Client sockets may inherit non-blocking mode from listener.
63
- Set to blocking so read() waits for data. */
64
- set_blocking(client_fd);
118
+ /* Ensure the client socket is in blocking mode for reads.
119
+ Some platforms inherit O_NONBLOCK from the listening socket. */
120
+ int flags = fcntl(client_fd, F_GETFL, 0);
121
+ if (flags >= 0 && (flags & O_NONBLOCK)) {
122
+ fcntl(client_fd, F_SETFL, flags & ~O_NONBLOCK);
123
+ }
124
+
125
+ /* Set a read timeout so workers don't block on stale connections */
126
+ struct timeval tv = { 5, 0 }; /* 5 seconds */
127
+ setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
65
128
 
66
- char buf[CERVER_READ_BUF];
67
- ssize_t n = read(client_fd, buf, sizeof(buf) - 1);
129
+ /* Read the full request with buffering */
130
+ size_t req_len = 0;
131
+ char *buf = read_full_request(client_fd, &req_len);
68
132
 
69
- if (n <= 0) {
133
+ if (!buf || req_len == 0) {
134
+ if (buf) free(buf);
70
135
  close(client_fd);
71
136
  return;
72
137
  }
73
- buf[n] = '\0';
74
138
 
75
139
  /* Parse HTTP request */
76
140
  cerver_request_t req;
77
141
  memset(&req, 0, sizeof(req));
78
142
 
79
- if (cerver_parse_request(buf, (size_t)n, &req) < 0) {
143
+ if (cerver_parse_request(buf, req_len, &req) < 0) {
80
144
  /* Bad request — send 400 */
81
145
  const char *resp = "HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\nConnection: close\r\n\r\nBad Request";
82
146
  write(client_fd, resp, strlen(resp));
147
+ free(buf);
83
148
  close(client_fd);
84
149
  return;
85
150
  }
86
151
 
152
+ /*
153
+ * The parser malloc'd its own mutable copy (req._raw_buf) and all
154
+ * internal pointers (path, headers, etc.) reference that copy.
155
+ * We can now free our original read buffer.
156
+ */
157
+ free(buf);
158
+
87
159
  /* Prepare response */
88
160
  cerver_response_t res;
89
161
  memset(&res, 0, sizeof(res));
90
162
 
91
- /* Try to dispatch to a route handler */
92
- cerver_handler_fn handler = cerver_dispatch(srv, &req);
163
+ /* Try static assets first files take priority over route handlers */
164
+ if (cerver_serve_static(srv, &req, &res) < 0) {
165
+ /* No static file found — try route handlers */
166
+ cerver_handler_fn handler = cerver_dispatch(srv, &req);
93
167
 
94
- if (handler) {
95
- handler(&req, &res);
96
- } else {
97
- /* Try static assets */
98
- if (cerver_serve_static(srv, &req, &res) < 0) {
168
+ if (handler) {
169
+ handler(&req, &res);
170
+ } else {
99
171
  /* 404 */
100
172
  cerver_res_text(&res, 404, "Not Found");
101
173
  }
@@ -115,16 +187,91 @@ static void handle_connection(cerver_server_t *srv, int client_fd) {
115
187
  close(client_fd);
116
188
  }
117
189
 
190
+ /* ------------------------------------------------------------------ */
191
+ /* Thread pool — task queue (ring buffer) */
192
+ /* ------------------------------------------------------------------ */
193
+
194
+ static int enqueue_task(cerver_server_t *srv, int client_fd) {
195
+ pthread_mutex_lock(&srv->tq_mutex);
196
+
197
+ if (srv->tq_count >= CERVER_TASK_QUEUE_SIZE) {
198
+ /* Queue full — drop connection */
199
+ pthread_mutex_unlock(&srv->tq_mutex);
200
+ const char *resp = "HTTP/1.1 503 Service Unavailable\r\n"
201
+ "Content-Length: 19\r\nConnection: close\r\n\r\n"
202
+ "Service Unavailable";
203
+ write(client_fd, resp, strlen(resp));
204
+ close(client_fd);
205
+ return -1;
206
+ }
207
+
208
+ srv->task_queue[srv->tq_tail] = client_fd;
209
+ srv->tq_tail = (srv->tq_tail + 1) % CERVER_TASK_QUEUE_SIZE;
210
+ srv->tq_count++;
211
+
212
+ pthread_cond_signal(&srv->tq_cond);
213
+ pthread_mutex_unlock(&srv->tq_mutex);
214
+ return 0;
215
+ }
216
+
217
+ static int dequeue_task(cerver_server_t *srv) {
218
+ pthread_mutex_lock(&srv->tq_mutex);
219
+
220
+ while (srv->tq_count == 0 && srv->running) {
221
+ pthread_cond_wait(&srv->tq_cond, &srv->tq_mutex);
222
+ }
223
+
224
+ if (!srv->running && srv->tq_count == 0) {
225
+ pthread_mutex_unlock(&srv->tq_mutex);
226
+ return -1; /* shutdown signal */
227
+ }
228
+
229
+ int fd = srv->task_queue[srv->tq_head];
230
+ srv->tq_head = (srv->tq_head + 1) % CERVER_TASK_QUEUE_SIZE;
231
+ srv->tq_count--;
232
+
233
+ pthread_mutex_unlock(&srv->tq_mutex);
234
+ return fd;
235
+ }
236
+
237
+ /* ------------------------------------------------------------------ */
238
+ /* Worker thread entry point */
239
+ /* ------------------------------------------------------------------ */
240
+
241
+ static void *worker_thread(void *arg) {
242
+ cerver_server_t *srv = (cerver_server_t *)arg;
243
+
244
+ while (srv->running) {
245
+ int client_fd = dequeue_task(srv);
246
+ if (client_fd < 0) break; /* shutdown */
247
+
248
+ handle_connection(srv, client_fd);
249
+ }
250
+
251
+ return NULL;
252
+ }
253
+
118
254
  /* ------------------------------------------------------------------ */
119
255
  /* Server init */
120
256
  /* ------------------------------------------------------------------ */
121
257
 
122
- int cerver_init(cerver_server_t *srv, int port) {
258
+ int cerver_init(cerver_server_t *srv, int port, int threads) {
123
259
  memset(srv, 0, sizeof(*srv));
124
260
  srv->port = port;
125
261
  srv->sock_fd = -1;
126
262
  srv->running = 0;
127
263
  srv->public_dir = NULL;
264
+
265
+ /* Thread pool config */
266
+ srv->thread_count = (threads > 0) ? threads : CERVER_THREAD_POOL_DEFAULT;
267
+ srv->threads = NULL;
268
+ srv->tq_head = 0;
269
+ srv->tq_tail = 0;
270
+ srv->tq_count = 0;
271
+
272
+ pthread_mutex_init(&srv->tq_mutex, NULL);
273
+ pthread_cond_init(&srv->tq_cond, NULL);
274
+
128
275
  return 0;
129
276
  }
130
277
 
@@ -144,6 +291,43 @@ void cerver_set_public_dir(cerver_server_t *srv, const char *dir) {
144
291
  srv->public_dir = dir;
145
292
  }
146
293
 
294
+ /* ------------------------------------------------------------------ */
295
+ /* Start thread pool */
296
+ /* ------------------------------------------------------------------ */
297
+
298
+ static int start_thread_pool(cerver_server_t *srv) {
299
+ srv->threads = malloc(sizeof(pthread_t) * (size_t)srv->thread_count);
300
+ if (!srv->threads) {
301
+ perror("cerver: malloc threads");
302
+ return -1;
303
+ }
304
+
305
+ /* Use 2 MB stack per worker (handles deep call chains with large buffers) */
306
+ pthread_attr_t attr;
307
+ pthread_attr_init(&attr);
308
+ pthread_attr_setstacksize(&attr, 2 * 1024 * 1024);
309
+
310
+ for (int i = 0; i < srv->thread_count; i++) {
311
+ if (pthread_create(&srv->threads[i], &attr, worker_thread, srv) != 0) {
312
+ perror("cerver: pthread_create");
313
+ /* Clean up already-created threads */
314
+ srv->running = 0;
315
+ pthread_cond_broadcast(&srv->tq_cond);
316
+ for (int j = 0; j < i; j++) {
317
+ pthread_join(srv->threads[j], NULL);
318
+ }
319
+ free(srv->threads);
320
+ srv->threads = NULL;
321
+ return -1;
322
+ }
323
+ }
324
+
325
+ pthread_attr_destroy(&attr);
326
+
327
+ printf("cerver: started %d worker thread(s)\n", srv->thread_count);
328
+ return 0;
329
+ }
330
+
147
331
  /* ------------------------------------------------------------------ */
148
332
  /* Server listen — event loop */
149
333
  /* ------------------------------------------------------------------ */
@@ -190,7 +374,13 @@ int cerver_listen(cerver_server_t *srv) {
190
374
 
191
375
  srv->running = 1;
192
376
 
193
- printf("cerver: listening on http://0.0.0.0:%d\n", srv->port);
377
+ printf("cerver: listening on http://localhost:%d\n", srv->port);
378
+
379
+ /* Start the thread pool */
380
+ if (start_thread_pool(srv) < 0) {
381
+ close(srv->sock_fd);
382
+ return -1;
383
+ }
194
384
 
195
385
  /* ================================================================== */
196
386
  /* kqueue event loop (macOS / FreeBSD) */
@@ -236,7 +426,7 @@ int cerver_listen(cerver_server_t *srv) {
236
426
  perror("cerver: accept");
237
427
  break;
238
428
  }
239
- handle_connection(srv, client_fd);
429
+ enqueue_task(srv, client_fd);
240
430
  }
241
431
  }
242
432
  }
@@ -285,7 +475,7 @@ int cerver_listen(cerver_server_t *srv) {
285
475
  perror("cerver: accept");
286
476
  break;
287
477
  }
288
- handle_connection(srv, client_fd);
478
+ enqueue_task(srv, client_fd);
289
479
  }
290
480
  }
291
481
  }
@@ -319,7 +509,7 @@ int cerver_listen(cerver_server_t *srv) {
319
509
  (struct sockaddr *)&client_addr,
320
510
  &client_len);
321
511
  if (client_fd >= 0) {
322
- handle_connection(srv, client_fd);
512
+ enqueue_task(srv, client_fd);
323
513
  }
324
514
  }
325
515
  }
@@ -335,10 +525,31 @@ int cerver_listen(cerver_server_t *srv) {
335
525
  /* ------------------------------------------------------------------ */
336
526
 
337
527
  void cerver_shutdown(cerver_server_t *srv) {
528
+ srv->running = 0;
529
+
530
+ /* Wake all worker threads so they can exit */
531
+ pthread_mutex_lock(&srv->tq_mutex);
532
+ pthread_cond_broadcast(&srv->tq_cond);
533
+ pthread_mutex_unlock(&srv->tq_mutex);
534
+
535
+ /* Join all worker threads */
536
+ if (srv->threads) {
537
+ for (int i = 0; i < srv->thread_count; i++) {
538
+ pthread_join(srv->threads[i], NULL);
539
+ }
540
+ free(srv->threads);
541
+ srv->threads = NULL;
542
+ }
543
+
544
+ /* Close listener socket */
338
545
  if (srv->sock_fd >= 0) {
339
546
  close(srv->sock_fd);
340
547
  srv->sock_fd = -1;
341
548
  }
342
- srv->running = 0;
549
+
550
+ /* Destroy synchronization primitives */
551
+ pthread_mutex_destroy(&srv->tq_mutex);
552
+ pthread_cond_destroy(&srv->tq_cond);
553
+
343
554
  printf("\ncerver: server stopped\n");
344
555
  }
package/runtime/static.c CHANGED
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * In embedded mode, serves from the compiled-in asset array.
5
5
  * In external mode, serves from the filesystem (public/ directory).
6
+ * Supports pre-compressed gzip/brotli variants and cache headers.
6
7
  */
7
8
 
8
9
  #include "cerver.h"
@@ -29,6 +30,40 @@ static int path_is_safe(const char *path) {
29
30
  return 1;
30
31
  }
31
32
 
33
+ /* ------------------------------------------------------------------ */
34
+ /* Accept-Encoding parsing */
35
+ /* ------------------------------------------------------------------ */
36
+
37
+ typedef struct {
38
+ int accepts_gzip;
39
+ int accepts_br;
40
+ } encoding_prefs_t;
41
+
42
+ static encoding_prefs_t parse_accept_encoding(const cerver_request_t *req) {
43
+ encoding_prefs_t prefs = { 0, 0 };
44
+ const char *ae = cerver_req_header(req, "Accept-Encoding");
45
+ if (!ae) return prefs;
46
+
47
+ if (strstr(ae, "br")) prefs.accepts_br = 1;
48
+ if (strstr(ae, "gzip")) prefs.accepts_gzip = 1;
49
+
50
+ return prefs;
51
+ }
52
+
53
+ /* ------------------------------------------------------------------ */
54
+ /* Cache header helper */
55
+ /* ------------------------------------------------------------------ */
56
+
57
+ static void add_cache_headers(cerver_response_t *res, const char *path) {
58
+ /* Hashed/versioned assets (in /static/) get long cache */
59
+ if (strstr(path, "/static/") || strstr(path, "/assets/")) {
60
+ cerver_res_header(res, "Cache-Control", "public, max-age=31536000, immutable");
61
+ } else {
62
+ /* HTML and other top-level files get short cache with revalidation */
63
+ cerver_res_header(res, "Cache-Control", "public, max-age=3600, must-revalidate");
64
+ }
65
+ }
66
+
32
67
  /* ------------------------------------------------------------------ */
33
68
  /* Serve from embedded assets */
34
69
  /* ------------------------------------------------------------------ */
@@ -38,33 +73,59 @@ static int serve_embedded(cerver_server_t *srv, cerver_request_t *req,
38
73
  if (!srv->assets || srv->asset_count == 0) return -1;
39
74
 
40
75
  const char *path = req->path;
76
+ const cerver_asset_t *found = NULL;
41
77
 
42
78
  /* Try exact match first */
43
79
  for (int i = 0; i < srv->asset_count; i++) {
44
80
  if (strcmp(srv->assets[i].path, path) == 0) {
45
- cerver_res_file(res, 200, srv->assets[i].mime_type,
46
- srv->assets[i].data, srv->assets[i].data_len);
47
- return 0;
81
+ found = &srv->assets[i];
82
+ break;
48
83
  }
49
84
  }
50
85
 
51
86
  /* Try with /index.html appended (for directory-like paths) */
52
- char index_path[CERVER_MAX_PATH];
53
- if (path[strlen(path) - 1] == '/') {
54
- snprintf(index_path, sizeof(index_path), "%sindex.html", path);
55
- } else {
56
- snprintf(index_path, sizeof(index_path), "%s/index.html", path);
57
- }
87
+ if (!found) {
88
+ char index_path[CERVER_MAX_PATH];
89
+ if (path[strlen(path) - 1] == '/') {
90
+ snprintf(index_path, sizeof(index_path), "%sindex.html", path);
91
+ } else {
92
+ snprintf(index_path, sizeof(index_path), "%s/index.html", path);
93
+ }
58
94
 
59
- for (int i = 0; i < srv->asset_count; i++) {
60
- if (strcmp(srv->assets[i].path, index_path) == 0) {
61
- cerver_res_file(res, 200, srv->assets[i].mime_type,
62
- srv->assets[i].data, srv->assets[i].data_len);
63
- return 0;
95
+ for (int i = 0; i < srv->asset_count; i++) {
96
+ if (strcmp(srv->assets[i].path, index_path) == 0) {
97
+ found = &srv->assets[i];
98
+ break;
99
+ }
64
100
  }
65
101
  }
66
102
 
67
- return -1;
103
+ if (!found) return -1;
104
+
105
+ /* Check for pre-compressed variants */
106
+ encoding_prefs_t enc = parse_accept_encoding(req);
107
+
108
+ if (enc.accepts_br && found->data_br && found->data_br_len > 0) {
109
+ /* Serve brotli */
110
+ cerver_res_file(res, 200, found->mime_type,
111
+ found->data_br, found->data_br_len);
112
+ cerver_res_header(res, "Content-Encoding", "br");
113
+ cerver_res_header(res, "Vary", "Accept-Encoding");
114
+ } else if (enc.accepts_gzip && found->data_gz && found->data_gz_len > 0) {
115
+ /* Serve gzip */
116
+ cerver_res_file(res, 200, found->mime_type,
117
+ found->data_gz, found->data_gz_len);
118
+ cerver_res_header(res, "Content-Encoding", "gzip");
119
+ cerver_res_header(res, "Vary", "Accept-Encoding");
120
+ } else {
121
+ /* Serve uncompressed */
122
+ cerver_res_file(res, 200, found->mime_type,
123
+ found->data, found->data_len);
124
+ }
125
+
126
+ add_cache_headers(res, found->path);
127
+
128
+ return 0;
68
129
  }
69
130
 
70
131
  /* ------------------------------------------------------------------ */
@@ -123,6 +184,8 @@ static int serve_filesystem(cerver_server_t *srv, cerver_request_t *req,
123
184
  res->body_len = file_size;
124
185
  res->_body_owned = 1; /* We malloc'd this */
125
186
 
187
+ add_cache_headers(res, path);
188
+
126
189
  return 0;
127
190
  }
128
191