]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/shared/edit-util.c
process-util: add new FORK_DEATHSIG_SIGKILL flag, rename FORK_DEATHSIG → FORK_DEATHSI...
[thirdparty/systemd.git] / src / shared / edit-util.c
CommitLineData
a01c4bc9
MY
1/* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3#include <errno.h>
4#include <stdio.h>
5
6#include "alloc-util.h"
7#include "copy.h"
8#include "edit-util.h"
9#include "fd-util.h"
10#include "fileio.h"
11#include "fs-util.h"
12#include "mkdir-label.h"
13#include "path-util.h"
14#include "process-util.h"
a01c4bc9
MY
15#include "string-util.h"
16#include "strv.h"
54ad6aa1 17#include "tmpfile-util-label.h"
a01c4bc9 18
9a11b4f9
MY
19void edit_file_context_done(EditFileContext *context) {
20 int r;
21
22 assert(context);
23
24 FOREACH_ARRAY(i, context->files, context->n_files) {
8992667f 25 unlink_and_free(i->temp);
9a11b4f9
MY
26
27 if (context->remove_parent) {
28 _cleanup_free_ char *parent = NULL;
29
30 r = path_extract_directory(i->path, &parent);
31 if (r < 0)
32 log_debug_errno(r, "Failed to extract directory from '%s', ignoring: %m", i->path);
d3bf024f
MY
33 else if (rmdir(parent) < 0 && !IN_SET(errno, ENOENT, ENOTEMPTY))
34 log_debug_errno(errno, "Failed to remove parent directory '%s', ignoring: %m", parent);
9a11b4f9 35 }
a01c4bc9 36
a01c4bc9 37 free(i->path);
9a11b4f9
MY
38 free(i->original_path);
39 strv_free(i->comment_paths);
a01c4bc9
MY
40 }
41
9a11b4f9
MY
42 context->files = mfree(context->files);
43 context->n_files = 0;
44}
45
46bool edit_files_contains(const EditFileContext *context, const char *path) {
47 assert(context);
48 assert(path);
49
50 FOREACH_ARRAY(i, context->files, context->n_files)
00b6baf2 51 if (path_equal(i->path, path))
9a11b4f9
MY
52 return true;
53
54 return false;
a01c4bc9
MY
55}
56
9a11b4f9
MY
57int edit_files_add(
58 EditFileContext *context,
59 const char *path,
60 const char *original_path,
61 char * const *comment_paths) {
62
63 _cleanup_free_ char *new_path = NULL, *new_original_path = NULL;
64 _cleanup_strv_free_ char **new_comment_paths = NULL;
65
66 assert(context);
67 assert(path);
68
69 if (edit_files_contains(context, path))
70 return 0;
71
da037170 72 if (!GREEDY_REALLOC(context->files, context->n_files + 1))
9a11b4f9
MY
73 return log_oom();
74
75 new_path = strdup(path);
76 if (!new_path)
77 return log_oom();
78
79 if (original_path) {
80 new_original_path = strdup(original_path);
81 if (!new_original_path)
82 return log_oom();
83 }
84
85 if (comment_paths) {
86 new_comment_paths = strv_copy(comment_paths);
87 if (!new_comment_paths)
88 return log_oom();
89 }
90
91 context->files[context->n_files] = (EditFile) {
043db340 92 .context = context,
9a11b4f9
MY
93 .path = TAKE_PTR(new_path),
94 .original_path = TAKE_PTR(new_original_path),
95 .comment_paths = TAKE_PTR(new_comment_paths),
96 };
97 context->n_files++;
98
99 return 1;
100}
101
c629a5bc 102static int create_edit_temp_file(EditFile *e) {
0a742f36 103 _cleanup_(unlink_and_freep) char *temp = NULL;
54ad6aa1
MY
104 _cleanup_fclose_ FILE *f = NULL;
105 const char *source;
106 bool has_original, has_target;
fa2413dd 107 unsigned line = 1;
a01c4bc9
MY
108 int r;
109
c629a5bc
YW
110 assert(e);
111 assert(e->context);
112 assert(e->path);
113 assert(!e->comment_paths || (e->context->marker_start && e->context->marker_end));
114
115 if (e->temp)
116 return 0;
a01c4bc9 117
54ad6aa1 118 r = mkdir_parents_label(e->path, 0755);
a01c4bc9 119 if (r < 0)
54ad6aa1 120 return log_error_errno(r, "Failed to create parent directories for '%s': %m", e->path);
a01c4bc9 121
54ad6aa1 122 r = fopen_temporary_label(e->path, e->path, &f, &temp);
a01c4bc9 123 if (r < 0)
54ad6aa1 124 return log_error_errno(r, "Failed to create temporary file for '%s': %m", e->path);
a01c4bc9 125
54ad6aa1
MY
126 if (fchmod(fileno(f), 0644) < 0)
127 return log_error_errno(errno, "Failed to change mode of temporary file '%s': %m", temp);
52073ba2 128
54ad6aa1
MY
129 has_original = e->original_path && access(e->original_path, F_OK) >= 0;
130 has_target = access(e->path, F_OK) >= 0;
52073ba2 131
bc6c7a58
MY
132 if (has_original && (!has_target || e->context->overwrite_with_origin))
133 /* We are asked to overwrite target with original_path or target doesn't exist. */
54ad6aa1
MY
134 source = e->original_path;
135 else if (has_target)
bc6c7a58 136 /* Target exists and shouldn't be overwritten. */
54ad6aa1
MY
137 source = e->path;
138 else
139 source = NULL;
fa2413dd 140
c629a5bc 141 if (e->comment_paths) {
54ad6aa1 142 _cleanup_free_ char *source_contents = NULL;
a01c4bc9 143
54ad6aa1
MY
144 if (source) {
145 r = read_full_file(source, &source_contents, NULL);
146 if (r < 0)
147 return log_error_errno(r, "Failed to read source file '%s': %m", source);
148 }
a01c4bc9
MY
149
150 fprintf(f,
151 "### Editing %s\n"
152 "%s\n"
153 "\n"
154 "%s%s"
155 "\n"
156 "%s\n",
c629a5bc
YW
157 e->path,
158 e->context->marker_start,
54ad6aa1
MY
159 strempty(source_contents),
160 source_contents && endswith(source_contents, "\n") ? "" : "\n",
c629a5bc 161 e->context->marker_end);
a01c4bc9 162
fa2413dd 163 line = 4; /* Start editing at the contents area */
a01c4bc9 164
c629a5bc 165 STRV_FOREACH(path, e->comment_paths) {
54ad6aa1 166 _cleanup_free_ char *comment = NULL;
a01c4bc9 167
54ad6aa1
MY
168 /* Skip the file which is being edited and the source file (can be the same) */
169 if (PATH_IN_SET(*path, e->path, source))
a01c4bc9
MY
170 continue;
171
54ad6aa1 172 r = read_full_file(*path, &comment, NULL);
a01c4bc9 173 if (r < 0)
54ad6aa1 174 return log_error_errno(r, "Failed to read comment file '%s': %m", *path);
a01c4bc9
MY
175
176 fprintf(f, "\n\n### %s", *path);
a01c4bc9 177
54ad6aa1
MY
178 if (!isempty(comment)) {
179 _cleanup_free_ char *c = NULL;
180
181 c = strreplace(strstrip(comment), "\n", "\n# ");
182 if (!c)
a01c4bc9 183 return log_oom();
fa2413dd 184
54ad6aa1 185 fprintf(f, "\n# %s", c);
a01c4bc9
MY
186 }
187 }
54ad6aa1
MY
188 } else if (source) {
189 r = copy_file_fd(source, fileno(f), COPY_REFLINK);
190 if (r < 0) {
191 assert(r != -ENOENT);
192 return log_error_errno(r, "Failed to copy file '%s' to temporary file '%s': %m", source, temp);
193 }
a01c4bc9
MY
194 }
195
54ad6aa1
MY
196 r = fflush_and_check(f);
197 if (r < 0)
198 return log_error_errno(r, "Failed to write to temporary file '%s': %m", temp);
199
c629a5bc
YW
200 e->temp = TAKE_PTR(temp);
201 e->line = line;
a01c4bc9
MY
202
203 return 0;
204}
205
f8970df5
MY
206static int run_editor_child(const EditFileContext *context) {
207 _cleanup_strv_free_ char **args = NULL;
208 const char *editor;
a01c4bc9
MY
209 int r;
210
f8970df5
MY
211 /* SYSTEMD_EDITOR takes precedence over EDITOR which takes precedence over VISUAL.
212 * If neither SYSTEMD_EDITOR nor EDITOR nor VISUAL are present, we try to execute
213 * well known editors. */
214 editor = getenv("SYSTEMD_EDITOR");
215 if (!editor)
216 editor = getenv("EDITOR");
217 if (!editor)
218 editor = getenv("VISUAL");
a01c4bc9 219
f8970df5
MY
220 if (!isempty(editor)) {
221 _cleanup_strv_free_ char **editor_args = NULL;
a01c4bc9 222
f8970df5
MY
223 editor_args = strv_split(editor, WHITESPACE);
224 if (!editor_args)
225 return log_oom();
a01c4bc9 226
f8970df5
MY
227 args = TAKE_PTR(editor_args);
228 }
a01c4bc9 229
f8970df5
MY
230 if (context->n_files == 1 && context->files[0].line > 1) {
231 /* If editing a single file only, use the +LINE syntax to put cursor on the right line */
232 r = strv_extendf(&args, "+%u", context->files[0].line);
233 if (r < 0)
234 return log_oom();
235 }
a01c4bc9 236
f8970df5
MY
237 FOREACH_ARRAY(i, context->files, context->n_files) {
238 r = strv_extend(&args, i->temp);
239 if (r < 0)
240 return log_oom();
241 }
a01c4bc9 242
f8970df5
MY
243 if (!isempty(editor))
244 execvp(args[0], (char* const*) args);
245
246 bool prepended = false;
247 FOREACH_STRING(name, "editor", "nano", "vim", "vi") {
248 if (!prepended) {
249 r = strv_prepend(&args, name);
250 prepended = true;
a01c4bc9 251 } else
f8970df5
MY
252 r = free_and_strdup(&args[0], name);
253 if (r < 0)
254 return log_oom();
255
256 execvp(args[0], (char* const*) args);
257
258 /* We do not fail if the editor doesn't exist because we want to try each one of them
259 * before failing. */
260 if (errno != ENOENT)
261 return log_error_errno(errno, "Failed to execute '%s': %m", name);
262 }
263
264 return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
265 "Cannot edit files, no editor available. Please set either $SYSTEMD_EDITOR, $EDITOR or $VISUAL.");
266}
a01c4bc9 267
f8970df5
MY
268static int run_editor(const EditFileContext *context) {
269 int r;
270
271 assert(context);
272
e9ccae31 273 r = safe_fork("(editor)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_RLIMIT_NOFILE_SAFE|FORK_LOG|FORK_WAIT, NULL);
f8970df5
MY
274 if (r < 0)
275 return r;
276 if (r == 0) { /* Child */
277 r = run_editor_child(context);
278 _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS);
a01c4bc9
MY
279 }
280
281 return 0;
282}
283
978e222f 284static int strip_edit_temp_file(EditFile *e) {
a01c4bc9 285 _cleanup_free_ char *old_contents = NULL, *new_contents = NULL;
978e222f 286 const char *stripped;
a01c4bc9
MY
287 int r;
288
aec8200b
YW
289 assert(e);
290 assert(e->context);
291 assert(e->temp);
fa2413dd 292
aec8200b 293 r = read_full_file(e->temp, &old_contents, NULL);
a01c4bc9 294 if (r < 0)
77c9bb17 295 return log_error_errno(r, "Failed to read temporary file '%s': %m", e->temp);
aec8200b
YW
296
297 if (e->context->marker_start) {
978e222f 298 /* Trim out the lines between the two markers */
aec8200b
YW
299 char *contents_start, *contents_end;
300
301 assert(e->context->marker_end);
a01c4bc9 302
d791013f
LP
303 contents_start = strstrafter(old_contents, e->context->marker_start);
304 if (!contents_start)
aec8200b 305 contents_start = old_contents;
a01c4bc9 306
aec8200b
YW
307 contents_end = strstr(contents_start, e->context->marker_end);
308 if (contents_end)
978e222f 309 *contents_end = '\0';
a01c4bc9 310
978e222f 311 stripped = strstrip(contents_start);
aec8200b 312 } else
978e222f
MY
313 stripped = strstrip(old_contents);
314 if (isempty(stripped))
315 return 0; /* File is empty (has no real changes) */
a01c4bc9 316
978e222f
MY
317 /* Trim prefix and suffix, but ensure suffixed by single newline */
318 new_contents = strjoin(stripped, "\n");
a01c4bc9
MY
319 if (!new_contents)
320 return log_oom();
321
322 if (streq(old_contents, new_contents)) /* Don't touch the file if the above didn't change a thing */
978e222f 323 return 1; /* Contents unchanged after stripping but has changes */
a01c4bc9 324
aec8200b 325 r = write_string_file(e->temp, new_contents, WRITE_STRING_FILE_CREATE | WRITE_STRING_FILE_TRUNCATE | WRITE_STRING_FILE_AVOID_NEWLINE);
a01c4bc9 326 if (r < 0)
77c9bb17 327 return log_error_errno(r, "Failed to strip temporary file '%s': %m", e->temp);
a01c4bc9 328
978e222f 329 return 1; /* Contents have real changes and are changed after stripping */
a01c4bc9 330}
9a11b4f9
MY
331
332int do_edit_files_and_install(EditFileContext *context) {
333 int r;
334
335 assert(context);
336
337 if (context->n_files == 0)
338 return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "Got no files to edit.");
339
c629a5bc
YW
340 FOREACH_ARRAY(i, context->files, context->n_files) {
341 r = create_edit_temp_file(i);
342 if (r < 0)
343 return r;
344 }
9a11b4f9
MY
345
346 r = run_editor(context);
347 if (r < 0)
348 return r;
349
350 FOREACH_ARRAY(i, context->files, context->n_files) {
978e222f
MY
351 /* Always call strip_edit_temp_file which will tell if the temp file has actual changes */
352 r = strip_edit_temp_file(i);
9a11b4f9
MY
353 if (r < 0)
354 return r;
355 if (r == 0) /* temp file doesn't carry actual changes, ignoring */
356 continue;
357
358 r = RET_NERRNO(rename(i->temp, i->path));
359 if (r < 0)
77c9bb17
MY
360 return log_error_errno(r,
361 "Failed to rename temporary file '%s' to target file '%s': %m",
362 i->temp,
363 i->path);
9a11b4f9
MY
364 i->temp = mfree(i->temp);
365
366 log_info("Successfully installed edited file '%s'.", i->path);
367 }
368
369 return 0;
370}