Compare commits

...

26 Commits

Author SHA1 Message Date
e88dc0426a Add more logging 2025-07-06 00:19:41 +02:00
5ee7e02f7a blog: Asset packing with zip.c - add images 2025-06-28 14:13:27 +02:00
e1f0d04409 blog: Asset packing with zip.c 2025-06-28 14:05:11 +02:00
48f4a73b0a Remove needless mutex locks and unlocks 2025-06-28 02:16:42 +02:00
2e70abd0de Add compile_flags.txt, bundle.zip, commit.h to cleaning 2025-06-28 01:50:25 +02:00
b3a5860beb Minify simple css 2025-06-28 01:48:12 +02:00
486952dcbf Prefix blogs with blog-, fix minor leaks 2025-06-28 01:38:39 +02:00
2988a5b3d6 Rework memory management, use per-thread arenas 2025-06-28 01:29:49 +02:00
2badc915d6 Pack the assets into a ZIP bundle 2025-06-27 20:11:42 +02:00
447362c74d dark-easy theme 2025-06-26 14:37:52 +02:00
7a9d5aa371 Self-host marked.js 2025-06-26 00:11:14 +02:00
d9071a4947 Rename etc dump to bakedres dump 2025-06-26 00:07:12 +02:00
632d731118 Self-host highlight.js 2025-06-25 23:58:47 +02:00
bbd23cc8ba Add highlight.js 2025-06-25 23:19:10 +02:00
037b0a123a Themes and a theme picker 2025-06-25 22:47:07 +02:00
ab37785d2a add meta tags 2025-06-25 21:12:42 +02:00
4254c66aa3 Fix: make static pthread mutecies actually use static storage class 2025-06-25 02:47:29 +02:00
06574d86c9 Listen on localhost in release mode 2025-06-25 01:15:32 +02:00
7d911d73fa release port 5000 2025-06-25 00:51:05 +02:00
cb6ec2caa0 The making of aboba: fix wording 2025-06-23 20:47:38 +02:00
2f5465a0a0 The making of aboba article 2025-06-23 17:38:11 +02:00
e230bb01c0 Fix: store route_hashtable keys on the heap 2025-06-22 15:54:10 +02:00
68dcc80d1c Change static assets to /etc/* 2025-06-22 15:34:48 +02:00
5ee77b4628 Protect global data with locks 2025-06-22 15:32:13 +02:00
a7f8ebea09 Static assets finally 2025-06-22 14:57:24 +02:00
e04e6c135e Refer to The Lair's gebs 2025-06-20 17:15:27 +02:00
34 changed files with 2675 additions and 1142 deletions

2
.gitignore vendored
View File

@ -4,3 +4,5 @@ build
gpp1
watcher
commit.h
compile_flags.txt
bundle.zip

5
.gitmodules vendored
View File

@ -3,7 +3,7 @@
url = https://github.com/cesanta/mongoose.git
[submodule "gebs"]
path = gebs
url = https://gitlab.com/kamkow1/gebs.git
url = http://git.kamkow1lair.pl/kamkow1/gebs.git
[submodule "incbin"]
path = incbin
url = https://github.com/graphitemaster/incbin.git
@ -16,3 +16,6 @@
[submodule "md5-c"]
path = md5-c
url = https://github.com/Zunawe/md5-c.git
[submodule "zip"]
path = zip
url = https://github.com/kuba--/zip.git

View File

@ -3,3 +3,6 @@
./gpp1
./watcher
./mongoose.o
./bundle.zip
./compile_flags.txt
./commit.h

View File

@ -4,7 +4,9 @@
#if MY_DEBUG
# define CONFIG_LISTEN_URL "http://localhost:8080"
#else
# define CONFIG_LISTEN_URL "http://0.0.0.0:80"
# define CONFIG_LISTEN_URL "http://localhost:5000"
#endif
#define BUNDLE_ZIP_COMPRESSION 10
#endif // CONFIG_H_

84
baked.c
View File

@ -3,28 +3,15 @@
#include "gebs/gebs.h"
#include "incbin/incbin.h"
#include "stb/stb_ds.h"
#include "zip.h"
#include "baked.h"
#include "locked.h"
#include "CONFIG.h"
INCBIN(gpp1, "./gpp1");
INCBIN(bundle_zip, "./bundle.zip");
INCBIN(home_html, "./tmpls/home.html");
INCBIN(page_missing_html, "./tmpls/page-missing.html");
INCBIN(template_blog_html, "./tmpls/template-blog.html");
INCBIN(blog_html, "./tmpls/blog.html");
INCBIN(simple_css, "./etc/simple.css");
INCBIN(favicon_ico, "./etc/favicon.ico");
#if MY_DEBUG
INCBIN(hotreload_js, "./etc/hotreload.js");
#endif
INCBIN(me_jpg, "./etc/me.jpg");
INCBIN(blog_welcome_md, "./blog/welcome.md");
INCBIN(blog_weird_page_md, "./blog/weird-page.md");
INCBIN(blog_curious_case_of_gebs_md, "./blog/curious-case-of-gebs.md");
Baked_Resource *baked_resources = NULL;
static locked(Baked_Resource *) baked_resources = locked_init(nil);
void add_baked_resource(char *key, const uchar *data, size_t size)
{
@ -34,49 +21,62 @@ void add_baked_resource(char *key, const uchar *data, size_t size)
abort();
}
write(fd, data, size);
shput(baked_resources, key, fd);
shput(baked_resources.value, key, ((Baked_Resource_Value){ .memfd = fd, .bufptr = (void *)data }));
LOGI("Added baked resource: %s\n", key);
}
void init_baked_resources(void)
{
add_baked_resource("home.html", home_html_data, home_html_size);
add_baked_resource("page-missing.html", page_missing_html_data, page_missing_html_size);
add_baked_resource("template-blog.html", template_blog_html_data, template_blog_html_size);
add_baked_resource("blog.html", blog_html_data, blog_html_size);
add_baked_resource("gpp1", gpp1_data, gpp1_size);
add_baked_resource("simple.css", simple_css_data, simple_css_size);
add_baked_resource("favicon.ico", favicon_ico_data, favicon_ico_size);
#if MY_DEBUG
add_baked_resource("hotreload.js", hotreload_js_data, hotreload_js_size);
#endif
add_baked_resource("me.jpg", me_jpg_data, me_jpg_size);
add_baked_resource("blog-welcome.md", blog_welcome_md_data, blog_welcome_md_size);
add_baked_resource("blog-weird-page.md", blog_weird_page_md_data, blog_weird_page_md_size);
add_baked_resource("blog-curious-case-of-gebs.md", blog_curious_case_of_gebs_md_data, blog_curious_case_of_gebs_md_size);
LOGI("Initializing baked resources...\n");
struct zip_t *zip = zip_stream_open(bundle_zip_data, bundle_zip_size, BUNDLE_ZIP_COMPRESSION, 'r');
size_t n = zip_entries_total(zip);
for (size_t i = 0; i < n; i++) {
zip_entry_openbyindex(zip, i);
const char *name = strdup(zip_entry_name(zip));
size_t size = zip_entry_size(zip);
char *buf = malloc(size);
zip_entry_noallocread(zip, buf, size);
add_baked_resource((char *)name, buf, size);
zip_entry_close(zip);
}
zip_stream_close(zip);
LOGI("baked resources done\n");
}
void free_baked_resources(void)
{
for (size_t i = 0; i < shlen(baked_resources); i++) {
close(baked_resources[i].value);
LOGI("freeing baked resouces\n");
for (size_t i = 0; i < shlen(baked_resources.value); i++) {
close(baked_resources.value[i].value.memfd);
free(baked_resources.value[i].key);
free(baked_resources.value[i].value.bufptr);
}
shfree(baked_resources);
shfree(baked_resources.value);
LOGI("baked resources done\n");
}
bool get_baked_resource_path(char *key, char *buf, size_t size)
{
if (shgeti(baked_resources, key) != -1) {
int fd = shget(baked_resources, key);
snprintf(buf, size, "/proc/%d/fd/%d", getpid(), fd);
LOGI("Request for baked resource path: %s\n", key);
lockx(&baked_resources);
if (shgeti(baked_resources.value, key) != -1) {
Baked_Resource_Value brv = shget(baked_resources.value, key);
snprintf(buf, size, "/proc/%d/fd/%d", getpid(), brv.memfd);
LOGI("Memfd path %s\n", buf);
unlockx(&baked_resources);
return true;
}
unlockx(&baked_resources);
return false;
}
void baked_resource_each(void (*f)(Baked_Resource *resource))
void baked_resource_each(void (*f)(Baked_Resource *resource, void *udata), void *udata)
{
for (size_t i = 0; i < shlen(baked_resources); i++) {
f(&baked_resources[i]);
for (size_t i = 0; i < shlen(baked_resources.value); i++) {
f(&baked_resources.value[i], udata);
}
}

27
baked.h
View File

@ -1,34 +1,19 @@
#ifndef BAKED_H_
#define BAKED_H_
#include "incbin/incbin.h"
INCBIN_EXTERN(gpp1);
INCBIN_EXTERN(home_html);
INCBIN_EXTERN(page_missing_html);
INCBIN_EXTERN(template_blog_html);
INCBIN_EXTERN(blog_html);
INCBIN_EXTERN(simple_css);
INCBIN_EXTERN(favicon_ico);
#if MY_DEBUG
INCBIN_EXTERN(hotreload_js);
#endif
INCBIN_EXTERN(me_jpg);
INCBIN_EXTERN(blog_welcome_md);
INCBIN_EXTERN(blog_weird_page_md);
INCBIN_EXTERN(blog_curious_case_of_gebs_md);
typedef struct {
int memfd;
void *bufptr;
} Baked_Resource_Value;
typedef struct {
char *key; // path
int value; // memfd
Baked_Resource_Value value;
} Baked_Resource;
void init_baked_resources(void);
void free_baked_resources(void);
bool get_baked_resource_path(char *key, char *buf, size_t size);
void baked_resource_each(void (*f)(Baked_Resource *resource));
void baked_resource_each(void (*f)(Baked_Resource *resource, void *udata), void *udata);
#endif // BAKED_H_

View File

@ -0,0 +1,273 @@
# Asset packing with [zip.c](https://raw.githubusercontent.com/kuba--/zip/refs/heads/master/src/zip.c)
One of the technical decisions that I had to make when writing this website is the way that I'm going to
distribute the assets (images, GPP, page templates, etc.). I've decided to go with packing or baking-in
the assets into the main binary, so that I don't have to worry about updating an asset folder alongside
the application.
One flaw of this approach is that as the website gets bigger, the binary will too. Fortunately I don't
have to worry about this too much right now, since I've reimplemented asset packing with the excellent
zip library by \`kuba--\` (Polska GUROM!!!111!!). In this article I'd like to demonstrate how I've
minimised the size of our assets using \`zip\` and \`incbin\`.
![zip file](/bakedres/apwz-zip-icon.png)
## References
- zip: https://raw.githubusercontent.com/kuba--/zip/refs/heads/master/src/zip.c
- incbin: https://github.com/graphitemaster/incbin
- zip format: https://en.wikipedia.org/wiki/ZIP_(file_format)
- the source code: https://git.kamkow1lair.pl/kamkow1/aboba
## Generating the bundle - \`bundle.zip\`
To compress our assets, first we need to generate a bundle file. I've decided to call it bundle.zip - simple
and descriptive.
Generating the bundle requires some changes to our build program. See https://git.kamkow1lair.pl/kamkow1/aboba/src/branch/master/build.c for reference.
\`\`\`
#define BUNDLED_FILES \\
"./gpp1", \\
"./tmpls/home.html", \\
"./tmpls/page-missing.html", \\
"./tmpls/template-blog.html", \\
"./tmpls/blog.html", \\
"./etc/hotreload.js", \\
"./etc/theme.js", \\
"./etc/simple.css", \\
"./etc/highlight.js", \\
"./etc/hljs-rainbow.css", \\
"./etc/marked.js", \\
"./etc/favicon.ico", \\
"./etc/me.jpg", \\
"./etc/tmoa-engine.jpg", \\
"./etc/tmoa-garbage.jpg", \\
"./blog/blog-welcome.md", \\
"./blog/blog-weird-page.md", \\
"./blog/blog-curious-case-of-gebs.md", \\
"./blog/blog-the-making-of-aboba.md", \\
"./blog/blog-asset-packing-with-zip.c.md"
const char *bundle_zip_deps[] = { BUNDLED_FILES };
RULE_ARRAY("./bundle.zip", bundle_zip_deps) {
RULE("./gpp1", "./gpp/gpp.c") {
CMD("cc", "-DHAVE_STRDUP", "-DHAVE_FNMATCH_H", "-o", "gpp1", "gpp/gpp.c");
}
struct zip_t *zip = zip_open("./bundle.zip", BUNDLE_ZIP_COMPRESSION, 'w');
defer { zip_close(zip); }
for (size_t i = 0; i < sizeof(bundle_zip_deps)/sizeof(bundle_zip_deps[0]); i++) {
char *copy = strdup(bundle_zip_deps[i]);
defer { free(copy); }
char *name = basename(copy);
String_Builder sb = {0};
defer { sb_free(&sb); }
sb_read_file(&sb, bundle_zip_deps[i]);
zip_entry_open(zip, name);
zip_entry_write(zip, sb.items, sb.count);
zip_entry_close(zip);
}
LOGI("Generated bundle.zip\\n");
}
RULE("./aboba",
"./main.c",
"./routes.c",
"./routes.h",
"./baked.c",
"./baked.h",
"./commit.h",
"./timer.c",
"./timer.h",
"./CONFIG.h",
"./locked.h",
"./mongoose.o",
"./bundle.zip",
BUNDLED_FILES
) {
// build mongoose.o - skipped
// Generate commit.h - skipped
#define CC "cc"
#define TARGET "-o", "aboba"
#if MY_DEBUG
#define CFLAGS "-fsanitize=address", "-fPIC", "-ggdb"
#define DEFINES "-DMY_DEBUG=1", "-D_GNU_SOURCE", "-DGEBS_NO_PREFIX", "-DINCBIN_PREFIX=", "-DINCBIN_STYLE=INCBIN_STYLE_SNAKE", \\
"-DGEBS_ENABLE_PTHREAD_FEATURES"
#define EXTRA_SOURCES "./cJSON/cJSON.c", "./zip/src/zip.c", "./md5-c/md5.c"
#else
#define CFLAGS "-fPIC"
#define DEFINES "-DMY_DEBUG=0", "-D_GNU_SOURCE", "-DGEBS_NO_PREFIX", "-DINCBIN_PREFIX=", "-DINCBIN_STYLE=INCBIN_STYLE_SNAKE", \\
"-DGEBS_ENABLE_PTHREAD_FEATURES"
#define EXTRA_SOURCES "./cJSON/cJSON.c", "./zip/src/zip.c"
#endif
#define SOURCES "./main.c", "./routes.c", "./baked.c", "./timer.c"
#define OBJECTS "./mongoose.o"
#define LINK_FLAGS "-Wl,-z,execstack", "-lpthread"
#define INC_FLAGS "-I.", "-I./zip/src"
CMD(CC, TARGET, CFLAGS, DEFINES, INC_FLAGS, SOURCES, OBJECTS, EXTRA_SOURCES, LINK_FLAGS);
// generate compile_flags.txt - skipped
// #undef macros - skipped
}
\`\`\`
If you go through the commit history, you'll see that apart from just generating the bundle file, I've also cleaned up the
build commands a bit with \`#define\`s. Let's take a closer look at the bundle generation code.
\`\`\`
const char *bundle_zip_deps[] = { BUNDLED_FILES };
RULE_ARRAY("./bundle.zip", bundle_zip_deps) {
RULE("./gpp1", "./gpp/gpp.c") {
CMD("cc", "-DHAVE_STRDUP", "-DHAVE_FNMATCH_H", "-o", "gpp1", "gpp/gpp.c");
}
struct zip_t *zip = zip_open("./bundle.zip", BUNDLE_ZIP_COMPRESSION, 'w');
defer { zip_close(zip); }
for (size_t i = 0; i < sizeof(bundle_zip_deps)/sizeof(bundle_zip_deps[0]); i++) {
char *copy = strdup(bundle_zip_deps[i]);
defer { free(copy); }
char *name = basename(copy);
String_Builder sb = {0};
defer { sb_free(&sb); }
sb_read_file(&sb, bundle_zip_deps[i]);
zip_entry_open(zip, name);
zip_entry_write(zip, sb.items, sb.count);
zip_entry_close(zip);
}
LOGI("Generated bundle.zip\\n");
}
\`\`\`
We declare a dependency (using \`RULE\*()\` macro), which says that \`./bundle.zip\` depends on files defined by \`BUNDLED_FILES\`.
To generate the bundle we use \`zip_open()\` \`zip_close()\`. To call \`zip_open()\`, we have to provide a so called "compression level".
The zip library provides us only with \`ZIP_DEFAULT_COMPRESSION_LEVEL\`, which is a macro that evaluates to 6. I wasn't satisfied with
it, so after looking at \`miniz.h\` (a backing library that zip uses), I've found that zip uses \`MZ_DEFAULT_COMPRESSION_LEVEL\`, which
is 6, but we can use the value of \`MZ_UBER_COMPRESSION\`, which is 10. This way we can achieve the most size-efficient compression.
I've decided to \`#define\` the compression level to avoid using arbitrary magic numbers and we can also use that definition both in
the application and in the build program. Here's how \`CONFIG.h\` looks like now:
\`\`\`
#ifndef CONFIG_H_
#define CONFIG_H_
#if MY_DEBUG
# define CONFIG_LISTEN_URL "http://localhost:8080"
#else
# define CONFIG_LISTEN_URL "http://localhost:5000"
#endif
#define BUNDLE_ZIP_COMPRESSION 10
#endif // CONFIG_H_
\`\`\`
The only "downside" here is that, since we're compressing so hard, it's going to take more time to generate the bundle. I've put "downside"
in quotes for purpose, because this does not apply in our case. The files that we're packing are quite small already and there aren't
many of them. We're just doing this to sqeeze out *extra* spacial performance.
Previously we were baking-in the assets like so:
from https://git.kamkow1lair.pl/kamkow1/aboba/src/commit/447362c74dda85838d37d6ee3cb218697abf6106/baked.c
\`\`\`
INCBIN(gpp1, "./gpp1");
INCBIN(home_html, "./tmpls/home.html");
INCBIN(page_missing_html, "./tmpls/page-missing.html");
INCBIN(template_blog_html, "./tmpls/template-blog.html");
INCBIN(blog_html, "./tmpls/blog.html");
INCBIN(simple_css, "./etc/simple.css");
INCBIN(favicon_ico, "./etc/favicon.ico");
#if MY_DEBUG
INCBIN(hotreload_js, "./etc/hotreload.js");
#endif
INCBIN(theme_js, "./etc/theme.js");
INCBIN(highlight_js, "./etc/highlight.js");
INCBIN(hljs_rainbow_css, "./etc/hljs-rainbow.css");
INCBIN(marked_js, "./etc/marked.js");
INCBIN(me_jpg, "./etc/me.jpg");
INCBIN(tmoa_engine_jpg, "./etc/tmoa-engine.jpg");
INCBIN(tmoa_garbage_jpg, "./etc/tmoa-garbage.jpg");
INCBIN(blog_welcome_md, "./blog/welcome.md");
INCBIN(blog_weird_page_md, "./blog/weird-page.md");
INCBIN(blog_curious_case_of_gebs_md, "./blog/curious-case-of-gebs.md");
INCBIN(blog_the_making_of_aboba_md, "./blog/the-making-of-aboba.md");
\`\`\`
Now that we have our \`bundle.zip\`, we do it like this:
\`\`\`
INCBIN(bundle_zip, "./bundle.zip");
\`\`\`
And there we go, we have our bundle!
I've also had to slightly change the way we add the assets to the resource hash table:
\`\`\`
void add_baked_resource(char *key, const uchar *data, size_t size)
{
int fd = memfd_create(key, 0);
if (fd < 0) {
LOGE("Could not create resource %s. Aborting...\n", key);
abort();
}
write(fd, data, size);
shput(baked_resources.value, key, ((Baked_Resource_Value){ .memfd = fd, .bufptr = (void *)data }));
}
void init_baked_resources(void)
{
struct zip_t *zip = zip_stream_open(bundle_zip_data, bundle_zip_size, BUNDLE_ZIP_COMPRESSION, 'r');
size_t n = zip_entries_total(zip);
for (size_t i = 0; i < n; i++) {
zip_entry_openbyindex(zip, i);
const char *name = strdup(zip_entry_name(zip));
size_t size = zip_entry_size(zip);
char *buf = malloc(size);
zip_entry_noallocread(zip, buf, size);
add_baked_resource((char *)name, buf, size);
zip_entry_close(zip);
}
zip_stream_close(zip);
}
\`\`\`
\`add_baked_resource()\` hasn't changed here, but \`init_baked_resources()\` has. Here we use one of zip's abilities,
which is unpacking an in-memory .zip file. We iterate each entry in the bundle, get the name and size, preallocate
a buffer and read said entry into the buffer. We can then add the resource to the hash table as we did previously.
## Conclusion
![xkcd](/bakedres/apwz-xkcd1.png)
One question you may ask is, how much space are we saving? I don't remeber the exact sizes of the binary, but
I remember that before compression it was \`~1.2M\` and now after compression is implemented, it's \`~1.6M\`. How is
that an improvement if the binary gained weight? That \`~.4M\` is likely due to zip format overhead - metadata, file and
directory headers and so on. What we gain here is that the binary hasn't changed in size much despite adding more files,
like for eg. this article that I'm writing right now. It has stayed at \`~1.6M\` so far and the size doesn't go up.
*(So far)* We aren't even making byte-sized gains, which can be checked with \`stat --printf "%s" ./aboba\`. What
we're gaining here is slowed down size increase. Previously if I wanted to add a 30K jpeg, the binary would literally
go up by 30K.

View File

@ -0,0 +1,407 @@
# The making of aboba (this website)
In this article I'd like to present to you the internals of this website,
how the code is architectured and some cool tricks that are used throughout the project.
## Our "engine"
![the engine](/bakedres/tmoa-engine.jpg)
This image is a joke, obviously.
The "engine" here is a web server that we're going to be using. I've decided to pick mongoose (-> [their page](https://mongoose.ws)),
partly because I didn't know about/couldn't find other solutions, but I'm really happy with my pick. The only "downside" here is that
mongoose is not http-specific, but also has websockets, MQTT, SNTP and even RPC. While that's really cool, I only need http and not
much else. I haven't dove deeper into mongoose, but I'd be cool if they provided some \`#ifdef\`s to just disable these protocols
(ie. strip down the code that implements them). That way I could make mongoose even more lightweight and only use the features that I need.
Here's roughly how we work with mongoose. Refer to their documentation for more context.
Let's start with \`main()\`
\`\`\`
volatile bool alive = true;
void graceful_shutdown(int no) { alive = false; }
int main(int argc, char ** argv)
{
signal(SIGINT, &graceful_shutdown);
// skip BS
mg_log_set(MG_LL_DEBUG);
struct mg_mgr mgr;
mg_mgr_init(&mgr);
// skip BS
mg_wakeup_init(&mgr); // We need this for multithreading
mg_http_listen(&mgr, CONFIG_LISTEN_URL, &event_handler, NULL);
while (alive) {
mg_mgr_poll(&mgr, 1000);
// skip BS
}
mg_mgr_free(&mgr);
// skip BS
return 0;
}
\`\`\`
As you can see it's quite simple to set up mongoose. Here's what the used functions do:
- \`mg_log_set()\` - set the log level. \`MG_LL_DEBUG\` is very verbose, but it's good for
when the application breaks and we have no clue why.
- \`struct mg_mgr\` & \`mg_mgr_init()\` - this is the mongoose "manager". The detailed explaination
can be found [here](https://mongoose.ws/documentation/#2-minute-integration-guide), but it can be
essentially boiled down to "overall state of the web server".
- \`mg_wakeup_init()\` - this is needed to make our application multithreaded. In the docs it says
that it's used to "initialize the *wakeup scheme*". This basically means that we can now talk between
multiple threads using \`mg_wakeup()\`, which is the only thread-safe function provided by mongoose.
- \`mg_mgr_poll()\` - handle the next conection if there's any incoming data to work with. We can
also specify the timeout for a connection. Here we provide 1 second (1000 ms).
That's all you really need to know to get started with mongoose. Let's get to the \`event_handler()\` now.
\`\`\`
void event_handler(struct mg_connection *conn, int ev, void *ev_data)
{
if (ev == MG_EV_HTTP_MSG) {
// Run handler in a new thread
} else if (ev == MG_EV_WAKEUP) {
// We've woken up from a handler by mg_wakeup(). Send the reply back to the client
}
}
\`\`\`
I've removed a lot of code here, because it's irrelevant at the current point. This allowes us to look at
the simplified image of the \`event_handler()\` function.
Let's stop to talk about the parameters for a second.
- \`struct mg_connection *conn\` - the structure that describes the incoming connection. We will also
use it to send back our reply.
- \`int ev\` - this is the event enumeration. Basically tells us what event we're currently handling
inside of mongoose's event loop.
- \`void *ev_data\` - additional event data. The value of this parameter differs based on the value of \`int ev\`.
More on that a little bit later.
What goes on inside the \`MG_EV_HTTP_MSG\` branch?
\`\`\`
if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *msg = (struct mg_http_message *)ev_data;
Route_Thread_Data *data = calloc(1, sizeof(*data));
data->message = mg_strdup(msg->message);
data->conn_id = conn->id;
data->mgr = conn->mgr;
run_in_thread(&route_thread_function, data);
\`\`\`
If we have an "HTTP Message" event incoming, ev_data is a pointer to \`struct mg_http_message\`.
This structure contains things like the message body, query parameters, the uri, the method and so on. Here we
duplicate the \`message\` field, which encompasses the entire HTTP message. We also save the reference to the
mongoose manager for later use when we will want to wake up from a thread.
Here's how the thread is spawned. This code is taken from the mongoose tutorial: https://github.com/cesanta/mongoose/blob/master/tutorials/core/multi-threaded/main.c#L11
\`\`\`
void run_in_thread(void *(*f)(void *), void *p)
{
pthread_t tid = 0;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, f, p);
pthread_attr_destroy(&attr);
}
\`\`\`
And then here's what goes on inside a thread:
\`\`\`
void *route_thread_function(void *param)
{
Route_Thread_Data *data = (Route_Thread_Data *)param;
struct mg_http_message http_msg = {0};
int r = mg_http_parse(data->message.buf, data->message.len, &http_msg);
if (r <= 0) {
// Unparsable HTTP request
}
if (mg_match(http_msg.uri, mg_str("/bakedres/*"), nil)) {
// Request for static resource
}
// Request for a dynamic page
}
\`\`\`
This is quite simple, so there's not much to explain here. One cool thing I'd like to mention is the
\`mg_match()\` function. Normally I'd have to implement uri string matching myself, but I'm glad that
this functionality comes with mongoose built-in. God bless you mongoose!
## Dynamic pages and static assets.
![the assets](/bakedres/tmoa-garbage.jpg)
Let's stop here now and talk about something different entirely (dont' worry, we'll come back later).
I'd like to show you how assets/pages are implemented in aboba, so we get the whole picture.
In most applications you typically have a distribution model that goes like this: you have your application's
binary ("aboba" or "aboba.exe" or "aboba.bin"...) and then you have an "assets"/"resources" directory, placed
somewhere within the filesystem. There are now 2 ways to go about this. You can have a fixed path to the assets
directory or let the user configure it based on how they've installed the application. In the first case the
app could require that it's assets must be located in \`/usr/share/my-app/assets\` and otherwise it will bail
out with an error "Err: Could not find assets directory blah blah" or something. In the other case the application
would require that the user configures the path to the assets directory themselves, which is not a bad solution
(it may sound like it at first). This really depends what audience are we targetting. "Are our users tech-savvy
enough to do it themselves?" is the question we'd have to ask ourselves.
**Enter embedded assets**
The downsides of both solutions is that we still have to distribute our application with an assets directory. We
have to manage 1 executable file + an entire directory. The *obvious* solution would be to just create a
windows-style setup wizard or a \`make install\` or some other install script thingy. This problem is easily
solvable by literally compiling the bytes of the assets into our program. That way we can distribute only the
executable since it's self contained and doesn't need to reach out to the filesystem at runtime to get it's resources.
This technique is fairly old and the most notable uses I can think of are: [XPM Format](https://en.wikipedia.org/wiki/X_PixMap),
[Windows resources](https://en.wikipedia.org/wiki/Resource_(Windows)) and raylib's [rres](https://github.com/raysan5/rres).
Even C23 has acknowledged the significance of embedded assets (-> [article](https://thephd.dev/finally-embed-in-c23)).
Of course there are some pitfalls of this approach. What if the files we're trying to embed are too *thicc*? We obviously
don't want to end up with a 5GB executable. Imagine the OS trying to load that program into the ram. That would just eat up 5
gigs before anything meaningful ever happens. This pattern of asset distribution is the most suitable for embedded applications
where there's no underlying filesystem to work with, games which are not too large, GUI apps that need some icons and fonts.
So how does aboba go about embedding it's assets? [incbin](https://github.com/graphitemaster/incbin) comes to the rescue!
Here are external declarations for our resources
\`\`\`
#include "incbin/incbin.h"
INCBIN_EXTERN(gpp1);
INCBIN_EXTERN(home_html);
INCBIN_EXTERN(page_missing_html);
INCBIN_EXTERN(template_blog_html);
INCBIN_EXTERN(blog_html);
INCBIN_EXTERN(simple_css);
INCBIN_EXTERN(favicon_ico);
#if MY_DEBUG
INCBIN_EXTERN(hotreload_js);
#endif
INCBIN_EXTERN(me_jpg);
INCBIN_EXTERN(tmoa_engine_jpg);
INCBIN_EXTERN(blog_welcome_md);
INCBIN_EXTERN(blog_weird_page_md);
INCBIN_EXTERN(blog_curious_case_of_gebs_md);
INCBIN_EXTERN(blog_the_making_of_aboba_md);
\`\`\`
And here's where the actual inclusion happens:
\`\`\`
INCBIN(gpp1, "./gpp1");
INCBIN(home_html, "./tmpls/home.html");
INCBIN(page_missing_html, "./tmpls/page-missing.html");
INCBIN(template_blog_html, "./tmpls/template-blog.html");
INCBIN(blog_html, "./tmpls/blog.html");
INCBIN(simple_css, "./etc/simple.css");
INCBIN(favicon_ico, "./etc/favicon.ico");
#if MY_DEBUG
INCBIN(hotreload_js, "./etc/hotreload.js");
#endif
INCBIN(me_jpg, "./etc/me.jpg");
INCBIN(tmoa_engine_jpg, "./etc/tmoa-engine.jpg");
INCBIN(tmoa_garbage.jpg, "./etc/tmoa-garbage.jpg");
INCBIN(blog_welcome_md, "./blog/welcome.md");
INCBIN(blog_weird_page_md, "./blog/weird-page.md");
INCBIN(blog_curious_case_of_gebs_md, "./blog/curious-case-of-gebs.md");
INCBIN(blog_the_making_of_aboba_md, "./blog/the-making-of-aboba.md");
\`\`\`
As you can see, with incbin embedding assets is extremely easy. In the past I've worked with my
own asset packer utility \`x2h.c\`, but it was kind of sloppy and I don't have the will to
rewrite it (maybe one day ;) ).
Incbin works (on GCC, which I'm using here) by calling into the assembler using \`__asm__()\` and then uses \`.incbin\`
directive to include the binary file. More on that can be found in the docs: https://sourceware.org/binutils/docs/as/Incbin.html
Now let the **trickery** begin...
## Tangent about templating
Don't worry, in this section I'll briefly explain how templating is implemented, so that we can circle back to our embedded assets system to better understand it.
How are templating engines implemented in general? You usually have a pseudo-HTML page (for eg. \`my-page.tmpl\`), which
contains plain old HTML, but also has some escaped blocks that can embed dynamic variables, loop over lists, include other pages
and whatnot. You then take that pseudo-page and run it through some kind of a (keyword) *preprocessor*, which given an environment,
can expand variable *macros*, unfold loops and other tricks.
Do you already see where I'm going with my wording?
I want to keep this a *mostly* a pure C project, where every bit of it that can be written in C, is written in C. Yes, even the templates
(*in a way*). In order to expand the page templates I've decided to use GPP or the General PreProcessor written by Denis Auroux and maintained
by Tristan Miller. Here's a link to GPP's website (-> [click](https://logological.org/gpp)). Why not use regular CPP that comes with the GCC
suite, you may ask. This is because GPP has a special mode - the HTML mode, which makes it so that it's better suited for working with HTML.
For example \`\#define\` vs. \`<\#define>\` or \`\#ifdef\` and \`\#endif\` vs. \`<\#ifdef>\` and \`<\#endif>\`.
How do we interact with GPP then? To preprocess our templates, we can call GPP via it's command line interface (CLI) - set HTML mode, give path
to the template file and collect the output, which we then can send back to the client.
But wait, if we call out to an external executable, doesn't that mean that we'll have to ship GPP alongside aboba? The answer is yes! Since
we follow the *principle of a single-executable-deployment*, why don't we pack GPP into our binary, just like any other asset? Another question
arises then - how do we call a packed program and how to we work with it's CLI? I'll answer this in the next part...
## Back to the assets
So now that we've established why and how we embed assets into an executable and the way we work with templates, we can now discuss the
**embedded assets system**.
Here's the core API of the system:
\`\`\`
typedef struct {
char *key; // path
int value; // memfd
} Baked_Resource;
void init_baked_resources(void);
void free_baked_resources(void);
// skip BS
bool get_baked_resource_path(char *key, char *buf, size_t size);
// skip BS
\`\`\`
The \`Baked_Resource\` struct is defined in such a way that works with \`stb_ds.h\`'s string hashmap. stb_ds.h can be found here: https://nothings.org/stb_ds/.
Let's take a closer look at the fields:
- \`key\` - a key to a file within our hashmap of baked resources
- \`value\` - a memfd associated with the baked resource
Here's the initialization and deinitialization of baked resources:
\`\`\`
void add_baked_resource(char *key, const uchar *data, size_t size)
{
int fd = memfd_create(key, 0);
if (fd < 0) {
LOGE("Could not create resource %s. Aborting...", key);
abort();
}
write(fd, data, size);
shput(baked_resources.value, key, fd);
}
void init_baked_resources(void)
{
lockx(&baked_resources);
add_baked_resource("home.html", home_html_data, home_html_size);
add_baked_resource("page-missing.html", page_missing_html_data, page_missing_html_size);
add_baked_resource("template-blog.html", template_blog_html_data, template_blog_html_size);
add_baked_resource("blog.html", blog_html_data, blog_html_size);
add_baked_resource("gpp1", gpp1_data, gpp1_size);
add_baked_resource("simple.css", simple_css_data, simple_css_size);
add_baked_resource("favicon.ico", favicon_ico_data, favicon_ico_size);
#if MY_DEBUG
add_baked_resource("hotreload.js", hotreload_js_data, hotreload_js_size);
#endif
add_baked_resource("me.jpg", me_jpg_data, me_jpg_size);
add_baked_resource("tmoa-engine.jpg", tmoa_engine_jpg_data, tmoa_engine_jpg_size);
add_baked_resource("tmoa-garbage.jpg", tmoa_garbage_jpg_data, tmoa_garbage_jpg_size);
add_baked_resource("blog-welcome.md", blog_welcome_md_data, blog_welcome_md_size);
add_baked_resource("blog-weird-page.md", blog_weird_page_md_data, blog_weird_page_md_size);
add_baked_resource("blog-curious-case-of-gebs.md", blog_curious_case_of_gebs_md_data, blog_curious_case_of_gebs_md_size);
add_baked_resource("blog-the-making-of-aboba.md", blog_the_making_of_aboba_md_data, blog_the_making_of_aboba_md_size);
unlockx(&baked_resources);
}
void free_baked_resources(void)
{
lockx(&baked_resources);
for (size_t i = 0; i < shlen(baked_resources.value); i++) {
close(baked_resources.value[i].value);
}
shfree(baked_resources.value);
unlockx(&baked_resources);
}
\`\`\`
Here we use memfd API to convert a baked-in file into a file that has a file descriptor associated with it. Why? We do this, because
we have no way of passing files down to GPP's CLI. We only have the raw bytes of a file, which we can't really work with. memfds
allow us to create a virtual memory-mapped file and get it's file descriptor. Using said file descriptor we can then write our file's
bytes into the virtual file, making it accessible via Linux's VFS. The virtual file can be accessed via a path like \`/proc/<PID>/fd/<THE FILE>\`.
Now that we've successfully converted a baked-in file into a "*pathed*" file, we can then pass the path down to GPP. Heck, we can even run
GPP itself from a memory-mapped file!
Here's how we get the memory-mapped file's path in aboba:
\`\`\`
bool get_baked_resource_path(char *key, char *buf, size_t size)
{
if (shgeti(baked_resources.value, key) != -1) {
int fd = shget(baked_resources.value, key);
snprintf(buf, size, "/proc/%d/fd/%d", getpid(), fd);
unlockx(&baked_resources);
return true;
}
return false;
}
\`\`\`
And then we run GPP like so:
\`\`\`
bool gpp_run(char *path, NString_List *env, String_Builder *out)
{
Cmd cmd = {0};
defer { cmd_free(&cmd); }
char gpp1[PATH_MAX];
if (!get_baked_resource_path("gpp1", gpp1, sizeof(gpp1))) {
return false;
}
cmd_append(&cmd, gpp1);
cmd_append(&cmd, "-H");
cmd_append(&cmd, "-x");
cmd_append(&cmd, "--nostdinc");
cmd_append(&cmd, path);
for (size_t i = 0; i < env->count; i++) {
cmd_append(&cmd, env->items[i]);
}
return cmd_run_collect(&cmd, out) == 0;
}
\`\`\`
In the logger we can now see commands like this: \`Info: cmd /proc/1210675/fd/8 -H -x --nostdinc /proc/1210675/fd/6 ...\`, where
/proc/1210675/fd/8 is memory-mapped file for GPP and /proc/1210675/fd/6 is a memory-mapped file for the template. Pretty cool, eh?
## Left out topics
I think this is out of scope of this article, so I'm not going to talk about it here, but a big part of this project was making
live hotreloading. I can basically edit the website inside of my editor and it auto-refreshes in the browser, kinda like a vite js
project. A video of this can be found here: https://www.reddit.com/r/C_Programming/comments/1lbzjvi/webdev_in_c_pt2_true_live_hotreloading_no_more/
## Summary
During this project I've learned a lot about Linux, the memfd API and webdev in general. Normally I wouldn't pick up a website project
simply because I'm tired of webdev. I've been through a webdev phase and it sucked. Using JavaScript, 300MB of node_modules for a bare
react hello world project, npm installing countless slop libraries and so on.
Then on the backend you have the same amount of C# or Java or TypeScript slop, but now you can call yourself a *backend engineer* or
whatever the hell. Writing this website in C put me on a different view of webdev and made it actually fun to write.
You can go check out the code for aboba @ http://git.kamkow1lair.pl/kamkow1/aboba.git.

117
build.c
View File

@ -1,6 +1,9 @@
#include <libgen.h>
#define GEBS_NO_PREFIX
#define GEBS_IMPLEMENTATION
#include "gebs/gebs.h"
#include "./zip/src/zip.c"
#include "CONFIG.h"
char *prog = NULL;
@ -15,34 +18,72 @@ int main(int argc, char ** argv)
prog = SHIFT(&argc, &argv);
char *cmd = SHIFT(&argc, &argv);
if (strcmp(cmd, "make") == 0) {
#define BUNDLED_FILES \
"./gpp1", \
"./tmpls/home.html", \
"./tmpls/page-missing.html", \
"./tmpls/template-blog.html", \
"./tmpls/blog.html", \
"./etc/hotreload.js", \
"./etc/theme.js", \
"./etc/simple.css", \
"./etc/highlight.js", \
"./etc/hljs-rainbow.css", \
"./etc/marked.js", \
"./etc/favicon.ico", \
"./etc/me.jpg", \
"./etc/tmoa-engine.jpg", \
"./etc/tmoa-garbage.jpg", \
"./etc/apwz-zip-icon.png", \
"./etc/apwz-xkcd1.png", \
"./blog/blog-welcome.md", \
"./blog/blog-weird-page.md", \
"./blog/blog-curious-case-of-gebs.md", \
"./blog/blog-the-making-of-aboba.md", \
"./blog/blog-asset-packing-with-zip.c.md"
const char *bundle_zip_deps[] = { BUNDLED_FILES };
RULE_ARRAY("./bundle.zip", bundle_zip_deps) {
RULE("./gpp1", "./gpp/gpp.c") {
CMD("cc", "-DHAVE_STRDUP", "-DHAVE_FNMATCH_H", "-o", "gpp1", "gpp/gpp.c");
}
struct zip_t *zip = zip_open("./bundle.zip", BUNDLE_ZIP_COMPRESSION, 'w');
defer { zip_close(zip); }
for (size_t i = 0; i < sizeof(bundle_zip_deps)/sizeof(bundle_zip_deps[0]); i++) {
char *copy = strdup(bundle_zip_deps[i]);
defer { free(copy); }
char *name = basename(copy);
String_Builder sb = {0};
defer { sb_free(&sb); }
sb_read_file(&sb, bundle_zip_deps[i]);
zip_entry_open(zip, name);
zip_entry_write(zip, sb.items, sb.count);
zip_entry_close(zip);
}
LOGI("Generated bundle.zip\n");
}
RULE("./aboba",
"./main.c",
"./routes.c",
"./routes.h",
"./baked.c",
"./baked.h",
"./clonestr.h",
"./commit.h",
"./timer.c",
"./timer.h",
"./CONFIG.h",
"./locked.h",
"./mongoose.o",
"./gpp1",
"./tmpls/home.html",
"./tmpls/page-missing.html",
"./tmpls/template-blog.html",
"./tmpls/blog.html",
"./etc/hotreload.js",
"./etc/simple.css",
"./etc/favicon.ico",
"./etc/me.jpg",
"./blog/welcome.md",
"./blog/weird-page.md",
"./blog/curious-case-of-gebs.md"
"./bundle.zip",
BUNDLED_FILES
) {
RULE("./mongoose.o", "./mongoose/mongoose.c") {
@ -55,10 +96,6 @@ int main(int argc, char ** argv)
#endif
}
RULE("./gpp1", "./gpp/gpp.c") {
CMD("cc", "-DHAVE_STRDUP", "-DHAVE_FNMATCH_H", "-o", "gpp1", "gpp/gpp.c");
}
RULE("./commit.h") {
String_Builder commit = {0};
defer { sb_free(&commit); }
@ -87,18 +124,39 @@ int main(int argc, char ** argv)
}
}
#define CC "cc"
#define TARGET "-o", "aboba"
#if MY_DEBUG
CMD("cc", "-fPIC", "-ggdb", "-I.", "-DMY_DEBUG=1", "-D_GNU_SOURCE", "-DGEBS_NO_PREFIX",
"-DINCBIN_PREFIX=", "-DINCBIN_STYLE=INCBIN_STYLE_SNAKE", "-Wl,-z,execstack", "-o", "./aboba",
"./main.c", "./routes.c", "./baked.c", "./timer.c", "./mongoose.o", "./cJSON/cJSON.c",
"./md5-c/md5.c",
"-lpthread");
#define CFLAGS "-fsanitize=address", "-fPIC", "-ggdb"
#define DEFINES "-DMY_DEBUG=1", "-D_GNU_SOURCE", "-DGEBS_NO_PREFIX", "-DINCBIN_PREFIX=", "-DINCBIN_STYLE=INCBIN_STYLE_SNAKE", \
"-DGEBS_ENABLE_PTHREAD_FEATURES"
#define EXTRA_SOURCES "./cJSON/cJSON.c", "./zip/src/zip.c", "./md5-c/md5.c"
#else
CMD("cc", "-fPIC", "-I.", "-DMY_DEBUG=0", "-D_GNU_SOURCE", "-DGEBS_NO_PREFIX",
"-DINCBIN_PREFIX=", "-DINCBIN_STYLE=INCBIN_STYLE_SNAKE", "-Wl,-z,execstack", "-o", "./aboba",
"./main.c", "./routes.c", "./baked.c", "./timer.c", "./mongoose.o", "./cJSON/cJSON.c",
"-lpthread");
#define CFLAGS "-fPIC"
#define DEFINES "-DMY_DEBUG=0", "-D_GNU_SOURCE", "-DGEBS_NO_PREFIX", "-DINCBIN_PREFIX=", "-DINCBIN_STYLE=INCBIN_STYLE_SNAKE", \
"-DGEBS_ENABLE_PTHREAD_FEATURES"
#define EXTRA_SOURCES "./cJSON/cJSON.c", "./zip/src/zip.c"
#endif
#define SOURCES "./main.c", "./routes.c", "./baked.c", "./timer.c"
#define OBJECTS "./mongoose.o"
#define LINK_FLAGS "-Wl,-z,execstack", "-lpthread"
#define INC_FLAGS "-I.", "-I./zip/src"
CMD(CC, TARGET, CFLAGS, DEFINES, INC_FLAGS, SOURCES, OBJECTS, EXTRA_SOURCES, LINK_FLAGS);
RULE("./compile_flags.txt") {
make_compile_flags(TARGET, CFLAGS, DEFINES, INC_FLAGS, SOURCES, OBJECTS, EXTRA_SOURCES, LINK_FLAGS);
}
#undef CC
#undef TARGET
#undef CFLAGS
#undef DEFINES
#undef EXTRA_SOURCES
#undef SOURCES
#undef OBJECTS
#undef LINK_FLAGS
#undef INC_FLAGS
}
} else if (strcmp(cmd, "clean") == 0) {
remove1("./build");
@ -106,6 +164,9 @@ int main(int argc, char ** argv)
remove1("./mongoose.o");
remove1("./gpp1");
remove1("./watcher");
remove1("./compile_flags.txt");
remove1("./bundle.zip");
remove1("./commit.h");
} else if (strcmp(cmd, "make-watcher") == 0) {
CMD("cc", "-o", "watcher", "watcher.c");
} else if (strcmp(cmd, "watch") == 0) {

View File

@ -1,11 +1,5 @@
#ifndef CLONESTR_H_
#define CLONESTR_H_
#define clonestr(str) \
({ \
char *__p = malloc(strlen((str))+1); \
strcpy(__p, (str)); \
(__p); \
})
#endif // CLONESTR_H_

BIN
etc/apwz-xkcd1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
etc/apwz-zip-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

1244
etc/highlight.js Normal file

File diff suppressed because one or more lines are too long

1
etc/hljs-rainbow.css Normal file
View File

@ -0,0 +1 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#474949;color:#d1d9e1}.hljs-comment,.hljs-quote{color:#969896;font-style:italic}.hljs-addition,.hljs-keyword,.hljs-literal,.hljs-selector-tag,.hljs-type{color:#c9c}.hljs-number,.hljs-selector-attr,.hljs-selector-pseudo{color:#f99157}.hljs-doctag,.hljs-regexp,.hljs-string{color:#8abeb7}.hljs-built_in,.hljs-name,.hljs-section,.hljs-title{color:#b5bd68}.hljs-class .hljs-title,.hljs-selector-id,.hljs-template-variable,.hljs-title.class_,.hljs-variable{color:#fc6}.hljs-name,.hljs-section,.hljs-strong{font-weight:700}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-subst,.hljs-symbol{color:#f99157}.hljs-deletion{color:#dc322f}.hljs-formula{background:#eee8d5}.hljs-attr,.hljs-attribute{color:#81a2be}.hljs-emphasis{font-style:italic}

69
etc/marked.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

131
etc/theme.js Normal file
View File

@ -0,0 +1,131 @@
if (!localStorage.getItem("theme")) {
localStorage.setItem("theme", "light-breeze");
}
function plan9_acme()
{
document.documentElement.style.setProperty("--bg", "#FFFFEC");
document.documentElement.style.setProperty("--accent-bg", "#EEEEA7");
document.documentElement.style.setProperty("--text", "#424242");
document.documentElement.style.setProperty("--text-light", "#999957");
document.documentElement.style.setProperty("--border", "#B7B19C");
document.documentElement.style.setProperty("--accent", "#030093");
document.documentElement.style.setProperty("--accent-hover", "#2A8DC5");
document.documentElement.style.setProperty("--accent-text", "var(--bg)");
document.documentElement.style.setProperty("--code", "#57864E");
document.documentElement.style.setProperty("--preformatted", "#424242");
document.documentElement.style.setProperty("--marked", "#B85C57");
document.documentElement.style.setProperty("--disabled", "#EAEBDB");
}
function light_breeze()
{
document.documentElement.style.setProperty("--bg", "#F0F2F5");
document.documentElement.style.setProperty("--accent-bg", "#E3E6EA");
document.documentElement.style.setProperty("--text", "#2C2C2C");
document.documentElement.style.setProperty("--text-light", "#9A9A9A");
document.documentElement.style.setProperty("--border", "#D1D7DC");
document.documentElement.style.setProperty("--accent", "#3454D1");
document.documentElement.style.setProperty("--accent-hover", "#6A8ED8");
document.documentElement.style.setProperty("--accent-text", "var(--bg)");
document.documentElement.style.setProperty("--code", "#3C9E47");
document.documentElement.style.setProperty("--preformatted", "#2C2C2C");
document.documentElement.style.setProperty("--marked", "#D6674B");
document.documentElement.style.setProperty("--disabled", "#F1F3F6");
}
function bold_navy()
{
document.documentElement.style.setProperty("--bg", "#F0F4FF");
document.documentElement.style.setProperty("--accent-bg", "#E0E6F1");
document.documentElement.style.setProperty("--text", "#2D3A58");
document.documentElement.style.setProperty("--text-light", "#7A8C9E");
document.documentElement.style.setProperty("--border", "#BCC6D8");
document.documentElement.style.setProperty("--accent", "#1E2A5B");
document.documentElement.style.setProperty("--accent-hover", "#2A3F80");
document.documentElement.style.setProperty("--accent-text", "var(--bg)");
document.documentElement.style.setProperty("--code", "#4A7C2F");
document.documentElement.style.setProperty("--preformatted", "#2D3A58");
document.documentElement.style.setProperty("--marked", "#F18F5D");
document.documentElement.style.setProperty("--disabled", "#E8ECF5");
}
function soft_lavander()
{
document.documentElement.style.setProperty("--bg", "#F5F2FF");
document.documentElement.style.setProperty("--accent-bg", "#E1D1FF");
document.documentElement.style.setProperty("--text", "#3D2B6F");
document.documentElement.style.setProperty("--text-light", "#8D8B9B");
document.documentElement.style.setProperty("--border", "#C1B5D6");
document.documentElement.style.setProperty("--accent", "#7A4FE3");
document.documentElement.style.setProperty("--accent-hover", "#8D6DE8");
document.documentElement.style.setProperty("--accent-text", "var(--bg)");
document.documentElement.style.setProperty("--code", "#5D7C42");
document.documentElement.style.setProperty("--preformatted", "#3D2B6F");
document.documentElement.style.setProperty("--marked", "#F77A63");
document.documentElement.style.setProperty("--disabled", "#E9E4FF");
}
function neon_cyber()
{
document.documentElement.style.setProperty("--bg", "#121212");
document.documentElement.style.setProperty("--accent-bg", "#1C1C1C");
document.documentElement.style.setProperty("--text", "#00FF00");
document.documentElement.style.setProperty("--text-light", "#A1FF3D");
document.documentElement.style.setProperty("--border", "#006400");
document.documentElement.style.setProperty("--accent", "#FF007F");
document.documentElement.style.setProperty("--accent-hover", "#FF3399");
document.documentElement.style.setProperty("--accent-text", "var(--bg)");
document.documentElement.style.setProperty("--code", "#2AFF32");
document.documentElement.style.setProperty("--preformatted", "#00FF00");
document.documentElement.style.setProperty("--marked", "#FF6F00");
document.documentElement.style.setProperty("--disabled", "#333333");
}
function dark_easy()
{
document.documentElement.style.setProperty("--bg", "#1A1A1A");
document.documentElement.style.setProperty("--accent-bg", "#2E2E2E");
document.documentElement.style.setProperty("--text", "#DCDCDC");
document.documentElement.style.setProperty("--text-light", "#A3A3A3");
document.documentElement.style.setProperty("--border", "#585858");
document.documentElement.style.setProperty("--accent", "#FF5722");
document.documentElement.style.setProperty("--accent-hover", "#F4511E");
document.documentElement.style.setProperty("--accent-text", "#1A1A1A");
document.documentElement.style.setProperty("--code", "#8BC34A");
document.documentElement.style.setProperty("--preformatted", "#A3A3A3");
document.documentElement.style.setProperty("--marked", "#FF7043");
document.documentElement.style.setProperty("--disabled", "#383838");
}
const themes = {
"light-breeze": light_breeze,
"plan9-acme": plan9_acme,
"bold-navy": bold_navy,
"soft-lavander": soft_lavander,
"neon-cyber": neon_cyber,
"dark-easy": dark_easy,
};
const footer = document.getElementsByTagName("footer")[0];
const select = document.createElement("select");
select.id = "theme-picker";
footer.children[0].appendChild(select);
for (const prop in themes) {
const option = document.createElement("option");
option.value = prop;
option.innerHTML = prop;
if (prop == localStorage.getItem("theme")) {
option.selected = true;
}
select.appendChild(option);
}
const theme_picker = document.getElementById("theme-picker");
theme_picker.addEventListener("change", function (e) {
const theme_name = e.target.value;
localStorage.setItem("theme", theme_name);
themes[localStorage.getItem("theme")]()
});
themes[localStorage.getItem("theme")]()

BIN
etc/tmoa-engine.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
etc/tmoa-garbage.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

2
gebs

Submodule gebs updated: 6db36114d5...db8389f7d5

12
locked.h Normal file
View File

@ -0,0 +1,12 @@
#ifndef LOCKED_H_
#define LOCKED_H_
#include <pthread.h>
#define locked_init(x) { .value = x, .lock = PTHREAD_MUTEX_INITIALIZER }
#define locked(T) struct { T value; pthread_mutex_t lock; }
#define lockx(x) pthread_mutex_lock(&(x)->lock)
#define unlockx(x) pthread_mutex_unlock(&(x)->lock)
#endif // LOCKED_H_

259
main.c
View File

@ -1,25 +1,47 @@
#include <libgen.h>
#include <pthread.h>
#include <ftw.h>
#include <signal.h>
#include <sys/sendfile.h>
#define GEBS_IMPLEMENTATION
#include "gebs/gebs.h"
#include "mongoose/mongoose.h"
#define STB_DS_IMPLEMENTATION
#include "stb/stb_ds.h"
#include "md5-c/md5.h"
#include "routes.h"
#include "baked.h"
#include "timer.h"
#include "CONFIG.h"
#include "locked.h"
Route *route_hashtable = NULL;
static locked(Route *) route_hashtable = locked_init(nil);
static locked(char *) baked_dump_path = locked_init(nil);
char *clonestr_alloc(Allocator *alloc, char *s)
{
char *p = gebs_malloc(alloc, strlen(s)+1);
strcpy(p, s);
return p;
}
void *zmalloc_alloc(Allocator *alloc, size_t size)
{
void *p = gebs_malloc(alloc, size);
memset(p, 0, size);
return p;
}
void run_in_thread(void *(*f)(void *), void *p)
{
pthread_t tid = 0;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, f, p);
pthread_attr_destroy(&attr);
}
@ -27,122 +49,242 @@ void run_in_thread(void *(*f)(void *), void *p)
void *route_thread_function(void *param)
{
Route_Thread_Data *data = (Route_Thread_Data *)param;
Arena *arena = data->arena;
struct mg_http_message http_msg = {0};
int r = mg_http_parse(data->message.buf, data->message.len, &http_msg);
if (r <= 0) {
Route_Result result = {0};
result.status_code = 400;
Route_Result *result = zmalloc_alloc((Allocator *)arena, sizeof(*result));
result->arena = arena;
result->status_code = 400;
result->type = ROUTE_RESULT_DYNAMIC;
sb_append_nstr_alloc(arena, &result->body, "Could not parse HTTP request");
sb_finish_alloc(arena, &result->body);
mg_wakeup(data->mgr, data->conn_id, &result, sizeof(result));
return nil;
}
free(data->message.buf);
free(data);
pthread_exit(nil);
} else {
if (mg_match(http_msg.uri, mg_str("/bakedres/*"), nil)) {
Route_Result *result = zmalloc_alloc((Allocator *)arena, sizeof(*result));
result->arena = arena;
result->type = ROUTE_RESULT_STATIC;
char *p = gebs_malloc(arena, http_msg.uri.len+1);
strncpy(p, http_msg.uri.buf, http_msg.uri.len);
p[http_msg.uri.len] = 0;
char *file = basename(p);
char *full_path = gebs_malloc(arena, PATH_MAX);
lockx(&baked_dump_path);
snprintf(full_path, PATH_MAX, "%s/%s", baked_dump_path.value, file);
unlockx(&baked_dump_path);
sb_append_nstr_alloc(arena, &result->body, full_path);
sb_finish_alloc(arena, &result->body);
mg_wakeup(data->mgr, data->conn_id, &result, sizeof(result));
free(data->message.buf);
free(data);
pthread_exit(nil);
} else {
char key[MG_PATH_MAX] = {0};
strncpy(key, http_msg.uri.buf, http_msg.uri.len);
ssize_t idx = shgeti(route_hashtable, key);
lockx(&route_hashtable);
ssize_t idx = shgeti(route_hashtable.value, key);
Route_Result result = {0};
route_hashtable[idx].value(&http_msg, &result, route_hashtable[idx].context_data);
Route_Result *result = zmalloc_alloc((Allocator *)arena, sizeof(*result));
result->arena = arena;
route_hashtable.value[idx].value((Allocator *)arena, &http_msg, result, route_hashtable.value[idx].context_data);
unlockx(&route_hashtable);
mg_wakeup(data->mgr, data->conn_id, &result, sizeof(result));
free(data->message.buf);
free(data);
pthread_exit(nil);
}
}
return nil;
}
void event_handler(struct mg_connection *conn, int ev, void *ev_data)
{
if (ev == MG_EV_HTTP_MSG) {
LOGI("HTTP EVENT\n");
struct mg_http_message *msg = (struct mg_http_message *)ev_data;
LOGI("Route: %.*s\n", msg->uri.len, msg->uri.buf);
Route_Thread_Data *data = calloc(1, sizeof(*data));
data->message = mg_strdup(msg->message);
data->conn_id = conn->id;
data->mgr = conn->mgr;
data->arena = malloc(sizeof(*data->arena));
*data->arena = arena_get();
LOGI("starting handler thread...\n");
run_in_thread(&route_thread_function, data);
} else if (ev == MG_EV_WAKEUP) {
LOGI("WAKEUP EVENT\n");
struct mg_str *data = (struct mg_str *)ev_data;
Route_Result *result = (Route_Result *)data->buf;
defer {
for (size_t i = 0; i < result->headers.count; i++) {
free(result->headers.items[i]);
}
list_free(&result->headers);
sb_free(&result->body);
}
Route_Result *result = *(Route_Result **)data->buf;
Arena *arena = result->arena;
Gebs_String_Builder sb = {0};
defer { sb_free(&sb); }
nsl_join(&result->headers, &sb, "\r\n");
nsl_join_alloc(arena, &result->headers, &sb, "\r\n");
if (result->headers.count > 0) {
sb_append_nstr(&sb, "\r\n");
sb_append_nstr_alloc(arena, &sb, "\r\n");
}
sb_finish(&sb);
sb_finish_alloc(arena, &sb);
if (result->type == ROUTE_RESULT_TEXT) {
if (result->type == ROUTE_RESULT_DYNAMIC) {
mg_http_reply(conn, result->status_code, sb.items, "%s", result->body.items);
} else if (result->type == ROUTE_RESULT_BINARY) {
char *reply = fmt(
"HTTP/1.1 %d OK\r\n"
"%s"
"Content-Length: %zu\r\n"
"\r\n",
result->status_code,
sb.items,
result->body.count
);
printf("%s\n", reply);
mg_printf(conn, "%s", reply);
mg_send(conn, result->body.items, result->body.count);
} else if (result->type == ROUTE_RESULT_STATIC) {
char *path = result->body.items;
mg_http_serve_file(conn, ev_data, path, &(struct mg_http_serve_opts){0});
}
arena_destroy(arena);
free(arena);
LOGI("WAKEUP done, arena cleaned\n");
}
}
void init_route_hashtable(void)
{
shdefault(route_hashtable, &route_page_not_found);
shput(route_hashtable, "/", &route_home);
shput(route_hashtable, "/page-missing", &route_page_not_found);
shput(route_hashtable, "/simple.css", &route_simple_css);
shput(route_hashtable, "/favicon.ico", &route_favicon);
#if MY_DEBUG
shput(route_hashtable, "/hotreload.js", &route_hotreload_js);
shput(route_hashtable, "/build-id", &route_build_id);
#endif
shput(route_hashtable, "/me.jpg", &route_me_jpg);
shput(route_hashtable, "/blog", &route_blog);
void put_blogs(Baked_Resource *resource)
void route_hashtable_put_blogs(Baked_Resource *resource, void *udata)
{
if ((strlen(resource->key) >= strlen("blog-"))
&& strncmp(resource->key, "blog-", strlen("blog-")) == 0) {
char *path = malloc(MG_PATH_MAX);
snprintf(path, MG_PATH_MAX, "/%s", resource->key);
shput(route_hashtable, path, &route_generic_blog);
route_hashtable[shgeti(route_hashtable, path)].context_data = (void *)resource;
shput(route_hashtable.value, path, &route_generic_blog);
route_hashtable.value[shgeti(route_hashtable.value, path)].context_data = (void *)resource;
}
}
baked_resource_each(&put_blogs);
void init_route_hashtable(void)
{
shdefault(route_hashtable.value, &route_page_not_found);
shput(route_hashtable.value, clonestr_alloc(&default_allocator, "/"), &route_home);
shput(route_hashtable.value, clonestr_alloc(&default_allocator, "/page-missing"), &route_page_not_found);
shput(route_hashtable.value, clonestr_alloc(&default_allocator, "/blog"), &route_blog);
#if MY_DEBUG
shput(route_hashtable.value, clonestr_alloc(&default_allocator, "/build-id"), &route_build_id);
#endif
baked_resource_each(&route_hashtable_put_blogs, nil);
}
void free_route_hashtable(void)
{
for (size_t i = 0; i < shlen(route_hashtable); i++) {
if (strlen(route_hashtable[i].key) >= strlen("blog-")
&& strncmp(route_hashtable[i].key, "blog-", strlen("blog-")) == 0) {
free(route_hashtable[i].key);
for (size_t i = 0; i < shlen(route_hashtable.value); i++) {
free(route_hashtable.value[i].key);
}
shfree(route_hashtable.value);
}
char *init_baked_dump(void)
{
char template[] = "/tmp/aboba-bakedres.XXXXXX";
char *baked_dump1 = mkdtemp(template);
char *baked_dump = malloc(strlen(baked_dump1)+1);
strcpy(baked_dump, baked_dump1);
if (baked_dump == nil) {
LOGE("Could not create bakedres dump\n");
return nil;
}
LOGI("bakedres dump dir is %s\n", baked_dump);
return baked_dump;
}
int baked_dump_rm_cb(const char *path,
discard const struct stat *st,
discard int type,
discard struct FTW *ftw)
{
return remove(path);
}
bool free_baked_dump(char *baked_dump)
{
LOGI("Removing bakedres dump %s\n", baked_dump);
bool ok = nftw(baked_dump, baked_dump_rm_cb, FOPEN_MAX, FTW_DEPTH | FTW_MOUNT | FTW_PHYS) != -1;
if (!ok) {
LOGE("Could not remove %s\n", baked_dump);
}
free(baked_dump);
return ok;
}
// https://stackoverflow.com/questions/2180079/how-can-i-copy-a-file-on-unix-using-c
int cp(const char* source, const char* destination)
{
int result = 0;
int input, output;
if ((input = open(source, O_RDONLY)) == -1)
{
return -1;
}
if ((output = creat(destination, 0660)) == -1)
{
close(input);
return -1;
}
// sendfile will work with non-socket output (i.e. regular file) under
// Linux 2.6.33+ and some other unixy systems.
struct stat file_stat = {0};
result = fstat(input, &file_stat);
off_t copied = 0;
while (result == 0 && copied < file_stat.st_size) {
ssize_t written = sendfile(output, input, &copied, SSIZE_MAX);
copied += written;
if (written == -1) {
result = -1;
}
}
shfree(route_hashtable);
close(input);
close(output);
return result;
}
void copy_baked_resources_to_baked_dump(Baked_Resource *resource, void *udata)
{
char *baked_dump = (char *)udata;
char path[PATH_MAX];
get_baked_resource_path(resource->key, path, sizeof(path));
char dest[PATH_MAX];
snprintf(dest, sizeof(dest), "%s/%s", baked_dump, resource->key);
cp(path, dest);
}
void populate_baked_dump(char *baked_dump)
{
baked_resource_each(&copy_baked_resources_to_baked_dump, baked_dump);
}
volatile sig_atomic_t alive = true;
void graceful_shutdown(int no) { alive = false; }
int main(int argc, char ** argv)
{
struct sigaction sa;
sa.sa_handler = &graceful_shutdown;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, nil);
start_timer();
init_baked_resources();
if ((baked_dump_path.value = init_baked_dump()) == nil) {
return 1;
}
populate_baked_dump(baked_dump_path.value);
mg_log_set(MG_LL_DEBUG);
struct mg_mgr mgr;
@ -153,13 +295,14 @@ int main(int argc, char ** argv)
mg_wakeup_init(&mgr);
mg_http_listen(&mgr, CONFIG_LISTEN_URL, &event_handler, NULL);
for (;;) {
while (alive) {
mg_mgr_poll(&mgr, 1000);
scratch_arena_reset();
}
mg_mgr_free(&mgr);
free_baked_dump(baked_dump_path.value);
free_baked_resources();
return 0;

328
routes.c
View File

@ -7,84 +7,78 @@
#include "routes.h"
#include "baked.h"
#include "clonestr.h"
#include "commit.h"
#include "timer.h"
extern char *clonestr_alloc(Allocator *alloc, char *s);
#define INTERNAL_SERVER_ERROR_MSG "Internal server error ;(. Try again later..."
void make_internal_server_error(Route_Result *result)
{
result->status_code = 500;
result->type = ROUTE_RESULT_TEXT;
list_append(&result->headers, "Content-Type: text/plain");
sb_append_nstr(&result->body, INTERNAL_SERVER_ERROR_MSG);
sb_finish(&result->body);
}
#define META_TAGS \
"<meta name=\"description\" content=\"kamkow1's personal website and blog. Focused on C programming, weight lifting and personal thoughts.\" />" \
"<meta name=\"keywords\" content=\"C, C Programming, Blog, kamkow1, Linux\" />" \
"<meta name=\"author\" content=\"kamkow1\">" \
"<meta name=\"application-name\" content=\"aboba\">"
void make_application_json(Route_Result *result, int code, cJSON *root)
void make_application_json(Allocator *alloc, Route_Result *result, int code, cJSON *root)
{
char *root_text = cJSON_PrintUnformatted(root);
defer { free(root_text); }
size_t buf_size = 512;
char *buf = gebs_malloc(alloc, buf_size);
cJSON_PrintPreallocated(root, buf, buf_size, false);
result->status_code = code;
result->type = ROUTE_RESULT_TEXT;
list_append(&result->headers, clonestr("Content-Type: application/json"));
sb_append_nstr(&result->body, root_text);
sb_finish(&result->body);
result->type = ROUTE_RESULT_DYNAMIC;
list_append_alloc(alloc, &result->headers, clonestr_alloc(alloc, "Content-Type: application/json"));
sb_append_nstr_alloc(alloc, &result->body, buf);
sb_finish_alloc(alloc, &result->body);
}
void make_binary(Route_Result *result, char *content_type, int code, char *data, size_t size)
void make_text(Allocator *alloc, Route_Result *result, char *subtype, int code, char *in)
{
char type[100];
snprintf(type, sizeof(type), "Content-Type: %s", content_type);
result->status_code = code;
result->type = ROUTE_RESULT_BINARY;
list_append(&result->headers, clonestr(type));
for (size_t i = 0; i < size; i++) {
sb_append_char(&result->body, data[i]);
}
result->type = ROUTE_RESULT_DYNAMIC;
list_append_alloc(alloc, &result->headers,
clonestr_alloc(alloc, fmt("Content-Type: text/%s", subtype)));
sb_append_nstr_alloc(alloc, &result->body, in);
sb_finish(&result->body);
}
void make_text(Route_Result *result, char *subtype, int code, char *in)
void make_internal_server_error(Allocator *alloc, Route_Result *result)
{
char type[100];
snprintf(type, sizeof(type), "Content-Type: text/%s", subtype);
result->status_code = code;
result->type = ROUTE_RESULT_TEXT;
list_append(&result->headers, clonestr(type));
sb_append_nstr(&result->body, in);
sb_finish(&result->body);
make_text(alloc, result, "plain", 500, INTERNAL_SERVER_ERROR_MSG);
}
bool gpp_run(char *path, NString_List *env, String_Builder *out)
bool gpp_run(Allocator *alloc, char *path, NString_List *env, String_Builder *out)
{
Cmd cmd = {0};
defer { cmd_free(&cmd); }
char gpp1[PATH_MAX];
if (!get_baked_resource_path("gpp1", gpp1, sizeof(gpp1))) {
return false;
}
cmd_append(&cmd, gpp1);
cmd_append(&cmd, "-H");
cmd_append(&cmd, "-x");
cmd_append(&cmd, "--nostdinc");
cmd_append(&cmd, path);
cmd_append_alloc(alloc, &cmd, gpp1);
cmd_append_alloc(alloc, &cmd, "-H");
cmd_append_alloc(alloc, &cmd, "-x");
cmd_append_alloc(alloc, &cmd, "--nostdinc");
cmd_append_alloc(alloc, &cmd, path);
for (size_t i = 0; i < env->count; i++) {
cmd_append(&cmd, env->items[i]);
cmd_append_alloc(alloc, &cmd, env->items[i]);
}
return cmd_run_collect(&cmd, out) == 0;
bool ok = cmd_run_collect_alloc(alloc, &cmd, out) == 0;
size_t len = out->count > 100 ? 100 : out->count;
LOGI("Output (first 100): \n-------------------------------\n%.*s\n-------------------------------\n", len, out->items);
return ok;
}
#if MY_DEBUG
ROUTE_HANDLER(build_id)
void route_build_id(Allocator *alloc, struct mg_http_message *msg, Route_Result *result, void *context_data)
{
LOGI("handler build_id (%.*s)\n", msg->uri.len, msg->uri.buf);
// TODO: Somehow use our arena in here. This will require a slight rework of the way cJSON works.
cJSON *root = cJSON_CreateObject();
defer { cJSON_Delete(root); }
@ -92,283 +86,217 @@ ROUTE_HANDLER(build_id)
uchar md5_buf[16];
md5String(time, md5_buf);
String_Builder sb = {0};
defer { sb_free(&sb); }
for (size_t i = 0; i < 16; i++) {
sb_append_nstr(&sb, fmt("%02x", md5_buf[i]));
sb_append_nstr_alloc(alloc, &sb, clonestr_alloc(alloc, fmt("%02x", md5_buf[i])));
}
sb_finish(&sb);
sb_finish_alloc(alloc, &sb);
cJSON_AddItemToObject(root, "build_id", cJSON_CreateString(sb.items));
make_application_json(result, 200, root);
make_application_json(alloc, result, 200, root);
LOGI("handler build_id done\n");
}
#endif
ROUTE_HANDLER(page_not_found)
void route_page_not_found(Allocator *alloc, struct mg_http_message *msg, Route_Result *result, void *context_data)
{
LOGI("handler page not found (%.*s)\n", msg->uri.len, msg->uri.buf);
NString_List env = {0};
defer { list_free(&env); }
list_append(&env, fmt("-DURL=%.*s", msg->uri.len, msg->uri.buf));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DURL=%.*s", msg->uri.len, msg->uri.buf)));
String_Builder out = {0};
defer { sb_free(&out); }
char path[PATH_MAX] = {0};
if (!get_baked_resource_path("page-missing.html", path, sizeof(path))) {
make_internal_server_error(result);
LOGE("Failed to get baked resource page-missing.html\n");
make_internal_server_error(alloc, result);
return;
}
list_append(&env, fmt("-DCOMMIT=%s", COMMIT_STRING));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DCOMMIT=%s", COMMIT_STRING)));
char timer[100];
get_timer_string(timer, sizeof(timer));
list_append(&env, fmt("-DRUNNING_SINCE=%s", timer));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DRUNNING_SINCE=%s", timer)));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DMETA_TAGS=%s", META_TAGS)));
#if MY_DEBUG
list_append(&env, "-DHOTRELOAD");
list_append_alloc(alloc, &env, "-DHOTRELOAD");
#endif
bool ok = gpp_run(path, &env, &out);
sb_finish(&out);
bool ok = gpp_run(alloc, path, &env, &out);
sb_finish_alloc(alloc, &out);
if (!ok) {
make_internal_server_error(result);
make_internal_server_error(alloc, result);
} else {
make_text(result, "html", 200, out.items);
make_text(alloc, result, "html", 200, out.items);
}
LOGI("handler page not found done\n");
}
ROUTE_HANDLER(simple_css)
{
char path[PATH_MAX] = {0};
if (!get_baked_resource_path("simple.css", path, sizeof(path))) {
make_internal_server_error(result);
return;
}
String_Builder sb = {0};
defer { sb_free(&sb); }
if (!sb_read_file(&sb, path)) {
make_internal_server_error(result);
return;
}
sb_finish(&sb);
make_text(result, "css", 200, sb.items);
}
ROUTE_HANDLER(me_jpg)
{
char path[PATH_MAX] = {0};
if (!get_baked_resource_path("me.jpg", path, sizeof(path))) {
make_internal_server_error(result);
return;
}
String_Builder sb = {0};
defer { sb_free(&sb); }
if (!sb_read_file(&sb, path)) {
make_internal_server_error(result);
return;
}
make_binary(result, "image/jpeg", 200, sb.items, sb.count);
}
ROUTE_HANDLER(favicon)
{
char path[PATH_MAX] = {0};
if (!get_baked_resource_path("favicon.ico", path, sizeof(path))) {
make_internal_server_error(result);
return;
}
String_Builder sb = {0};
defer { sb_free(&sb); }
if (!sb_read_file(&sb, path)) {
make_internal_server_error(result);
return;
}
make_binary(result, "image/x-icon", 200, sb.items, sb.count);
}
#if MY_DEBUG
ROUTE_HANDLER(hotreload_js)
{
char path[PATH_MAX] = {0};
if (!get_baked_resource_path("hotreload.js", path, sizeof(path))) {
make_internal_server_error(result);
return;
}
String_Builder sb = {0};
defer { sb_free(&sb); }
if (!sb_read_file(&sb, path)) {
make_internal_server_error(result);
return;
}
sb_finish(&sb);
make_text(result, "javascript", 200, sb.items);
}
#endif
ROUTE_HANDLER(home)
void route_home(Allocator *alloc, struct mg_http_message *msg, Route_Result *result, void *context_data)
{
LOGI("handler home (%.*s)\n", msg->uri.len, msg->uri.buf);
NString_List env = {0};
defer { list_free(&env); }
String_Builder out = {0};
defer { sb_free(&out); }
char path[PATH_MAX] = {0};
if (!get_baked_resource_path("home.html", path, sizeof(path))) {
make_internal_server_error(result);
LOGE("Failed to get baked resource home.html\n");
make_internal_server_error(alloc, result);
return;
}
list_append(&env, fmt("-DCOMMIT=%s", COMMIT_STRING));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DCOMMIT=%s", COMMIT_STRING)));
char timer[100];
get_timer_string(timer, sizeof(timer));
list_append(&env, fmt("-DRUNNING_SINCE=%s", timer));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DRUNNING_SINCE=%s", timer)));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DMETA_TAGS=%s", META_TAGS)));
#if MY_DEBUG
list_append(&env, "-DHOTRELOAD");
list_append_alloc(alloc, &env, "-DHOTRELOAD");
#endif
bool ok = gpp_run(path, &env, &out);
sb_finish(&out);
bool ok = gpp_run(alloc, path, &env, &out);
sb_finish_alloc(alloc, &out);
if (!ok) {
make_internal_server_error(result);
make_internal_server_error(alloc, result);
} else {
make_text(result, "html", 200, out.items);
make_text(alloc, result, "html", 200, out.items);
}
LOGI("handler home done\n");
}
ROUTE_HANDLER(generic_blog)
void route_generic_blog(Allocator *alloc, struct mg_http_message *msg, Route_Result *result, void *context_data)
{
LOGI("handler generic blog (%.*s)\n", msg->uri.len, msg->uri.buf);
NString_List env = {0};
defer { list_free(&env); }
String_Builder out = {0};
defer { sb_free(&out); }
Baked_Resource *resource = (Baked_Resource *)context_data;
char md_path[PATH_MAX] = {0};
if (!get_baked_resource_path(resource->key, md_path, sizeof(md_path))) {
make_internal_server_error(result);
LOGE("Failed to get baked resource %s\n", resource->key);
make_internal_server_error(alloc, result);
return;
}
list_append(&env, fmt("-DCOMMIT=%s", COMMIT_STRING));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DCOMMIT=%s", COMMIT_STRING)));
char timer[100];
get_timer_string(timer, sizeof(timer));
list_append(&env, fmt("-DRUNNING_SINCE=%s", timer));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DRUNNING_SINCE=%s", timer)));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DMETA_TAGS=%s", META_TAGS)));
#if MY_DEBUG
list_append(&env, "-DHOTRELOAD");
list_append_alloc(alloc, &env, "-DHOTRELOAD");
#endif
String_Builder md = {0};
defer { sb_free(&md); }
if (!sb_read_file(&md, md_path)) {
make_internal_server_error(result);
if (!sb_read_file_alloc(alloc, &md, md_path)) {
make_internal_server_error(alloc, result);
return;
}
String_Builder md_prepared = {0};
defer { sb_free(&md_prepared); }
for (size_t i = 0; i < md.count; i++) {
char c = md.items[i];
switch (c) {
case '\"': sb_append_nstr(&md_prepared, "\\\""); break;
case '\'': sb_append_nstr(&md_prepared, "\\\'"); break;
case '\\': sb_append_nstr(&md_prepared, "\\\\"); break;
case '\a': sb_append_nstr(&md_prepared, "\\a"); break;
case '\b': sb_append_nstr(&md_prepared, "\\b"); break;
case '\r': sb_append_nstr(&md_prepared, "\\r"); break;
case '\n': sb_append_nstr(&md_prepared, "\\n"); break;
case '\t': sb_append_nstr(&md_prepared, "\\t"); break;
case '\"': sb_append_nstr_alloc(alloc, &md_prepared, "\\\""); break;
case '\'': sb_append_nstr_alloc(alloc, &md_prepared, "\\\'"); break;
case '\\': sb_append_nstr_alloc(alloc, &md_prepared, "\\\\"); break;
case '\a': sb_append_nstr_alloc(alloc, &md_prepared, "\\a"); break;
case '\b': sb_append_nstr_alloc(alloc, &md_prepared, "\\b"); break;
case '\r': sb_append_nstr_alloc(alloc, &md_prepared, "\\r"); break;
case '\n': sb_append_nstr_alloc(alloc, &md_prepared, "\\n"); break;
case '\t': sb_append_nstr_alloc(alloc, &md_prepared, "\\t"); break;
default:
if (iscntrl(md.items[i])) {
sb_append_nstr(&md_prepared, fmt("\\%03o", c));
sb_append_nstr_alloc(alloc, &md_prepared, clonestr_alloc(alloc, fmt("\\%03o", c)));
} else {
sb_append_char(&md_prepared, c);
sb_append_char_alloc(alloc, &md_prepared, c);
}
break;
}
}
sb_finish(&md_prepared);
sb_finish_alloc(alloc, &md_prepared);
list_append(&env, fmt("-DBLOG_POST_TITLE=%s", resource->key));
list_append(&env, fmt("-DBLOG_POST_MARKDOWN=%s", md_prepared.items));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DBLOG_POST_TITLE=%s", resource->key)));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DBLOG_POST_MARKDOWN=%s", md_prepared.items)));
char tmpl_file[PATH_MAX] = {0};
if (!get_baked_resource_path("template-blog.html", tmpl_file, sizeof(tmpl_file))) {
make_internal_server_error(result);
make_internal_server_error(alloc, result);
return;
}
bool ok = gpp_run(tmpl_file, &env, &out);
sb_finish(&out);
bool ok = gpp_run(alloc, tmpl_file, &env, &out);
sb_finish_alloc(alloc, &out);
if (!ok) {
make_internal_server_error(result);
make_internal_server_error(alloc, result);
} else {
make_text(result, "html", 200, out.items);
make_text(alloc, result, "html", 200, out.items);
}
LOGI("handler generic blog done\n");
}
ROUTE_HANDLER(blog)
{
NString_List env = {0};
defer { list_free(&env); }
String_Builder out = {0};
defer { sb_free(&out); }
Arena tmp = arena_get();
defer { arena_destroy(&tmp); }
String_Builder sb = {0};
void collect_blogs(Baked_Resource *resource)
void collect_blogs(Baked_Resource *resource, void *udata)
{
struct { Allocator *alloc; String_Builder *sb; } *ud = udata;
if ((strlen(resource->key) >= strlen("blog-"))
&& strncmp(resource->key, "blog-", strlen("blog-")) == 0) {
sb_append_nstr_alloc(&tmp, &sb, fmt("<li><a href=\"/%s\">%s</a></li>", resource->key, resource->key));
sb_append_nstr_alloc(ud->alloc, ud->sb,
clonestr_alloc(ud->alloc, fmt("<li><a href=\"/%s\">%s</a></li>", resource->key, resource->key)));
}
}
baked_resource_each(&collect_blogs);
sb_finish_alloc(&tmp, &sb);
list_append(&env, fmt("-DBLOG_POSTS=%s", sb.items));
void route_blog(Allocator *alloc, struct mg_http_message *msg, Route_Result *result, void *context_data)
{
NString_List env = {0};
String_Builder out = {0};
String_Builder sb = {0};
struct { Allocator *alloc; String_Builder *sb; } collect_blogs_data = {
.alloc = alloc,
.sb = &sb,
};
baked_resource_each(&collect_blogs, &collect_blogs_data);
sb_finish_alloc(alloc, &sb);
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DBLOG_POSTS=%s", sb.items)));
char path[PATH_MAX] = {0};
if (!get_baked_resource_path("blog.html", path, sizeof(path))) {
make_internal_server_error(result);
make_internal_server_error(alloc, result);
return;
}
list_append(&env, fmt("-DCOMMIT=%s", COMMIT_STRING));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DCOMMIT=%s", COMMIT_STRING)));
char timer[100];
get_timer_string(timer, sizeof(timer));
list_append(&env, fmt("-DRUNNING_SINCE=%s", timer));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DRUNNING_SINCE=%s", timer)));
list_append_alloc(alloc, &env, clonestr_alloc(alloc, fmt("-DMETA_TAGS=%s", META_TAGS)));
#if MY_DEBUG
list_append(&env, "-DHOTRELOAD");
list_append_alloc(alloc, &env, "-DHOTRELOAD");
#endif
bool ok = gpp_run(path, &env, &out);
sb_finish(&out);
bool ok = gpp_run(alloc, path, &env, &out);
sb_finish_alloc(alloc, &out);
if (!ok) {
make_internal_server_error(result);
make_internal_server_error(alloc, result);
} else {
make_text(result, "html", 200, out.items);
make_text(alloc, result, "html", 200, out.items);
}
}

View File

@ -6,21 +6,23 @@
typedef struct {
enum {
ROUTE_RESULT_TEXT,
ROUTE_RESULT_BINARY,
ROUTE_RESULT_DYNAMIC,
ROUTE_RESULT_STATIC,
} type;
int status_code;
NString_List headers;
String_Builder body;
Arena *arena;
} Route_Result;
typedef struct {
struct mg_mgr *mgr;
ulong conn_id;
struct mg_str message;
Arena *arena;
} Route_Thread_Data;
typedef void (*Route_Handler)(struct mg_http_message *msg, Route_Result *result, void *context_data);
typedef void (*Route_Handler)(Allocator *alloc, struct mg_http_message *msg, Route_Result *result, void *context_data);
typedef struct {
char *key; // path
@ -28,18 +30,12 @@ typedef struct {
void *context_data;
} Route;
#define ROUTE_HANDLER(name) void route_##name(struct mg_http_message *msg, Route_Result *result, void *context_data)
ROUTE_HANDLER(page_not_found);
ROUTE_HANDLER(simple_css);
ROUTE_HANDLER(favicon);
ROUTE_HANDLER(me_jpg);
#if MY_DEBUG
ROUTE_HANDLER(hotreload_js);
ROUTE_HANDLER(build_id);
void route_build_id(Allocator *alloc, struct mg_http_message *msg, Route_Result *result, void *context_data);
#endif
ROUTE_HANDLER(home);
ROUTE_HANDLER(generic_blog);
ROUTE_HANDLER(blog);
void route_page_not_found(Allocator *alloc, struct mg_http_message *msg, Route_Result *result, void *context_data);
void route_home(Allocator *alloc, struct mg_http_message *msg, Route_Result *result, void *context_data);
void route_generic_blog(Allocator *alloc, struct mg_http_message *msg, Route_Result *result, void *context_data);
void route_blog(Allocator *alloc, struct mg_http_message *msg, Route_Result *result, void *context_data);
#endif // ROUTES_H_

11
timer.c
View File

@ -3,22 +3,27 @@
#include <stdio.h>
#include "timer.h"
#include "locked.h"
time_t rawtime;
static locked(time_t) rawtime;
void start_timer(void)
{
time(&rawtime);
lockx(&rawtime);
time(&rawtime.value);
unlockx(&rawtime);
}
void get_timer_string(char *output, size_t size)
{
lockx(&rawtime);
struct tm * timeinfo;
timeinfo = localtime(&rawtime);
timeinfo = localtime(&rawtime.value);
snprintf(output, size, "[%02d %02d %d %02d:%02d:%02d]", timeinfo->tm_mday,
timeinfo->tm_mon + 1, timeinfo->tm_year + 1900,
timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec);
unlockx(&rawtime);
}

View File

@ -3,7 +3,8 @@
<head>
<title>Blog</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/simple.css" />
<link rel="stylesheet" href="/bakedres/simple.css" />
<#META_TAGS>
</head>
<body>
<h1>Blog posts</h1>
@ -23,7 +24,8 @@
</footer>
<#ifdef HOTRELOAD>
<script src="/hotreload.js"></script>
<script src="/bakedres/hotreload.js"></script>
<#endif>
<script src="/bakedres/theme.js"></script>
</body>
</html>

View File

@ -3,7 +3,8 @@
<head>
<title>Kamil's personal website</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/simple.css" />
<link rel="stylesheet" href="/bakedres/simple.css" />
<#META_TAGS>
</head>
<body>
<h1>Kamil's personal website</h1>
@ -53,7 +54,7 @@
</ul>
</section>
<section>
<img src="/me.jpg" alt="literally me" />
<img src="/bakedres/me.jpg" alt="literally me" />
</section>
<footer>
<div style="float: left;">
@ -64,7 +65,8 @@
</footer>
<#ifdef HOTRELOAD>
<script src="/hotreload.js"></script>
<script src="/bakedres/hotreload.js"></script>
<#endif>
<script src="/bakedres/theme.js"></script>
</body>
</html>

View File

@ -2,7 +2,8 @@
<html>
<head>
<title>404 - Page not found</title>
<link rel="stylesheet" href="/simple.css" />
<link rel="stylesheet" href="/bakedres/simple.css" />
<#META_TAGS>
</head>
<body>
<h1>The page you were looking for doesn't exist!</h1>
@ -18,7 +19,8 @@
</footer>
<#ifdef HOTRELOAD>
<script src="/hotreload.js"></script>
<script src="/bakedres/hotreload.js"></script>
<#endif>
<script src="/bakedres/theme.js"></script>
</body>
</html>

View File

@ -3,7 +3,10 @@
<head>
<meta charset="utf-8" />
<title><#BLOG_POST_TITLE></title>
<link rel="stylesheet" href="/simple.css" />
<link rel="stylesheet" href="/bakedres/simple.css" />
<#META_TAGS>
<link rel="stylesheet" href="/bakedres/hljs-rainbow.css">
<script src="/bakedres/highlight.js"></script>
</head>
<body>
<div id="content"></div>
@ -14,13 +17,15 @@
<span>Running since: <#RUNNING_SINCE></span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="/bakedres/marked.js"></script>
<script>
document.getElementById("content").innerHTML = marked.parse(`<#BLOG_POST_MARKDOWN>`);
</script>
<#ifdef HOTRELOAD>
<script src="/hotreload.js"></script>
<script src="/bakedres/hotreload.js"></script>
<#endif>
<script src="/bakedres/theme.js"></script>
<script>hljs.highlightAll();</script>
</body>
</html>

View File

@ -183,7 +183,7 @@ int main(int argc, char ** argv)
if (cmd_pid != -1) {
LOGI("Killing %d\n", cmd_pid);
kill(cmd_pid, SIGKILL);
kill(cmd_pid, SIGINT);
}
cmd_pid = cmd_run_async(&cmd);

1
zip Submodule

Submodule zip added at 649a16c5ea