gui: Add a login UI and support the os::IPC::Notifier API
All checks were successful
Build and test / build (push) Successful in 3m2s

This commit is contained in:
apio 2024-07-31 19:50:20 +02:00
parent d3fbddb191
commit bfb45c7d4a
Signed by: apio
GPG Key ID: B8A7D06E42258954
7 changed files with 224 additions and 31 deletions

View File

@ -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).

View File

@ -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

View File

@ -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)

View File

@ -10,6 +10,7 @@
#include <errno.h>
#include <os/ArgumentParser.h>
#include <os/File.h>
#include <os/IPC.h>
#include <os/LocalServer.h>
#include <os/Process.h>
#include <os/Security.h>
@ -66,6 +67,9 @@ Result<int> 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<OwnedPtr<os::IPC::ClientConnection>> clients;
Vector<struct pollfd> fds;
TRY(fds.try_append({ .fd = server->fd(), .events = POLLIN, .revents = 0 }));

171
gui/loginui.cpp Normal file
View File

@ -0,0 +1,171 @@
/**
* @file loginui.cpp
* @author apio (cloudapio.eu)
* @brief Graphical login prompt.
*
* @copyright Copyright (c) 2024, the Luna authors.
*
*/
#include <luna/String.h>
#include <os/ArgumentParser.h>
#include <os/File.h>
#include <os/FileSystem.h>
#include <os/IPC.h>
#include <os/Process.h>
#include <pwd.h>
#include <shadow.h>
#include <sys/stat.h>
#include <ui/App.h>
#include <ui/Button.h>
#include <ui/InputField.h>
#include <ui/Label.h>
#include <ui/Layout.h>
#include <unistd.h>
enum Stage
{
UsernameInput,
PasswordInput,
};
static constexpr ui::Color BACKGROUND_COLOR = ui::Color::from_rgb(89, 89, 89);
Result<int> 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<StringView>(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<StringView>(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<StringView>(startui_command, 2));
unreachable();
}
});
return app.run();
}

View File

@ -8,6 +8,7 @@
#include <moon/Keyboard.h>
#include <os/ArgumentParser.h>
#include <os/File.h>
#include <os/IPC.h>
#include <os/LocalServer.h>
#include <os/Process.h>
#include <os/Security.h>
@ -115,6 +116,9 @@ Result<int> 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<OwnedPtr<Client>> clients;

View File

@ -11,6 +11,8 @@
#include <grp.h>
#include <os/ArgumentParser.h>
#include <os/File.h>
#include <os/FileSystem.h>
#include <os/IPC.h>
#include <os/Main.h>
#include <os/Process.h>
#include <pwd.h>
@ -18,19 +20,6 @@
#include <sys/stat.h>
#include <unistd.h>
// Timeout should be provided in milliseconds.
Result<void> 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<Vector<gid_t>> read_supplementary_groups(const char* name)
{
Vector<gid_t> extra_groups;
@ -70,12 +59,6 @@ Result<void> spawn_process_as_user(Slice<StringView> arguments, uid_t user, gid_
Result<int> 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<int> 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<int> luna_main(int argc, char** argv)
setsid();
// First of all, start the display server.
// 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" };
TRY(os::Process::spawn(wind_command[0], Slice<StringView>(wind_command, 1)));
os::Process::spawn(wind_command[0], Slice<StringView>(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<int> luna_main(int argc, char** argv)
setenv("HOME", pw->pw_dir, 1);
setenv("SHELL", pw->pw_shell, 1);
// Next, start the required UI components.
// 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" };
TRY(spawn_process_as_user(Slice<StringView>(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));
spawn_process_as_user(Slice<StringView>(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<StringView>(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<StringView>(init_command, 2), pw->pw_uid, pw->pw_gid, groups.slice()));