Include fmt 11.0.2
[openal-soft.git] / core / helpers.cpp
blob3b2e088b7c9ae485c1ebf4ed3c725bd7ba71d880
2 #include "config.h"
4 #include "helpers.h"
6 #if defined(_WIN32)
7 #define WIN32_LEAN_AND_MEAN
8 #include <windows.h>
9 #endif
11 #include <algorithm>
12 #include <cstdlib>
13 #include <cstring>
14 #include <filesystem>
15 #include <limits>
16 #include <mutex>
17 #include <optional>
18 #include <string>
19 #include <system_error>
21 #include "almalloc.h"
22 #include "alnumeric.h"
23 #include "alspan.h"
24 #include "alstring.h"
25 #include "logging.h"
26 #include "strutils.h"
29 namespace {
31 using namespace std::string_view_literals;
33 std::mutex gSearchLock;
35 void DirectorySearch(const std::filesystem::path &path, const std::string_view ext,
36 std::vector<std::string> *const results)
38 namespace fs = std::filesystem;
40 const auto base = results->size();
42 try {
43 auto fpath = path.lexically_normal();
44 if(!fs::exists(fpath))
45 return;
47 TRACE("Searching %s for *%.*s\n", fpath.u8string().c_str(), al::sizei(ext), ext.data());
48 for(auto&& dirent : fs::directory_iterator{fpath})
50 auto&& entrypath = dirent.path();
51 if(!entrypath.has_extension())
52 continue;
54 if(fs::status(entrypath).type() == fs::file_type::regular
55 && al::case_compare(entrypath.extension().u8string(), ext) == 0)
56 results->emplace_back(entrypath.u8string());
59 catch(std::exception& e) {
60 ERR("Exception enumerating files: %s\n", e.what());
63 const auto newlist = al::span{*results}.subspan(base);
64 std::sort(newlist.begin(), newlist.end());
65 for(const auto &name : newlist)
66 TRACE(" got %s\n", name.c_str());
69 } // namespace
71 #ifdef _WIN32
73 #include <cctype>
74 #include <shlobj.h>
76 const PathNamePair &GetProcBinary()
78 auto get_procbin = []
80 #if !ALSOFT_UWP
81 DWORD pathlen{256};
82 auto fullpath = std::wstring(pathlen, L'\0');
83 DWORD len{GetModuleFileNameW(nullptr, fullpath.data(), pathlen)};
84 while(len == fullpath.size())
86 pathlen <<= 1;
87 if(pathlen == 0)
89 /* pathlen overflow (more than 4 billion characters??) */
90 len = 0;
91 break;
93 fullpath.resize(pathlen);
94 len = GetModuleFileNameW(nullptr, fullpath.data(), pathlen);
96 if(len == 0)
98 ERR("Failed to get process name: error %lu\n", GetLastError());
99 return PathNamePair{};
102 fullpath.resize(len);
103 #else
104 const WCHAR *exePath{__wargv[0]};
105 if(!exePath)
107 ERR("Failed to get process name: __wargv[0] == nullptr\n");
108 return PathNamePair{};
110 std::wstring fullpath{exePath};
111 #endif
112 std::replace(fullpath.begin(), fullpath.end(), L'/', L'\\');
114 PathNamePair res{};
115 if(auto seppos = fullpath.rfind(L'\\'); seppos < fullpath.size())
117 res.path = wstr_to_utf8(std::wstring_view{fullpath}.substr(0, seppos));
118 res.fname = wstr_to_utf8(std::wstring_view{fullpath}.substr(seppos+1));
120 else
121 res.fname = wstr_to_utf8(fullpath);
123 TRACE("Got binary: %s, %s\n", res.path.c_str(), res.fname.c_str());
124 return res;
126 static const PathNamePair procbin{get_procbin()};
127 return procbin;
130 namespace {
132 #if !ALSOFT_UWP && !defined(_GAMING_XBOX)
133 struct CoTaskMemDeleter {
134 void operator()(void *mem) const { CoTaskMemFree(mem); }
136 #endif
138 } // namespace
140 auto SearchDataFiles(const std::string_view ext) -> std::vector<std::string>
142 auto srchlock = std::lock_guard{gSearchLock};
144 /* Search the app-local directory. */
145 auto results = std::vector<std::string>{};
146 if(auto localpath = al::getenv(L"ALSOFT_LOCAL_PATH"))
147 DirectorySearch(*localpath, ext, &results);
148 else if(auto curpath = std::filesystem::current_path(); !curpath.empty())
149 DirectorySearch(curpath, ext, &results);
151 return results;
154 auto SearchDataFiles(const std::string_view ext, const std::string_view subdir)
155 -> std::vector<std::string>
157 std::lock_guard<std::mutex> srchlock{gSearchLock};
159 /* If the path is absolute, use it directly. */
160 std::vector<std::string> results;
161 auto path = std::filesystem::u8path(subdir);
162 if(path.is_absolute())
164 DirectorySearch(path, ext, &results);
165 return results;
168 #if !ALSOFT_UWP && !defined(_GAMING_XBOX)
169 /* Search the local and global data dirs. */
170 for(const auto &folderid : std::array{FOLDERID_RoamingAppData, FOLDERID_ProgramData})
172 std::unique_ptr<WCHAR,CoTaskMemDeleter> buffer;
173 const HRESULT hr{SHGetKnownFolderPath(folderid, KF_FLAG_DONT_UNEXPAND, nullptr,
174 al::out_ptr(buffer))};
175 if(FAILED(hr) || !buffer || !*buffer)
176 continue;
178 DirectorySearch(std::filesystem::path{buffer.get()}/path, ext, &results);
180 #endif
182 return results;
185 void SetRTPriority()
187 #if !ALSOFT_UWP
188 if(RTPrioLevel > 0)
190 if(!SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL))
191 ERR("Failed to set priority level for thread\n");
193 #endif
196 #else
198 #include <cerrno>
199 #include <dirent.h>
200 #include <unistd.h>
201 #ifdef __FreeBSD__
202 #include <sys/sysctl.h>
203 #endif
204 #ifdef __HAIKU__
205 #include <FindDirectory.h>
206 #endif
207 #ifdef HAVE_PROC_PIDPATH
208 #include <libproc.h>
209 #endif
210 #if defined(HAVE_PTHREAD_SETSCHEDPARAM) && !defined(__OpenBSD__)
211 #include <pthread.h>
212 #include <sched.h>
213 #endif
214 #if HAVE_RTKIT
215 #include <sys/resource.h>
217 #include "dbus_wrap.h"
218 #include "rtkit.h"
219 #ifndef RLIMIT_RTTIME
220 #define RLIMIT_RTTIME 15
221 #endif
222 #endif
224 const PathNamePair &GetProcBinary()
226 auto get_procbin = []
228 std::string pathname;
229 #ifdef __FreeBSD__
230 size_t pathlen{};
231 std::array<int,4> mib{{CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}};
232 if(sysctl(mib.data(), mib.size(), nullptr, &pathlen, nullptr, 0) == -1)
233 WARN("Failed to sysctl kern.proc.pathname: %s\n",
234 std::generic_category().message(errno).c_str());
235 else
237 auto procpath = std::vector<char>(pathlen+1, '\0');
238 sysctl(mib.data(), mib.size(), procpath.data(), &pathlen, nullptr, 0);
239 pathname = procpath.data();
241 #endif
242 #ifdef HAVE_PROC_PIDPATH
243 if(pathname.empty())
245 std::array<char,PROC_PIDPATHINFO_MAXSIZE> procpath{};
246 const pid_t pid{getpid()};
247 if(proc_pidpath(pid, procpath.data(), procpath.size()) < 1)
248 ERR("proc_pidpath(%d, ...) failed: %s\n", pid,
249 std::generic_category().message(errno).c_str());
250 else
251 pathname = procpath.data();
253 #endif
254 #ifdef __HAIKU__
255 if(pathname.empty())
257 std::array<char,PATH_MAX> procpath{};
258 if(find_path(B_APP_IMAGE_SYMBOL, B_FIND_PATH_IMAGE_PATH, NULL, procpath.data(), procpath.size()) == B_OK)
259 pathname = procpath.data();
261 #endif
262 #ifndef __SWITCH__
263 if(pathname.empty())
265 const std::array SelfLinkNames{
266 "/proc/self/exe"sv,
267 "/proc/self/file"sv,
268 "/proc/curproc/exe"sv,
269 "/proc/curproc/file"sv,
272 for(const std::string_view name : SelfLinkNames)
274 try {
275 if(!std::filesystem::exists(name))
276 continue;
277 if(auto path = std::filesystem::read_symlink(name); !path.empty())
279 pathname = path.u8string();
280 break;
283 catch(std::exception& e) {
284 WARN("Exception getting symlink %.*s: %s\n", al::sizei(name), name.data(),
285 e.what());
289 #endif
291 PathNamePair res{};
292 if(auto seppos = pathname.rfind('/'); seppos < pathname.size())
294 res.path = std::string_view{pathname}.substr(0, seppos);
295 res.fname = std::string_view{pathname}.substr(seppos+1);
297 else
298 res.fname = pathname;
300 TRACE("Got binary: \"%s\", \"%s\"\n", res.path.c_str(), res.fname.c_str());
301 return res;
303 static const PathNamePair procbin{get_procbin()};
304 return procbin;
307 auto SearchDataFiles(const std::string_view ext) -> std::vector<std::string>
309 auto srchlock = std::lock_guard{gSearchLock};
311 /* Search the app-local directory. */
312 auto results = std::vector<std::string>{};
313 if(auto localpath = al::getenv("ALSOFT_LOCAL_PATH"))
314 DirectorySearch(*localpath, ext, &results);
315 else if(auto curpath = std::filesystem::current_path(); !curpath.empty())
316 DirectorySearch(curpath, ext, &results);
318 return results;
321 auto SearchDataFiles(const std::string_view ext, const std::string_view subdir)
322 -> std::vector<std::string>
324 std::lock_guard<std::mutex> srchlock{gSearchLock};
326 std::vector<std::string> results;
327 auto path = std::filesystem::u8path(subdir);
328 if(path.is_absolute())
330 DirectorySearch(path, ext, &results);
331 return results;
334 /* Search local data dir */
335 if(auto datapath = al::getenv("XDG_DATA_HOME"))
336 DirectorySearch(std::filesystem::path{*datapath}/path, ext, &results);
337 else if(auto homepath = al::getenv("HOME"))
338 DirectorySearch(std::filesystem::path{*homepath}/".local/share"/path, ext, &results);
340 /* Search global data dirs */
341 std::string datadirs{al::getenv("XDG_DATA_DIRS").value_or("/usr/local/share/:/usr/share/")};
343 size_t curpos{0u};
344 while(curpos < datadirs.size())
346 size_t nextpos{datadirs.find(':', curpos)};
348 std::string_view pathname{(nextpos != std::string::npos)
349 ? std::string_view{datadirs}.substr(curpos, nextpos++ - curpos)
350 : std::string_view{datadirs}.substr(curpos)};
351 curpos = nextpos;
353 if(!pathname.empty())
354 DirectorySearch(std::filesystem::path{pathname}/path, ext, &results);
357 #ifdef ALSOFT_INSTALL_DATADIR
358 /* Search the installation data directory */
359 if(auto instpath = std::filesystem::path{ALSOFT_INSTALL_DATADIR}; !instpath.empty())
360 DirectorySearch(instpath/path, ext, &results);
361 #endif
363 return results;
366 namespace {
368 bool SetRTPriorityPthread(int prio [[maybe_unused]])
370 int err{ENOTSUP};
371 #if defined(HAVE_PTHREAD_SETSCHEDPARAM) && !defined(__OpenBSD__)
372 /* Get the min and max priority for SCHED_RR. Limit the max priority to
373 * half, for now, to ensure the thread can't take the highest priority and
374 * go rogue.
376 int rtmin{sched_get_priority_min(SCHED_RR)};
377 int rtmax{sched_get_priority_max(SCHED_RR)};
378 rtmax = (rtmax-rtmin)/2 + rtmin;
380 struct sched_param param{};
381 param.sched_priority = std::clamp(prio, rtmin, rtmax);
382 #ifdef SCHED_RESET_ON_FORK
383 err = pthread_setschedparam(pthread_self(), SCHED_RR|SCHED_RESET_ON_FORK, &param);
384 if(err == EINVAL)
385 #endif
386 err = pthread_setschedparam(pthread_self(), SCHED_RR, &param);
387 if(err == 0) return true;
388 #endif
389 WARN("pthread_setschedparam failed: %s (%d)\n", std::generic_category().message(err).c_str(),
390 err);
391 return false;
394 bool SetRTPriorityRTKit(int prio [[maybe_unused]])
396 #if HAVE_RTKIT
397 if(!HasDBus())
399 WARN("D-Bus not available\n");
400 return false;
402 dbus::Error error;
403 dbus::ConnectionPtr conn{dbus_bus_get(DBUS_BUS_SYSTEM, &error.get())};
404 if(!conn)
406 WARN("D-Bus connection failed with %s: %s\n", error->name, error->message);
407 return false;
410 /* Don't stupidly exit if the connection dies while doing this. */
411 dbus_connection_set_exit_on_disconnect(conn.get(), false);
413 int nicemin{};
414 int err{rtkit_get_min_nice_level(conn.get(), &nicemin)};
415 if(err == -ENOENT)
417 err = std::abs(err);
418 ERR("Could not query RTKit: %s (%d)\n", std::generic_category().message(err).c_str(), err);
419 return false;
421 int rtmax{rtkit_get_max_realtime_priority(conn.get())};
422 TRACE("Maximum real-time priority: %d, minimum niceness: %d\n", rtmax, nicemin);
424 auto limit_rttime = [](DBusConnection *c) -> int
426 using ulonglong = unsigned long long;
427 long long maxrttime{rtkit_get_rttime_usec_max(c)};
428 if(maxrttime <= 0) return static_cast<int>(std::abs(maxrttime));
429 const ulonglong umaxtime{static_cast<ulonglong>(maxrttime)};
431 struct rlimit rlim{};
432 if(getrlimit(RLIMIT_RTTIME, &rlim) != 0)
433 return errno;
435 TRACE("RTTime max: %llu (hard: %llu, soft: %llu)\n", umaxtime,
436 static_cast<ulonglong>(rlim.rlim_max), static_cast<ulonglong>(rlim.rlim_cur));
437 if(rlim.rlim_max > umaxtime)
439 rlim.rlim_max = static_cast<rlim_t>(std::min<ulonglong>(umaxtime,
440 std::numeric_limits<rlim_t>::max()));
441 rlim.rlim_cur = std::min(rlim.rlim_cur, rlim.rlim_max);
442 if(setrlimit(RLIMIT_RTTIME, &rlim) != 0)
443 return errno;
445 return 0;
447 if(rtmax > 0)
449 if(AllowRTTimeLimit)
451 err = limit_rttime(conn.get());
452 if(err != 0)
453 WARN("Failed to set RLIMIT_RTTIME for RTKit: %s (%d)\n",
454 std::generic_category().message(err).c_str(), err);
457 /* Limit the maximum real-time priority to half. */
458 rtmax = (rtmax+1)/2;
459 prio = std::clamp(prio, 1, rtmax);
461 TRACE("Making real-time with priority %d (max: %d)\n", prio, rtmax);
462 err = rtkit_make_realtime(conn.get(), 0, prio);
463 if(err == 0) return true;
465 err = std::abs(err);
466 WARN("Failed to set real-time priority: %s (%d)\n",
467 std::generic_category().message(err).c_str(), err);
469 /* Don't try to set the niceness for non-Linux systems. Standard POSIX has
470 * niceness as a per-process attribute, while the intent here is for the
471 * audio processing thread only to get a priority boost. Currently only
472 * Linux is known to have per-thread niceness.
474 #ifdef __linux__
475 if(nicemin < 0)
477 TRACE("Making high priority with niceness %d\n", nicemin);
478 err = rtkit_make_high_priority(conn.get(), 0, nicemin);
479 if(err == 0) return true;
481 err = std::abs(err);
482 WARN("Failed to set high priority: %s (%d)\n",
483 std::generic_category().message(err).c_str(), err);
485 #endif /* __linux__ */
487 #else
489 WARN("D-Bus not supported\n");
490 #endif
491 return false;
494 } // namespace
496 void SetRTPriority()
498 if(RTPrioLevel <= 0)
499 return;
501 if(SetRTPriorityPthread(RTPrioLevel))
502 return;
503 if(SetRTPriorityRTKit(RTPrioLevel))
504 return;
507 #endif