/******************************************************************************* Copyright(C) Jonas 'Sortie' Termansen 2015, 2016. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . sysupgrade.c Operating system upgrader. *******************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "conf.h" #include "devices.h" #include "execute.h" #include "fileops.h" #include "interactive.h" #include "manifest.h" #include "release.h" const char* prompt_man_section = "7"; const char* prompt_man_page = "upgrade"; struct installation { struct blockdevice* bdev; struct release release; }; static struct installation* installations; static size_t installations_count; static size_t installations_length; static bool add_installation(struct blockdevice* bdev, struct release* release) { if ( installations_count == installations_length ) { size_t new_length = installations_length; if ( !new_length ) new_length = 16; struct installation* new_installations = (struct installation*) reallocarray(NULL, new_length, sizeof(struct installation)); if ( !new_installations ) return false; installations = new_installations; installations_length = new_length; } struct installation* installation = &installations[installations_count++]; installation->bdev = bdev; installation->release = *release; return true; } static void search_installation_path(const char* mnt, struct blockdevice* bdev) { char* release_errpath; if ( asprintf(&release_errpath, "%s: /etc/sortix-release", path_of_blockdevice(bdev)) < 0 ) { warn("malloc"); return; } char* release_path; if ( asprintf(&release_path, "%s/etc/sortix-release", mnt) < 0 ) { warn("malloc"); free(release_errpath); return; } struct release release; if ( os_release_load(&release, release_path, release_errpath) && !add_installation(bdev, &release) ) release_free(&release); free(release_path); free(release_errpath); } // TODO: Switch to mountpoint_mount(). static bool await_mount(const char* mnt, pid_t pid, struct stat* oldst, const char* bdev_path, const char* driver) { while ( true ) { struct stat newst; if ( stat(mnt, &newst) < 0 ) { warn("%s", mnt); return false; } if ( newst.st_dev != oldst->st_dev || newst.st_ino != oldst->st_ino ) break; int code; pid_t child = waitpid(pid, &code, WNOHANG); if ( child < 0 ) { err(2, "waitpid"); return false; } if ( child != 0 ) { if ( WIFSIGNALED(code) ) warnx("%s: Mount failed: %s: %s", bdev_path, driver, strsignal(WTERMSIG(code))); else if ( !WIFEXITED(code) ) warnx("%s: Mount failed: %s: %s", bdev_path, driver, "Unexpected unusual termination"); else if ( WEXITSTATUS(code) == 127 ) warnx("%s: Mount failed: %s: %s", bdev_path, driver, "Filesystem driver is absent"); #if 0 else if ( WEXITSTATUS(code) == 0 ) warnx("%s: Mount failed: %s: Unexpected successful exit", bdev_path, driver); else warnx("%s: Mount failed: %s: Exited with status %i", bdev_path, driver, WEXITSTATUS(code)); #endif return false; } struct timespec delay = timespec_make(0, 50L * 1000L * 1000L); nanosleep(&delay, NULL); } return true; } static pid_t begin_mount(const char* mnt, struct blockdevice* bdev) { struct filesystem* fs = bdev->fs; if ( !fs ) return -1; if ( !fs->driver ) return -1; if ( fs->flags & FILESYSTEM_FLAG_FSCK_MUST && !fsck(fs) ) return -1; struct stat fs_oldstat; if ( stat(mnt, &fs_oldstat) < 0 ) { warn("stat: %s", mnt); return -1; } const char* bdev_path = path_of_blockdevice(fs->bdev); pid_t fs_pid = fork(); if ( fs_pid < 0 ) { warn("fork"); return -1; } if ( fs_pid == 0 ) { setpgid(0, 0); const char* argv[] = { fs->driver, "--foreground", bdev_path, mnt, NULL }; execvp(argv[0], (char* const*) argv); warn("%s", argv[0]); _exit(127); } if ( !await_mount(mnt, fs_pid, &fs_oldstat, bdev_path, fs->driver) ) return false; return fs_pid; } static void end_mount(const char* mnt, pid_t fs_pid) { sched_yield(); unmount(mnt, 0); waitpid(fs_pid, NULL, 0); } static void search_installation_bdev(const char* mnt, struct blockdevice* bdev) { pid_t fs_pid = begin_mount(mnt, bdev); if ( fs_pid < 0 ) return; search_installation_path(mnt, bdev); end_mount(mnt, fs_pid); } static void search_installations(const char* mnt) { for ( size_t i = 0; i < installations_count; i++ ) release_free(&installations[i].release); free(installations); installations_count = 0; installations_length = 0; for ( size_t i = 0; i < hds_count; i++ ) { struct harddisk* hd = hds[i]; if ( hd->bdev.pt ) { for ( size_t n = 0; n < hd->bdev.pt->partitions_count; n++ ) { struct partition* p = hd->bdev.pt->partitions[n]; search_installation_bdev(mnt, &p->bdev); } } else search_installation_bdev(mnt, &hd->bdev); } } static void next_version(const struct release* current, struct release* new) { // Next release of a development snapshot is the final release. if ( current->version_dev ) { new->version_major = current->version_major; new->version_minor = current->version_minor; new->version_dev = false; return; } // Releases increment by 0.1. new->version_major = current->version_major; new->version_minor = current->version_minor + 1; new->version_dev = false; // Major increments instead of minor release 10. if ( new->version_minor == 10 ) { new->version_major++; new->version_minor = 0; } } static bool downgrading_version(const struct release* old, const struct release* new) { if ( new->version_major < old->version_major ) return true; if ( new->version_major > old->version_major ) return false; if ( new->version_major < old->version_major ) return true; if ( new->version_major > old->version_major ) return false; if ( new->version_dev && !old->version_dev ) return true; return false; } static bool skipping_version(const struct release* old, const struct release* new) { // Not skipping a release if upgrading to older release. if ( downgrading_version(old, new) ) return false; // Not skipping a release if upgrading to same release. if ( new->version_major == old->version_major && new->version_minor == old->version_minor && new->version_dev == old->version_dev ) return false; // Not skipping a release if upgrading to the next release. struct release next; next_version(old, &next); if ( new->version_major == next.version_major && new->version_minor == next.version_minor ) return false; return true; } static void preserve_src(const char* where) { if ( access_or_die(where, F_OK) < 0 ) return; if ( access_or_die("oldsrc", F_OK) < 0 ) { if ( mkdir("oldsrc", 0755) < 0 ) { warn("oldsrc"); _exit(1); } } time_t now = time(NULL); struct tm tm; localtime_r(&now, &tm); char buf[64]; snprintf(buf, sizeof(buf), "oldsrc/%s-%i-%02i-%02i", where, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); if ( access_or_die(buf, F_OK) == 0 ) { snprintf(buf, sizeof(buf), "oldsrc/%s-%i-%02i-%02i-%02i-%02i-%02i", where, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec); if ( access_or_die(buf, F_OK) == 0 ) { snprintf(buf, sizeof(buf), "oldsrc/%s.XXXXXX", where); if ( !mkdtemp(buf) ) { warnx("failed to find location to store old /%s", where); _exit(1); } rmdir(buf); } } printf(" - Moving /%s to /%s\n", where, buf); if ( rename(where, buf) < 0 ) { warn("rename: %s -> %s", where, buf); _exit(1); } } int main(void) { shlvl(); if ( !isatty(0) ) errx(2, "fatal: stdin is not a terminal"); if ( !isatty(1) ) errx(2, "fatal: stdout is not a terminal"); if ( !isatty(2) ) errx(2, "fatal: stderr is not a terminal"); if ( getuid() != 0 ) errx(2, "You need to be root to install %s", BRAND_DISTRIBUTION_NAME); if ( getgid() != 0 ) errx(2, "You need to be group root to install %s", BRAND_DISTRIBUTION_NAME); struct utsname uts; uname(&uts); static char input[256]; textf("Hello and welcome to the " BRAND_DISTRIBUTION_NAME " " VERSIONSTR "" " upgrader for %s.\n\n", uts.machine); // '|' rather than '||' is to ensure side effects. if ( missing_program("cut") | missing_program("dash") | missing_program("fsck.ext2") | missing_program("grub-install") | missing_program("man") | missing_program("sed") | missing_program("xargs") ) { text("Warning: This system does not have the necessary third party " "software installed to properly upgrade installations.\n"); while ( true ) { prompt(input, sizeof(input), "Sure you want to proceed?", "no"); if ( strcasecmp(input, "no") == 0 ) return 0; if ( strcasecmp(input, "yes") == 0 ) break; } text("\n"); } text("This program will upgrade an existing installation to this " "version. You can always escape to a shell by answering '!' to any " "regular prompt. You can view the upgrade(7) manual page by answering " "'!man'. Default answers are in []'s and can be selected by pressing " "enter.\n\n"); const char* readies[] = { "Ready", "Yes", "Yeah", "Yep", "Let's go", "Let's do this", "Betcha", "Sure am", "You bet", "This time it will listen to my music", }; size_t num_readies = sizeof(readies) / sizeof(readies[0]); const char* ready = readies[arc4random_uniform(num_readies)]; prompt(input, sizeof(input), "Ready?", ready); text("\n"); while ( true ) { // TODO: Detect the name of the current keyboard layout. prompt(input, sizeof(input), "Choose your keyboard layout ('?' or 'L' for list)", "default"); if ( !strcmp(input, "?") || !strcmp(input, "l") || !strcmp(input, "L") ) { DIR* dir = opendir("/share/kblayout"); if ( !dir ) { warn("%s", "/share/kblayout"); continue; } bool space = false; struct dirent* entry; while ( (entry = readdir(dir)) ) { if ( entry->d_name[0] == '.' ) continue; if ( space ) putchar(' '); fputs(entry->d_name, stdout); space = true; } closedir(dir); if ( !space ) fputs("(No keyboard layouts available)", stdout); putchar('\n'); continue; } if ( !strcmp(input, "default") ) break; const char* argv[] = { "chkblayout", "--", input, NULL }; if ( execute(argv, "f") == 0 ) break; } text("\n"); struct dispmsg_get_driver_name dgdn = { 0 }; dgdn.msgid = DISPMSG_GET_DRIVER_NAME; dgdn.device = 0; dgdn.driver_index = 0; dgdn.name.byte_size = 0; dgdn.name.str = NULL; if ( dispmsg_issue(&dgdn, sizeof(dgdn)) == 0 || errno != ENODEV ) { while ( true ) { prompt(input, sizeof(input), "Select display resolution? (yes/no)", "yes"); if ( strcasecmp(input, "no") && strcasecmp(input, "yes") ) continue; if ( strcasecmp(input, "no") == 0 ) break; if ( execute((const char*[]) { "chvideomode", NULL }, "f") != 0 ) continue; break; } } text("\n"); struct release new_release; if ( !os_release_load(&new_release, "/etc/sortix-release", "/etc/sortix-release") ) exit(2); char mnt[] = "/tmp/fs.XXXXXX"; if ( !mkdtemp(mnt) ) err(2, "mkdtemp: %s", "/tmp/fs.XXXXXX"); struct installation* target = NULL; while ( true ) { text("Searching for existing installations...\n"); scan_devices(); search_installations(mnt); text("\n"); if ( installations_count == 0 ) { while ( true) { prompt(input, sizeof(input), "No existing installations found, " "run installer instead? (yes/no)", "yes"); if ( !strcasecmp(input, "no") || !strcasecmp(input, "yes") ) break; } if ( !strcasecmp(input, "yes") ) { text("\n"); rmdir(mnt); execlp("sysinstall", "sysinstall", (const char*) NULL); warn("sysinstall"); if ( !mkdtemp(mnt) ) err(2, "mkdtemp: %s", "/tmp/fs.XXXXXX"); text("\n"); } continue; } while ( true ) { for ( size_t i = 0; i < installations_count; i++ ) { struct installation* installation = &installations[i]; printf(" %-16s %s\n", path_of_blockdevice(installation->bdev), installation->release.pretty_name); } text("\n"); const char* def = NULL; if ( installations_count == 1 ) def = path_of_blockdevice(installations[0].bdev); prompt(input, sizeof(input), "Which installation to upgrade?", def); target = NULL; for ( size_t i = 0; i < installations_count; i++ ) { struct installation* installation = &installations[i]; const char* path = path_of_blockdevice(installation->bdev); if ( strcmp(input, path) != 0 ) continue; target = installation; } if ( !target ) { text("Answer was not one of the found devices.\n\n"); continue; } break; } break; } text("\n"); struct release* target_release = &target->release; // TODO: Check if /etc/machine matches the current architecture. // TODO: Some 1.0dev systems don't have /etc/machine, assume it matches if // absent. Remove this compatibility after releasing Sortix 1.0. if ( downgrading_version(target_release, &new_release) ) { text("Warning: You are downgrading an existing installation to an " "earlier release. This is not supported and there is no promise " "this will work!\n\n"); while ( true) { prompt(input, sizeof(input), "Downgrade to an earlier release?", "no"); if ( !strcasecmp(input, "no") || !strcasecmp(input, "yes") ) break; } if ( !strcasecmp(input, "no") ) errx(2, "Upgrade aborted due to version downgrade"); text("\n"); } else if ( skipping_version(target_release, &new_release) ) { text("Warning: You are not upgrading this installation to its next " "release. You cannot skip releases. This is not supported and " "there is no promise this will will work!\n\n"); while ( true ) { prompt(input, sizeof(input), "Skip across releases?", "no"); if ( !strcasecmp(input, "no") || !strcasecmp(input, "yes") ) break; } if ( !strcasecmp(input, "no") ) errx(2, "Upgrade aborted due to skipping releases"); text("\n"); } if ( new_release.abi_major < target_release->abi_major || (target_release->abi_major == new_release.abi_major && new_release.abi_minor < target_release->abi_minor) ) { text("Warning: You are downgrading an existing installation to an " "release with an earlier ABI. This is not supported and there is " "no promise this will work!\n\n"); while ( true) { prompt(input, sizeof(input), "Downgrade to an earlier ABI?", "no"); if ( !strcasecmp(input, "no") || !strcasecmp(input, "yes") ) break; } if ( !strcasecmp(input, "no") ) errx(2, "Upgrade aborted due to ABI downgrade"); text("\n"); } bool can_run_old_abi = target_release->abi_major == new_release.abi_major && target_release->abi_minor <= new_release.abi_minor; struct blockdevice* bdev = target->bdev; const char* bdev_path = path_of_blockdevice(bdev); pid_t fs_pid = begin_mount(mnt, bdev); if ( fs_pid < 0 ) err(2, "mounting %s at %s", bdev_path, mnt); if ( chdir(mnt) < 0 ) err(2, "%s", mnt); if ( access_or_die("sysmerge", F_OK) == 0 ) { text("Warning: A sysmerge(8) upgrade is scheduled for the next boot. " "You must cancel this to proceed.\n\n"); if ( !can_run_old_abi ) { text("Error: Can't pending upgrade due to ABI change.\n"); errx(2, "Upgrade aborted due to pending sysmerge(8) upgrade"); } while ( true ) { prompt(input, sizeof(input), "Cancel pending sysmerge upgrade?", "yes"); if ( !strcasecmp(input, "no") || !strcasecmp(input, "yes") ) break; } if ( !strcasecmp(input, "no") ) errx(2, "Upgrade aborted due to pending sysmerge(8) upgrade"); text("\n"); execute((const char*[]) { "chroot", "-d", "sysmerge", "--cancel", NULL }, "e"); } // TODO: Remove after releasing Sortix 1.0. if ( target_release->version_major == 1 && target_release->version_minor == 0 && target_release->version_dev && access_or_die("etc/upgrade.conf", F_OK) < 0 ) { text("1.0dev compatibility: Creating /etc/upgrade.conf\n"); FILE* fp = fopen("etc/upgrade.conf", "w"); if ( !fp ) err(2, "etc/upgrade.conf"); if ( access_or_die("src", F_OK) == 0 ) fprintf(fp, "src = yes\n"); if ( access_or_die("boot/grub/grub.cfg", F_OK) == 0 ) fprintf(fp, "grub = yes\n"); if ( ferror(fp) || fflush(fp) == EOF ) err(2, "etc/upgrade.conf"); if ( fclose(fp) == EOF ) err(2, "etc/upgrade.conf"); text("\n"); } struct conf conf; while ( true ) { load_upgrade_conf(&conf, "etc/upgrade.conf"); textf("We are now ready to upgrade to %s %s. Take a moment to verify " "everything is sane.\n", BRAND_DISTRIBUTION_NAME, VERSIONSTR); text("\n"); char abibuf[16]; printf(" %-16s system architecture\n", uts.machine); printf(" %-16s root filesystem\n", bdev_path); printf(" %-16s old version\n", target_release->pretty_name); printf(" %-16s new version\n", new_release.pretty_name); snprintf(abibuf, sizeof(abibuf), "%lu.%lu", target_release->abi_major, target_release->abi_minor); printf(" %-16s old ABI\n", abibuf); snprintf(abibuf, sizeof(abibuf), "%lu.%lu", new_release.abi_major, new_release.abi_minor); printf(" %-16s new ABI\n", abibuf); if ( conf.system ) printf(" %-16s will be updated\n", "system"); else printf(" %-16s will not be updated\n", "system"); if ( conf.ports ) printf(" %-16s will be updated\n", "ports"); else printf(" %-16s will not be updated\n", "ports"); if ( has_manifest("src") ) { if ( conf.newsrc ) printf(" %-16s new source code\n", "/newsrc"); else if ( conf.src ) printf(" %-16s will be updated\n", "/src"); else printf(" %-16s will not be updated\n", "/src"); } else printf(" %-16s will not be updated\n", "/src"); text("\n"); prompt(input, sizeof(input), "Upgrade? (yes/no)", "yes"); if ( strcasecmp(input, "yes") != 0 ) { text("Everything isn't sane? Answer '!' to get a shell or type ^C " "to abort the upgrade.\n"); continue; } break; } text("\n"); // TODO: Switch to local time zone of the existing system? text("Upgrading to " BRAND_DISTRIBUTION_NAME " " VERSIONSTR " now:\n"); pid_t upgrade_pid = fork(); if ( upgrade_pid < 0 ) err(2, "fork"); if ( upgrade_pid == 0 ) { umask(0022); // TODO: Use an upgrade manifest system that notices files that are now // untracked or moved from one manifest to another. if ( conf.system ) install_manifest("system", "", "."); if ( has_manifest("src") ) { if ( conf.newsrc ) { bool has_src = access_or_die("src", F_OK) == 0; if ( has_src ) { preserve_src("newsrc"); if ( rename("src", "src.tmp") < 0 ) { warn("rename: /src -> /src.tmp"); _exit(1); } } install_manifest("src", "", "."); if ( has_src ) { if ( rename("src", "newsrc") < 0 ) { warn("rename: /src -> /newsrc"); _exit(1); } if ( rename("src.tmp", "src") < 0 ) { warn("rename: /src.tmp -> /src"); _exit(1); } } } else if ( conf.src ) { preserve_src("src"); install_manifest("src", "", "."); } } if ( conf.ports ) install_ports("", "."); if ( conf.system ) { printf(" - Creating initrd...\n"); execute((const char*[]) { "update-initrd", "--sysroot", mnt, NULL }, "_e"); } if ( (conf.ports || (conf.system && can_run_old_abi)) && conf.grub ) { printf(" - Installing bootloader...\n"); execute((const char*[]) { "chroot", "-d", ".", "grub-install", device_path_of_blockdevice(bdev), NULL }, "_eqQ"); printf(" - Configuring bootloader...\n"); execute((const char*[]) { "chroot", "-d", ".", "update-grub", NULL }, "_eqQ"); } else if ( conf.system && access_or_die("/etc/grub.d/10_sortix", F_OK) == 0 ) { // Help dual booters by making /etc/grub.d/10_sortix.cache. printf(" - Creating bootloader fragment...\n"); execute((const char*[]) { "chroot", "-d", ".", "/etc/grub.d/10_sortix", NULL }, "_eq"); } printf(" - Finishing upgrade...\n"); _exit(0); } int upgrade_code; waitpid(upgrade_pid, &upgrade_code, 0); if ( WIFEXITED(upgrade_code) && WEXITSTATUS(upgrade_code) == 0 ) { } else if ( WIFEXITED(upgrade_code) ) errx(2, "upgrade failed with exit status %i", WEXITSTATUS(upgrade_code)); else if ( WIFSIGNALED(upgrade_code) ) errx(2, "upgrade failed: %s", strsignal(WTERMSIG(upgrade_code))); else errx(2, "upgrade failed: unknown waitpid code %i", upgrade_code); text("\n"); if ( chdir("/") < 0 ) err(2, "%s", "/"); end_mount(mnt, fs_pid); if ( conf.system ) textf("The %s installation has now been upgraded to %s.\n\n", bdev_path, new_release.pretty_name); else if ( conf.newsrc ) textf("The %s installation now contains the new source code in /newsrc. " "You need to build it as described in development(7).\n\n", bdev_path); else if ( conf.src ) textf("The %s installation now contains the new source code in /src. " "You need to build it as described in development(7).\n\n", bdev_path); else textf("The %s installation has been upgraded to %s as requested.\n\n", bdev_path, new_release.pretty_name); if ( target_release->abi_major < new_release.abi_major ) { text("Note: The system has been upgraded across a major ABI change. " "Locally compiled programs must be recompiled as they no longer " "can be expected to work.\n\n"); } else if ( target_release->abi_major == new_release.abi_major && target_release->abi_minor < new_release.abi_minor ) { text("Note: The system has been upgraded across a minor ABI change.\n\n"); } else if ( new_release.abi_major < target_release->abi_major || (target_release->abi_major == new_release.abi_major && new_release.abi_minor < target_release->abi_minor) ) { text("Note: The system has been downgraded to an earlier ABI. " "Locally compiled programs must be recompiled as they no longer " "can be expected to work.\n\n"); } while ( true ) { prompt(input, sizeof(input), "What now? (poweroff/reboot)", "reboot"); if ( !strcasecmp(input, "poweroff") ) return 0; if ( !strcasecmp(input, "reboot") ) return 1; } }