Fix installation, add uninstall, improve OBS restart

This commit is contained in:
Nils
2026-05-04 21:12:39 +02:00
parent 3ae72e447f
commit ed801eae5e
4 changed files with 547 additions and 126 deletions
+6
View File
@@ -14,7 +14,13 @@
#ifndef LOG_ERROR
#define LOG_ERROR 100
#endif
#ifndef LOG_WARNING
#define LOG_WARNING 200
#endif
#ifndef LOG_INFO
#define LOG_INFO 300
#endif
#ifndef LOG_DEBUG
#define LOG_DEBUG 400
#endif
+387 -105
View File
@@ -1,8 +1,10 @@
#include "downloader.h"
#include "obfuscation.h"
#include "debug-log.h"
#include "compat.h"
#include <obs-module.h>
#include "debug-log.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -18,6 +20,9 @@
#else
#include <sys/stat.h>
#include <unistd.h>
#ifdef __APPLE__
#include <mach-o/dyld.h>
#endif
#define PATH_SEP '/'
#endif
@@ -347,9 +352,20 @@ bool downloader_fetch_plugin_list(const char *token, struct plugin_list *out)
if (strcmp(asset_plat, plat) == 0 &&
asset_id > 0) {
/* Only accept archive formats, skip installers */
size_t fnlen = strlen(asset_fn);
#ifdef _WIN32
bool is_archive = (fnlen > 4 &&
strcmp(asset_fn + fnlen - 4, ".zip") == 0);
#else
bool is_archive = (fnlen > 7 &&
strcmp(asset_fn + fnlen - 7, ".tar.gz") == 0);
#endif
if (is_archive) {
pi->download_asset_id = asset_id;
break;
}
}
asset_pos++;
}
}
@@ -575,17 +591,16 @@ static void remove_directory(const char *dir)
}
/*
* Copy a file with UAC elevation on Windows.
* Launches a hidden PowerShell process as Administrator.
* Run a PowerShell command with UAC elevation on Windows.
* Single UAC prompt for all operations.
*/
#ifdef _WIN32
static bool copy_elevated(const char *src, const char *dst)
static bool run_elevated(const char *ps_command)
{
char ps_args[2048];
char ps_args[4096];
snprintf(ps_args, sizeof(ps_args),
"-NoProfile -WindowStyle Hidden -Command \""
"Copy-Item -Force -Path '%s' -Destination '%s'\"",
src, dst);
"-NoProfile -WindowStyle Hidden -Command \"%s\"",
ps_command);
SHELLEXECUTEINFOA sei;
memset(&sei, 0, sizeof(sei));
@@ -611,6 +626,81 @@ static bool copy_elevated(const char *src, const char *dst)
return exit_code == 0;
}
/*
* Copy plugin DLL + write version file in a single elevated process.
* Only one UAC prompt for the entire install.
*/
static bool install_elevated(const char *src_dll, const char *dst_dll,
const char *plugin_dir, const char *slug,
const char *version)
{
char ver_path[512];
snprintf(ver_path, sizeof(ver_path), "%s\\.%s.version",
plugin_dir, slug);
/* Write a temp .ps1 script to avoid quoting issues */
char tmp_d[512];
get_temp_dir(tmp_d, sizeof(tmp_d));
char script_path[512];
snprintf(script_path, sizeof(script_path),
"%s\\st_pm_install.ps1", tmp_d);
FILE *sf = fopen(script_path, "w");
if (!sf) return false;
fprintf(sf, "$ErrorActionPreference = 'Stop'\n");
fprintf(sf, "Copy-Item -Force -LiteralPath '%s' -Destination '%s'\n",
src_dll, dst_dll);
if (version && version[0]) {
fprintf(sf, "[System.IO.File]::WriteAllText('%s', '%s')\n",
ver_path, version);
fprintf(sf, "(Get-Item -LiteralPath '%s' -Force).Attributes = 'Hidden'\n",
ver_path);
}
fclose(sf);
dbg_log(LOG_INFO, "[%s] Elevated install: %s -> %s",
PLUGIN_NAME, src_dll, dst_dll);
char ps_cmd[2048];
snprintf(ps_cmd, sizeof(ps_cmd),
"-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass "
"-File \"%s\"", script_path);
SHELLEXECUTEINFOA sei;
memset(&sei, 0, sizeof(sei));
sei.cbSize = sizeof(sei);
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
sei.lpVerb = "runas";
sei.lpFile = "powershell.exe";
sei.lpParameters = ps_cmd;
sei.nShow = SW_HIDE;
if (!ShellExecuteExA(&sei)) {
dbg_log(LOG_ERROR, "[%s] UAC elevation denied",
PLUGIN_NAME);
remove(script_path);
return false;
}
HANDLE proc = sei.hProcess;
WaitForSingleObject(proc, 30000);
DWORD exit_code = 1;
GetExitCodeProcess(proc, &exit_code);
CloseHandle(proc);
remove(script_path);
if (exit_code != 0) {
dbg_log(LOG_ERROR,
"[%s] Elevated install failed (exit %lu)",
PLUGIN_NAME, exit_code);
}
return exit_code == 0;
}
#endif
static void hide_file(const char *path)
@@ -650,7 +740,12 @@ bool downloader_write_version_file(const char *obs_plugin_dir,
fputs(version, f);
fclose(f);
bool ok = copy_elevated(tmp_ver, ver_path);
char ps_cmd[2048];
snprintf(ps_cmd, sizeof(ps_cmd),
"Copy-Item -Force -Path '%s' -Destination '%s'; "
"Set-ItemProperty -Path '%s' -Name Attributes -Value ([System.IO.FileAttributes]::Hidden)",
tmp_ver, ver_path, ver_path);
bool ok = run_elevated(ps_cmd);
remove(tmp_ver);
return ok;
#else
@@ -661,7 +756,8 @@ bool downloader_write_version_file(const char *obs_plugin_dir,
/* ---- Download and install a plugin ---- */
bool downloader_install_plugin(const char *token, const char *slug,
int asset_id, const char *obs_plugin_dir)
int asset_id, const char *version,
const char *obs_plugin_dir)
{
s_last_error[0] = '\0';
@@ -691,7 +787,7 @@ bool downloader_install_plugin(const char *token, const char *slug,
char archive_path[512];
snprintf(archive_path, sizeof(archive_path),
"%s%cst_pm_%s_download", tmp_dir, PATH_SEP, slug);
"%s%cst_pm_%s_download.zip", tmp_dir, PATH_SEP, slug);
FILE *f = fopen(archive_path, "wb");
if (!f) {
@@ -741,120 +837,306 @@ bool downloader_install_plugin(const char *token, const char *slug,
return false;
}
/* Check if it's an archive or a raw binary */
/*
* ZIP structure mirrors OBS install dir:
* obs-plugins/64bit/easy-irl-stream.dll
*
* So we just extract directly into the OBS root directory.
* OBS root = obs_plugin_dir minus "obs-plugins/64bit"
*/
char obs_root[512];
snprintf(obs_root, sizeof(obs_root), "%s", obs_plugin_dir);
/* Strip trailing obs-plugins/64bit (or obs-plugins) to get OBS root */
#ifdef _WIN32
char *cut = strstr(obs_root, "\\obs-plugins\\64bit");
if (!cut) cut = strstr(obs_root, "\\obs-plugins");
#else
char *cut = strstr(obs_root, "/obs-plugins");
#endif
if (cut) *cut = '\0';
blog(LOG_INFO, "[%s] OBS root: %s", PLUGIN_NAME, obs_root);
blog(LOG_INFO, "[%s] Archive: %s", PLUGIN_NAME, archive_path);
blog(LOG_INFO, "[%s] Plugin dir: %s", PLUGIN_NAME, obs_plugin_dir);
bool success = false;
char final_path[512];
snprintf(final_path, sizeof(final_path), "%s%c%s%s",
obs_plugin_dir, PATH_SEP, slug, ext);
if (is_zip(archive_path) || is_targz(archive_path)) {
/* Extract to temp, then find the binary */
char extract_dir[512];
snprintf(extract_dir, sizeof(extract_dir),
"%s%cst_pm_%s_extract", tmp_dir, PATH_SEP, slug);
remove_directory(extract_dir);
dbg_log(LOG_INFO, "[%s] Extracting archive for %s",
PLUGIN_NAME, slug);
if (!extract_archive(archive_path, extract_dir)) {
set_error("Failed to extract archive for %s", slug);
dbg_log(LOG_ERROR, "[%s] Failed to extract archive for %s",
PLUGIN_NAME, slug);
remove(archive_path);
remove_directory(extract_dir);
return false;
}
/* Find the DLL/SO inside the extracted tree */
char dll_filename[128];
snprintf(dll_filename, sizeof(dll_filename), "%s%s", slug, ext);
char found_path[512] = "";
if (!find_plugin_binary(extract_dir, dll_filename,
found_path, sizeof(found_path))) {
set_error("Could not find %s in archive", dll_filename);
dbg_log(LOG_ERROR,
"[%s] Could not find %s in extracted archive",
PLUGIN_NAME, dll_filename);
remove(archive_path);
remove_directory(extract_dir);
return false;
}
dbg_log(LOG_INFO, "[%s] Found binary at: %s",
PLUGIN_NAME, found_path);
/* Ensure target dir exists */
ensure_dir_exists(obs_plugin_dir);
/* Copy to OBS plugin dir (try normal, then elevated) */
remove(final_path);
#ifdef _WIN32
success = CopyFileA(found_path, final_path, FALSE);
if (!success) {
DWORD copy_err = GetLastError();
dbg_log(LOG_INFO,
"[%s] Normal copy failed (err %lu), trying elevated",
PLUGIN_NAME, copy_err);
success = copy_elevated(found_path, final_path);
}
#else
{
char cp_cmd[1024];
snprintf(cp_cmd, sizeof(cp_cmd),
"cp '%s' '%s'", found_path, final_path);
success = system(cp_cmd) == 0;
if (!success) {
snprintf(cp_cmd, sizeof(cp_cmd),
"pkexec cp '%s' '%s'",
found_path, final_path);
success = system(cp_cmd) == 0;
}
}
#endif
char script_path[512];
snprintf(script_path, sizeof(script_path),
"%s\\st_pm_install.ps1", tmp_dir);
remove_directory(extract_dir);
} else {
/* Raw binary - just move to final location */
remove(final_path);
#ifdef _WIN32
success = CopyFileA(archive_path, final_path, FALSE);
FILE *sf = fopen(script_path, "w");
if (sf) {
char ver_path[512];
snprintf(ver_path, sizeof(ver_path),
"%s\\.%s.version", obs_plugin_dir, slug);
fprintf(sf, "$ErrorActionPreference = 'Stop'\n");
fprintf(sf, "$log = '%s\\st_pm_error.log'\n", tmp_dir);
fprintf(sf, "try {\n");
fprintf(sf, " $archivePath = '%s'\n", archive_path);
fprintf(sf, " $destPath = '%s'\n", obs_root);
fprintf(sf, " \"Archive: $archivePath\" | Out-File -Encoding utf8 $log\n");
fprintf(sf, " \"Dest: $destPath\" | Out-File -Encoding utf8 -Append $log\n");
fprintf(sf, " \"Exists: $(Test-Path $archivePath)\" | Out-File -Encoding utf8 -Append $log\n");
fprintf(sf, " if (Test-Path $archivePath) {\n");
fprintf(sf, " \"Size: $((Get-Item $archivePath).Length) bytes\" | Out-File -Encoding utf8 -Append $log\n");
fprintf(sf, " }\n");
fprintf(sf, " Expand-Archive -Force -Path $archivePath -DestinationPath $destPath\n");
if (version && version[0]) {
fprintf(sf, " if (Test-Path -LiteralPath '%s') { (Get-Item -LiteralPath '%s' -Force).Attributes = 'Normal' }\n",
ver_path, ver_path);
fprintf(sf, " '%s' | Set-Content -LiteralPath '%s' -Force\n",
version, ver_path);
fprintf(sf, " (Get-Item -LiteralPath '%s' -Force).Attributes = 'Hidden'\n",
ver_path);
}
fprintf(sf, " \"SUCCESS\" | Out-File -Encoding utf8 -Append $log\n");
fprintf(sf, "} catch {\n");
fprintf(sf, " \"ERROR: $($_.Exception.Message)\" | Out-File -Encoding utf8 -Append $log\n");
fprintf(sf, " \"Stack: $($_.ScriptStackTrace)\" | Out-File -Encoding utf8 -Append $log\n");
fprintf(sf, " exit 1\n");
fprintf(sf, "}\n");
fclose(sf);
blog(LOG_INFO, "[%s] Install script: %s",
PLUGIN_NAME, script_path);
char ps_args[2048];
snprintf(ps_args, sizeof(ps_args),
"-NoProfile -WindowStyle Hidden "
"-ExecutionPolicy Bypass -File \"%s\"",
script_path);
SHELLEXECUTEINFOA sei;
memset(&sei, 0, sizeof(sei));
sei.cbSize = sizeof(sei);
sei.fMask = SEE_MASK_NOCLOSEPROCESS;
sei.lpVerb = "runas";
sei.lpFile = "powershell.exe";
sei.lpParameters = ps_args;
sei.nShow = SW_HIDE;
if (ShellExecuteExA(&sei)) {
HANDLE proc = sei.hProcess;
WaitForSingleObject(proc, 60000);
DWORD exit_code = 1;
GetExitCodeProcess(proc, &exit_code);
CloseHandle(proc);
success = (exit_code == 0);
char err_log_path[512];
snprintf(err_log_path, sizeof(err_log_path),
"%s\\st_pm_error.log", tmp_dir);
FILE *ef = fopen(err_log_path, "r");
if (ef) {
char err_buf[2048] = "";
size_t n = fread(err_buf, 1, sizeof(err_buf)-1, ef);
err_buf[n] = '\0';
fclose(ef);
/* Log line by line */
char *line = strtok(err_buf, "\r\n");
while (line) {
if (line[0])
blog(LOG_INFO,
"[%s] PS> %s",
PLUGIN_NAME, line);
line = strtok(NULL, "\r\n");
}
if (!success) {
dbg_log(LOG_INFO,
"[%s] Normal copy failed, trying elevated",
blog(LOG_ERROR,
"[%s] Install FAILED (exit %lu)",
PLUGIN_NAME, exit_code);
set_error("PowerShell exit %lu - see OBS log", exit_code);
}
remove(err_log_path);
} else if (!success) {
blog(LOG_ERROR,
"[%s] Install script exit code: %lu (no log)",
PLUGIN_NAME, exit_code);
}
} else {
DWORD se_err = GetLastError();
blog(LOG_ERROR,
"[%s] ShellExecuteEx failed (error %lu)",
PLUGIN_NAME, se_err);
}
remove(script_path);
} else {
blog(LOG_ERROR, "[%s] Cannot create install script",
PLUGIN_NAME);
success = copy_elevated(archive_path, final_path);
}
}
#else
success = rename(archive_path, final_path) == 0;
if (is_targz(archive_path)) {
char cmd[2048];
snprintf(cmd, sizeof(cmd),
"tar xzf '%s' -C '%s'", archive_path, obs_root);
success = system(cmd) == 0;
if (!success) {
char cp_cmd[1024];
snprintf(cp_cmd, sizeof(cp_cmd),
"pkexec cp '%s' '%s'",
archive_path, final_path);
success = system(cp_cmd) == 0;
snprintf(cmd, sizeof(cmd),
"pkexec tar xzf '%s' -C '%s'",
archive_path, obs_root);
success = system(cmd) == 0;
}
} else {
char cmd[2048];
snprintf(cmd, sizeof(cmd),
"unzip -o '%s' -d '%s'", archive_path, obs_root);
success = system(cmd) == 0;
if (!success) {
snprintf(cmd, sizeof(cmd),
"pkexec unzip -o '%s' -d '%s'",
archive_path, obs_root);
success = system(cmd) == 0;
}
}
if (success && version && version[0])
downloader_write_version_file(obs_plugin_dir, slug, version);
#endif
}
remove(archive_path);
if (success) {
dbg_log(LOG_INFO, "[%s] Installed %s to %s",
PLUGIN_NAME, slug, final_path);
PLUGIN_NAME, slug, obs_root);
} else {
#ifdef _WIN32
DWORD err = GetLastError();
set_error("Failed to copy to %s (error %lu)", final_path, err);
#else
set_error("Failed to copy to %s", final_path);
#endif
set_error("Failed to extract to %s", obs_root);
dbg_log(LOG_ERROR, "[%s] Failed to install %s to %s",
PLUGIN_NAME, slug, final_path);
PLUGIN_NAME, slug, obs_root);
}
return success;
}
bool downloader_uninstall_plugin(const char *slug, const char *obs_plugin_dir)
{
if (!slug || !obs_plugin_dir) return false;
char dll_path[512];
char ver_path[512];
#ifdef _WIN32
snprintf(dll_path, sizeof(dll_path), "%s\\%s.dll", obs_plugin_dir, slug);
snprintf(ver_path, sizeof(ver_path), "%s\\.%s.version", obs_plugin_dir, slug);
#else
snprintf(dll_path, sizeof(dll_path), "%s/%s.so", obs_plugin_dir, slug);
snprintf(ver_path, sizeof(ver_path), "%s/.%s.version", obs_plugin_dir, slug);
#endif
blog(LOG_INFO, "[%s] Uninstalling %s from %s", PLUGIN_NAME, slug, dll_path);
char tmp_dir[512];
get_temp_dir(tmp_dir, sizeof(tmp_dir));
#ifdef _WIN32
{
DWORD pid = GetCurrentProcessId();
char exe[512];
GetModuleFileNameA(NULL, exe, sizeof(exe));
char script_path[512];
snprintf(script_path, sizeof(script_path),
"%s\\st_pm_uninstall.ps1", tmp_dir);
FILE *sf = fopen(script_path, "w");
if (!sf) {
set_error("Cannot create uninstall script");
return false;
}
/* Derive OBS bin directory from exe path */
char obs_bin[512];
snprintf(obs_bin, sizeof(obs_bin), "%s", exe);
char *last_slash = strrchr(obs_bin, '\\');
if (last_slash) *last_slash = '\0';
fprintf(sf, "do { Start-Sleep -Seconds 1 } while (Get-Process -Id %lu -ErrorAction SilentlyContinue)\n",
(unsigned long)pid);
fprintf(sf, "Start-Sleep -Seconds 2\n");
fprintf(sf, "Remove-Item -LiteralPath '%s' -Force -ErrorAction SilentlyContinue\n", dll_path);
fprintf(sf, "if (Test-Path -LiteralPath '%s') { (Get-Item -LiteralPath '%s' -Force).Attributes = 'Normal'; Remove-Item -LiteralPath '%s' -Force }\n",
ver_path, ver_path, ver_path);
fprintf(sf, "Start-Sleep -Seconds 2\n");
fprintf(sf, "Start-Process -FilePath '%s' -WorkingDirectory '%s'\n", exe, obs_bin);
fprintf(sf, "Remove-Item -LiteralPath '%s' -Force\n", script_path);
fclose(sf);
char ps_args[2048];
snprintf(ps_args, sizeof(ps_args),
"-NoProfile -WindowStyle Hidden "
"-ExecutionPolicy Bypass -File \"%s\"",
script_path);
SHELLEXECUTEINFOA sei;
memset(&sei, 0, sizeof(sei));
sei.cbSize = sizeof(sei);
sei.lpVerb = "runas";
sei.lpFile = "powershell.exe";
sei.lpParameters = ps_args;
sei.nShow = SW_HIDE;
if (!ShellExecuteExA(&sei)) {
remove(script_path);
set_error("UAC denied");
return false;
}
}
#else
{
pid_t pid = getpid();
/* Get OBS executable path (platform-specific) */
char exe[512] = "";
#ifdef __APPLE__
uint32_t exe_size = sizeof(exe);
_NSGetExecutablePath(exe, &exe_size);
#else
ssize_t len = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
if (len > 0) exe[len] = '\0';
#endif
char script_path[512];
snprintf(script_path, sizeof(script_path),
"%s/st_pm_uninstall.sh", tmp_dir);
FILE *sf = fopen(script_path, "w");
if (!sf) {
set_error("Cannot create uninstall script");
return false;
}
/* Detect if we need elevated privileges */
bool needs_elevation = (access(obs_plugin_dir, W_OK) != 0);
fprintf(sf, "#!/bin/sh\n");
fprintf(sf, "while kill -0 %d 2>/dev/null; do sleep 1; done\n", pid);
fprintf(sf, "sleep 2\n");
if (needs_elevation) {
fprintf(sf, "pkexec rm -f '%s' '%s'\n", dll_path, ver_path);
} else {
fprintf(sf, "rm -f '%s' '%s'\n", dll_path, ver_path);
}
fprintf(sf, "sleep 2\n");
if (exe[0]) {
fprintf(sf, "nohup '%s' >/dev/null 2>&1 &\n", exe);
}
fprintf(sf, "rm -f '%s'\n", script_path);
fclose(sf);
chmod(script_path, 0755);
if (fork() == 0) {
setsid();
execl("/bin/sh", "sh", script_path, NULL);
_exit(1);
}
}
#endif
blog(LOG_INFO, "[%s] Uninstall script created for %s", PLUGIN_NAME, slug);
return true;
}
+3 -1
View File
@@ -29,7 +29,9 @@ struct plugin_list {
bool downloader_fetch_plugin_list(const char *token, struct plugin_list *out);
bool downloader_install_plugin(const char *token, const char *slug,
int asset_id, const char *obs_plugin_dir);
int asset_id, const char *version,
const char *obs_plugin_dir);
bool downloader_uninstall_plugin(const char *slug, const char *obs_plugin_dir);
void downloader_detect_installed(struct plugin_list *list,
const char *obs_plugin_dir);
bool downloader_get_obs_plugin_dir(char *buf, size_t sz);
+144 -13
View File
@@ -15,6 +15,14 @@
#include <QThread>
#include <QTimer>
#ifdef _WIN32
#include <windows.h>
#include <shellapi.h>
#else
#include <unistd.h>
#include <sys/stat.h>
#endif
#include <obs-frontend-api.h>
extern "C" {
@@ -79,11 +87,11 @@ public:
mainLayout->addWidget(sep);
/* ---- Plugin table ---- */
m_table = new QTableWidget(0, 4);
m_table = new QTableWidget(0, 5);
m_table->setHorizontalHeaderLabels(
{de ? "Plugin" : "Plugin",
de ? "Installiert" : "Installed",
de ? "Verfügbar" : "Available", ""});
de ? "Verfügbar" : "Available", "", ""});
m_table->horizontalHeader()->setStretchLastSection(false);
m_table->horizontalHeader()->setSectionResizeMode(
0, QHeaderView::Stretch);
@@ -93,9 +101,12 @@ public:
2, QHeaderView::Fixed);
m_table->horizontalHeader()->setSectionResizeMode(
3, QHeaderView::Fixed);
m_table->horizontalHeader()->setSectionResizeMode(
4, QHeaderView::Fixed);
m_table->setColumnWidth(1, 120);
m_table->setColumnWidth(2, 90);
m_table->setColumnWidth(3, 110);
m_table->setColumnWidth(4, 110);
m_table->verticalHeader()->setVisible(false);
m_table->setSelectionMode(QAbstractItemView::NoSelection);
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
@@ -273,6 +284,16 @@ private:
connect(btn, &QPushButton::clicked, this,
&ManagerDialog::onInstallClicked);
m_table->setCellWidget(i, 3, btn);
auto *delBtn = new QPushButton(
de ? "Deinstallieren" : "Uninstall");
delBtn->setEnabled(pi->installed);
delBtn->setProperty("slug", QString(pi->slug));
delBtn->setStyleSheet(
"QPushButton { color: #cc3333; }");
connect(delBtn, &QPushButton::clicked, this,
&ManagerDialog::onUninstallClicked);
m_table->setCellWidget(i, 4, delBtn);
}
}
@@ -309,23 +330,85 @@ private:
bool ok = downloader_install_plugin(
auth_get_token(), slug.toUtf8().constData(),
asset_id, obs_dir);
asset_id, version.toUtf8().constData(), obs_dir);
if (ok) {
downloader_write_version_file(
obs_dir, slug.toUtf8().constData(),
version.toUtf8().constData());
btn->setText(de ? "Aktuell" : "Up to date");
btn->setEnabled(false);
QMessageBox::information(
this, "stools Plugin Manager",
QString(de ? "%1 wurde installiert. Bitte OBS neu starten."
: "%1 has been installed. Please restart OBS.")
.arg(slug));
onRefresh();
auto answer = QMessageBox::question(
this, "stools Plugin Manager",
de ? "Installation erfolgreich!\nOBS jetzt neu starten?"
: "Installation successful!\nRestart OBS now?",
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes);
if (answer == QMessageBox::Yes) {
char exe[512];
#ifdef _WIN32
DWORD pid = GetCurrentProcessId();
GetModuleFileNameA(NULL, exe, sizeof(exe));
char obs_bin[512];
snprintf(obs_bin, sizeof(obs_bin), "%s", exe);
char *ls = strrchr(obs_bin, '\\');
if (ls) *ls = '\0';
char tmp[512];
GetTempPathA(sizeof(tmp), tmp);
char ps_path[512];
snprintf(ps_path, sizeof(ps_path), "%sst_pm_restart.ps1", tmp);
FILE *bf = fopen(ps_path, "w");
if (bf) {
fprintf(bf, "do { Start-Sleep -Seconds 1 } while (Get-Process -Id %lu -ErrorAction SilentlyContinue)\n",
(unsigned long)pid);
fprintf(bf, "Start-Sleep -Seconds 2\n");
fprintf(bf, "Start-Process -FilePath '%s' -WorkingDirectory '%s'\n", exe, obs_bin);
fprintf(bf, "Remove-Item -LiteralPath '%s' -Force\n", ps_path);
fclose(bf);
char ps_args[2048];
snprintf(ps_args, sizeof(ps_args),
"-NoProfile -WindowStyle Hidden "
"-ExecutionPolicy Bypass -File \"%s\"",
ps_path);
ShellExecuteA(NULL, "open", "powershell.exe",
ps_args, NULL, SW_HIDE);
}
#else
{
pid_t pid = getpid();
char tmp_dir[256] = "/tmp";
char script[512];
snprintf(script, sizeof(script),
"%s/st_pm_restart.sh", tmp_dir);
FILE *sf = fopen(script, "w");
if (sf) {
fprintf(sf, "#!/bin/sh\n");
fprintf(sf, "while kill -0 %d 2>/dev/null; do sleep 1; done\n", pid);
fprintf(sf, "sleep 2\n");
#ifdef __APPLE__
fprintf(sf, "open -a OBS\n");
#else
char self[512] = "";
ssize_t l = readlink("/proc/self/exe", self, sizeof(self)-1);
if (l > 0) { self[l] = '\0'; fprintf(sf, "nohup '%s' >/dev/null 2>&1 &\n", self); }
#endif
fprintf(sf, "rm -f '%s'\n", script);
fclose(sf);
chmod(script, 0755);
if (fork() == 0) {
setsid();
execl("/bin/sh", "sh", script, (char *)NULL);
_exit(1);
}
}
}
#endif
/* Gracefully close OBS via main window */
QMainWindow *main = (QMainWindow *)obs_frontend_get_main_window();
if (main)
main->close();
}
} else {
const char *err = downloader_last_error();
QString detail = err && err[0]
@@ -342,6 +425,54 @@ private:
btn->setEnabled(true);
}
}
void onUninstallClicked()
{
auto *btn = qobject_cast<QPushButton *>(sender());
if (!btn) return;
QString slug = btn->property("slug").toString();
bool de = is_de(m_locale.toUtf8().constData());
auto answer = QMessageBox::question(
this, "stools Plugin Manager",
QString(de ? "%1 deinstallieren?\nOBS wird dafür neu gestartet."
: "Uninstall %1?\nOBS will restart for this.").arg(slug),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No);
if (answer != QMessageBox::Yes) return;
char obs_dir[512];
if (!downloader_get_obs_plugin_dir(obs_dir, sizeof(obs_dir))) {
QMessageBox::critical(
this, "stools Plugin Manager",
de ? "OBS Plugin-Verzeichnis nicht gefunden."
: "OBS plugin directory not found.");
return;
}
bool ok = downloader_uninstall_plugin(
slug.toUtf8().constData(), obs_dir);
if (ok) {
/* Script is running in background, waiting for OBS to exit.
Close OBS gracefully - script will delete files then restart. */
QMainWindow *main = (QMainWindow *)obs_frontend_get_main_window();
if (main)
main->close();
} else {
const char *err = downloader_last_error();
QString detail = err && err[0]
? QString::fromUtf8(err)
: (de ? "Unbekannter Fehler" : "Unknown error");
QMessageBox::critical(
this, "stools Plugin Manager",
QString(de ? "Deinstallation von %1 fehlgeschlagen:\n%2"
: "Uninstall of %1 failed:\n%2")
.arg(slug, detail));
}
}
};
void manager_dialog_show(const char *locale)