1 #include <stdlib.h>
2 #include <stdio.h>
3 #include <string.h>
4 #include <stdint.h>
5 #include <stdbool.h>
6 #include <unistd.h>
7 #include <getopt.h>
8 #include <signal.h>
9 #include <errno.h>
10
11 #include <sys/socket.h>
12 #include <sys/un.h>
13 #include <sys/stat.h>
14
15 #include <tllist.h>
16
17 #define LOG_MODULE "foot-client"
18 #define LOG_ENABLE_DBG 0
19 #include "log.h"
20 #include "client-protocol.h"
21 #include "debug.h"
22 #include "foot-features.h"
23 #include "macros.h"
24 #include "util.h"
25 #include "version.h"
26 #include "xmalloc.h"
27
28 struct override {
29 size_t len;
30 char *str;
31 };
32 typedef tll(struct override) override_list_t;
33
34 static volatile sig_atomic_t aborted = 0;
35
36 static void
sig_handler(int signo)37 sig_handler(int signo)
38 {
39 aborted = 1;
40 }
41
42 static ssize_t
sendall(int sock,const void * _buf,size_t len)43 sendall(int sock, const void *_buf, size_t len)
44 {
45 const uint8_t *buf = _buf;
46 size_t left = len;
47
48 while (left > 0) {
49 ssize_t r = send(sock, buf, left, MSG_NOSIGNAL);
50 if (r < 0) {
51 if (errno == EINTR)
52 continue;
53 return r;
54 }
55
56 buf += r;
57 left -= r;
58 }
59
60 return len;
61 }
62
63 static const char *
version_and_features(void)64 version_and_features(void)
65 {
66 static char buf[256];
67 snprintf(buf, sizeof(buf), "version: %s %cpgo %cime %cgraphemes %cassertions",
68 FOOT_VERSION,
69 feature_pgo() ? '+' : '-',
70 feature_ime() ? '+' : '-',
71 feature_graphemes() ? '+' : '-',
72 feature_assertions() ? '+' : '-');
73 return buf;
74 }
75
76 static void
print_usage(const char * prog_name)77 print_usage(const char *prog_name)
78 {
79 static const char options[] =
80 "\nOptions:\n"
81 " -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n"
82 " -T,--title=TITLE initial window title (foot)\n"
83 " -a,--app-id=ID window application ID (foot)\n"
84 " -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n"
85 " -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n"
86 " -m,--maximized start in maximized mode\n"
87 " -F,--fullscreen start in fullscreen mode\n"
88 " -L,--login-shell start shell as a login shell\n"
89 " -D,--working-directory=DIR directory to start in (CWD)\n"
90 " -s,--server-socket=PATH path to the server UNIX domain socket (default=$XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)\n"
91 " -H,--hold remain open after child process exits\n"
92 " -N,--no-wait detach the client process from the running terminal, exiting immediately\n"
93 " -o,--override=[section.]key=value override configuration option\n"
94 " -d,--log-level={info|warning|error|none} log level (info)\n"
95 " -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n"
96 " -v,--version show the version number and quit\n"
97 " -e ignored (for compatibility with xterm -e)\n";
98
99 printf("Usage: %s [OPTIONS...]\n", prog_name);
100 printf("Usage: %s [OPTIONS...] command [ARGS...]\n", prog_name);
101 puts(options);
102 }
103
104 static bool NOINLINE
push_override(override_list_t * overrides,const char * s,uint64_t * total_len)105 push_override(override_list_t *overrides, const char *s, uint64_t *total_len)
106 {
107 size_t len = strlen(s) + 1;
108 if (len >= 1 << (8 * sizeof(struct client_string))) {
109 LOG_ERR("override length overflow");
110 return false;
111 }
112
113 struct override o = {len, xstrdup(s)};
114 tll_push_back(*overrides, o);
115 *total_len += sizeof(struct client_string) + o.len;
116 return true;
117 }
118
119 int
main(int argc,char * const * argv)120 main(int argc, char *const *argv)
121 {
122 /* Custom exit code, to enable users to differentiate between foot
123 * itself failing, and the client application failiing */
124 static const int foot_exit_failure = -36;
125 int ret = foot_exit_failure;
126
127 const char *const prog_name = argv[0];
128
129 static const struct option longopts[] = {
130 {"term", required_argument, NULL, 't'},
131 {"title", required_argument, NULL, 'T'},
132 {"app-id", required_argument, NULL, 'a'},
133 {"window-size-pixels", required_argument, NULL, 'w'},
134 {"window-size-chars", required_argument, NULL, 'W'},
135 {"maximized", no_argument, NULL, 'm'},
136 {"fullscreen", no_argument, NULL, 'F'},
137 {"login-shell", no_argument, NULL, 'L'},
138 {"working-directory", required_argument, NULL, 'D'},
139 {"server-socket", required_argument, NULL, 's'},
140 {"hold", no_argument, NULL, 'H'},
141 {"no-wait", no_argument, NULL, 'N'},
142 {"override", required_argument, NULL, 'o'},
143 {"log-level", required_argument, NULL, 'd'},
144 {"log-colorize", optional_argument, NULL, 'l'},
145 {"version", no_argument, NULL, 'v'},
146 {"help", no_argument, NULL, 'h'},
147 {NULL, no_argument, NULL, 0},
148 };
149
150 const char *custom_cwd = NULL;
151 const char *server_socket_path = NULL;
152 enum log_class log_level = LOG_CLASS_INFO;
153 enum log_colorize log_colorize = LOG_COLORIZE_AUTO;
154 bool hold = false;
155
156 /* Used to format overrides */
157 bool no_wait = false;
158
159 /* For XDG activation */
160 const char *token = getenv("XDG_ACTIVATION_TOKEN");
161 bool xdga_token = token != NULL;
162 size_t token_len = xdga_token ? strlen(token) + 1 : 0;
163
164 char buf[1024];
165
166 /* Total packet length, not (yet) including overrides or argv[] */
167 uint64_t total_len = 0;
168
169 /* malloc:ed and needs to be in scope of all goto's */
170 int fd = -1;
171 char *_cwd = NULL;
172 override_list_t overrides = tll_init();
173 struct client_string *cargv = NULL;
174
175 while (true) {
176 int c = getopt_long(argc, argv, "+t:T:a:w:W:mFLD:s:HNo:d:l::veh", longopts, NULL);
177 if (c == -1)
178 break;
179
180 switch (c) {
181 case 't':
182 snprintf(buf, sizeof(buf), "term=%s", optarg);
183 if (!push_override(&overrides, buf, &total_len))
184 goto err;
185 break;
186
187 case 'T':
188 snprintf(buf, sizeof(buf), "title=%s", optarg);
189 if (!push_override(&overrides, buf, &total_len))
190 goto err;
191 break;
192
193 case 'a':
194 snprintf(buf, sizeof(buf), "app-id=%s", optarg);
195 if (!push_override(&overrides, buf, &total_len))
196 goto err;
197 break;
198
199 case 'L':
200 if (!push_override(&overrides, "login-shell=yes", &total_len))
201 goto err;
202 break;
203
204 case 'D': {
205 struct stat st;
206 if (stat(optarg, &st) < 0 || !(st.st_mode & S_IFDIR)) {
207 fprintf(stderr, "error: %s: not a directory\n", optarg);
208 goto err;
209 }
210 custom_cwd = optarg;
211 break;
212 }
213
214 case 'w': {
215 unsigned width, height;
216 if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) {
217 fprintf(stderr, "error: invalid window-size-pixels: %s\n", optarg);
218 goto err;
219 }
220
221 snprintf(buf, sizeof(buf), "initial-window-size-pixels=%ux%u", width, height);
222 if (!push_override(&overrides, buf, &total_len))
223 goto err;
224 break;
225 }
226
227 case 'W': {
228 unsigned width, height;
229 if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) {
230 fprintf(stderr, "error: invalid window-size-chars: %s\n", optarg);
231 goto err;
232 }
233
234 snprintf(buf, sizeof(buf), "initial-window-size-chars=%ux%u", width, height);
235 if (!push_override(&overrides, buf, &total_len))
236 goto err;
237 break;
238 }
239
240 case 'm':
241 if (!push_override(&overrides, "initial-window-mode=maximized", &total_len))
242 goto err;
243 break;
244
245 case 'F':
246 if (!push_override(&overrides, "initial-window-mode=fullscreen", &total_len))
247 goto err;
248 break;
249
250 case 's':
251 server_socket_path = optarg;
252 break;
253
254 case 'H':
255 hold = true;
256 break;
257
258 case 'N':
259 no_wait = true;
260 break;
261
262 case 'o':
263 if (!push_override(&overrides, optarg, &total_len))
264 goto err;
265 break;
266
267 case 'd': {
268 int lvl = log_level_from_string(optarg);
269 if (unlikely(lvl < 0)) {
270 fprintf(
271 stderr,
272 "-d,--log-level: %s: argument must be one of %s\n",
273 optarg,
274 log_level_string_hint());
275 goto err;
276 }
277 log_level = lvl;
278 break;
279 }
280
281 case 'l':
282 if (optarg == NULL || strcmp(optarg, "auto") == 0)
283 log_colorize = LOG_COLORIZE_AUTO;
284 else if (strcmp(optarg, "never") == 0)
285 log_colorize = LOG_COLORIZE_NEVER;
286 else if (strcmp(optarg, "always") == 0)
287 log_colorize = LOG_COLORIZE_ALWAYS;
288 else {
289 fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg);
290 goto err;
291 }
292 break;
293
294 case 'v':
295 printf("footclient %s\n", version_and_features());
296 ret = EXIT_SUCCESS;
297 goto err;
298
299 case 'h':
300 print_usage(prog_name);
301 ret = EXIT_SUCCESS;
302 goto err;
303
304 case 'e':
305 break;
306
307 case '?':
308 goto err;
309 }
310 }
311
312 argc -= optind;
313 argv += optind;
314
315 log_init(log_colorize, false, LOG_FACILITY_USER, log_level);
316
317 fd = socket(AF_UNIX, SOCK_STREAM, 0);
318 if (fd == -1) {
319 LOG_ERRNO("failed to create socket");
320 goto err;
321 }
322
323 struct sockaddr_un addr = {.sun_family = AF_UNIX};
324
325 if (server_socket_path != NULL) {
326 strncpy(addr.sun_path, server_socket_path, sizeof(addr.sun_path) - 1);
327 if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) {
328 LOG_ERR("%s: failed to connect (is 'foot --server' running?)", server_socket_path);
329 goto err;
330 }
331 } else {
332 bool connected = false;
333
334 const char *xdg_runtime = getenv("XDG_RUNTIME_DIR");
335 if (xdg_runtime != NULL) {
336 const char *wayland_display = getenv("WAYLAND_DISPLAY");
337 if (wayland_display != NULL)
338 snprintf(addr.sun_path, sizeof(addr.sun_path),
339 "%s/foot-%s.sock", xdg_runtime, wayland_display);
340 else
341 snprintf(addr.sun_path, sizeof(addr.sun_path),
342 "%s/foot.sock", xdg_runtime);
343
344 if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0)
345 connected = true;
346 else
347 LOG_WARN("%s: failed to connect, will now try /tmp/foot.sock", addr.sun_path);
348 }
349
350 if (!connected) {
351 strncpy(addr.sun_path, "/tmp/foot.sock", sizeof(addr.sun_path) - 1);
352 if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) {
353 LOG_ERRNO("failed to connect (is 'foot --server' running?)");
354 goto err;
355 }
356 }
357 }
358
359 const char *cwd = custom_cwd;
360 if (cwd == NULL) {
361 errno = 0;
362 size_t buf_len = 1024;
363 do {
364 _cwd = xrealloc(_cwd, buf_len);
365 if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) {
366 LOG_ERRNO("failed to get current working directory");
367 goto err;
368 }
369 buf_len *= 2;
370 } while (errno == ERANGE);
371 cwd = _cwd;
372 }
373
374 /* String lengths, including NULL terminator */
375 const size_t cwd_len = strlen(cwd) + 1;
376 const size_t override_count = tll_length(overrides);
377
378 const struct client_data data = {
379 .hold = hold,
380 .no_wait = no_wait,
381 .xdga_token = xdga_token,
382 .token_len = token_len,
383 .cwd_len = cwd_len,
384 .override_count = override_count,
385 .argc = argc,
386 };
387
388 /* Total packet length, not (yet) including argv[] */
389 total_len += sizeof(data) + cwd_len + token_len;
390
391 /* Add argv[] size to total packet length */
392 cargv = xmalloc(argc * sizeof(cargv[0]));
393 for (size_t i = 0; i < argc; i++) {
394 const size_t arg_len = strlen(argv[i]) + 1;
395
396 if (arg_len >= 1 << (8 * sizeof(cargv[i].len))) {
397 LOG_ERR("argv length overflow");
398 goto err;
399 }
400
401 cargv[i].len = arg_len;
402 total_len += sizeof(cargv[i]) + cargv[i].len;
403 }
404
405 /* Check for size overflows */
406 if (total_len >= 1llu << (8 * sizeof(uint32_t)) ||
407 cwd_len >= 1 << (8 * sizeof(data.cwd_len)) ||
408 token_len >= 1 << (8 * sizeof(data.token_len)) ||
409 override_count > (size_t)(unsigned int)data.override_count ||
410 argc > (int)(unsigned int)data.argc)
411 {
412 LOG_ERR("size overflow");
413 goto err;
414 }
415
416 /* Send everything except argv[] */
417 if (sendall(fd, &(uint32_t){total_len}, sizeof(uint32_t)) < 0 ||
418 sendall(fd, &data, sizeof(data)) < 0 ||
419 sendall(fd, cwd, cwd_len) < 0)
420 {
421 LOG_ERRNO("failed to send setup packet to server");
422 goto err;
423 }
424
425 /* Send XDGA token, if we have one */
426 if (xdga_token) {
427 if (sendall(fd, token, token_len) != token_len)
428 {
429 LOG_ERRNO("failed to send xdg activation token to server");
430 goto err;
431 }
432 }
433
434 /* Send overrides */
435 tll_foreach(overrides, it) {
436 const struct override *o = &it->item;
437 struct client_string s = {o->len};
438 if (sendall(fd, &s, sizeof(s)) < 0 ||
439 sendall(fd, o->str, o->len) < 0)
440 {
441 LOG_ERRNO("failed to send setup packet (overrides) to server");
442 goto err;
443 }
444 }
445
446 /* Send argv[] */
447 for (size_t i = 0; i < argc; i++) {
448 if (sendall(fd, &cargv[i], sizeof(cargv[i])) < 0 ||
449 sendall(fd, argv[i], cargv[i].len) < 0)
450 {
451 LOG_ERRNO("failed to send setup packet (argv) to server");
452 goto err;
453 }
454 }
455
456 const struct sigaction sa = {.sa_handler = &sig_handler};
457 if (sigaction(SIGINT, &sa, NULL) < 0 || sigaction(SIGTERM, &sa, NULL) < 0) {
458 LOG_ERRNO("failed to register signal handlers");
459 goto err;
460 }
461
462 int exit_code;
463 ssize_t rcvd = recv(fd, &exit_code, sizeof(exit_code), 0);
464
465 if (rcvd == -1 && errno == EINTR)
466 xassert(aborted);
467 else if (rcvd != sizeof(exit_code))
468 LOG_ERRNO("failed to read server response");
469 else
470 ret = exit_code;
471
472 err:
473 tll_foreach(overrides, it) {
474 free(it->item.str);
475 tll_remove(overrides, it);
476 }
477 free(cargv);
478 free(_cwd);
479 if (fd != -1)
480 close(fd);
481 log_deinit();
482 return ret;
483 }
484