| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * Copyright (C) 2021 Oracle. All Rights Reserved. |
| * Author: Darrick J. Wong <djwong@kernel.org> |
| */ |
| #include "platform_defs.h" |
| #include "command.h" |
| #include "init.h" |
| #include "io.h" |
| #include "libfrog/logging.h" |
| #include "libfrog/fsgeom.h" |
| #include "libfrog/fiexchange.h" |
| #include "libfrog/file_exchange.h" |
| |
| struct update_info { |
| /* File object for the file that we're updating. */ |
| struct xfs_fd file_fd; |
| |
| /* FIEXCHANGE_RANGE request to commit the changes. */ |
| struct file_xchg_range xchg_req; |
| |
| /* Name of the file we're updating. */ |
| char *old_fname; |
| |
| /* fd we're using to stage the updates. */ |
| int temp_fd; |
| }; |
| |
| enum finish_how { |
| FINISH_ABORT, |
| FINISH_COMMIT, |
| FINISH_CHECK |
| }; |
| |
| static struct update_info *updates; |
| static unsigned int nr_updates; |
| |
| static void |
| startupdate_help(void) |
| { |
| printf(_( |
| "\n" |
| " Prepare for an atomic file update, if supported by the filesystem.\n" |
| " A temporary file will be opened for writing and inserted into the file\n" |
| " table. The current file will be changed to this temporary file. Neither\n" |
| " file can be closed for the duration of the update.\n" |
| "\n" |
| " -e -- Start with an empty file\n" |
| "\n")); |
| } |
| |
| int |
| startupdate_f( |
| int argc, |
| char *argv[]) |
| { |
| struct fsxattr attr; |
| struct xfs_fsop_geom fsgeom; |
| struct fs_path fspath; |
| struct stat stat; |
| struct update_info *p; |
| char *fname; |
| char *path = NULL, *d; |
| size_t fname_len; |
| int flags = IO_TMPFILE | IO_ATOMICUPDATE; |
| int temp_fd = -1; |
| bool clone_file = true; |
| int c; |
| int ret; |
| |
| while ((c = getopt(argc, argv, "e")) != -1) { |
| switch (c) { |
| case 'e': |
| clone_file = false; |
| break; |
| default: |
| startupdate_help(); |
| return 0; |
| } |
| } |
| if (optind != argc) { |
| startupdate_help(); |
| return 0; |
| } |
| |
| /* Allocate a new slot. */ |
| p = realloc(updates, (++nr_updates) * sizeof(*p)); |
| if (!p) { |
| perror("startupdate realloc"); |
| goto fail; |
| } |
| updates = p; |
| |
| /* Fill out the update information so that we can commit later. */ |
| p = &updates[nr_updates - 1]; |
| memset(p, 0, sizeof(*p)); |
| p->file_fd.fd = file->fd; |
| ret = xfd_prepare_geometry(&p->file_fd); |
| if (ret) { |
| xfrog_perror(ret, file->name); |
| goto fail; |
| } |
| |
| ret = fstat(file->fd, &stat); |
| if (ret) { |
| perror(file->name); |
| goto fail; |
| } |
| |
| /* Is the current file realtime? If so, the temp file must match. */ |
| ret = ioctl(file->fd, FS_IOC_FSGETXATTR, &attr); |
| if (ret == 0 && attr.fsx_xflags & FS_XFLAG_REALTIME) |
| flags |= IO_REALTIME; |
| |
| /* Compute path to the directory that the current file is in. */ |
| path = strdup(file->name); |
| d = strrchr(path, '/'); |
| if (!d) { |
| fprintf(stderr, _("%s: cannot compute dirname?"), path); |
| goto fail; |
| } |
| *d = 0; |
| |
| /* Open a temporary file to stage the extents. */ |
| temp_fd = openfile(path, &fsgeom, flags, 0600, &fspath); |
| if (temp_fd < 0) { |
| perror(path); |
| goto fail; |
| } |
| |
| /* |
| * Snapshot the original file metadata in anticipation of the later |
| * extent swap request. |
| */ |
| ret = xfrog_file_exchange_prep(&p->file_fd, FILE_XCHG_RANGE_COMMIT, 0, |
| temp_fd, 0, stat.st_size, &p->xchg_req); |
| if (ret) { |
| perror("update prep"); |
| goto fail; |
| } |
| |
| /* Clone all the data from the original file into the temporary file. */ |
| if (clone_file) { |
| ret = ioctl(temp_fd, XFS_IOC_CLONE, p->file_fd.fd); |
| if (ret) { |
| perror(path); |
| goto fail; |
| } |
| } |
| |
| /* Prepare a new path string for the duration of the update. */ |
| #define FILEUPDATE_STR " (fileupdate)" |
| fname_len = strlen(file->name) + strlen(FILEUPDATE_STR); |
| fname = malloc(fname_len + 1); |
| if (!fname) { |
| perror("new path"); |
| goto fail; |
| } |
| snprintf(fname, fname_len + 1, "%s%s", file->name, FILEUPDATE_STR); |
| |
| /* |
| * Install the temporary file into the same slot of the file table as |
| * the original file. Ensure that the original file cannot be closed. |
| */ |
| file->flags |= IO_ATOMICUPDATE; |
| p->old_fname = file->name; |
| file->name = fname; |
| p->temp_fd = file->fd = temp_fd; |
| |
| free(path); |
| return 0; |
| fail: |
| if (temp_fd >= 0) |
| close(temp_fd); |
| free(path); |
| nr_updates--; |
| exitcode = 1; |
| return 1; |
| } |
| |
| static int |
| finish_update( |
| enum finish_how how, |
| uint64_t flags) |
| { |
| struct update_info *p; |
| size_t length; |
| unsigned int i; |
| unsigned int offset; |
| int temp_fd; |
| int ret; |
| |
| /* Find our update descriptor. */ |
| for (i = 0, p = updates; i < nr_updates; i++, p++) { |
| if (p->temp_fd == file->fd) |
| break; |
| } |
| |
| if (i == nr_updates) { |
| fprintf(stderr, |
| _("Current file is not the staging file for an atomic update.\n")); |
| exitcode = 1; |
| return 0; |
| } |
| |
| p->xchg_req.flags |= flags; |
| |
| /* |
| * Commit our changes, if desired. If the extent swap fails, we stop |
| * processing immediately so that we can run more xfs_io commands. |
| */ |
| switch (how) { |
| case FINISH_CHECK: |
| p->xchg_req.flags |= FILE_XCHG_RANGE_DRY_RUN; |
| fallthrough; |
| case FINISH_COMMIT: |
| ret = xfrog_file_exchange(&p->file_fd, &p->xchg_req); |
| if (ret) { |
| xfrog_perror(ret, _("committing update")); |
| exitcode = 1; |
| return 0; |
| } |
| printf(_("Committed updates to '%s'.\n"), p->old_fname); |
| break; |
| case FINISH_ABORT: |
| printf(_("Cancelled updates to '%s'.\n"), p->old_fname); |
| break; |
| } |
| |
| /* |
| * Reset the filetable to point to the original file, and close the |
| * temporary file. |
| */ |
| free(file->name); |
| file->name = p->old_fname; |
| file->flags &= ~IO_ATOMICUPDATE; |
| temp_fd = file->fd; |
| file->fd = p->file_fd.fd; |
| ret = close(temp_fd); |
| if (ret) |
| perror(_("closing temporary file")); |
| |
| /* Remove the atomic update context, shifting things down. */ |
| offset = p - updates; |
| length = nr_updates * sizeof(struct update_info); |
| length -= (offset + 1) * sizeof(struct update_info); |
| if (length) |
| memmove(p, p + 1, length); |
| nr_updates--; |
| return 0; |
| } |
| |
| static void |
| cancelupdate_help(void) |
| { |
| printf(_( |
| "\n" |
| " Cancels an atomic file update. The temporary file will be closed, and the\n" |
| " current file set back to the original file.\n" |
| "\n")); |
| } |
| |
| int |
| cancelupdate_f( |
| int argc, |
| char *argv[]) |
| { |
| return finish_update(FINISH_ABORT, 0); |
| } |
| |
| static void |
| commitupdate_help(void) |
| { |
| printf(_( |
| "\n" |
| " Commits an atomic file update. File contents written to the temporary file\n" |
| " will be swapped atomically with the corresponding range in the original\n" |
| " file. The temporary file will be closed, and the current file set back to\n" |
| " the original file.\n" |
| "\n" |
| " -h -- Do not swap sparse areas of the temporary file.\n" |
| " -k -- Do not change file size.\n" |
| " -n -- Check parameters but do not change anything.\n")); |
| } |
| |
| int |
| commitupdate_f( |
| int argc, |
| char *argv[]) |
| { |
| enum finish_how how = FINISH_COMMIT; |
| uint64_t flags = FILE_XCHG_RANGE_TO_EOF; |
| int c; |
| |
| while ((c = getopt(argc, argv, "hkn")) != -1) { |
| switch (c) { |
| case 'h': |
| flags |= FILE_XCHG_RANGE_SKIP_FILE1_HOLES; |
| break; |
| case 'k': |
| flags &= ~FILE_XCHG_RANGE_TO_EOF; |
| break; |
| case 'n': |
| how = FINISH_CHECK; |
| break; |
| default: |
| commitupdate_help(); |
| return 0; |
| } |
| } |
| if (optind != argc) { |
| commitupdate_help(); |
| return 0; |
| } |
| |
| return finish_update(how, flags); |
| } |
| |
| static struct cmdinfo startupdate_cmd = { |
| .name = "startupdate", |
| .cfunc = startupdate_f, |
| .argmin = 0, |
| .argmax = -1, |
| .flags = CMD_FLAG_ONESHOT | CMD_NOMAP_OK, |
| .help = startupdate_help, |
| }; |
| |
| static struct cmdinfo cancelupdate_cmd = { |
| .name = "cancelupdate", |
| .cfunc = cancelupdate_f, |
| .argmin = 0, |
| .argmax = 0, |
| .flags = CMD_FLAG_ONESHOT | CMD_NOMAP_OK, |
| .help = cancelupdate_help, |
| }; |
| |
| static struct cmdinfo commitupdate_cmd = { |
| .name = "commitupdate", |
| .cfunc = commitupdate_f, |
| .argmin = 0, |
| .argmax = -1, |
| .flags = CMD_FLAG_ONESHOT | CMD_NOMAP_OK, |
| .help = commitupdate_help, |
| }; |
| |
| void |
| atomicupdate_init(void) |
| { |
| startupdate_cmd.oneline = _("start an atomic update of a file"); |
| startupdate_cmd.args = _("[-e]"); |
| |
| cancelupdate_cmd.oneline = _("cancel an atomic update"); |
| |
| commitupdate_cmd.oneline = _("commit a file update atomically"); |
| commitupdate_cmd.args = _("[-h] [-n]"); |
| |
| add_command(&startupdate_cmd); |
| add_command(&cancelupdate_cmd); |
| add_command(&commitupdate_cmd); |
| } |