From bfb45c7d4ab7602df265604c2652c35166f865a6 Mon Sep 17 00:00:00 2001 From: apio Date: Wed, 31 Jul 2024 19:50:20 +0200 Subject: [PATCH] gui: Add a login UI and support the os::IPC::Notifier API --- README.md | 6 ++ base/etc/init/99-login | 2 +- gui/CMakeLists.txt | 2 + gui/launch.cpp | 4 + gui/loginui.cpp | 171 +++++++++++++++++++++++++++++++++++++++++ gui/wind/main.cpp | 4 + system/startui.cpp | 66 ++++++++-------- 7 files changed, 224 insertions(+), 31 deletions(-) create mode 100644 gui/loginui.cpp diff --git a/README.md b/README.md index c86452eb..f2b35511 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,12 @@ Additionally, the build process needs some extra dependencies to run: `cmake`, ` If you have no toolchain set up, `run.sh` will build it automatically, which means that you don't necessarily have to run `setup.sh` manually since `run.sh` does it for you. +## Login UI + +For development convenience, the system automatically starts a GUI session as the default user, without prompting for a password. + +Despite this, Luna does have a login window built-in. If you'd like to try this feature out or start a GUI session as a different user, you'll need to edit [base/etc/init/99-login](base/etc/init/99-login) and change the line that says `Command=/usr/bin/loginui --autologin=selene` to `Command=/usr/bin/loginui`. + ## Prebuilt images Prebuilt ISO images for every release version can be found at [pub.cloudapio.eu](https://pub.cloudapio.eu/luna/releases). diff --git a/base/etc/init/99-login b/base/etc/init/99-login index c6878d7f..227367be 100644 --- a/base/etc/init/99-login +++ b/base/etc/init/99-login @@ -1,6 +1,6 @@ Name=login Description=Start a graphical user session. -Command=/usr/bin/startui --user=selene +Command=/usr/bin/loginui --autologin=selene StandardOutput=/dev/uart0 StandardError=/dev/uart0 Restart=true diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 3340e4e8..3578929d 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -13,3 +13,5 @@ add_subdirectory(apps) luna_service(launch.cpp launch) luna_service(run.cpp run) +luna_service(loginui.cpp loginui) +target_link_libraries(loginui PRIVATE ui) diff --git a/gui/launch.cpp b/gui/launch.cpp index f409953a..981b3222 100644 --- a/gui/launch.cpp +++ b/gui/launch.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -66,6 +67,9 @@ Result luna_main(int argc, char** argv) auto server = TRY(os::LocalServer::create(socket_path, false)); TRY(server->listen(20)); + // We're ready now. + os::IPC::notify_parent(); + Vector> clients; Vector fds; TRY(fds.try_append({ .fd = server->fd(), .events = POLLIN, .revents = 0 })); diff --git a/gui/loginui.cpp b/gui/loginui.cpp new file mode 100644 index 00000000..81f17dc1 --- /dev/null +++ b/gui/loginui.cpp @@ -0,0 +1,171 @@ +/** + * @file loginui.cpp + * @author apio (cloudapio.eu) + * @brief Graphical login prompt. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum Stage +{ + UsernameInput, + PasswordInput, +}; + +static constexpr ui::Color BACKGROUND_COLOR = ui::Color::from_rgb(89, 89, 89); + +Result luna_main(int argc, char** argv) +{ + StringView username; + + os::ArgumentParser parser; + parser.add_description("Login prompt for a graphical UI session."); + parser.add_system_program_info("loginui"_sv); + // FIXME: Make this a config option instead of a switch. + // Also, calling "loginui --autologin=user" is functionally identical to calling "startui --user=user", the only + // difference is that it makes the init config easier to change (only adding or removing the autologin flag, instead + // of changing the program to use) + parser.add_value_argument(username, ' ', "autologin", "login as a specific user without prompting"); + parser.parse(argc, argv); + + if (geteuid() != 0) + { + os::eprintln("error: %s can only be started as root.", argv[0]); + return 1; + } + + setsid(); + + bool success = os::IPC::Notifier::run_and_wait( + [&] { + StringView wind_command[] = { "/usr/bin/wind" }; + os::Process::spawn(wind_command[0], Slice(wind_command, 1)); + }, + 1000); + + if (!success) + { + os::eprintln("loginui: failed to start wind, timed out"); + return 1; + } + + if (!username.is_empty()) + { + auto flag = String::format("--user=%s"_sv, username.chars()).release_value(); + + StringView startui_command[] = { "/usr/bin/startui", flag.view() }; + os::Process::exec(startui_command[0], Slice(startui_command, 2)); + unreachable(); + } + + ui::App app; + TRY(app.init()); + + auto* window = TRY(ui::Window::create(ui::Rect { 300, 300, 400, 300 })); + app.set_main_window(window); + + window->set_title("Log in"); + window->set_background(BACKGROUND_COLOR); + + ui::VerticalLayout main_layout; + window->set_main_widget(main_layout); + + ui::Label label("Username:"); + main_layout.add_widget(label); + + ui::InputField input(ui::Font::default_font()); + main_layout.add_widget(input); + + ui::Label error(""); + error.set_font(ui::Font::default_bold_font()); + error.set_color(ui::RED); + main_layout.add_widget(error); + + Stage stage = Stage::UsernameInput; + struct passwd* pw; + + input.on_submit([&](StringView data) { + error.set_text(""); + if (stage == Stage::UsernameInput) + { + struct passwd* entry = getpwnam(data.chars()); + if (!entry) + { + error.set_text("User not found."); + input.clear(); + return; + } + + pw = entry; + stage = Stage::PasswordInput; + label.set_text("Password:"); + + String title = String::format("Log in: %s"_sv, data.chars()).release_value(); + window->set_title(title.view()); + + input.clear(); + + return; + } + else + { + const char* passwd = pw->pw_passwd; + + // If the user's password entry is 'x', read their password from the shadow file instead. + if (!strcmp(pw->pw_passwd, "x")) + { + struct spwd* sp = getspnam(pw->pw_name); + + if (!sp) + { + error.set_text("User not found in shadow file."); + input.clear(); + return; + } + + endspent(); + + passwd = sp->sp_pwdp; + } + + if (!strcmp(passwd, "!")) + { + error.set_text("User's password is disabled."); + input.clear(); + return; + } + + if (strcmp(data.chars(), passwd)) + { + error.set_text("Incorrect password."); + input.clear(); + return; + } + + auto flag = String::format("--user=%s"_sv, pw->pw_name).release_value(); + + StringView startui_command[] = { "/usr/bin/startui", flag.view() }; + os::Process::exec(startui_command[0], Slice(startui_command, 2)); + unreachable(); + } + }); + + return app.run(); +} diff --git a/gui/wind/main.cpp b/gui/wind/main.cpp index 37823118..f00263da 100644 --- a/gui/wind/main.cpp +++ b/gui/wind/main.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -115,6 +116,9 @@ Result luna_main(int argc, char** argv) close(fd); } + // We're ready now. + os::IPC::notify_parent(); + ui::Color background = ui::Color::from_rgb(0x10, 0x10, 0x10); Vector> clients; diff --git a/system/startui.cpp b/system/startui.cpp index e0494a5f..65bd4ca3 100644 --- a/system/startui.cpp +++ b/system/startui.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include #include @@ -18,19 +20,6 @@ #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; @@ -70,12 +59,6 @@ Result spawn_process_as_user(Slice arguments, uid_t user, gid_ 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; @@ -84,6 +67,12 @@ Result luna_main(int argc, char** argv) parser.add_value_argument(username, 'u', "user", "the user to start the UI session as"); parser.parse(argc, argv); + if (geteuid() != 0) + { + os::eprintln("error: %s can only be started as root.", argv[0]); + return 1; + } + if (username.is_empty()) { os::eprintln("error: startui needs a --user parameter to run."); @@ -105,9 +94,22 @@ Result luna_main(int argc, char** argv) 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))); + // First of all, start the display server, in case we haven't been started by loginui. + if (!os::FileSystem::exists("/tmp/wind.sock")) + { + // We need to wait for this one, since its sockets are required later. + bool success = os::IPC::Notifier::run_and_wait( + [&] { + StringView wind_command[] = { "/usr/bin/wind" }; + os::Process::spawn(wind_command[0], Slice(wind_command, 1)); + }, + 1000); + if (!success) + { + os::eprintln("startui: failed to start wind, timed out"); + return 1; + } + } clearenv(); chdir(pw->pw_dir); @@ -116,18 +118,22 @@ Result luna_main(int argc, char** argv) 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())); - - TRY(wait_until_file_created("/tmp/wsys.sock", 10000)); - TRY(wait_until_file_created("/tmp/launch.sock", 10000)); + // We also need to wait for this one, since taskbar requires launch.sock. + bool success = os::IPC::Notifier::run_and_wait( + [&] { + StringView launch_command[] = { "/usr/bin/launch" }; + spawn_process_as_user(Slice(launch_command, 1), pw->pw_uid, pw->pw_gid, groups.slice()); + }, + 1000); + if (!success) + { + os::eprintln("startui: failed to start launch server, timed out"); + return 1; + } 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()));