gui: Add a login UI and support the os::IPC::Notifier API
All checks were successful
Build and test / build (push) Successful in 3m2s
All checks were successful
Build and test / build (push) Successful in 3m2s
This commit is contained in:
parent
d3fbddb191
commit
bfb45c7d4a
@ -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.
|
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 images
|
||||||
|
|
||||||
Prebuilt ISO images for every release version can be found at [pub.cloudapio.eu](https://pub.cloudapio.eu/luna/releases).
|
Prebuilt ISO images for every release version can be found at [pub.cloudapio.eu](https://pub.cloudapio.eu/luna/releases).
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
Name=login
|
Name=login
|
||||||
Description=Start a graphical user session.
|
Description=Start a graphical user session.
|
||||||
Command=/usr/bin/startui --user=selene
|
Command=/usr/bin/loginui --autologin=selene
|
||||||
StandardOutput=/dev/uart0
|
StandardOutput=/dev/uart0
|
||||||
StandardError=/dev/uart0
|
StandardError=/dev/uart0
|
||||||
Restart=true
|
Restart=true
|
||||||
|
@ -13,3 +13,5 @@ add_subdirectory(apps)
|
|||||||
|
|
||||||
luna_service(launch.cpp launch)
|
luna_service(launch.cpp launch)
|
||||||
luna_service(run.cpp run)
|
luna_service(run.cpp run)
|
||||||
|
luna_service(loginui.cpp loginui)
|
||||||
|
target_link_libraries(loginui PRIVATE ui)
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <os/ArgumentParser.h>
|
#include <os/ArgumentParser.h>
|
||||||
#include <os/File.h>
|
#include <os/File.h>
|
||||||
|
#include <os/IPC.h>
|
||||||
#include <os/LocalServer.h>
|
#include <os/LocalServer.h>
|
||||||
#include <os/Process.h>
|
#include <os/Process.h>
|
||||||
#include <os/Security.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));
|
auto server = TRY(os::LocalServer::create(socket_path, false));
|
||||||
TRY(server->listen(20));
|
TRY(server->listen(20));
|
||||||
|
|
||||||
|
// We're ready now.
|
||||||
|
os::IPC::notify_parent();
|
||||||
|
|
||||||
Vector<OwnedPtr<os::IPC::ClientConnection>> clients;
|
Vector<OwnedPtr<os::IPC::ClientConnection>> clients;
|
||||||
Vector<struct pollfd> fds;
|
Vector<struct pollfd> fds;
|
||||||
TRY(fds.try_append({ .fd = server->fd(), .events = POLLIN, .revents = 0 }));
|
TRY(fds.try_append({ .fd = server->fd(), .events = POLLIN, .revents = 0 }));
|
||||||
|
171
gui/loginui.cpp
Normal file
171
gui/loginui.cpp
Normal 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();
|
||||||
|
}
|
@ -8,6 +8,7 @@
|
|||||||
#include <moon/Keyboard.h>
|
#include <moon/Keyboard.h>
|
||||||
#include <os/ArgumentParser.h>
|
#include <os/ArgumentParser.h>
|
||||||
#include <os/File.h>
|
#include <os/File.h>
|
||||||
|
#include <os/IPC.h>
|
||||||
#include <os/LocalServer.h>
|
#include <os/LocalServer.h>
|
||||||
#include <os/Process.h>
|
#include <os/Process.h>
|
||||||
#include <os/Security.h>
|
#include <os/Security.h>
|
||||||
@ -115,6 +116,9 @@ Result<int> luna_main(int argc, char** argv)
|
|||||||
close(fd);
|
close(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We're ready now.
|
||||||
|
os::IPC::notify_parent();
|
||||||
|
|
||||||
ui::Color background = ui::Color::from_rgb(0x10, 0x10, 0x10);
|
ui::Color background = ui::Color::from_rgb(0x10, 0x10, 0x10);
|
||||||
|
|
||||||
Vector<OwnedPtr<Client>> clients;
|
Vector<OwnedPtr<Client>> clients;
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
#include <grp.h>
|
#include <grp.h>
|
||||||
#include <os/ArgumentParser.h>
|
#include <os/ArgumentParser.h>
|
||||||
#include <os/File.h>
|
#include <os/File.h>
|
||||||
|
#include <os/FileSystem.h>
|
||||||
|
#include <os/IPC.h>
|
||||||
#include <os/Main.h>
|
#include <os/Main.h>
|
||||||
#include <os/Process.h>
|
#include <os/Process.h>
|
||||||
#include <pwd.h>
|
#include <pwd.h>
|
||||||
@ -18,19 +20,6 @@
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <unistd.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)
|
Result<Vector<gid_t>> read_supplementary_groups(const char* name)
|
||||||
{
|
{
|
||||||
Vector<gid_t> extra_groups;
|
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)
|
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;
|
StringView username;
|
||||||
|
|
||||||
os::ArgumentParser parser;
|
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.add_value_argument(username, 'u', "user", "the user to start the UI session as");
|
||||||
parser.parse(argc, argv);
|
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())
|
if (username.is_empty())
|
||||||
{
|
{
|
||||||
os::eprintln("error: startui needs a --user parameter to run.");
|
os::eprintln("error: startui needs a --user parameter to run.");
|
||||||
@ -105,9 +94,22 @@ Result<int> luna_main(int argc, char** argv)
|
|||||||
|
|
||||||
setsid();
|
setsid();
|
||||||
|
|
||||||
// First of all, start the display server.
|
// First of all, start the display server, in case we haven't been started by loginui.
|
||||||
StringView wind_command[] = { "/usr/bin/wind" };
|
if (!os::FileSystem::exists("/tmp/wind.sock"))
|
||||||
TRY(os::Process::spawn(wind_command[0], Slice<StringView>(wind_command, 1)));
|
{
|
||||||
|
// 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<StringView>(wind_command, 1));
|
||||||
|
},
|
||||||
|
1000);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
os::eprintln("startui: failed to start wind, timed out");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
clearenv();
|
clearenv();
|
||||||
chdir(pw->pw_dir);
|
chdir(pw->pw_dir);
|
||||||
@ -116,18 +118,22 @@ Result<int> luna_main(int argc, char** argv)
|
|||||||
setenv("HOME", pw->pw_dir, 1);
|
setenv("HOME", pw->pw_dir, 1);
|
||||||
setenv("SHELL", pw->pw_shell, 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.
|
||||||
StringView launch_command[] = { "/usr/bin/launch" };
|
bool success = os::IPC::Notifier::run_and_wait(
|
||||||
TRY(spawn_process_as_user(Slice<StringView>(launch_command, 1), pw->pw_uid, pw->pw_gid, groups.slice()));
|
[&] {
|
||||||
|
StringView launch_command[] = { "/usr/bin/launch" };
|
||||||
TRY(wait_until_file_created("/tmp/wsys.sock", 10000));
|
spawn_process_as_user(Slice<StringView>(launch_command, 1), pw->pw_uid, pw->pw_gid, groups.slice());
|
||||||
TRY(wait_until_file_created("/tmp/launch.sock", 10000));
|
},
|
||||||
|
1000);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
os::eprintln("startui: failed to start launch server, timed out");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
StringView taskbar_command[] = { "/usr/bin/taskbar" };
|
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(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.
|
// Finally, start init in user mode to manage all configured optional services.
|
||||||
StringView init_command[] = { "/usr/bin/init", "--user" };
|
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()));
|
TRY(spawn_process_as_user(Slice<StringView>(init_command, 2), pw->pw_uid, pw->pw_gid, groups.slice()));
|
||||||
|
Loading…
Reference in New Issue
Block a user