mirror of
https://github.com/thinkonmay/sunshine-sdk.git
synced 2026-01-25 17:14:20 +00:00
564 lines
17 KiB
Plaintext
564 lines
17 KiB
Plaintext
/**
|
|
* @file src/platform/macos/misc.mm
|
|
* @brief todo
|
|
*/
|
|
|
|
// Required for IPV6_PKTINFO with Darwin headers
|
|
#ifndef __APPLE_USE_RFC_3542
|
|
#define __APPLE_USE_RFC_3542 1
|
|
#endif
|
|
|
|
#include <Foundation/Foundation.h>
|
|
#include <arpa/inet.h>
|
|
#include <dlfcn.h>
|
|
#include <fcntl.h>
|
|
#include <ifaddrs.h>
|
|
#include <mach-o/dyld.h>
|
|
#include <net/if_dl.h>
|
|
#include <pwd.h>
|
|
|
|
#include "misc.h"
|
|
#include "src/main.h"
|
|
#include "src/platform/common.h"
|
|
|
|
#include <boost/asio/ip/address.hpp>
|
|
#include <boost/process.hpp>
|
|
|
|
using namespace std::literals;
|
|
namespace fs = std::filesystem;
|
|
namespace bp = boost::process;
|
|
|
|
namespace platf {
|
|
|
|
// Even though the following two functions are available starting in macOS 10.15, they weren't
|
|
// actually in the Mac SDK until Xcode 12.2, the first to include the SDK for macOS 11
|
|
#if __MAC_OS_X_VERSION_MAX_ALLOWED < 110000 // __MAC_11_0
|
|
// If they're not in the SDK then we can use our own function definitions.
|
|
// Need to use weak import so that this will link in macOS 10.14 and earlier
|
|
extern "C" bool
|
|
CGPreflightScreenCaptureAccess(void) __attribute__((weak_import));
|
|
extern "C" bool
|
|
CGRequestScreenCaptureAccess(void) __attribute__((weak_import));
|
|
#endif
|
|
|
|
std::unique_ptr<deinit_t>
|
|
init() {
|
|
// This will generate a warning about CGPreflightScreenCaptureAccess and
|
|
// CGRequestScreenCaptureAccess being unavailable before macOS 10.15, but
|
|
// we have a guard to prevent it from being called on those earlier systems.
|
|
// Unfortunately the supported way to silence this warning, using @available,
|
|
// produces linker errors for __isPlatformVersionAtLeast, so we have to use
|
|
// a different method.
|
|
// We also ignore "tautological-pointer-compare" because when compiling with
|
|
// Xcode 12.2 and later, these functions are not weakly linked and will never
|
|
// be null, and therefore generate this warning. Since we are weakly linking
|
|
// when compiling with earlier Xcode versions, the check for null is
|
|
// necessary and so we ignore the warning.
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
|
|
#pragma clang diagnostic ignored "-Wtautological-pointer-compare"
|
|
if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:((NSOperatingSystemVersion) { 10, 15, 0 })] &&
|
|
// Double check that these weakly-linked symbols have been loaded:
|
|
CGPreflightScreenCaptureAccess != nullptr && CGRequestScreenCaptureAccess != nullptr &&
|
|
!CGPreflightScreenCaptureAccess()) {
|
|
BOOST_LOG(error) << "No screen capture permission!"sv;
|
|
BOOST_LOG(error) << "Please activate it in 'System Preferences' -> 'Privacy' -> 'Screen Recording'"sv;
|
|
CGRequestScreenCaptureAccess();
|
|
return nullptr;
|
|
}
|
|
#pragma clang diagnostic pop
|
|
return std::make_unique<deinit_t>();
|
|
}
|
|
|
|
fs::path
|
|
appdata() {
|
|
const char *homedir;
|
|
if ((homedir = getenv("HOME")) == nullptr) {
|
|
homedir = getpwuid(geteuid())->pw_dir;
|
|
}
|
|
|
|
return fs::path { homedir } / ".config/sunshine"sv;
|
|
}
|
|
|
|
using ifaddr_t = util::safe_ptr<ifaddrs, freeifaddrs>;
|
|
|
|
ifaddr_t
|
|
get_ifaddrs() {
|
|
ifaddrs *p { nullptr };
|
|
|
|
getifaddrs(&p);
|
|
|
|
return ifaddr_t { p };
|
|
}
|
|
|
|
std::string
|
|
from_sockaddr(const sockaddr *const ip_addr) {
|
|
char data[INET6_ADDRSTRLEN] = {};
|
|
|
|
auto family = ip_addr->sa_family;
|
|
if (family == AF_INET6) {
|
|
inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data,
|
|
INET6_ADDRSTRLEN);
|
|
}
|
|
else if (family == AF_INET) {
|
|
inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data,
|
|
INET_ADDRSTRLEN);
|
|
}
|
|
|
|
return std::string { data };
|
|
}
|
|
|
|
std::pair<std::uint16_t, std::string>
|
|
from_sockaddr_ex(const sockaddr *const ip_addr) {
|
|
char data[INET6_ADDRSTRLEN] = {};
|
|
|
|
auto family = ip_addr->sa_family;
|
|
std::uint16_t port = 0;
|
|
if (family == AF_INET6) {
|
|
inet_ntop(AF_INET6, &((sockaddr_in6 *) ip_addr)->sin6_addr, data,
|
|
INET6_ADDRSTRLEN);
|
|
port = ((sockaddr_in6 *) ip_addr)->sin6_port;
|
|
}
|
|
else if (family == AF_INET) {
|
|
inet_ntop(AF_INET, &((sockaddr_in *) ip_addr)->sin_addr, data,
|
|
INET_ADDRSTRLEN);
|
|
port = ((sockaddr_in *) ip_addr)->sin_port;
|
|
}
|
|
|
|
return { port, std::string { data } };
|
|
}
|
|
|
|
std::string
|
|
get_mac_address(const std::string_view &address) {
|
|
auto ifaddrs = get_ifaddrs();
|
|
|
|
for (auto pos = ifaddrs.get(); pos != nullptr; pos = pos->ifa_next) {
|
|
if (pos->ifa_addr && address == from_sockaddr(pos->ifa_addr)) {
|
|
BOOST_LOG(verbose) << "Looking for MAC of "sv << pos->ifa_name;
|
|
|
|
struct ifaddrs *ifap, *ifaptr;
|
|
unsigned char *ptr;
|
|
std::string mac_address;
|
|
|
|
if (getifaddrs(&ifap) == 0) {
|
|
for (ifaptr = ifap; ifaptr != NULL; ifaptr = (ifaptr)->ifa_next) {
|
|
if (!strcmp((ifaptr)->ifa_name, pos->ifa_name) && (((ifaptr)->ifa_addr)->sa_family == AF_LINK)) {
|
|
ptr = (unsigned char *) LLADDR((struct sockaddr_dl *) (ifaptr)->ifa_addr);
|
|
char buff[100];
|
|
|
|
snprintf(buff, sizeof(buff), "%02x:%02x:%02x:%02x:%02x:%02x",
|
|
*ptr, *(ptr + 1), *(ptr + 2), *(ptr + 3), *(ptr + 4), *(ptr + 5));
|
|
mac_address = buff;
|
|
break;
|
|
}
|
|
}
|
|
|
|
freeifaddrs(ifap);
|
|
|
|
if (ifaptr != NULL) {
|
|
BOOST_LOG(verbose) << "Found MAC of "sv << pos->ifa_name << ": "sv << mac_address;
|
|
return mac_address;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
BOOST_LOG(warning) << "Unable to find MAC address for "sv << address;
|
|
return "00:00:00:00:00:00"s;
|
|
}
|
|
|
|
bp::child
|
|
run_command(bool elevated, bool interactive, const std::string &cmd, boost::filesystem::path &working_dir, const bp::environment &env, FILE *file, std::error_code &ec, bp::group *group) {
|
|
if (!group) {
|
|
if (!file) {
|
|
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec);
|
|
}
|
|
else {
|
|
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec);
|
|
}
|
|
}
|
|
else {
|
|
if (!file) {
|
|
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > bp::null, bp::std_err > bp::null, ec, *group);
|
|
}
|
|
else {
|
|
return bp::child(cmd, env, bp::start_dir(working_dir), bp::std_out > file, bp::std_err > file, ec, *group);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Open a url in the default web browser.
|
|
* @param url The url to open.
|
|
*/
|
|
void
|
|
open_url(const std::string &url) {
|
|
boost::filesystem::path working_dir;
|
|
std::string cmd = R"(open ")" + url + R"(")";
|
|
|
|
boost::process::environment _env = boost::this_process::environment();
|
|
std::error_code ec;
|
|
auto child = run_command(false, false, cmd, working_dir, _env, nullptr, ec, nullptr);
|
|
if (ec) {
|
|
BOOST_LOG(warning) << "Couldn't open url ["sv << url << "]: System: "sv << ec.message();
|
|
}
|
|
else {
|
|
BOOST_LOG(info) << "Opened url ["sv << url << "]"sv;
|
|
child.detach();
|
|
}
|
|
}
|
|
|
|
void
|
|
adjust_thread_priority(thread_priority_e priority) {
|
|
// Unimplemented
|
|
}
|
|
|
|
void
|
|
streaming_will_start() {
|
|
// Nothing to do
|
|
}
|
|
|
|
void
|
|
streaming_will_stop() {
|
|
// Nothing to do
|
|
}
|
|
|
|
void
|
|
restart_on_exit() {
|
|
char executable[2048];
|
|
uint32_t size = sizeof(executable);
|
|
if (_NSGetExecutablePath(executable, &size) < 0) {
|
|
BOOST_LOG(fatal) << "NSGetExecutablePath() failed: "sv << errno;
|
|
return;
|
|
}
|
|
|
|
// ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves
|
|
int openmax = (int) sysconf(_SC_OPEN_MAX);
|
|
for (int fd = STDERR_FILENO + 1; fd < openmax; fd++) {
|
|
close(fd);
|
|
}
|
|
|
|
// Re-exec ourselves with the same arguments
|
|
if (execv(executable, lifetime::get_argv()) < 0) {
|
|
BOOST_LOG(fatal) << "execv() failed: "sv << errno;
|
|
return;
|
|
}
|
|
}
|
|
|
|
void
|
|
restart() {
|
|
// Gracefully clean up and restart ourselves instead of exiting
|
|
atexit(restart_on_exit);
|
|
lifetime::exit_sunshine(0, true);
|
|
}
|
|
|
|
/**
|
|
* @brief Attempt to gracefully terminate a process group.
|
|
* @param native_handle The process group ID.
|
|
* @return true if termination was successfully requested.
|
|
*/
|
|
bool
|
|
request_process_group_exit(std::uintptr_t native_handle) {
|
|
if (killpg((pid_t) native_handle, SIGTERM) == 0 || errno == ESRCH) {
|
|
BOOST_LOG(debug) << "Successfully sent SIGTERM to process group: "sv << native_handle;
|
|
return true;
|
|
}
|
|
else {
|
|
BOOST_LOG(warning) << "Unable to send SIGTERM to process group ["sv << native_handle << "]: "sv << errno;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Checks if a process group still has running children.
|
|
* @param native_handle The process group ID.
|
|
* @return true if processes are still running.
|
|
*/
|
|
bool
|
|
process_group_running(std::uintptr_t native_handle) {
|
|
return waitpid(-((pid_t) native_handle), nullptr, WNOHANG) >= 0;
|
|
}
|
|
|
|
struct sockaddr_in
|
|
to_sockaddr(boost::asio::ip::address_v4 address, uint16_t port) {
|
|
struct sockaddr_in saddr_v4 = {};
|
|
|
|
saddr_v4.sin_family = AF_INET;
|
|
saddr_v4.sin_port = htons(port);
|
|
|
|
auto addr_bytes = address.to_bytes();
|
|
memcpy(&saddr_v4.sin_addr, addr_bytes.data(), sizeof(saddr_v4.sin_addr));
|
|
|
|
return saddr_v4;
|
|
}
|
|
|
|
struct sockaddr_in6
|
|
to_sockaddr(boost::asio::ip::address_v6 address, uint16_t port) {
|
|
struct sockaddr_in6 saddr_v6 = {};
|
|
|
|
saddr_v6.sin6_family = AF_INET6;
|
|
saddr_v6.sin6_port = htons(port);
|
|
saddr_v6.sin6_scope_id = address.scope_id();
|
|
|
|
auto addr_bytes = address.to_bytes();
|
|
memcpy(&saddr_v6.sin6_addr, addr_bytes.data(), sizeof(saddr_v6.sin6_addr));
|
|
|
|
return saddr_v6;
|
|
}
|
|
|
|
bool
|
|
send_batch(batched_send_info_t &send_info) {
|
|
// Fall back to unbatched send calls
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
send(send_info_t &send_info) {
|
|
auto sockfd = (int) send_info.native_socket;
|
|
struct msghdr msg = {};
|
|
|
|
// Convert the target address into a sockaddr
|
|
struct sockaddr_in taddr_v4 = {};
|
|
struct sockaddr_in6 taddr_v6 = {};
|
|
if (send_info.target_address.is_v6()) {
|
|
taddr_v6 = to_sockaddr(send_info.target_address.to_v6(), send_info.target_port);
|
|
|
|
msg.msg_name = (struct sockaddr *) &taddr_v6;
|
|
msg.msg_namelen = sizeof(taddr_v6);
|
|
}
|
|
else {
|
|
taddr_v4 = to_sockaddr(send_info.target_address.to_v4(), send_info.target_port);
|
|
|
|
msg.msg_name = (struct sockaddr *) &taddr_v4;
|
|
msg.msg_namelen = sizeof(taddr_v4);
|
|
}
|
|
|
|
union {
|
|
char buf[std::max(CMSG_SPACE(sizeof(struct in_pktinfo)), CMSG_SPACE(sizeof(struct in6_pktinfo)))];
|
|
struct cmsghdr alignment;
|
|
} cmbuf;
|
|
socklen_t cmbuflen = 0;
|
|
|
|
msg.msg_control = cmbuf.buf;
|
|
msg.msg_controllen = sizeof(cmbuf.buf);
|
|
|
|
auto pktinfo_cm = CMSG_FIRSTHDR(&msg);
|
|
if (send_info.source_address.is_v6()) {
|
|
struct in6_pktinfo pktInfo;
|
|
|
|
struct sockaddr_in6 saddr_v6 = to_sockaddr(send_info.source_address.to_v6(), 0);
|
|
pktInfo.ipi6_addr = saddr_v6.sin6_addr;
|
|
pktInfo.ipi6_ifindex = 0;
|
|
|
|
cmbuflen += CMSG_SPACE(sizeof(pktInfo));
|
|
|
|
pktinfo_cm->cmsg_level = IPPROTO_IPV6;
|
|
pktinfo_cm->cmsg_type = IPV6_PKTINFO;
|
|
pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo));
|
|
memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo));
|
|
}
|
|
else {
|
|
struct in_pktinfo pktInfo;
|
|
|
|
struct sockaddr_in saddr_v4 = to_sockaddr(send_info.source_address.to_v4(), 0);
|
|
pktInfo.ipi_spec_dst = saddr_v4.sin_addr;
|
|
pktInfo.ipi_ifindex = 0;
|
|
|
|
cmbuflen += CMSG_SPACE(sizeof(pktInfo));
|
|
|
|
pktinfo_cm->cmsg_level = IPPROTO_IP;
|
|
pktinfo_cm->cmsg_type = IP_PKTINFO;
|
|
pktinfo_cm->cmsg_len = CMSG_LEN(sizeof(pktInfo));
|
|
memcpy(CMSG_DATA(pktinfo_cm), &pktInfo, sizeof(pktInfo));
|
|
}
|
|
|
|
struct iovec iov = {};
|
|
iov.iov_base = (void *) send_info.buffer;
|
|
iov.iov_len = send_info.size;
|
|
|
|
msg.msg_iov = &iov;
|
|
msg.msg_iovlen = 1;
|
|
|
|
msg.msg_controllen = cmbuflen;
|
|
|
|
auto bytes_sent = sendmsg(sockfd, &msg, 0);
|
|
|
|
// If there's no send buffer space, wait for some to be available
|
|
while (bytes_sent < 0 && errno == EAGAIN) {
|
|
struct pollfd pfd;
|
|
|
|
pfd.fd = sockfd;
|
|
pfd.events = POLLOUT;
|
|
|
|
if (poll(&pfd, 1, -1) != 1) {
|
|
BOOST_LOG(warning) << "poll() failed: "sv << errno;
|
|
break;
|
|
}
|
|
|
|
// Try to send again
|
|
bytes_sent = sendmsg(sockfd, &msg, 0);
|
|
}
|
|
|
|
if (bytes_sent < 0) {
|
|
BOOST_LOG(warning) << "sendmsg() failed: "sv << errno;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// We can't track QoS state separately for each destination on this OS,
|
|
// so we keep a ref count to only disable QoS options when all clients
|
|
// are disconnected.
|
|
static std::atomic<int> qos_ref_count = 0;
|
|
|
|
class qos_t: public deinit_t {
|
|
public:
|
|
qos_t(int sockfd, std::vector<std::tuple<int, int, int>> options):
|
|
sockfd(sockfd), options(options) {
|
|
qos_ref_count++;
|
|
}
|
|
|
|
virtual ~qos_t() {
|
|
if (--qos_ref_count == 0) {
|
|
for (const auto &tuple : options) {
|
|
auto reset_val = std::get<2>(tuple);
|
|
if (setsockopt(sockfd, std::get<0>(tuple), std::get<1>(tuple), &reset_val, sizeof(reset_val)) < 0) {
|
|
BOOST_LOG(warning) << "Failed to reset option: "sv << errno;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private:
|
|
int sockfd;
|
|
std::vector<std::tuple<int, int, int>> options;
|
|
};
|
|
|
|
/**
|
|
* @brief Enables QoS on the given socket for traffic to the specified destination.
|
|
* @param native_socket The native socket handle.
|
|
* @param address The destination address for traffic sent on this socket.
|
|
* @param port The destination port for traffic sent on this socket.
|
|
* @param data_type The type of traffic sent on this socket.
|
|
* @param dscp_tagging Specifies whether to enable DSCP tagging on outgoing traffic.
|
|
*/
|
|
std::unique_ptr<deinit_t>
|
|
enable_socket_qos(uintptr_t native_socket, boost::asio::ip::address &address, uint16_t port, qos_data_type_e data_type, bool dscp_tagging) {
|
|
int sockfd = (int) native_socket;
|
|
std::vector<std::tuple<int, int, int>> reset_options;
|
|
|
|
// We can use SO_NET_SERVICE_TYPE to set link-layer prioritization without DSCP tagging
|
|
int service_type = 0;
|
|
switch (data_type) {
|
|
case qos_data_type_e::video:
|
|
service_type = NET_SERVICE_TYPE_VI;
|
|
break;
|
|
case qos_data_type_e::audio:
|
|
service_type = NET_SERVICE_TYPE_VO;
|
|
break;
|
|
default:
|
|
BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type;
|
|
break;
|
|
}
|
|
|
|
if (service_type) {
|
|
if (setsockopt(sockfd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &service_type, sizeof(service_type)) == 0) {
|
|
// Reset SO_NET_SERVICE_TYPE to best-effort when QoS is disabled
|
|
reset_options.emplace_back(std::make_tuple(SOL_SOCKET, SO_NET_SERVICE_TYPE, NET_SERVICE_TYPE_BE));
|
|
}
|
|
else {
|
|
BOOST_LOG(error) << "Failed to set SO_NET_SERVICE_TYPE: "sv << errno;
|
|
}
|
|
}
|
|
|
|
if (dscp_tagging) {
|
|
int level;
|
|
int option;
|
|
if (address.is_v6()) {
|
|
level = IPPROTO_IPV6;
|
|
option = IPV6_TCLASS;
|
|
}
|
|
else {
|
|
level = IPPROTO_IP;
|
|
option = IP_TOS;
|
|
}
|
|
|
|
// The specific DSCP values here are chosen to be consistent with Windows,
|
|
// except that we use CS6 instead of CS7 for audio traffic.
|
|
int dscp = 0;
|
|
switch (data_type) {
|
|
case qos_data_type_e::video:
|
|
dscp = 40;
|
|
break;
|
|
case qos_data_type_e::audio:
|
|
dscp = 48;
|
|
break;
|
|
default:
|
|
BOOST_LOG(error) << "Unknown traffic type: "sv << (int) data_type;
|
|
break;
|
|
}
|
|
|
|
if (dscp) {
|
|
// Shift to put the DSCP value in the correct position in the TOS field
|
|
dscp <<= 2;
|
|
|
|
if (setsockopt(sockfd, level, option, &dscp, sizeof(dscp)) == 0) {
|
|
// Reset TOS to -1 when QoS is disabled
|
|
reset_options.emplace_back(std::make_tuple(level, option, -1));
|
|
}
|
|
else {
|
|
BOOST_LOG(error) << "Failed to set TOS/TCLASS: "sv << errno;
|
|
}
|
|
}
|
|
}
|
|
|
|
return std::make_unique<qos_t>(sockfd, reset_options);
|
|
}
|
|
|
|
} // namespace platf
|
|
|
|
namespace dyn {
|
|
void *
|
|
handle(const std::vector<const char *> &libs) {
|
|
void *handle;
|
|
|
|
for (auto lib : libs) {
|
|
handle = dlopen(lib, RTLD_LAZY | RTLD_LOCAL);
|
|
if (handle) {
|
|
return handle;
|
|
}
|
|
}
|
|
|
|
std::stringstream ss;
|
|
ss << "Couldn't find any of the following libraries: ["sv << libs.front();
|
|
std::for_each(std::begin(libs) + 1, std::end(libs), [&](auto lib) {
|
|
ss << ", "sv << lib;
|
|
});
|
|
|
|
ss << ']';
|
|
|
|
BOOST_LOG(error) << ss.str();
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
int
|
|
load(void *handle, const std::vector<std::tuple<apiproc *, const char *>> &funcs, bool strict) {
|
|
int err = 0;
|
|
for (auto &func : funcs) {
|
|
TUPLE_2D_REF(fn, name, func);
|
|
|
|
*fn = (void (*)()) dlsym(handle, name);
|
|
|
|
if (!*fn && strict) {
|
|
BOOST_LOG(error) << "Couldn't find function: "sv << name;
|
|
|
|
err = -1;
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
} // namespace dyn
|