@velox0/cerver 0.2.0 → 0.3.1
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/README.md +5 -1
- package/lib/assets/embed.js +16 -0
- package/lib/codegen/generator.js +1 -1
- package/lib/commands/new.js +92 -2
- package/lib/compiler/compile.js +1 -0
- package/lib/config.js +4 -0
- package/package.json +1 -1
- package/runtime/cerver.h +16 -1
- package/runtime/server.c +238 -27
- package/templates/cerver.png +0 -0
- package/templates/favicon.ico +0 -0
- package/test/run.js +2 -0
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="templates/cerver.png" alt="Cerver Logo" width="120" />
|
|
3
|
+
</div>
|
|
4
|
+
|
|
1
5
|
# Cerver
|
|
2
6
|
|
|
3
7
|
A lightweight, compile-time web framework that transpiles restricted JavaScript server logic into highly optimized native C HTTP server binaries.
|
|
4
8
|
|
|
5
|
-
Cerver takes a Next.js-style file-based routing structure (written in a strict subset of JavaScript), parses it, generates equivalent C code, embeds your static assets, and compiles it all into a single, standalone executable
|
|
9
|
+
Cerver takes a Next.js-style file-based routing structure (written in a strict subset of JavaScript), parses it, generates equivalent C code, embeds your static assets, and compiles it all into a single, standalone executable that runs with zero Node.js dependency.
|
|
6
10
|
|
|
7
11
|
## Features
|
|
8
12
|
|
package/lib/assets/embed.js
CHANGED
|
@@ -154,6 +154,22 @@ async function generateEmbeddedAssets(assets, shouldMinify, compression) {
|
|
|
154
154
|
brName,
|
|
155
155
|
brLen,
|
|
156
156
|
});
|
|
157
|
+
|
|
158
|
+
/* Auto-alias: /any/path/index.html → /any/path */
|
|
159
|
+
if (asset.servePath.endsWith("/index.html")) {
|
|
160
|
+
const dirPath = asset.servePath === "/index.html"
|
|
161
|
+
? "/"
|
|
162
|
+
: asset.servePath.slice(0, -"/index.html".length);
|
|
163
|
+
assetEntries.push({
|
|
164
|
+
name,
|
|
165
|
+
servePath: dirPath,
|
|
166
|
+
mime,
|
|
167
|
+
gzName,
|
|
168
|
+
gzLen,
|
|
169
|
+
brName,
|
|
170
|
+
brLen,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
/* Generate the asset table */
|
package/lib/codegen/generator.js
CHANGED
|
@@ -85,7 +85,7 @@ function generateServer(routes, config, assetsCode) {
|
|
|
85
85
|
lines.push(" if (env_port) port = atoi(env_port);");
|
|
86
86
|
lines.push("");
|
|
87
87
|
lines.push(" cerver_server_t srv;");
|
|
88
|
-
lines.push(
|
|
88
|
+
lines.push(` cerver_init(&srv, port, ${config.threads});`);
|
|
89
89
|
lines.push("");
|
|
90
90
|
lines.push(
|
|
91
91
|
" cerver_add_routes(&srv, cerver_routes, cerver_route_count);"
|
package/lib/commands/new.js
CHANGED
|
@@ -53,15 +53,103 @@ function newProject(name) {
|
|
|
53
53
|
<meta charset="utf-8">
|
|
54
54
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
55
55
|
<title>${name}</title>
|
|
56
|
+
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
|
57
|
+
<style>
|
|
58
|
+
:root {
|
|
59
|
+
--bg-color: #0f172a;
|
|
60
|
+
--text-color: #f8fafc;
|
|
61
|
+
--accent-color: #38bdf8;
|
|
62
|
+
--card-bg: rgba(30, 41, 59, 0.7);
|
|
63
|
+
}
|
|
64
|
+
body {
|
|
65
|
+
margin: 0;
|
|
66
|
+
padding: 0;
|
|
67
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
68
|
+
background-color: var(--bg-color);
|
|
69
|
+
color: var(--text-color);
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
min-height: 100vh;
|
|
75
|
+
background: radial-gradient(circle at top right, #1e293b, #0f172a);
|
|
76
|
+
}
|
|
77
|
+
.container {
|
|
78
|
+
background: var(--card-bg);
|
|
79
|
+
backdrop-filter: blur(12px);
|
|
80
|
+
-webkit-backdrop-filter: blur(12px);
|
|
81
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
82
|
+
padding: 3rem;
|
|
83
|
+
border-radius: 16px;
|
|
84
|
+
text-align: center;
|
|
85
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
86
|
+
max-width: 500px;
|
|
87
|
+
width: 90%;
|
|
88
|
+
animation: fadeUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
89
|
+
opacity: 0;
|
|
90
|
+
transform: translateY(20px);
|
|
91
|
+
}
|
|
92
|
+
@keyframes fadeUp {
|
|
93
|
+
to { opacity: 1; transform: translateY(0); }
|
|
94
|
+
}
|
|
95
|
+
.logo {
|
|
96
|
+
width: 120px;
|
|
97
|
+
height: 120px;
|
|
98
|
+
margin-bottom: 1.5rem;
|
|
99
|
+
border-radius: 24%;
|
|
100
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
|
|
101
|
+
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
102
|
+
}
|
|
103
|
+
.logo:hover {
|
|
104
|
+
transform: scale(1.08) rotate(-3deg);
|
|
105
|
+
}
|
|
106
|
+
h1 {
|
|
107
|
+
margin: 0 0 1rem 0;
|
|
108
|
+
font-size: 2.5rem;
|
|
109
|
+
font-weight: 700;
|
|
110
|
+
letter-spacing: -0.025em;
|
|
111
|
+
}
|
|
112
|
+
p {
|
|
113
|
+
margin: 0;
|
|
114
|
+
color: #94a3b8;
|
|
115
|
+
font-size: 1.125rem;
|
|
116
|
+
line-height: 1.6;
|
|
117
|
+
}
|
|
118
|
+
.badge {
|
|
119
|
+
display: inline-block;
|
|
120
|
+
margin-top: 2rem;
|
|
121
|
+
padding: 0.5rem 1rem;
|
|
122
|
+
background: rgba(56, 189, 248, 0.1);
|
|
123
|
+
color: var(--accent-color);
|
|
124
|
+
border-radius: 9999px;
|
|
125
|
+
font-size: 0.875rem;
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
border: 1px solid rgba(56, 189, 248, 0.2);
|
|
128
|
+
}
|
|
129
|
+
</style>
|
|
56
130
|
</head>
|
|
57
131
|
<body>
|
|
58
|
-
<
|
|
59
|
-
|
|
132
|
+
<div class="container">
|
|
133
|
+
<img src="/cerver.png" alt="cerver logo" class="logo">
|
|
134
|
+
<h1>${name}</h1>
|
|
135
|
+
<p>Your ultra-fast, native web application is running.</p>
|
|
136
|
+
<div class="badge">Powered by cerver</div>
|
|
137
|
+
</div>
|
|
60
138
|
</body>
|
|
61
139
|
</html>
|
|
62
140
|
`
|
|
63
141
|
);
|
|
64
142
|
|
|
143
|
+
// Copy standard static assets
|
|
144
|
+
fs.copyFileSync(
|
|
145
|
+
path.join(templatesDir, "cerver.png"),
|
|
146
|
+
path.join(projectDir, "public", "cerver.png")
|
|
147
|
+
);
|
|
148
|
+
fs.copyFileSync(
|
|
149
|
+
path.join(templatesDir, "favicon.ico"),
|
|
150
|
+
path.join(projectDir, "public", "favicon.ico")
|
|
151
|
+
);
|
|
152
|
+
|
|
65
153
|
// package.json
|
|
66
154
|
fs.writeFileSync(
|
|
67
155
|
path.join(projectDir, "package.json"),
|
|
@@ -83,6 +171,8 @@ function newProject(name) {
|
|
|
83
171
|
console.log(" Created:");
|
|
84
172
|
console.log(" app/routes/index.js");
|
|
85
173
|
console.log(" public/index.html");
|
|
174
|
+
console.log(" public/cerver.png");
|
|
175
|
+
console.log(" public/favicon.ico");
|
|
86
176
|
console.log(" cerver.config.js");
|
|
87
177
|
console.log(" package.json");
|
|
88
178
|
console.log("");
|
package/lib/compiler/compile.js
CHANGED
package/lib/config.js
CHANGED
|
@@ -8,6 +8,7 @@ const DEFAULTS = {
|
|
|
8
8
|
embed: true,
|
|
9
9
|
minify: true,
|
|
10
10
|
compression: "none",
|
|
11
|
+
threads: 4,
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -45,6 +46,9 @@ function loadConfig(projectDir) {
|
|
|
45
46
|
if (!["none", "gzip", "brotli", "both"].includes(config.compression)) {
|
|
46
47
|
throw new Error(`cerver: unsupported compression "${config.compression}"`);
|
|
47
48
|
}
|
|
49
|
+
if (!Number.isInteger(config.threads) || config.threads < 1 || config.threads > 64) {
|
|
50
|
+
throw new Error(`cerver: invalid thread count ${config.threads} (must be 1-64)`);
|
|
51
|
+
}
|
|
48
52
|
|
|
49
53
|
return config;
|
|
50
54
|
}
|
package/package.json
CHANGED
package/runtime/cerver.h
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
#include <stddef.h>
|
|
13
13
|
#include <stdint.h>
|
|
14
|
+
#include <pthread.h>
|
|
14
15
|
|
|
15
16
|
/* ------------------------------------------------------------------ */
|
|
16
17
|
/* Limits */
|
|
@@ -22,8 +23,12 @@
|
|
|
22
23
|
#define CERVER_MAX_PATH 2048
|
|
23
24
|
#define CERVER_MAX_HEADER_VAL 4096
|
|
24
25
|
#define CERVER_READ_BUF 8192
|
|
26
|
+
#define CERVER_READ_BUF_MAX (1 << 20) /* 1 MB hard limit */
|
|
25
27
|
#define CERVER_MAX_ROUTES 256
|
|
26
28
|
|
|
29
|
+
#define CERVER_THREAD_POOL_DEFAULT 4
|
|
30
|
+
#define CERVER_TASK_QUEUE_SIZE 256
|
|
31
|
+
|
|
27
32
|
/* ------------------------------------------------------------------ */
|
|
28
33
|
/* Key-value pair (used for headers, query params, route params) */
|
|
29
34
|
/* ------------------------------------------------------------------ */
|
|
@@ -146,10 +151,20 @@ typedef struct {
|
|
|
146
151
|
int asset_count;
|
|
147
152
|
const char *public_dir; /* NULL if embedded mode */
|
|
148
153
|
volatile int running;
|
|
154
|
+
|
|
155
|
+
/* Thread pool */
|
|
156
|
+
int thread_count;
|
|
157
|
+
pthread_t *threads;
|
|
158
|
+
int task_queue[CERVER_TASK_QUEUE_SIZE];
|
|
159
|
+
int tq_head;
|
|
160
|
+
int tq_tail;
|
|
161
|
+
int tq_count;
|
|
162
|
+
pthread_mutex_t tq_mutex;
|
|
163
|
+
pthread_cond_t tq_cond;
|
|
149
164
|
} cerver_server_t;
|
|
150
165
|
|
|
151
166
|
/* Server lifecycle */
|
|
152
|
-
int cerver_init(cerver_server_t *srv, int port);
|
|
167
|
+
int cerver_init(cerver_server_t *srv, int port, int threads);
|
|
153
168
|
int cerver_add_routes(cerver_server_t *srv, cerver_route_t *routes, int count);
|
|
154
169
|
int cerver_set_assets(cerver_server_t *srv, cerver_asset_t *assets, int count);
|
|
155
170
|
void cerver_set_public_dir(cerver_server_t *srv, const char *dir);
|
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
|
-
/*
|
|
55
|
+
/* memmem fallback for systems that lack it */
|
|
53
56
|
/* ------------------------------------------------------------------ */
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
/*
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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 (
|
|
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,
|
|
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
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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://
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
Binary file
|
|
Binary file
|
package/test/run.js
CHANGED
|
@@ -198,6 +198,7 @@ test("loadConfig merges defaults and supports export default configs", () => {
|
|
|
198
198
|
embed: true,
|
|
199
199
|
minify: true,
|
|
200
200
|
compression: "none",
|
|
201
|
+
threads: 4,
|
|
201
202
|
});
|
|
202
203
|
|
|
203
204
|
writeFile(
|
|
@@ -210,6 +211,7 @@ test("loadConfig merges defaults and supports export default configs", () => {
|
|
|
210
211
|
embed: false,
|
|
211
212
|
minify: false,
|
|
212
213
|
compression: "gzip",
|
|
214
|
+
threads: 4,
|
|
213
215
|
});
|
|
214
216
|
} finally {
|
|
215
217
|
cleanup(dir);
|