Compare commits

...

5 Commits

Author SHA1 Message Date
bfb45c7d4a
gui: Add a login UI and support the os::IPC::Notifier API
All checks were successful
Build and test / build (push) Successful in 3m2s
2024-07-31 19:50:20 +02:00
d3fbddb191
taskbar: Use SIGQUIT to restart
Whenever a GUI session ends, SIGHUP is sent to all GUI processes. Previously, taskbar would catch this signal and wrongly restart.

Now, taskbar correctly dies alongside everyone else.
2024-07-31 19:46:02 +02:00
0ab8efd405
libos+init: Add a Notifier API to know when child processes have finished initialization
init now supports the "WaitUntilReady" key, which will wait until the child calls os::IPC::notify_parent().
2024-07-31 19:43:09 +02:00
2aefbdc4ee
libos: Fix Action 2024-07-31 19:32:47 +02:00
15dc71e8e1
libui/InputField: Fix a few bugs and add a clear() method 2024-07-31 19:32:22 +02:00
14 changed files with 404 additions and 38 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. 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).

View File

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

View File

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

View File

@ -23,7 +23,7 @@ void sigchld_handler(int)
wait(nullptr); wait(nullptr);
} }
void sighup_handler(int) void sigquit_handler(int)
{ {
// Reload the taskbar by exec-ing the executable, resetting everything. // Reload the taskbar by exec-ing the executable, resetting everything.
StringView args[] = { "/usr/bin/taskbar" }; StringView args[] = { "/usr/bin/taskbar" };
@ -151,7 +151,7 @@ Result<int> luna_main(int, char**)
TRY(app.init("/tmp/wsys.sock")); TRY(app.init("/tmp/wsys.sock"));
TRY(os::EventLoop::the().register_signal_handler(SIGCHLD, sigchld_handler)); TRY(os::EventLoop::the().register_signal_handler(SIGCHLD, sigchld_handler));
TRY(os::EventLoop::the().register_signal_handler(SIGHUP, sighup_handler)); TRY(os::EventLoop::the().register_signal_handler(SIGQUIT, sigquit_handler));
launcher_client = TRY(os::IPC::Client::connect("/tmp/launch.sock", false)); launcher_client = TRY(os::IPC::Client::connect("/tmp/launch.sock", false));

View File

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

View File

@ -23,6 +23,8 @@ namespace ui
Result<void> draw(ui::Canvas& canvas) override; Result<void> draw(ui::Canvas& canvas) override;
void clear();
StringView data(); StringView data();
void on_submit(os::Function<StringView>&& action) void on_submit(os::Function<StringView>&& action)

View File

@ -17,6 +17,8 @@ namespace ui
{ {
InputField::InputField(SharedPtr<ui::Font> font) : ui::TextInput(), m_font(font) InputField::InputField(SharedPtr<ui::Font> font) : ui::TextInput(), m_font(font)
{ {
u8 zero = 0;
m_data.append_data(&zero, 1);
} }
Result<ui::EventResult> InputField::handle_key_event(const ui::KeyEventRequest& request) Result<ui::EventResult> InputField::handle_key_event(const ui::KeyEventRequest& request)
@ -35,7 +37,7 @@ namespace ui
if (request.code == moon::K_RightArrow) if (request.code == moon::K_RightArrow)
{ {
if (m_cursor < m_data.size()) m_cursor++; if (m_cursor < (m_data.size() - 1)) m_cursor++;
else else
return ui::EventResult::DidNotHandle; return ui::EventResult::DidNotHandle;
update_cursor(); update_cursor();
@ -67,9 +69,7 @@ namespace ui
if (iscntrl(request.letter)) return ui::EventResult::DidNotHandle; if (iscntrl(request.letter)) return ui::EventResult::DidNotHandle;
if (m_cursor == m_data.size()) TRY(m_data.append_data((const u8*)&request.letter, 1)); TRY(insert_character(request.letter));
else
TRY(insert_character(request.letter));
m_cursor++; m_cursor++;
@ -78,6 +78,14 @@ namespace ui
return ui::EventResult::DidHandle; return ui::EventResult::DidHandle;
} }
void InputField::clear()
{
m_data.try_resize(0);
u8 zero = 0;
m_data.append_data(&zero, 1);
m_cursor = 0;
}
Result<void> InputField::draw(ui::Canvas& canvas) Result<void> InputField::draw(ui::Canvas& canvas)
{ {
int visible_characters = canvas.width / m_font->width(); int visible_characters = canvas.width / m_font->width();
@ -107,6 +115,7 @@ namespace ui
StringView InputField::data() StringView InputField::data()
{ {
if (!m_data.size()) return StringView {};
return StringView { (const char*)m_data.data(), m_data.size() }; return StringView { (const char*)m_data.data(), m_data.size() };
} }
} }

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

View File

@ -107,7 +107,7 @@ namespace os
void call() void call()
{ {
return m_function(); m_function();
} }
private: private:

View File

@ -320,5 +320,78 @@ namespace os
bool m_ipc_in_progress { false }; bool m_ipc_in_progress { false };
u8 m_ipc_saved_id { 0 }; u8 m_ipc_saved_id { 0 };
}; };
/**
* @brief API used to notify a parent process when a child process finishes initialization. The Notifier struct
* is the parent part of the API.
*
*/
struct Notifier
{
int pfds[2];
/**
* @brief Create a new Notifier.
*
* This function will create a pipe for the parent and child to communicate.
*
* @return Notifier The new Notifier object.
*/
static Notifier create();
/**
* @brief Hook the Notifier into any child process started afterwards.
*
* This will set an environment variable, which if detected by a child process, will use it to notify the
* parent whenever it's ready.
*
* The recommended order to call this API is:
* hook()
* fork+exec
* unhook()
*/
void hook();
/**
* @brief Remove the previously created environment variable, so that any future child processes will not
* notify this Notifier.
*
*/
void unhook();
/**
* @brief Wait for a child process to be ready. If several child processes are hooked by the hook() method,
* this method will only catch the first one that notifies the parent.
*
* @param timeout If positive, specifies the timeout after which the function fails if no notification is
* received.
* @return true The child is ready.
* @return false The method timed out.
*/
bool wait(int timeout = -1);
/**
* @brief Combines hook(), unhook() and wait() into one single method. This method takes a function, and
* executes it in a "hooked" context, so that any child process started by this function will automatically
* detect the parent when it's ready (if it supports the notification API). Then, the function waits for the
* child to be ready.
*
* @param action The function to run.
* @param timeout If positive, specifies the timeout after which the function fails if no notification is
* received.
* @return true The child is ready.
* @return false The method timed out.
*/
static bool run_and_wait(os::Action&& action, int timeout = -1);
};
/**
* @brief Use this function to notify a parent process whenever your program finishes initialization.
*
* If the parent has not used the Notifier API to request a notification from the child process, this function
* does nothing.
*
*/
void notify_parent();
} }
} }

View File

@ -7,7 +7,13 @@
* *
*/ */
#include <errno.h>
#include <fcntl.h>
#include <luna/String.h>
#include <os/IPC.h> #include <os/IPC.h>
#include <stdlib.h>
#include <sys/poll.h>
#include <unistd.h>
namespace os::IPC namespace os::IPC
{ {
@ -84,4 +90,60 @@ namespace os::IPC
Client::Client(OwnedPtr<LocalClient>&& connection) : m_connection(move(connection)) Client::Client(OwnedPtr<LocalClient>&& connection) : m_connection(move(connection))
{ {
} }
Notifier Notifier::create()
{
Notifier result;
pipe(result.pfds);
fcntl(result.pfds[0], F_SETFD, FD_CLOEXEC);
return result;
}
void Notifier::hook()
{
auto value = String::format("%d"_sv, pfds[1]).release_value();
setenv("LUNA_NOTIFY_FD", value.chars(), 1);
}
void Notifier::unhook()
{
unsetenv("LUNA_NOTIFY_FD");
}
bool Notifier::wait(int timeout)
{
close(pfds[1]);
struct pollfd fds[] = { { .fd = pfds[0], .events = POLLIN, .revents = 0 } };
poll:
int result = poll(fds, 1, timeout);
if (result < 0 && errno == EINTR) goto poll;
close(pfds[0]);
return result > 0;
}
bool Notifier::run_and_wait(os::Action&& action, int timeout)
{
auto notifier = create();
notifier.hook();
action();
notifier.unhook();
return notifier.wait(timeout);
}
void notify_parent()
{
char* fd_string = getenv("LUNA_NOTIFY_FD");
if (!fd_string) return;
int fd = atoi(fd_string);
u8 data = 0;
write(fd, &data, 1);
close(fd);
unsetenv("LUNA_NOTIFY_FD");
}
} }

View File

@ -8,6 +8,7 @@
#include <os/ArgumentParser.h> #include <os/ArgumentParser.h>
#include <os/Directory.h> #include <os/Directory.h>
#include <os/File.h> #include <os/File.h>
#include <os/IPC.h>
#include <os/Process.h> #include <os/Process.h>
#include <os/Security.h> #include <os/Security.h>
#include <pwd.h> #include <pwd.h>
@ -38,6 +39,7 @@ struct Service
Option<uid_t> user {}; Option<uid_t> user {};
Option<gid_t> group {}; Option<gid_t> group {};
bool wait { false }; bool wait { false };
bool wait_notify { false };
Option<pid_t> pid {}; Option<pid_t> pid {};
}; };
@ -132,9 +134,13 @@ static Result<void> try_start_service(Service& service)
new_stdin->set_close_on_exec(); new_stdin->set_close_on_exec();
} }
os::IPC::Notifier notifier;
if (service.wait_notify) { notifier = os::IPC::Notifier::create(); }
pid_t pid = TRY(os::Process::fork()); pid_t pid = TRY(os::Process::fork());
if (pid == 0) if (pid == 0)
{ {
if (service.wait_notify) { notifier.hook(); }
auto rc = service_child(service, new_stdout, new_stderr, new_stdin); auto rc = service_child(service, new_stdout, new_stderr, new_stdin);
if (rc.has_error()) if (rc.has_error())
{ {
@ -157,7 +163,17 @@ static Result<void> try_start_service(Service& service)
do_log("[init] child process %d exited with code %d\n", pid, WEXITSTATUS(status)); do_log("[init] child process %d exited with code %d\n", pid, WEXITSTATUS(status));
} }
else else
{
if (service.wait_notify)
{
bool success = notifier.wait(1000);
if (!success)
{
do_log("[init] service %s with pid %d failed to start successfully\n", service.name.chars(), pid);
}
}
service.pid = pid; service.pid = pid;
}
return {}; return {};
} }
@ -273,6 +289,17 @@ static Result<void> load_service(const os::Path& path)
continue; continue;
} }
if (parts[0].view() == "WaitUntilReady")
{
if (parts[1].view() == "true" || parts[1].view().to_uint().value_or(0) == 1)
{
service.wait_notify = true;
continue;
}
service.wait_notify = false;
continue;
}
do_log("[init] skipping unknown entry name %s\n", parts[0].chars()); do_log("[init] skipping unknown entry name %s\n", parts[0].chars());
} }

View File

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