mirror of
https://github.com/fergalmoran/ladybird.git
synced 2026-05-22 19:19:10 +00:00
This commit gets rid of hard coded file handlers in Launcher.cpp in favor of using values in the LaunchServer.ini config file. The previous commit adds checks for the existence of handler programs while registering handlers. This commit takes advantage of that and ensures that LaunchServer will not attempt to open a file with a nonexistent program and can properly report failure before spawning a new child process. Resolves #8120
337 lines
10 KiB
C++
337 lines
10 KiB
C++
/*
|
|
* Copyright (c) 2020, Nicholas Hollett <niax@niax.co.uk>, Andreas Kling <kling@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "Launcher.h"
|
|
#include <AK/Function.h>
|
|
#include <AK/JsonObject.h>
|
|
#include <AK/JsonObjectSerializer.h>
|
|
#include <AK/JsonValue.h>
|
|
#include <AK/LexicalPath.h>
|
|
#include <AK/StringBuilder.h>
|
|
#include <LibCore/ConfigFile.h>
|
|
#include <LibDesktop/AppFile.h>
|
|
#include <errno.h>
|
|
#include <serenity.h>
|
|
#include <spawn.h>
|
|
#include <stdio.h>
|
|
#include <sys/stat.h>
|
|
|
|
namespace LaunchServer {
|
|
|
|
static Launcher* s_the;
|
|
static bool spawn(String executable, const Vector<String>& arguments);
|
|
|
|
String Handler::name_from_executable(const StringView& executable)
|
|
{
|
|
auto separator = executable.find_last_of('/');
|
|
if (separator.has_value()) {
|
|
auto start = separator.value() + 1;
|
|
return executable.substring_view(start, executable.length() - start);
|
|
}
|
|
return executable;
|
|
}
|
|
|
|
void Handler::from_executable(Type handler_type, const String& executable)
|
|
{
|
|
this->handler_type = handler_type;
|
|
this->name = name_from_executable(executable);
|
|
this->executable = executable;
|
|
}
|
|
|
|
String Handler::to_details_str() const
|
|
{
|
|
StringBuilder builder;
|
|
JsonObjectSerializer obj { builder };
|
|
obj.add("executable", executable);
|
|
obj.add("name", name);
|
|
switch (handler_type) {
|
|
case Type::Application:
|
|
obj.add("type", "app");
|
|
break;
|
|
case Type::UserDefault:
|
|
obj.add("type", "userdefault");
|
|
break;
|
|
case Type::UserPreferred:
|
|
obj.add("type", "userpreferred");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
obj.finish();
|
|
return builder.build();
|
|
}
|
|
|
|
Launcher::Launcher()
|
|
{
|
|
VERIFY(s_the == nullptr);
|
|
s_the = this;
|
|
}
|
|
|
|
Launcher& Launcher::the()
|
|
{
|
|
VERIFY(s_the);
|
|
return *s_the;
|
|
}
|
|
|
|
void Launcher::load_handlers(const String& af_dir)
|
|
{
|
|
Desktop::AppFile::for_each([&](auto af) {
|
|
auto app_name = af->name();
|
|
auto app_executable = af->executable();
|
|
HashTable<String> file_types;
|
|
for (auto& file_type : af->launcher_file_types())
|
|
file_types.set(file_type);
|
|
HashTable<String> protocols;
|
|
for (auto& protocol : af->launcher_protocols())
|
|
protocols.set(protocol);
|
|
if (access(app_executable.characters(), X_OK) == 0)
|
|
m_handlers.set(app_executable, { Handler::Type::Default, app_name, app_executable, file_types, protocols });
|
|
},
|
|
af_dir);
|
|
}
|
|
|
|
void Launcher::load_config(const Core::ConfigFile& cfg)
|
|
{
|
|
for (auto key : cfg.keys("FileType")) {
|
|
auto handler = cfg.read_entry("FileType", key).trim_whitespace();
|
|
if (handler.is_empty())
|
|
continue;
|
|
if (access(handler.characters(), X_OK) != 0)
|
|
continue;
|
|
m_file_handlers.set(key.to_lowercase(), handler);
|
|
}
|
|
|
|
for (auto key : cfg.keys("Protocol")) {
|
|
auto handler = cfg.read_entry("Protocol", key).trim_whitespace();
|
|
if (handler.is_empty())
|
|
continue;
|
|
if (access(handler.characters(), X_OK) != 0)
|
|
continue;
|
|
m_protocol_handlers.set(key.to_lowercase(), handler);
|
|
}
|
|
}
|
|
|
|
Vector<String> Launcher::handlers_for_url(const URL& url)
|
|
{
|
|
Vector<String> handlers;
|
|
if (url.protocol() == "file") {
|
|
for_each_handler_for_path(url.path(), [&](auto& handler) -> bool {
|
|
handlers.append(handler.executable);
|
|
return true;
|
|
});
|
|
} else {
|
|
for_each_handler(url.protocol(), m_protocol_handlers, [&](const auto& handler) -> bool {
|
|
if (handler.handler_type != Handler::Type::Default || handler.protocols.contains(url.protocol())) {
|
|
handlers.append(handler.executable);
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
return handlers;
|
|
}
|
|
|
|
Vector<String> Launcher::handlers_with_details_for_url(const URL& url)
|
|
{
|
|
Vector<String> handlers;
|
|
if (url.protocol() == "file") {
|
|
for_each_handler_for_path(url.path(), [&](auto& handler) -> bool {
|
|
handlers.append(handler.to_details_str());
|
|
return true;
|
|
});
|
|
} else {
|
|
for_each_handler(url.protocol(), m_protocol_handlers, [&](const auto& handler) -> bool {
|
|
if (handler.handler_type != Handler::Type::Default || handler.protocols.contains(url.protocol())) {
|
|
handlers.append(handler.to_details_str());
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
return handlers;
|
|
}
|
|
|
|
bool Launcher::open_url(const URL& url, const String& handler_name)
|
|
{
|
|
if (!handler_name.is_null())
|
|
return open_with_handler_name(url, handler_name);
|
|
|
|
if (url.protocol() == "file")
|
|
return open_file_url(url);
|
|
|
|
return open_with_user_preferences(m_protocol_handlers, url.protocol(), { url.to_string() });
|
|
}
|
|
|
|
bool Launcher::open_with_handler_name(const URL& url, const String& handler_name)
|
|
{
|
|
auto handler_optional = m_handlers.get(handler_name);
|
|
if (!handler_optional.has_value())
|
|
return false;
|
|
|
|
auto& handler = handler_optional.value();
|
|
String argument;
|
|
if (url.protocol() == "file")
|
|
argument = url.path();
|
|
else
|
|
argument = url.to_string();
|
|
return spawn(handler.executable, { argument });
|
|
}
|
|
|
|
bool spawn(String executable, const Vector<String>& arguments)
|
|
{
|
|
Vector<const char*> argv { executable.characters() };
|
|
for (auto& arg : arguments)
|
|
argv.append(arg.characters());
|
|
argv.append(nullptr);
|
|
|
|
pid_t child_pid;
|
|
if ((errno = posix_spawn(&child_pid, executable.characters(), nullptr, nullptr, const_cast<char**>(argv.data()), environ))) {
|
|
perror("posix_spawn");
|
|
return false;
|
|
} else {
|
|
if (disown(child_pid) < 0)
|
|
perror("disown");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Handler Launcher::get_handler_for_executable(Handler::Type handler_type, const String& executable) const
|
|
{
|
|
Handler handler;
|
|
auto existing_handler = m_handlers.get(executable);
|
|
if (existing_handler.has_value()) {
|
|
handler = existing_handler.value();
|
|
handler.handler_type = handler_type;
|
|
} else {
|
|
handler.from_executable(handler_type, executable);
|
|
}
|
|
return handler;
|
|
}
|
|
|
|
bool Launcher::open_with_user_preferences(const HashMap<String, String>& user_preferences, const String& key, const Vector<String>& arguments, const String& default_program)
|
|
{
|
|
auto program_path = user_preferences.get(key);
|
|
if (program_path.has_value())
|
|
return spawn(program_path.value(), arguments);
|
|
|
|
// There wasn't a handler for this, so try the fallback instead
|
|
program_path = user_preferences.get("*");
|
|
if (program_path.has_value())
|
|
return spawn(program_path.value(), arguments);
|
|
|
|
// Absolute worst case, try the provided default program, if any
|
|
if (!default_program.is_empty())
|
|
return spawn(default_program, arguments);
|
|
|
|
return false;
|
|
}
|
|
|
|
void Launcher::for_each_handler(const String& key, HashMap<String, String>& user_preference, Function<bool(const Handler&)> f)
|
|
{
|
|
auto user_preferred = user_preference.get(key);
|
|
if (user_preferred.has_value())
|
|
f(get_handler_for_executable(Handler::Type::UserPreferred, user_preferred.value()));
|
|
|
|
size_t counted = 0;
|
|
for (auto& handler : m_handlers) {
|
|
// Skip over the existing item in the list
|
|
if (user_preferred.has_value() && user_preferred.value() == handler.value.executable)
|
|
continue;
|
|
if (f(handler.value))
|
|
counted++;
|
|
}
|
|
|
|
auto user_default = user_preference.get("*");
|
|
if (counted == 0 && user_default.has_value())
|
|
f(get_handler_for_executable(Handler::Type::UserDefault, user_default.value()));
|
|
}
|
|
|
|
void Launcher::for_each_handler_for_path(const String& path, Function<bool(const Handler&)> f)
|
|
{
|
|
struct stat st;
|
|
if (stat(path.characters(), &st) < 0) {
|
|
perror("stat");
|
|
return;
|
|
}
|
|
|
|
if (S_ISDIR(st.st_mode)) {
|
|
auto handler_optional = m_file_handlers.get("directory");
|
|
if (!handler_optional.has_value())
|
|
return;
|
|
auto& handler = handler_optional.value();
|
|
f(get_handler_for_executable(Handler::Type::Default, handler));
|
|
return;
|
|
}
|
|
|
|
if ((st.st_mode & S_IFMT) == S_IFREG && (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)))
|
|
f(get_handler_for_executable(Handler::Type::Application, path));
|
|
|
|
auto extension = LexicalPath(path).extension().to_lowercase();
|
|
|
|
for_each_handler(extension, m_file_handlers, [&](const auto& handler) -> bool {
|
|
if (handler.handler_type != Handler::Type::Default || handler.file_types.contains(extension))
|
|
return f(handler);
|
|
return false;
|
|
});
|
|
}
|
|
|
|
bool Launcher::open_file_url(const URL& url)
|
|
{
|
|
struct stat st;
|
|
if (stat(url.path().characters(), &st) < 0) {
|
|
perror("stat");
|
|
return false;
|
|
}
|
|
|
|
if (S_ISDIR(st.st_mode)) {
|
|
Vector<String> fm_arguments;
|
|
if (url.fragment().is_empty()) {
|
|
fm_arguments.append(url.path());
|
|
} else {
|
|
fm_arguments.append("-s");
|
|
fm_arguments.append("-r");
|
|
fm_arguments.append(String::formatted("{}/{}", url.path(), url.fragment()));
|
|
}
|
|
|
|
auto handler_optional = m_file_handlers.get("directory");
|
|
if (!handler_optional.has_value())
|
|
return false;
|
|
auto& handler = handler_optional.value();
|
|
|
|
return spawn(handler, fm_arguments);
|
|
}
|
|
|
|
if ((st.st_mode & S_IFMT) == S_IFREG && st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
|
|
return spawn(url.path(), {});
|
|
|
|
auto extension_parts = url.path().to_lowercase().split('.');
|
|
String extension = {};
|
|
if (extension_parts.size() > 1)
|
|
extension = extension_parts.last();
|
|
|
|
auto handler_optional = m_file_handlers.get("txt");
|
|
if (!handler_optional.has_value())
|
|
return false;
|
|
auto& default_handler = handler_optional.value();
|
|
|
|
// Additional parameters parsing, specific for the file protocol and txt file handlers
|
|
Vector<String> additional_parameters;
|
|
String filepath = url.path();
|
|
|
|
auto parameters = url.query().split('&');
|
|
for (auto const& parameter : parameters) {
|
|
auto pair = parameter.split('=');
|
|
if (pair.size() == 2 && pair[0] == "line_number") {
|
|
auto line = pair[1].to_int();
|
|
if (line.has_value())
|
|
// TextEditor uses file:line:col to open a file at a specific line number
|
|
filepath = String::formatted("{}:{}", filepath, line.value());
|
|
}
|
|
}
|
|
return open_with_user_preferences(m_file_handlers, extension, additional_parameters, default_handler);
|
|
}
|
|
}
|