]> git.proxmox.com Git - systemd.git/blame - src/xdg-autostart-generator/xdg-autostart-service.c
bump version to 252.11-pve1
[systemd.git] / src / xdg-autostart-generator / xdg-autostart-service.c
CommitLineData
a032b68d 1/* SPDX-License-Identifier: LGPL-2.1-or-later */
a10f5d05
MB
2
3#include <errno.h>
4#include <stdio.h>
5#include <unistd.h>
6
7#include "xdg-autostart-service.h"
8
9#include "conf-parser.h"
10#include "escape.h"
a10f5d05
MB
11#include "fd-util.h"
12#include "generator.h"
13#include "log.h"
928cf965
MB
14#include "nulstr-util.h"
15#include "parse-util.h"
16#include "path-util.h"
a10f5d05
MB
17#include "specifier.h"
18#include "string-util.h"
a10f5d05 19#include "strv.h"
928cf965
MB
20#include "unit-name.h"
21#include "user-util.h"
a10f5d05
MB
22
23XdgAutostartService* xdg_autostart_service_free(XdgAutostartService *s) {
24 if (!s)
25 return NULL;
26
27 free(s->name);
28 free(s->path);
29 free(s->description);
30
31 free(s->type);
32 free(s->exec_string);
a032b68d 33 free(s->working_directory);
a10f5d05
MB
34
35 strv_free(s->only_show_in);
36 strv_free(s->not_show_in);
37
38 free(s->try_exec);
39 free(s->autostart_condition);
40 free(s->kde_autostart_condition);
41
42 free(s->gnome_autostart_phase);
43
44 return mfree(s);
45}
46
47char *xdg_autostart_service_translate_name(const char *name) {
48 _cleanup_free_ char *c = NULL, *escaped = NULL;
49 char *res;
50
51 c = strdup(name);
52 if (!c)
53 return NULL;
54
55 res = endswith(c, ".desktop");
56 if (res)
57 *res = '\0';
58
59 escaped = unit_name_escape(c);
60 if (!escaped)
61 return NULL;
62
3a6ce677 63 return strjoin("app-", escaped, "@autostart.service");
a10f5d05
MB
64}
65
66static int xdg_config_parse_bool(
67 const char *unit,
68 const char *filename,
69 unsigned line,
70 const char *section,
71 unsigned section_line,
72 const char *lvalue,
73 int ltype,
74 const char *rvalue,
75 void *data,
76 void *userdata) {
77
086111aa 78 bool *b = ASSERT_PTR(data);
928cf965 79 int r;
a10f5d05
MB
80
81 assert(filename);
82 assert(lvalue);
83 assert(rvalue);
a10f5d05 84
928cf965
MB
85 r = parse_boolean(rvalue);
86 if (r < 0)
a10f5d05 87 return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL), "Invalid value for boolean: %s", rvalue);
928cf965 88 *b = r;
a10f5d05
MB
89 return 0;
90}
91
92/* Unescapes the string in-place, returns non-zero status on error. */
93static int xdg_unescape_string(
94 const char *unit,
95 const char *filename,
96 int line,
97 char *str) {
98
99 char *in;
100 char *out;
101
102 assert(str);
103
104 in = out = str;
105
106 for (; *in; in++, out++) {
107 if (*in == '\\') {
108 /* Move forward, and ensure it is a valid escape. */
109 in++;
110
111 switch (*in) {
112 case 's':
113 *out = ' ';
114 break;
115 case 'n':
116 *out = '\n';
117 break;
118 case 't':
119 *out = '\t';
120 break;
121 case 'r':
122 *out = '\r';
123 break;
124 case '\\':
125 *out = '\\';
126 break;
127 case ';':
128 /* Technically only permitted for strv. */
129 *out = ';';
130 break;
131 default:
132 return log_syntax(unit, LOG_ERR, filename, line, SYNTHETIC_ERRNO(EINVAL), "Undefined escape sequence \\%c.", *in);
133 }
134
135 continue;
136 }
137
138 *out = *in;
139 }
140 *out = '\0';
141
142 return 0;
143}
144
145/* Note: We do not bother with unescaping the strings, hence the _raw postfix. */
146static int xdg_config_parse_string(
147 const char *unit,
148 const char *filename,
149 unsigned line,
150 const char *section,
151 unsigned section_line,
152 const char *lvalue,
153 int ltype,
154 const char *rvalue,
155 void *data,
156 void *userdata) {
157
158 _cleanup_free_ char *res = NULL;
086111aa 159 char **out = ASSERT_PTR(data);
a10f5d05
MB
160 int r;
161
162 assert(filename);
163 assert(lvalue);
164 assert(rvalue);
a10f5d05
MB
165
166 /* XDG does not allow duplicate definitions. */
167 if (*out) {
a032b68d 168 log_syntax(unit, LOG_WARNING, filename, line, 0, "Key %s was defined multiple times, ignoring.", lvalue);
a10f5d05
MB
169 return 0;
170 }
171
172 res = strdup(rvalue);
173 if (!res)
174 return log_oom();
175
176 r = xdg_unescape_string(unit, filename, line, res);
177 if (r < 0)
178 return r;
179
180 *out = TAKE_PTR(res);
181 return 0;
182}
183
184static int strv_strndup_unescape_and_push(
185 const char *unit,
186 const char *filename,
187 unsigned line,
188 char ***sv,
a10f5d05
MB
189 size_t *n,
190 const char *start,
191 const char *end) {
192
193 if (end == start)
194 return 0;
195
196 _cleanup_free_ char *copy = NULL;
197 int r;
198
199 copy = strndup(start, end - start);
200 if (!copy)
201 return log_oom();
202
203 r = xdg_unescape_string(unit, filename, line, copy);
204 if (r < 0)
205 return r;
206
8b3d4ff0 207 if (!GREEDY_REALLOC(*sv, *n + 2)) /* One extra for NULL */
a10f5d05
MB
208 return log_oom();
209
210 (*sv)[*n] = TAKE_PTR(copy);
211 (*sv)[*n + 1] = NULL;
212 (*n)++;
213
214 return 0;
215}
216
217static int xdg_config_parse_strv(
218 const char *unit,
219 const char *filename,
220 unsigned line,
221 const char *section,
222 unsigned section_line,
223 const char *lvalue,
224 int ltype,
225 const char *rvalue,
226 void *data,
227 void *userdata) {
228
086111aa 229 char ***ret_sv = ASSERT_PTR(data);
a10f5d05
MB
230 int r;
231
232 assert(filename);
233 assert(lvalue);
234 assert(rvalue);
a10f5d05
MB
235
236 /* XDG does not allow duplicate definitions. */
237 if (*ret_sv) {
a032b68d 238 log_syntax(unit, LOG_WARNING, filename, line, 0, "Key %s was already defined, ignoring.", lvalue);
a10f5d05
MB
239 return 0;
240 }
241
8b3d4ff0 242 size_t n = 0;
a10f5d05
MB
243 _cleanup_strv_free_ char **sv = NULL;
244
8b3d4ff0 245 if (!GREEDY_REALLOC0(sv, 1))
a10f5d05
MB
246 return log_oom();
247
248 /* We cannot use strv_split because it does not handle escaping correctly. */
249 const char *start = rvalue, *end;
250
251 for (end = start; *end; end++) {
252 if (*end == '\\') {
253 /* Move forward, and ensure it is a valid escape. */
254 end++;
255 if (!strchr("sntr\\;", *end)) {
a032b68d 256 log_syntax(unit, LOG_WARNING, filename, line, 0, "Undefined escape sequence \\%c.", *end);
a10f5d05
MB
257 return 0;
258 }
259 continue;
260 }
261
262 if (*end == ';') {
263 r = strv_strndup_unescape_and_push(unit, filename, line,
8b3d4ff0 264 &sv, &n,
a10f5d05
MB
265 start, end);
266 if (r < 0)
267 return r;
268
269 start = end + 1;
270 }
271 }
272
273 /* Handle the trailing entry after the last separator */
274 r = strv_strndup_unescape_and_push(unit, filename, line,
8b3d4ff0 275 &sv, &n,
a10f5d05
MB
276 start, end);
277 if (r < 0)
278 return r;
279
280 *ret_sv = TAKE_PTR(sv);
281 return 0;
282}
283
284static int xdg_config_item_table_lookup(
285 const void *table,
286 const char *section,
287 const char *lvalue,
f5caa8fa
MB
288 ConfigParserCallback *ret_func,
289 int *ret_ltype,
290 void **ret_data,
a10f5d05
MB
291 void *userdata) {
292
293 assert(lvalue);
294
295 /* Ignore any keys with [] as those are translations. */
296 if (strchr(lvalue, '[')) {
f5caa8fa
MB
297 *ret_func = NULL;
298 *ret_ltype = 0;
299 *ret_data = NULL;
a10f5d05
MB
300 return 1;
301 }
302
f5caa8fa 303 return config_item_table_lookup(table, section, lvalue, ret_func, ret_ltype, ret_data, userdata);
a10f5d05
MB
304}
305
306XdgAutostartService *xdg_autostart_service_parse_desktop(const char *path) {
307 _cleanup_(xdg_autostart_service_freep) XdgAutostartService *service = NULL;
308 int r;
309
310 service = new0(XdgAutostartService, 1);
311 if (!service)
312 return NULL;
313
314 service->path = strdup(path);
315 if (!service->path)
316 return NULL;
317
318 const ConfigTableItem items[] = {
3a6ce677
BR
319 { "Desktop Entry", "Name", xdg_config_parse_string, 0, &service->description },
320 { "Desktop Entry", "Exec", xdg_config_parse_string, 0, &service->exec_string },
321 { "Desktop Entry", "Path", xdg_config_parse_string, 0, &service->working_directory },
322 { "Desktop Entry", "TryExec", xdg_config_parse_string, 0, &service->try_exec },
323 { "Desktop Entry", "Type", xdg_config_parse_string, 0, &service->type },
324 { "Desktop Entry", "OnlyShowIn", xdg_config_parse_strv, 0, &service->only_show_in },
325 { "Desktop Entry", "NotShowIn", xdg_config_parse_strv, 0, &service->not_show_in },
326 { "Desktop Entry", "Hidden", xdg_config_parse_bool, 0, &service->hidden },
327 { "Desktop Entry", "AutostartCondition", xdg_config_parse_string, 0, &service->autostart_condition },
328 { "Desktop Entry", "X-KDE-autostart-condition", xdg_config_parse_string, 0, &service->kde_autostart_condition },
329 { "Desktop Entry", "X-GNOME-Autostart-Phase", xdg_config_parse_string, 0, &service->gnome_autostart_phase },
330 { "Desktop Entry", "X-systemd-skip", xdg_config_parse_bool, 0, &service->systemd_skip },
a10f5d05
MB
331
332 /* Common entries that we do not use currently. */
3a6ce677
BR
333 { "Desktop Entry", "Categories", NULL, 0, NULL},
334 { "Desktop Entry", "Comment", NULL, 0, NULL},
335 { "Desktop Entry", "DBusActivatable", NULL, 0, NULL},
336 { "Desktop Entry", "Encoding", NULL, 0, NULL},
337 { "Desktop Entry", "GenericName", NULL, 0, NULL},
338 { "Desktop Entry", "Icon", NULL, 0, NULL},
339 { "Desktop Entry", "Keywords", NULL, 0, NULL},
340 { "Desktop Entry", "MimeType", NULL, 0, NULL},
341 { "Desktop Entry", "NoDisplay", NULL, 0, NULL},
342 { "Desktop Entry", "StartupNotify", NULL, 0, NULL},
343 { "Desktop Entry", "StartupWMClass", NULL, 0, NULL},
344 { "Desktop Entry", "Terminal", NULL, 0, NULL},
345 { "Desktop Entry", "URL", NULL, 0, NULL},
346 { "Desktop Entry", "Version", NULL, 0, NULL},
a10f5d05
MB
347 {}
348 };
349
350 r = config_parse(NULL, service->path, NULL,
351 "Desktop Entry\0",
352 xdg_config_item_table_lookup, items,
ecfb185f
LB
353 CONFIG_PARSE_RELAXED | CONFIG_PARSE_WARN,
354 service,
a10f5d05
MB
355 NULL);
356 /* If parsing failed, only hide the file so it will still mask others. */
357 if (r < 0) {
358 log_warning_errno(r, "Failed to parse %s, ignoring it", service->path);
359 service->hidden = true;
360 }
361
362 return TAKE_PTR(service);
363}
364
365int xdg_autostart_format_exec_start(
366 const char *exec,
367 char **ret_exec_start) {
368
369 _cleanup_strv_free_ char **exec_split = NULL;
370 char *res;
371 size_t n, i;
372 bool first_arg;
373 int r;
374
375 /*
3a6ce677
BR
376 * Unfortunately, there is a mismatch between systemd's idea of $PATH and XDGs. I.e. we need to
377 * ensure that we have an absolute path to support cases where $PATH has been modified from the
378 * default set.
a10f5d05 379 *
3a6ce677
BR
380 * Note that this is only needed for development environments though; so while it is important, this
381 * should have no effect in production environments.
a10f5d05 382 *
3a6ce677
BR
383 * To be compliant with the XDG specification, we also need to strip certain parameters and
384 * such. Doing so properly makes parsing the command line unavoidable.
a10f5d05 385 *
3a6ce677 386 * NOTE: Technically, XDG only specifies " as quotes, while this also accepts '.
a10f5d05 387 */
a032b68d
MB
388 r = strv_split_full(&exec_split, exec, NULL, EXTRACT_UNQUOTE | EXTRACT_RELAX);
389 if (r < 0)
390 return r;
a10f5d05
MB
391
392 if (strv_isempty(exec_split))
393 return log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Exec line is empty");
394
395 first_arg = true;
396 for (i = n = 0; exec_split[i]; i++) {
d9f5095a 397 _cleanup_free_ char *c = NULL, *raw = NULL, *percent = NULL, *tilde_expanded = NULL;
ea0999c9 398 ssize_t l;
a10f5d05 399
ea0999c9
MB
400 l = cunescape(exec_split[i], 0, &c);
401 if (l < 0)
402 return log_debug_errno(l, "Failed to unescape '%s': %m", exec_split[i]);
a10f5d05
MB
403
404 if (first_arg) {
405 _cleanup_free_ char *executable = NULL;
406
407 /* This is the executable, find it in $PATH */
408 first_arg = false;
a032b68d 409 r = find_executable(c, &executable);
a10f5d05
MB
410 if (r < 0)
411 return log_info_errno(r, "Exec binary '%s' does not exist: %m", c);
412
f5caa8fa 413 free_and_replace(exec_split[n++], executable);
a10f5d05
MB
414 continue;
415 }
416
417 /*
3a6ce677
BR
418 * Remove any standardised XDG fields; we assume they never appear as part of another
419 * argument as that just does not make any sense as they can be empty (GLib will e.g. turn
420 * "%f" into an empty argument). Other implementations may handle this differently.
a10f5d05
MB
421 */
422 if (STR_IN_SET(c,
423 "%f", "%F",
424 "%u", "%U",
425 "%d", "%D",
426 "%n", "%N",
3a6ce677
BR
427 "%i", /* Location of icon, could be implemented. */
428 "%c", /* Translated application name, could be implemented. */
429 "%k", /* Location of desktop file, could be implemented. */
a10f5d05
MB
430 "%v",
431 "%m"
432 ))
433 continue;
434
435 /*
3a6ce677
BR
436 * %% -> % and then % -> %% means that we correctly quote any % and also quote any left over
437 * (and invalid) % specifier from the desktop file.
a10f5d05
MB
438 */
439 raw = strreplace(c, "%%", "%");
440 if (!raw)
441 return log_oom();
f5caa8fa
MB
442 percent = strreplace(raw, "%", "%%");
443 if (!percent)
a10f5d05
MB
444 return log_oom();
445
d9f5095a 446 /*
086111aa
LB
447 * Expand ~ if it comes at the beginning of an argument to form a path.
448 *
449 * The specification does not mandate this, but we do it anyway for compatibility with
450 * older KDE code, which supported a more shell-like syntax for users making custom entries.
d9f5095a
MB
451 */
452 if (percent[0] == '~' && (isempty(percent + 1) || path_is_absolute(percent + 1))) {
453 _cleanup_free_ char *home = NULL;
454
455 r = get_home_dir(&home);
456 if (r < 0)
457 return r;
458
086111aa 459 tilde_expanded = path_join(home, &percent[1]);
d9f5095a
MB
460 if (!tilde_expanded)
461 return log_oom();
462 free_and_replace(exec_split[n++], tilde_expanded);
463 } else
464 free_and_replace(exec_split[n++], percent);
a10f5d05
MB
465 }
466 for (; exec_split[n]; n++)
467 exec_split[n] = mfree(exec_split[n]);
468
f5caa8fa 469 res = quote_command_line(exec_split, SHELL_ESCAPE_EMPTY);
a10f5d05
MB
470 if (!res)
471 return log_oom();
472
473 *ret_exec_start = res;
474 return 0;
475}
476
477static int xdg_autostart_generate_desktop_condition(
f5caa8fa 478 const XdgAutostartService *service,
a10f5d05
MB
479 FILE *f,
480 const char *test_binary,
481 const char *condition) {
482
483 int r;
484
485 /* Generate an ExecCondition for GNOME autostart condition */
486 if (!isempty(condition)) {
487 _cleanup_free_ char *gnome_autostart_condition_path = NULL, *e_autostart_condition = NULL;
488
a032b68d 489 r = find_executable(test_binary, &gnome_autostart_condition_path);
a10f5d05 490 if (r < 0) {
a032b68d 491 log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
f5caa8fa
MB
492 "%s: ExecCondition executable %s not found, unit will not be started automatically: %m",
493 service->path, test_binary);
a10f5d05 494 fprintf(f, "# ExecCondition using %s skipped due to missing binary.\n", test_binary);
a80b52fd 495 return 0;
a10f5d05
MB
496 }
497
498 e_autostart_condition = cescape(condition);
499 if (!e_autostart_condition)
500 return log_oom();
501
086111aa
LB
502 log_debug("%s: ExecCondition converted to %s --condition \"%s\"%s",
503 service->path, gnome_autostart_condition_path, e_autostart_condition,
504 special_glyph(SPECIAL_GLYPH_ELLIPSIS));
f5caa8fa 505
a10f5d05
MB
506 fprintf(f,
507 "ExecCondition=%s --condition \"%s\"\n",
508 gnome_autostart_condition_path,
509 e_autostart_condition);
510 }
511
512 return 0;
513}
514
515int xdg_autostart_service_generate_unit(
f5caa8fa 516 const XdgAutostartService *service,
a10f5d05
MB
517 const char *dest) {
518
519 _cleanup_free_ char *path_escaped = NULL, *exec_start = NULL, *unit = NULL;
520 _cleanup_fclose_ FILE *f = NULL;
521 int r;
522
523 assert(service);
524
525 /* Nothing to do for hidden services. */
526 if (service->hidden) {
f5caa8fa 527 log_debug("%s: not generating unit, entry is hidden.", service->path);
a10f5d05
MB
528 return 0;
529 }
530
531 if (service->systemd_skip) {
f5caa8fa 532 log_debug("%s: not generating unit, marked as skipped by generator.", service->path);
a10f5d05
MB
533 return 0;
534 }
535
536 /* Nothing to do if type is not Application. */
537 if (!streq_ptr(service->type, "Application")) {
f5caa8fa 538 log_debug("%s: not generating unit, Type=%s is not supported.", service->path, service->type);
a10f5d05
MB
539 return 0;
540 }
541
542 if (!service->exec_string) {
f5caa8fa 543 log_warning("%s: not generating unit, no Exec= line.", service->path);
a10f5d05
MB
544 return 0;
545 }
546
3a6ce677
BR
547 /* The TryExec key cannot be checked properly from the systemd unit, it is trivial to check using
548 * find_executable though. */
a10f5d05 549 if (service->try_exec) {
a032b68d 550 r = find_executable(service->try_exec, NULL);
a10f5d05 551 if (r < 0) {
a032b68d 552 log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r,
f5caa8fa
MB
553 "%s: not generating unit, could not find TryExec= binary %s: %m",
554 service->path, service->try_exec);
a10f5d05
MB
555 return 0;
556 }
557 }
558
559 r = xdg_autostart_format_exec_start(service->exec_string, &exec_start);
560 if (r < 0) {
f5caa8fa 561 log_warning_errno(r, "%s: not generating unit, error parsing Exec= line: %m", service->path);
a10f5d05
MB
562 return 0;
563 }
564
565 if (service->gnome_autostart_phase) {
566 /* There is no explicit value for the "Application" phase. */
f5caa8fa 567 log_debug("%s: not generating unit, startup phases are not supported.", service->path);
a10f5d05
MB
568 return 0;
569 }
570
571 path_escaped = specifier_escape(service->path);
572 if (!path_escaped)
573 return log_oom();
574
575 unit = path_join(dest, service->name);
576 if (!unit)
577 return log_oom();
578
579 f = fopen(unit, "wxe");
580 if (!f)
f5caa8fa 581 return log_error_errno(errno, "%s: failed to create unit file %s: %m", service->path, unit);
a10f5d05
MB
582
583 fprintf(f,
584 "# Automatically generated by systemd-xdg-autostart-generator\n\n"
585 "[Unit]\n"
586 "Documentation=man:systemd-xdg-autostart-generator(8)\n"
587 "SourcePath=%s\n"
588 "PartOf=graphical-session.target\n\n",
589 path_escaped);
590
591 if (service->description) {
592 _cleanup_free_ char *t = NULL;
593
594 t = specifier_escape(service->description);
595 if (!t)
596 return log_oom();
597
598 fprintf(f, "Description=%s\n", t);
599 }
600
601 /* Only start after the session is ready. */
602 fprintf(f,
603 "After=graphical-session.target\n");
604
605 fprintf(f,
606 "\n[Service]\n"
a032b68d 607 "Type=exec\n"
ea0999c9 608 "ExitType=cgroup\n"
a10f5d05
MB
609 "ExecStart=:%s\n"
610 "Restart=no\n"
611 "TimeoutSec=5s\n"
612 "Slice=app.slice\n",
613 exec_start);
614
a032b68d
MB
615 if (service->working_directory) {
616 _cleanup_free_ char *e_working_directory = NULL;
617
618 e_working_directory = cescape(service->working_directory);
619 if (!e_working_directory)
620 return log_oom();
621
622 fprintf(f, "WorkingDirectory=-%s\n", e_working_directory);
623 }
624
a10f5d05
MB
625 /* Generate an ExecCondition to check $XDG_CURRENT_DESKTOP */
626 if (!strv_isempty(service->only_show_in) || !strv_isempty(service->not_show_in)) {
627 _cleanup_free_ char *only_show_in = NULL, *not_show_in = NULL, *e_only_show_in = NULL, *e_not_show_in = NULL;
628
629 only_show_in = strv_join(service->only_show_in, ":");
630 not_show_in = strv_join(service->not_show_in, ":");
631 if (!only_show_in || !not_show_in)
632 return log_oom();
633
634 e_only_show_in = cescape(only_show_in);
635 e_not_show_in = cescape(not_show_in);
636 if (!e_only_show_in || !e_not_show_in)
637 return log_oom();
638
639 /* Just assume the values are reasonably sane */
640 fprintf(f,
641 "ExecCondition=" ROOTLIBEXECDIR "/systemd-xdg-autostart-condition \"%s\" \"%s\"\n",
642 e_only_show_in,
643 e_not_show_in);
644 }
645
f5caa8fa 646 r = xdg_autostart_generate_desktop_condition(service, f,
a10f5d05
MB
647 "gnome-systemd-autostart-condition",
648 service->autostart_condition);
649 if (r < 0)
650 return r;
651
f5caa8fa 652 r = xdg_autostart_generate_desktop_condition(service, f,
a10f5d05
MB
653 "kde-systemd-start-condition",
654 service->kde_autostart_condition);
655 if (r < 0)
656 return r;
657
086111aa
LB
658 log_debug("%s: symlinking %s in xdg-desktop-autostart.target/.wants%s",
659 service->path, service->name, special_glyph(SPECIAL_GLYPH_ELLIPSIS));
f5caa8fa 660 return generator_add_symlink(dest, "xdg-desktop-autostart.target", "wants", service->name);
a10f5d05 661}