/** * @file startui.cpp * @author apio (cloudapio.eu) * @brief Service to start and manage a UI session. * * @copyright Copyright (c) 2024, the Luna authors. * */ #include #include #include #include #include #include #include #include #include #include // Timeout should be provided in milliseconds. Result wait_until_file_created(StringView path, int timeout) { struct stat st; while (stat(path.chars(), &st) < 0) { if (timeout <= 0) return err(ETIMEDOUT); usleep(100000); // FIXME: Implement something like inotify or use signals to avoid polling. timeout -= 100; } return {}; } Result> read_supplementary_groups(const char* name) { Vector extra_groups; setgrent(); group* grp; while ((grp = getgrent())) { for (char** user = grp->gr_mem; *user; user++) { if (!strcmp(*user, name)) { TRY(extra_groups.try_append(grp->gr_gid)); break; } } } endgrent(); return extra_groups; } Result spawn_process_as_user(Slice arguments, uid_t user, gid_t group, Slice supplementary_groups) { pid_t child = TRY(os::Process::fork()); if (child == 0) { if (setgroups(static_cast(supplementary_groups.size()), supplementary_groups.data()) < 0) return err(errno); setgid(group); setuid(user); TRY(os::Process::exec(arguments[0], arguments, false)); } return {}; } Result luna_main(int argc, char** argv) { if (geteuid() != 0) { os::eprintln("error: %s can only be started as root.", argv[0]); return 1; } StringView username; os::ArgumentParser parser; parser.add_description("Service to start and manage a UI session."); parser.add_system_program_info("startui"_sv); parser.add_value_argument(username, 'u', "user", "the user to start the UI session as"); parser.parse(argc, argv); if (username.is_empty()) { os::eprintln("error: startui needs a --user parameter to run."); parser.short_usage(argv[0]); return 1; } struct passwd* pw = getpwnam(username.chars()); if (!pw) { os::eprintln("error: user %s not found.", username.chars()); return 1; } auto groups = TRY(read_supplementary_groups(username.chars())); auto system_groups = TRY(groups.shallow_copy()); TRY(system_groups.try_append(3)); setsid(); // First of all, start the display server. StringView wind_command[] = { "/usr/bin/wind" }; TRY(os::Process::spawn(wind_command[0], Slice(wind_command, 1))); TRY(wait_until_file_created("/tmp/wsys.sock", 10000)); clearenv(); chdir(pw->pw_dir); setenv("PATH", "/usr/bin:/usr/local/bin", 1); setenv("USER", pw->pw_name, 1); setenv("HOME", pw->pw_dir, 1); setenv("SHELL", pw->pw_shell, 1); // Next, start the required UI components. StringView launch_command[] = { "/usr/bin/launch" }; TRY(spawn_process_as_user(Slice(launch_command, 1), pw->pw_uid, pw->pw_gid, groups.slice())); StringView taskbar_command[] = { "/usr/bin/taskbar" }; TRY(spawn_process_as_user(Slice(taskbar_command, 1), pw->pw_uid, pw->pw_gid, system_groups.slice())); TRY(wait_until_file_created("/tmp/wind.sock", 10000)); // Finally, start init in user mode to manage all configured optional services. StringView init_command[] = { "/usr/bin/init", "--user" }; TRY(spawn_process_as_user(Slice(init_command, 2), pw->pw_uid, pw->pw_gid, groups.slice())); // If any process exits, it's either the display server, one of the required UI components, or init --user. We care // about all of these, so let's just crash the entire UI session if they exit. In the future we might restart some // of them and continue, but for now we'll just exit on all of them. os::Process::wait(os::Process::ANY_CHILD, nullptr); return 0; }