vtgl

terminal emulator implemented in OpenGL
git clone anongit@rnpnr.xyz:vtgl.git
Log | Files | Refs | Feed | LICENSE

platform_linux_common.c (13619B)


      1 #define FUTEX_WAIT 0
      2 #define FUTEX_WAKE 1
      3 
      4 #define CLOCK_MONOTONIC 1
      5 
      6 #define PR_SET_NAME 15
      7 
      8 #define PROT_READ   0x01
      9 #define PROT_RW     0x03
     10 
     11 #define MFD_CLOEXEC 0x01
     12 
     13 #define MAP_SHARED  0x01
     14 #define MAP_PRIVATE 0x02
     15 #define MAP_FIXED   0x10
     16 #define MAP_ANON    0x20
     17 
     18 #define MADV_HUGEPAGE 14
     19 
     20 #define O_RDONLY    0x00000
     21 #define O_WRONLY    0x00001
     22 #define O_RDWR      0x00002
     23 #define O_CREAT     0x00040
     24 #define O_NOCTTY    0x00100
     25 #define O_APPEND    0x00400
     26 #define O_NONBLOCK  0x00800
     27 #define O_CLOEXEC   0x80000
     28 
     29 #define IN_CLOSE_WRITE   0x00000008
     30 #define IN_CLOSE_NOWRITE 0x00000010
     31 #define IN_MODIFY        0x00000002
     32 
     33 #define LINUX_INOTIFY_MASK (IN_CLOSE_WRITE|IN_CLOSE_NOWRITE|IN_MODIFY)
     34 
     35 #define WNOHANG 1
     36 #define W_IF_EXITED(s) (!((s) & 0x7F))
     37 
     38 /* TODO: glibc/gcc indirectly include sys/select.h if you include immintrin.h. If that
     39  * header is removed this can also be removed */
     40 #undef FD_SET
     41 #undef FD_ISSET
     42 
     43 #define FD_SET(d, s)   ((s)[(d) / (8 * sizeof(*(s)))] |= (1ULL << ((d) % (8 * sizeof(*(s))))))
     44 #define FD_ISSET(d, s) ((s)[(d) / (8 * sizeof(*(s)))] &  (1ULL << ((d) % (8 * sizeof(*(s))))))
     45 
     46 #define TIOCSCTTY  0x540E
     47 #define TIOCSWINSZ 0x5414
     48 #define TIOCSPTLCK 0x40045431 /* (un)lock pty   */
     49 #define TIOCGPTN   0x80045430 /* get pty number */
     50 
     51 #ifndef VERSION
     52 #define VERSION "unknown"
     53 #endif
     54 
     55 #define OS_MAP_READ    PROT_READ
     56 #define OS_MAP_PRIVATE MAP_PRIVATE
     57 
     58 struct __attribute__((aligned(16))) stack_base {
     59 	void (*entry)(struct stack_base *stack);
     60 	Arena            thread_arena;
     61 	void            *window;
     62 	TerminalMemory  *terminal_memory;
     63 	TerminalInput   *input;
     64 	i32              work_futex;
     65 	b32              thread_asleep;
     66 };
     67 
     68 typedef struct {
     69 	platform_file_watch_callback_fn *fn;
     70 	u8   *path;
     71 	void *user_ctx;
     72 	i32   inode;
     73 	i32   handle;
     74 } linux_file_watch;
     75 
     76 typedef struct linux_deferred_file_reload_queue {
     77 	struct linux_deferred_file_reload_queue *next;
     78 	struct linux_deferred_file_reload_queue *last;
     79 	i32 index;
     80 	i32 failures;
     81 } linux_deferred_file_reload_queue;
     82 
     83 typedef struct {
     84 	iptr handle;
     85 	iptr process_id;
     86 } linux_platform_process;
     87 
     88 typedef struct {
     89 	Arena  platform_memory;
     90 	void  *window;
     91 
     92 	TerminalMemory memory;
     93 	TerminalInput  input;
     94 
     95 	Stream char_stream;
     96 
     97 	linux_platform_process child;
     98 	i32 inotify_fd;
     99 	i32 win_fd;
    100 
    101 	linux_deferred_file_reload_queue file_reload_queue;
    102 	linux_deferred_file_reload_queue *file_reload_free_list;
    103 	linux_file_watch file_watches[32];
    104 	i32 file_watch_count;
    105 
    106 	Stream error_stream;
    107 
    108 	struct stack_base *render_stack;
    109 
    110 #ifdef _DEBUG
    111 	void *library_handle;
    112 #endif
    113 } PlatformCtx;
    114 static PlatformCtx linux_ctx;
    115 
    116 static void
    117 os_write_err_msg(s8 msg)
    118 {
    119 	syscall3(SYS_write, 2, (iptr)msg.data, msg.len);
    120 }
    121 
    122 __attribute__((noreturn))
    123 static void
    124 os_fatal(s8 msg)
    125 {
    126 	os_write_err_msg(msg);
    127 	syscall1(SYS_exit_group, 1);
    128 	__builtin_unreachable();
    129 }
    130 
    131 static u32
    132 os_file_attribute_to_mode(u32 attr)
    133 {
    134 	u32 result = O_CREAT;
    135 	if (attr & FA_READ && attr & FA_WRITE) {
    136 		result |= O_RDWR;
    137 	} else if (attr & FA_READ) {
    138 		result |= O_RDONLY;
    139 	} else if (attr & FA_WRITE) {
    140 		result |= O_WRONLY;
    141 	}
    142 
    143 	if (attr & FA_APPEND)
    144 		result |= O_APPEND;
    145 
    146 	return result;
    147 }
    148 
    149 static iptr
    150 os_open(u8 *name, u32 attr)
    151 {
    152 	iptr result = syscall3(SYS_open, (iptr)name, os_file_attribute_to_mode(attr), 0660);
    153 	if (result > -4096UL)
    154 		result = INVALID_FILE;
    155 	return result;
    156 }
    157 
    158 static b32
    159 os_offset_write(iptr file, s8 raw, size offset)
    160 {
    161 	size result = syscall4(SYS_pwrite, file, (iptr)raw.data, raw.len, offset);
    162 	return result == raw.len;
    163 }
    164 
    165 static PLATFORM_WRITE_FN(os_write)
    166 {
    167 	size result = syscall3(SYS_write, file, (iptr)raw.data, raw.len);
    168 	return result == raw.len;
    169 }
    170 
    171 static void
    172 os_close(iptr file)
    173 {
    174 	syscall1(SYS_close, file);
    175 }
    176 
    177 static PLATFORM_READ_FN(os_read)
    178 {
    179 	size r = 0, remaining = buffer.len, total_bytes_read = 0;
    180 
    181 	do {
    182 		remaining        -= r;
    183 		total_bytes_read += r;
    184 		r = syscall3(SYS_read, file, (iptr)(buffer.data + total_bytes_read), remaining);
    185 	} while (r <= -4096UL && remaining != 0);
    186 
    187 	return total_bytes_read;
    188 }
    189 
    190 static PLATFORM_READ_FILE_FN(os_read_file)
    191 {
    192 	s8 result = {0};
    193 
    194 	stat_buffer sb;
    195 	i32 fd     = syscall3(SYS_open, (iptr)path, O_RDONLY, 0);
    196 	i64 status = syscall2(SYS_fstat, fd, (iptr)sb);
    197 
    198 	if (fd <= -4096UL && status == 0) {
    199 		result = s8alloc(a, STAT_FILE_SIZE(sb));
    200 		size rlen = os_read(fd, result);
    201 		syscall1(SYS_close, fd);
    202 		if (result.len != rlen)
    203 			result.len = 0;
    204 	}
    205 
    206 	return result;
    207 }
    208 
    209 static MemoryBlock
    210 linux_block_alloc(size requested_size)
    211 {
    212 	MemoryBlock result = {0};
    213 
    214 	/* TODO: query system for HUGETLB support and use those instead of page size */
    215 	size alloc_size = requested_size;
    216 	if (alloc_size % PAGE_SIZE != 0)
    217 		alloc_size += PAGE_SIZE - alloc_size % PAGE_SIZE;
    218 
    219 	i64 memory = syscall6(SYS_mmap, 0, alloc_size, PROT_RW, MAP_ANON|MAP_PRIVATE, -1, 0);
    220 	if (memory <= -4096UL) {
    221 		result.memory = (void *)memory;
    222 		result.size   = alloc_size;
    223 		syscall3(SYS_madvise, memory, alloc_size, MADV_HUGEPAGE);
    224 	}
    225 
    226 	return result;
    227 }
    228 
    229 static f64
    230 os_get_time(void)
    231 {
    232 	i64 timespec[2];
    233 	syscall2(SYS_clock_gettime, CLOCK_MONOTONIC, (iptr)timespec);
    234 	f64 result = timespec[0] + ((f64)timespec[1]) * 1e-9;
    235 	return result;
    236 }
    237 
    238 static os_mapped_file
    239 os_map_file(char *path, i32 mode, i32 perm)
    240 {
    241 	os_mapped_file result = {0};
    242 
    243 	i32 open_mode = 0;
    244 	switch (mode) {
    245 	case OS_MAP_READ: open_mode = O_RDONLY; break;
    246 	default: ASSERT(0);
    247 	}
    248 
    249 	stat_buffer sb;
    250 	i32 fd     = syscall3(SYS_open, (iptr)path, open_mode, 0);
    251 	i64 status = syscall2(SYS_fstat, fd, (iptr)sb);
    252 
    253 	if (fd <= -4096UL && status == 0) {
    254 		i64 memory = syscall6(SYS_mmap, 0, STAT_FILE_SIZE(sb), mode, perm, fd, 0);
    255 		if (memory <= -4096UL) {
    256 			result.data = (u8 *)memory;
    257 			result.len  = STAT_FILE_SIZE(sb);
    258 		}
    259 		syscall1(SYS_close, fd);
    260 	}
    261 
    262 	return result;
    263 }
    264 
    265 static PLATFORM_ALLOCATE_RING_BUFFER_FN(os_allocate_ring_buffer)
    266 {
    267 	/* TODO: query system for HUGETLB support and use those instead of page size */
    268 	if (capacity % PAGE_SIZE != 0)
    269 		capacity += PAGE_SIZE - capacity % PAGE_SIZE;
    270 	ASSERT(capacity % PAGE_SIZE == 0);
    271 
    272 	i32 fd = syscall2(SYS_memfd_create, (iptr)"vtgl:rb", MFD_CLOEXEC);
    273 	if (fd > -4096UL) os_fatal(s8("os_alloc_ring_buffer: failed to open mem_fd\n"));
    274 	syscall2(SYS_ftruncate, fd, capacity);
    275 
    276 	rb->widx   = 0;
    277 	rb->filled = 0;
    278 	rb->cap    = capacity;
    279 	rb->buf    = (u8 *)syscall6(SYS_mmap, 0, (iptr)(3 * rb->cap), 0, MAP_ANON|MAP_PRIVATE, -1, 0);
    280 	if ((iptr)rb->buf > -4096UL)
    281 		os_fatal(s8("os_alloc_ring_buffer: initial mmap failed\n"));
    282 	syscall3(SYS_madvise, (iptr)rb->buf, 3 * rb->cap, MADV_HUGEPAGE);
    283 
    284 	for (i32 i = 0; i < 3; i++) {
    285 		i64 memory = syscall6(SYS_mmap, (iptr)(rb->buf + i * rb->cap), rb->cap, PROT_RW,
    286 		                      MAP_FIXED|MAP_SHARED, fd, 0);
    287 		if (memory > -4096UL) {
    288 			u8 buf[256];
    289 			Stream err = {.buf = buf, .cap = sizeof(buf)};
    290 			stream_push_s8(&err, s8("os_alloc_ring_buffer: mmap("));
    291 			stream_push_u64(&err, i);
    292 			stream_push_s8(&err, s8(") failed\n"));
    293 			os_fatal(stream_to_s8(&err));
    294 		}
    295 	}
    296 	syscall1(SYS_close, fd);
    297 
    298 	/* NOTE: start in the middle page */
    299 	rb->buf += rb->cap;
    300 }
    301 
    302 static b32
    303 os_child_exited(iptr pid)
    304 {
    305 	i64 status;
    306 	i64 r = syscall4(SYS_wait4, pid, (iptr)&status, WNOHANG, 0);
    307 	return r == pid && W_IF_EXITED(status);
    308 }
    309 
    310 static linux_platform_process
    311 os_fork_child(s8 cmd, c8 **envp)
    312 {
    313 	i32 n = 0;
    314 
    315 	/* NOTE: we open in non-blocking mode so that we can try and fully drain the pipe
    316 	 * before processing. Otherwise a single read will be limited to the page size */
    317 	i64 m = syscall3(SYS_open, (iptr)"/dev/ptmx", O_RDWR|O_NOCTTY|O_NONBLOCK|O_CLOEXEC, 0);
    318 	if (m > -4096UL) os_fatal(s8("os_fork_child: failed to open master terminal\n"));
    319 	/* NOTE: first unlock the tty, then get a valid pty number */
    320 	if (syscall3(SYS_ioctl, m, TIOCSPTLCK, (iptr)&n) || syscall3(SYS_ioctl, m, TIOCGPTN, (iptr)&n))
    321 		os_fatal(s8("os_fork_child: failed to get a pty number\n"));
    322 
    323 	u8 buffer[20] = {"/dev/pts/"};
    324 	Stream sbuf = {.buf = buffer, .cap = 20, .widx = sizeof("/dev/pts/") - 1};
    325 	stream_push_i64(&sbuf, n);
    326 	stream_push_byte(&sbuf, 0);
    327 
    328 	i64 s = syscall3(SYS_open, (iptr)sbuf.buf, O_RDWR|O_NOCTTY, 0);
    329 	if (s > -4096UL) os_fatal(s8("os_fork_child: failed to open slave terminal\n"));
    330 
    331 	i64 pid = syscall1(SYS_fork, 0);
    332 	if (pid > -4096UL) os_fatal(s8("os_fork_child: failed to fork a child\n"));
    333 
    334 	if (pid == 0) {
    335 		syscall1(SYS_setsid, 0);
    336 		syscall2(SYS_dup2, s, 0);
    337 		syscall2(SYS_dup2, s, 1);
    338 		syscall2(SYS_dup2, s, 2);
    339 		syscall3(SYS_ioctl, s, TIOCSCTTY, 0);
    340 		if (s > 2) syscall1(SYS_close, s);
    341 		ASSERT(cmd.data[cmd.len] == 0);
    342 		u8 *argv[] = {cmd.data, 0};
    343 		syscall3(SYS_execve, (iptr)cmd.data, (iptr)argv, (iptr)envp);
    344 		__builtin_unreachable();
    345 		os_fatal(s8("failed to exec child\n"));
    346 	}
    347 	syscall1(SYS_close, s);
    348 
    349 	return (linux_platform_process){.process_id = pid, .handle = m};
    350 }
    351 
    352 static PLATFORM_SET_TERMINAL_SIZE_FN(os_set_terminal_size)
    353 {
    354 	u16 win_size[4];
    355 	win_size[0] = rows;
    356 	win_size[1] = columns;
    357 	win_size[2] = window_width;
    358 	win_size[3] = window_height;
    359 	if (syscall3(SYS_ioctl, child, TIOCSWINSZ, (iptr)win_size) > -4096UL)
    360 		os_write_err_msg(s8("os_set_term_size\n"));
    361 }
    362 
    363 static PLATFORM_ADD_FILE_WATCH_FN(linux_add_file_watch)
    364 {
    365 	stat_buffer sb;
    366 	syscall2(SYS_stat, (iptr)path, (iptr)sb);
    367 
    368 	i32 wd  = syscall3(SYS_inotify_add_watch, linux_ctx.inotify_fd, (iptr)path, LINUX_INOTIFY_MASK);
    369 	if (wd < 4096UL) {
    370 		i32 idx = linux_ctx.file_watch_count++;
    371 		ASSERT(idx < ARRAY_COUNT(linux_ctx.file_watches));
    372 		linux_ctx.file_watches[idx].fn       = fn;
    373 		linux_ctx.file_watches[idx].path     = path;
    374 		linux_ctx.file_watches[idx].handle   = wd;
    375 		linux_ctx.file_watches[idx].inode    = STAT_INODE(sb);
    376 		linux_ctx.file_watches[idx].user_ctx = user_ctx;
    377 	}
    378 }
    379 
    380 static void
    381 try_deferred_file_loads(PlatformCtx *ctx)
    382 {
    383 	linux_deferred_file_reload_queue *file = ctx->file_reload_queue.next;
    384 	while (file) {
    385 		linux_file_watch *fw = ctx->file_watches + file->index;
    386 
    387 		stat_buffer sb;
    388 		syscall2(SYS_stat, (iptr)fw->path, (iptr)sb);
    389 
    390 		fw->handle = syscall3(SYS_inotify_add_watch, ctx->inotify_fd, (iptr)fw->path,
    391 		                      LINUX_INOTIFY_MASK);
    392 		fw->inode  = STAT_INODE(sb);
    393 
    394 		if (fw->handle < -4096UL) {
    395 			fw->fn(fw->path, fw->user_ctx);
    396 			file->last->next = file->next;
    397 			file->next       = ctx->file_reload_free_list;
    398 			ctx->file_reload_free_list = file;
    399 			file = file->last;
    400 		} else {
    401 			file->failures++;
    402 			#if 0
    403 			TODO
    404 			if (file->failures > MAX_FILE_RELOAD_TRIES) {
    405 				log
    406 				remove from list
    407 			}
    408 			#endif
    409 		}
    410 		file = file->next;
    411 	}
    412 }
    413 
    414 static b32
    415 defer_file_reload(PlatformCtx *ctx, i32 file_watch_index, stat_buffer *sb)
    416 {
    417 	b32 result = 1;
    418 	linux_file_watch *fw = ctx->file_watches + file_watch_index;
    419 
    420 	fw->inode  = STAT_INODE(*sb);
    421 	fw->handle = syscall3(SYS_inotify_add_watch, ctx->inotify_fd, (iptr)fw->path, LINUX_INOTIFY_MASK);
    422 
    423 	if (fw->handle >= -4096UL) {
    424 		result = 0;
    425 
    426 		linux_deferred_file_reload_queue *new = ctx->file_reload_free_list;
    427 		if (new) ctx->file_reload_free_list = new->next;
    428 		else     new = push_struct(&ctx->platform_memory, typeof(*new));
    429 		new->index    = file_watch_index;
    430 		new->failures = 0;
    431 		DLLPushDown(&ctx->file_reload_queue, new);
    432 	}
    433 
    434 	return result;
    435 }
    436 
    437 static void
    438 dispatch_file_watch_events(PlatformCtx *ctx)
    439 {
    440 	struct {
    441 		i32 wd;
    442 		u32 mask, cookie, len;
    443 		c8  name[];
    444 	} *ie;
    445 
    446 	u8 *mem = alloc_(&ctx->platform_memory, 4096, 64, 1);
    447 	s8 buf = {.len = 4096, .data = mem};
    448 
    449 	for (;;) {
    450 		size rlen = syscall3(SYS_read, ctx->inotify_fd, (iptr)buf.data, buf.len);
    451 		if (rlen <= 0)
    452 			break;
    453 
    454 		for (u8 *data = buf.data; data < buf.data + rlen; data += sizeof(*ie) + ie->len) {
    455 			ie = (void *)data;
    456 			for (i32 i = 0; i < ctx->file_watch_count; i++) {
    457 				linux_file_watch *fw = ctx->file_watches + i;
    458 				if (fw->handle != ie->wd)
    459 					continue;
    460 
    461 				b32 file_changed  = (ie->mask & IN_CLOSE_WRITE) != 0;
    462 				file_changed     |= (ie->mask & IN_MODIFY) != 0;
    463 				/* NOTE: some editors and the compiler will rewrite a file
    464 				 * completely and thus the inode will change; here we
    465 				 * detect that and restart the watch */
    466 				stat_buffer sb = {0};
    467 				syscall2(SYS_stat, (iptr)fw->path, (iptr)sb);
    468 				if (fw->inode != STAT_INODE(sb)) {
    469 					syscall2(SYS_inotify_rm_watch, ctx->inotify_fd, fw->handle);
    470 					fw->handle   = INVALID_FILE;
    471 					file_changed = defer_file_reload(ctx, i, &sb);
    472 				}
    473 				if (file_changed)
    474 					fw->fn(fw->path, fw->user_ctx);
    475 			}
    476 		}
    477 	}
    478 }
    479 
    480 static struct stack_base *
    481 new_stack(size capacity)
    482 {
    483 	i64 p = syscall6(SYS_mmap, 0, capacity, PROT_RW, MAP_ANON|MAP_PRIVATE, -1, 0);
    484 	if (p > -4096UL)
    485 		os_fatal(s8("new_stack: mmap failed\n"));
    486 	i64 count = capacity / sizeof(struct stack_base);
    487 	/* NOTE: remember the stack grows down; we want to start at the highest address */
    488 	struct stack_base *result = (struct stack_base *)p + count - 1;
    489 	return result;
    490 }
    491 
    492 static void
    493 usage(char *argv0, Stream *err)
    494 {
    495 	stream_push_s8(err, s8("usage: "));
    496 	stream_push_s8(err, c_str_to_s8(argv0));
    497 	stream_push_s8(err, s8(" [-v] [-g COLxROW]\n"));
    498 	os_fatal(stream_to_s8(err));
    499 }
    500 
    501 static s8
    502 get_default_cmd(char **envp)
    503 {
    504 	s8 result = envp_lookup(s8("SHELL="), envp);
    505 	if (result.len == 0)
    506 		result = s8("/bin/sh");
    507 	return result;
    508 }
    509 
    510 static SLLVariableVector
    511 parse_environment(Arena *a, char **envp)
    512 {
    513 	SLLVariableVector env = {0};
    514 	for (; *envp; envp++) {
    515 		s8 e = c_str_to_s8(*envp);
    516 		if (!s8_prefix_of(s8("TERM="), e)) {
    517 			Variable *var = push_struct(a, Variable);
    518 			var->type = VT_S8;
    519 			var->s8   = e;
    520 			SLLVariableVectorPush(a, &env, var);
    521 		}
    522 	}
    523 
    524 	Variable *var = push_struct(a, Variable);
    525 	var->type = VT_S8;
    526 	/* TODO: don't pretend to be xterm ? */
    527 	var->s8   = s8("TERM=xterm");
    528 	SLLVariableVectorPush(a, &env, var);
    529 
    530 	return env;
    531 }