Skip to content

Commit e79a9d1

Browse files
committed
fix(playnite): trust IPC when reporting plugin status
Disconnected or signed-out RDP sessions can keep the Playnite connector alive, but the status API treated a failed per-user extension-path lookup as proof that the plugin was missing. That led the dashboard to tell users to reinstall the plugin even while IPC connectivity and sync still worked. Treat an active Playnite IPC connection as authoritative evidence that the plugin is installed, fall back to the extension file check only when IPC is inactive, and return an unknown install state when neither check can verify it. Update the Playnite UI flows to prefer active connectivity and only show missing-plugin recovery when the backend explicitly reports uninstalled. Generated with [Codex](https://openai.com/index/introducing-gpt-5-3-codex/) Model: GPT-5.4 X-High (cherry picked from commit 692a9f637f55f2aa4a085af1bcbafb06918b20fa)
1 parent e10b270 commit e79a9d1

File tree

5 files changed

+50
-35
lines changed

5 files changed

+50
-35
lines changed

src/confighttp_playnite.cpp

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,32 @@ namespace confighttp {
6060
void bad_request(resp_https_t response, req_https_t request, const std::string &error_message = "Bad Request");
6161
bool check_content_type(resp_https_t response, req_https_t request, const std::string_view &contentType);
6262

63-
// Helper: check if the Sunshine Playnite plugin is installed (by presence of files)
64-
static bool is_plugin_installed() {
63+
struct playnite_install_state_t {
64+
std::optional<bool> installed;
65+
std::filesystem::path extensions_dir;
66+
};
67+
68+
// Helper: determine whether the Playnite plugin is installed.
69+
// An active IPC connection is authoritative proof that the plugin is loaded,
70+
// even if the current service context cannot resolve the user's extension path.
71+
static playnite_install_state_t query_plugin_install_state(bool active) {
72+
playnite_install_state_t state;
6573
try {
6674
std::string destPath;
67-
if (!platf::playnite::get_extension_target_dir(destPath)) {
68-
return false;
75+
if (platf::playnite::get_extension_target_dir(destPath)) {
76+
state.extensions_dir = destPath;
77+
state.installed =
78+
std::filesystem::exists(state.extensions_dir / "extension.yaml") &&
79+
std::filesystem::exists(state.extensions_dir / "SunshinePlaynite.psm1");
80+
} else if (active) {
81+
state.installed = true;
6982
}
70-
std::filesystem::path dest = destPath;
71-
return std::filesystem::exists(dest / "extension.yaml") && std::filesystem::exists(dest / "SunshinePlaynite.psm1");
7283
} catch (...) {
73-
return false;
84+
if (active) {
85+
state.installed = true;
86+
}
7487
}
88+
return state;
7589
}
7690

7791
// Enhance app JSON with a Playnite-derived cover path when applicable.
@@ -101,22 +115,19 @@ namespace confighttp {
101115
platf::playnite::ensure_client_for_api();
102116
nlohmann::json out;
103117
// Active reflects current pipe/server connection only
104-
out["active"] = platf::playnite::is_active();
118+
const bool active = platf::playnite::is_active();
119+
out["active"] = active;
105120
// Deprecated fields removed: playnite_running, installed_unknown
106-
std::string destPath;
107-
std::filesystem::path dest;
108-
// Session requirement removed: IPC is available during RDP/lock; rely on Playnite process presence instead.
109-
// Resolve the user's Playnite extensions directory via URL association.
110-
// Requires user impersonation when running as SYSTEM.
111-
if (platf::playnite::get_extension_target_dir(destPath)) {
112-
dest = destPath;
113-
bool installed = std::filesystem::exists(dest / "extension.yaml") && std::filesystem::exists(dest / "SunshinePlaynite.psm1");
114-
out["installed"] = installed;
115-
out["extensions_dir"] = dest.string();
121+
// Session requirement removed: IPC is available during RDP/lock; rely on
122+
// active IPC first, then fall back to per-user extension path resolution.
123+
const auto install_state = query_plugin_install_state(active);
124+
const auto &dest = install_state.extensions_dir;
125+
if (install_state.installed.has_value()) {
126+
out["installed"] = *install_state.installed;
116127
} else {
117-
out["installed"] = false;
118-
out["extensions_dir"] = std::string();
128+
out["installed"] = nullptr;
119129
}
130+
out["extensions_dir"] = dest.string();
120131
// Version info and update flag
121132
auto normalize_ver = [](std::string s) {
122133
// strip leading 'v' and whitespace
@@ -176,7 +187,7 @@ namespace confighttp {
176187
out["packaged_version"] = packaged_ver;
177188
}
178189
bool update_available = false;
179-
if (out["installed"].get<bool>() && have_installed && have_packaged) {
190+
if (out["installed"].is_boolean() && out["installed"].get<bool>() && have_installed && have_packaged) {
180191
update_available = semver_cmp(installed_ver, packaged_ver) < 0;
181192
}
182193
out["update_available"] = update_available;
@@ -198,7 +209,7 @@ namespace confighttp {
198209
}
199210
print_req(request);
200211
try {
201-
if (!is_plugin_installed()) {
212+
if (!query_plugin_install_state(platf::playnite::is_active()).installed.value_or(false)) {
202213
SimpleWeb::CaseInsensitiveMultimap headers;
203214
headers.emplace("Content-Type", "application/json");
204215
headers.emplace("X-Frame-Options", "DENY");
@@ -228,7 +239,7 @@ namespace confighttp {
228239
}
229240
print_req(request);
230241
try {
231-
if (!is_plugin_installed()) {
242+
if (!query_plugin_install_state(platf::playnite::is_active()).installed.value_or(false)) {
232243
SimpleWeb::CaseInsensitiveMultimap headers;
233244
headers.emplace("Content-Type", "application/json");
234245
headers.emplace("X-Frame-Options", "DENY");
@@ -258,7 +269,7 @@ namespace confighttp {
258269
}
259270
print_req(request);
260271
try {
261-
if (!is_plugin_installed()) {
272+
if (!query_plugin_install_state(platf::playnite::is_active()).installed.value_or(false)) {
262273
SimpleWeb::CaseInsensitiveMultimap headers;
263274
headers.emplace("Content-Type", "application/json");
264275
headers.emplace("X-Frame-Options", "DENY");

src_assets/common/assets/web/components/AppEditModal.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2466,7 +2466,8 @@ async function refreshPlayniteStatus() {
24662466
const r = await http.get('/api/playnite/status', { validateStatus: () => true });
24672467
if (r.status === 200 && r.data && typeof r.data === 'object' && r.data !== null) {
24682468
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2469-
playniteInstalled.value = !!(r.data as any).installed;
2469+
const data = r.data as any;
2470+
playniteInstalled.value = data.installed === true || data.active === true;
24702471
}
24712472
} catch (_) {}
24722473
}

src_assets/common/assets/web/configs/tabs/Playnite.vue

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -719,15 +719,15 @@ const platform = computed(() =>
719719
const { t } = useI18n();
720720
721721
const status = reactive<{
722-
installed: boolean;
722+
installed: boolean | null;
723723
installed_unknown?: boolean;
724724
active: boolean;
725725
enabled?: boolean;
726726
playnite_running?: boolean;
727727
extensions_dir: string;
728728
plugin_version?: string;
729729
plugin_latest?: string;
730-
}>({ installed: false, active: false, extensions_dir: '' });
730+
}>({ installed: null, active: false, extensions_dir: '' });
731731
const launching = ref(false);
732732
const uninstalling = ref(false);
733733
const deletingAutosync = ref(false);
@@ -872,7 +872,7 @@ async function refreshStatus() {
872872
const r = await http.get('/api/playnite/status');
873873
if (r.status === 200 && r.data) {
874874
const d = r.data as any;
875-
status.installed = !!d.installed;
875+
status.installed = typeof d.installed === 'boolean' ? d.installed : null;
876876
status.active = !!d.active;
877877
// 'enabled' is no longer a config; presence is indicated by 'installed'
878878
if (typeof d.playnite_running === 'boolean') status.playnite_running = !!d.playnite_running;
@@ -1246,9 +1246,11 @@ onUnmounted(() => {
12461246
});
12471247
12481248
const statusKind = computed<'active' | 'waiting' | 'uninstalled' | 'unknown'>(() => {
1249+
if (status.active) return 'active';
12491250
if (!status.extensions_dir) return 'unknown';
1250-
if (!status.installed) return 'uninstalled';
1251-
return status.active ? 'active' : 'waiting';
1251+
if (status.installed === false) return 'uninstalled';
1252+
if (status.installed === true) return 'waiting';
1253+
return 'unknown';
12521254
});
12531255
const statusType = computed<'success' | 'warning' | 'error' | 'default'>(() => {
12541256
switch (statusKind.value) {
@@ -1300,12 +1302,12 @@ function cmpSemver(a?: string, b?: string): number {
13001302
}
13011303
13021304
const pluginOutdated = computed(() => {
1303-
if (!status.installed) return false;
1305+
if (status.installed !== true) return false;
13041306
if (!status.plugin_version || !status.plugin_latest) return false;
13051307
return cmpSemver(status.plugin_version, status.plugin_latest) < 0;
13061308
});
13071309
const canLaunch = computed(() => {
1308-
return !!(status.extensions_dir && status.installed && !status.active);
1310+
return !!(status.extensions_dir && status.installed === true && !status.active);
13091311
});
13101312
13111313
const statusTimer = ref<number | undefined>();

src_assets/common/assets/web/views/ApplicationsView.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,8 @@ async function fetchPlayniteStatus(): Promise<void> {
256256
'installed' in (r.data as Record<string, unknown>)
257257
) {
258258
// eslint-disable-next-line @typescript-eslint/no-explicit-any
259-
playniteInstalled.value = !!(r.data as any).installed;
259+
const data = r.data as any;
260+
playniteInstalled.value = data.installed === true || data.active === true;
260261
}
261262
} catch {
262263
// ignore; will retry on next auth change

src_assets/common/assets/web/views/DashboardView.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ const vigemInstalled = ref<boolean | null>(null);
495495
const vigemVersion = ref('');
496496
// Playnite extension status
497497
type PlayniteStatus = {
498-
installed: boolean;
498+
installed: boolean | null;
499499
active: boolean;
500500
extensions_dir?: string;
501501
installed_version?: string;
@@ -1023,7 +1023,7 @@ const hasPlayniteFullscreenApp = computed(() => {
10231023
const showPlayniteMissingPluginBanner = computed(() => {
10241024
const plat = (configStore.metadata?.platform || '').toLowerCase();
10251025
if (plat !== 'windows') return false;
1026-
if (!playnite.value || playnite.value.installed !== false) return false;
1026+
if (!playnite.value || playnite.value.active === true || playnite.value.installed !== false) return false;
10271027
return playniteAutoSyncedAppsCount.value > 0 || hasPlayniteFullscreenApp.value;
10281028
});
10291029
const playniteMissingPluginBannerText = computed(() => {

0 commit comments

Comments
 (0)