From 088cb54317c5f181ebb9d4d754a8c3d73d351902 Mon Sep 17 00:00:00 2001 From: Josiah Glosson Date: Mon, 7 Jul 2025 14:11:36 -0500 Subject: [PATCH 01/32] Fix failure when "Test"ing a Java installation (#3935) * Fix failure when "Test"ing a Java installation * Fix lint --- apps/app-frontend/src/components/ui/JavaSelector.vue | 1 - apps/app-frontend/src/helpers/jre.js | 4 ++-- packages/app-lib/src/api/jre.rs | 12 ++++++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/app-frontend/src/components/ui/JavaSelector.vue b/apps/app-frontend/src/components/ui/JavaSelector.vue index 9beea538..ebbeed89 100644 --- a/apps/app-frontend/src/components/ui/JavaSelector.vue +++ b/apps/app-frontend/src/components/ui/JavaSelector.vue @@ -108,7 +108,6 @@ async function testJava() { testingJava.value = true testingJavaSuccess.value = await test_jre( props.modelValue ? props.modelValue.path : '', - 1, props.version, ) testingJava.value = false diff --git a/apps/app-frontend/src/helpers/jre.js b/apps/app-frontend/src/helpers/jre.js index 0814e9b0..207c0258 100644 --- a/apps/app-frontend/src/helpers/jre.js +++ b/apps/app-frontend/src/helpers/jre.js @@ -36,8 +36,8 @@ export async function get_jre(path) { // Tests JRE version by running 'java -version' on it. // Returns true if the version is valid, and matches given (after extraction) -export async function test_jre(path, majorVersion, minorVersion) { - return await invoke('plugin:jre|jre_test_jre', { path, majorVersion, minorVersion }) +export async function test_jre(path, majorVersion) { + return await invoke('plugin:jre|jre_test_jre', { path, majorVersion }) } // Automatically installs specified java version diff --git a/packages/app-lib/src/api/jre.rs b/packages/app-lib/src/api/jre.rs index 93a63674..412d4472 100644 --- a/packages/app-lib/src/api/jre.rs +++ b/packages/app-lib/src/api/jre.rs @@ -166,10 +166,18 @@ pub async fn test_jre( path: PathBuf, major_version: u32, ) -> crate::Result { - let Ok(jre) = jre::check_java_at_filepath(&path).await else { - return Ok(false); + let jre = match jre::check_java_at_filepath(&path).await { + Ok(jre) => jre, + Err(e) => { + tracing::warn!("Invalid Java at {}: {e}", path.display()); + return Ok(false); + } }; let version = extract_java_version(&jre.version)?; + tracing::info!( + "Expected Java version {major_version}, and found {version} at {}", + path.display() + ); Ok(version == major_version) } -- 2.49.1 From 8ba6467f21ba495fdfa07816d8d89bdad80a189e Mon Sep 17 00:00:00 2001 From: Prospector Date: Mon, 7 Jul 2025 16:49:17 -0700 Subject: [PATCH 02/32] temp: do not retry MRS requests --- apps/frontend/src/composables/servers/servers-fetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/composables/servers/servers-fetch.ts b/apps/frontend/src/composables/servers/servers-fetch.ts index 137baea5..804960b6 100644 --- a/apps/frontend/src/composables/servers/servers-fetch.ts +++ b/apps/frontend/src/composables/servers/servers-fetch.ts @@ -134,7 +134,7 @@ export async function useServersFetch( ? errorMessages[statusCode] : `HTTP Error: ${statusCode || "unknown"} ${statusText}`; - const isRetryable = statusCode ? [408, 429, 500, 502, 504].includes(statusCode) : true; + const isRetryable = false; if (!isRetryable || attempts >= maxAttempts) { console.error("Fetch error:", error); -- 2.49.1 From e4e77dc0d27f1ee1a9c2ed34b161147cc3a9adba Mon Sep 17 00:00:00 2001 From: Prospector Date: Mon, 7 Jul 2025 17:07:27 -0700 Subject: [PATCH 03/32] Revert "temp: do not retry MRS requests" This reverts commit 8ba6467f21ba495fdfa07816d8d89bdad80a189e. --- apps/frontend/src/composables/servers/servers-fetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/composables/servers/servers-fetch.ts b/apps/frontend/src/composables/servers/servers-fetch.ts index 804960b6..137baea5 100644 --- a/apps/frontend/src/composables/servers/servers-fetch.ts +++ b/apps/frontend/src/composables/servers/servers-fetch.ts @@ -134,7 +134,7 @@ export async function useServersFetch( ? errorMessages[statusCode] : `HTTP Error: ${statusCode || "unknown"} ${statusText}`; - const isRetryable = false; + const isRetryable = statusCode ? [408, 429, 500, 502, 504].includes(statusCode) : true; if (!isRetryable || attempts >= maxAttempts) { console.error("Fetch error:", error); -- 2.49.1 From e0cde2d6ff1187b2ee2346ec6aea67a3190573cd Mon Sep 17 00:00:00 2001 From: Prospector Date: Mon, 7 Jul 2025 17:37:43 -0700 Subject: [PATCH 04/32] Revert "fix: error handling improvements (#3797)" This reverts commit 706976439db46125f9a9a9ea177ac9074d4147a8. --- .../composables/servers/modrinth-servers.ts | 111 +++--- .../composables/servers/modules/general.ts | 30 +- .../src/pages/servers/manage/[id].vue | 330 ++++++++---------- .../components/base/ErrorInformationCard.vue | 120 ------- packages/ui/src/components/index.ts | 1 - .../servers/errors/modrinth-server-error.ts | 2 +- .../errors/modrinth-servers-fetch-error.ts | 2 +- 7 files changed, 198 insertions(+), 398 deletions(-) delete mode 100644 packages/ui/src/components/base/ErrorInformationCard.vue diff --git a/apps/frontend/src/composables/servers/modrinth-servers.ts b/apps/frontend/src/composables/servers/modrinth-servers.ts index e91bb853..8c79cc62 100644 --- a/apps/frontend/src/composables/servers/modrinth-servers.ts +++ b/apps/frontend/src/composables/servers/modrinth-servers.ts @@ -124,63 +124,58 @@ export class ModrinthServer { return dataURL; } } catch (error) { - if (error instanceof ModrinthServerError && error.statusCode === 404) { - if (iconUrl) { - try { - const response = await fetch(iconUrl); - if (!response.ok) throw new Error("Failed to fetch icon"); - const file = await response.blob(); - const originalFile = new File([file], "server-icon-original.png", { - type: "image/png", - }); + if (error instanceof ModrinthServerError && error.statusCode === 404 && iconUrl) { + // Handle external icon processing + try { + const response = await fetch(iconUrl); + if (!response.ok) throw new Error("Failed to fetch icon"); + const file = await response.blob(); + const originalFile = new File([file], "server-icon-original.png", { + type: "image/png", + }); - if (import.meta.client) { - const dataURL = await new Promise((resolve) => { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - const img = new Image(); - img.onload = () => { - canvas.width = 64; - canvas.height = 64; - ctx?.drawImage(img, 0, 0, 64, 64); - canvas.toBlob(async (blob) => { - if (blob) { - const scaledFile = new File([blob], "server-icon.png", { - type: "image/png", - }); - await useServersFetch(`/create?path=/server-icon.png&type=file`, { - method: "POST", - contentType: "application/octet-stream", - body: scaledFile, - override: auth, - }); - await useServersFetch(`/create?path=/server-icon-original.png&type=file`, { - method: "POST", - contentType: "application/octet-stream", - body: originalFile, - override: auth, - }); - } - }, "image/png"); - const dataURL = canvas.toDataURL("image/png"); - sharedImage.value = dataURL; - resolve(dataURL); - URL.revokeObjectURL(img.src); - }; - img.src = URL.createObjectURL(file); - }); - return dataURL; - } - } catch (externalError: any) { - console.debug("Could not process external icon:", externalError.message); + if (import.meta.client) { + const dataURL = await new Promise((resolve) => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const img = new Image(); + img.onload = () => { + canvas.width = 64; + canvas.height = 64; + ctx?.drawImage(img, 0, 0, 64, 64); + canvas.toBlob(async (blob) => { + if (blob) { + const scaledFile = new File([blob], "server-icon.png", { type: "image/png" }); + await useServersFetch(`/create?path=/server-icon.png&type=file`, { + method: "POST", + contentType: "application/octet-stream", + body: scaledFile, + override: auth, + }); + await useServersFetch(`/create?path=/server-icon-original.png&type=file`, { + method: "POST", + contentType: "application/octet-stream", + body: originalFile, + override: auth, + }); + } + }, "image/png"); + const dataURL = canvas.toDataURL("image/png"); + sharedImage.value = dataURL; + resolve(dataURL); + URL.revokeObjectURL(img.src); + }; + img.src = URL.createObjectURL(file); + }); + return dataURL; } + } catch (error) { + console.error("Failed to process external icon:", error); } - } else { - throw error; } } - } catch (error: any) { - console.debug("Icon processing failed:", error.message); + } catch (error) { + console.error("Failed to process server icon:", error); } sharedImage.value = undefined; @@ -244,18 +239,6 @@ export class ModrinthServer { break; } } catch (error) { - if (error instanceof ModrinthServerError) { - if (error.statusCode === 404 && ["fs", "content"].includes(module)) { - console.debug(`Optional ${module} resource not found:`, error.message); - continue; - } - - if (error.statusCode === 503) { - console.debug(`Temporary ${module} unavailable:`, error.message); - continue; - } - } - this.errors[module] = { error: error instanceof ModrinthServerError diff --git a/apps/frontend/src/composables/servers/modules/general.ts b/apps/frontend/src/composables/servers/modules/general.ts index b2f10065..9e8a4c4c 100644 --- a/apps/frontend/src/composables/servers/modules/general.ts +++ b/apps/frontend/src/composables/servers/modules/general.ts @@ -194,25 +194,19 @@ export class GeneralModule extends ServerModule implements ServerGeneral { } async setMotd(motd: string): Promise { - try { - const props = (await this.server.fetchConfigFile("ServerProperties")) as any; - if (props) { - props.motd = motd; - const newProps = this.server.constructServerProperties(props); - const octetStream = new Blob([newProps], { type: "application/octet-stream" }); - const auth = await useServersFetch(`servers/${this.serverId}/fs`); + const props = (await this.server.fetchConfigFile("ServerProperties")) as any; + if (props) { + props.motd = motd; + const newProps = this.server.constructServerProperties(props); + const octetStream = new Blob([newProps], { type: "application/octet-stream" }); + const auth = await useServersFetch(`servers/${this.serverId}/fs`); - await useServersFetch(`/update?path=/server.properties`, { - method: "PUT", - contentType: "application/octet-stream", - body: octetStream, - override: auth, - }); - } - } catch { - console.error( - "[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.", - ); + await useServersFetch(`/update?path=/server.properties`, { + method: "PUT", + contentType: "application/octet-stream", + body: octetStream, + override: auth, + }); } } } diff --git a/apps/frontend/src/pages/servers/manage/[id].vue b/apps/frontend/src/pages/servers/manage/[id].vue index 566cc1ed..35fd2834 100644 --- a/apps/frontend/src/pages/servers/manage/[id].vue +++ b/apps/frontend/src/pages/servers/manage/[id].vue @@ -18,25 +18,48 @@ v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" > - +
+
+
+
+ +
+

Server upgrading

+
+

+ Your server's hardware is currently being upgraded and will be back online shortly! +

+
+
- +
+
+
+
+ +
+

Server suspended

+
+

+ {{ + serverData.suspension_reason === "cancelled" + ? "Your subscription has been cancelled." + : serverData.suspension_reason + ? `Your server has been suspended: ${serverData.suspension_reason}` + : "Your server has been suspended." + }} +
+ Contact Modrinth Support if you believe this is an error. +

+
+ + + +
- +
+
+
+
+ +
+

Server not found

+
+

+ You don't have permission to view this server or it no longer exists. If you believe this + is an error, please contact Modrinth Support. +

+
+ + + + + +
- - - +

+ Your server's node, where your Modrinth Server is physically hosted, is experiencing + issues. We are working with our datacenter to resolve the issue as quickly as possible. +

+

+ Your data is safe and will not be lost, and your server will be back online as soon as the + issue is resolved. +

+

+ For updates, please join the Modrinth Discord or contact Modrinth Support via the chat + bubble in the bottom right corner and we'll be happy to help. +

+ +
+ + +
+
+ + + + + + +
- - - +

+ Something went wrong, and we couldn't connect to your server. This is likely due to a + temporary network issue. You'll be reconnected automatically. +

+
+ + + + +
{ const stopUptimeUpdates = () => { if (uptimeIntervalId) { clearInterval(uptimeIntervalId); - pollingIntervalId = null; + intervalId = null; } }; @@ -992,7 +1055,7 @@ const notifyError = (title: string, text: string) => { }); }; -let pollingIntervalId: ReturnType | null = null; +let intervalId: ReturnType | null = null; const countdown = ref(15); const formattedTime = computed(() => { @@ -1036,142 +1099,23 @@ const backupInProgress = computed(() => { }); const stopPolling = () => { - if (pollingIntervalId) { - clearTimeout(pollingIntervalId); - pollingIntervalId = null; + if (intervalId) { + clearInterval(intervalId); + intervalId = null; } }; const startPolling = () => { - stopPolling(); - - let retryCount = 0; - const maxRetries = 10; - - const poll = async () => { - try { - await server.refresh(["general", "ws"]); - - if (!server.moduleErrors?.general?.error) { - stopPolling(); - connectWebSocket(); - return; - } - - retryCount++; - if (retryCount >= maxRetries) { - console.error("Max retries reached, stopping polling"); - stopPolling(); - return; - } - - // Exponential backoff: 3s, 6s, 12s, 24s, etc. - const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000); - - pollingIntervalId = setTimeout(poll, delay); - } catch (error) { - console.error("Polling failed:", error); - retryCount++; - - if (retryCount < maxRetries) { - const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000); - pollingIntervalId = setTimeout(poll, delay); - } + countdown.value = 15; + intervalId = setInterval(() => { + if (countdown.value <= 0) { + reloadNuxtApp(); + } else { + countdown.value--; } - }; - - poll(); + }, 1000); }; -const nodeUnavailableDetails = computed(() => [ - { - label: "Server ID", - value: server.serverId, - type: "inline" as const, - }, - { - label: "Node", - value: server.general?.datacenter ?? "Unknown! Please contact support!", - type: "inline" as const, - }, -]); - -const suspendedDescription = computed(() => { - if (serverData.value?.suspension_reason === "cancelled") { - return "Your subscription has been cancelled.\nContact Modrinth Support if you believe this is an error."; - } - if (serverData.value?.suspension_reason) { - return `Your server has been suspended: ${serverData.value.suspension_reason}\nContact Modrinth Support if you believe this is an error.`; - } - return "Your server has been suspended.\nContact Modrinth Support if you believe this is an error."; -}); - -const generalErrorDetails = computed(() => [ - { - label: "Server ID", - value: server.serverId, - type: "inline" as const, - }, - { - label: "Timestamp", - value: String(server.moduleErrors?.general?.timestamp), - type: "inline" as const, - }, - { - label: "Error Name", - value: server.moduleErrors?.general?.error.name, - type: "inline" as const, - }, - { - label: "Error Message", - value: server.moduleErrors?.general?.error.message, - type: "block" as const, - }, - ...(server.moduleErrors?.general?.error.originalError - ? [ - { - label: "Original Error", - value: String(server.moduleErrors.general.error.originalError), - type: "hidden" as const, - }, - ] - : []), - ...(server.moduleErrors?.general?.error.stack - ? [ - { - label: "Stack Trace", - value: server.moduleErrors.general.error.stack, - type: "hidden" as const, - }, - ] - : []), -]); - -const suspendedAction = computed(() => ({ - label: "Go to billing settings", - onClick: () => router.push("/settings/billing"), - color: "brand" as const, -})); - -const generalErrorAction = computed(() => ({ - label: "Go back to all servers", - onClick: () => router.push("/servers/manage"), - color: "brand" as const, -})); - -const nodeUnavailableAction = computed(() => ({ - label: "Join Modrinth Discord", - onClick: () => navigateTo("https://discord.modrinth.com", { external: true }), - color: "standard" as const, -})); - -const connectionLostAction = computed(() => ({ - label: "Reload", - onClick: () => reloadNuxtApp(), - color: "brand" as const, - disabled: formattedTime.value !== "00", -})); - const copyServerDebugInfo = () => { const debugInfo = `Server ID: ${serverData.value?.server_id}\nError: ${errorMessage.value}\nKind: ${serverData.value?.upstream?.kind}\nProject ID: ${serverData.value?.upstream?.project_id}\nVersion ID: ${serverData.value?.upstream?.version_id}\nLog: ${errorLog.value}`; navigator.clipboard.writeText(debugInfo); diff --git a/packages/ui/src/components/base/ErrorInformationCard.vue b/packages/ui/src/components/base/ErrorInformationCard.vue deleted file mode 100644 index 9edfc026..00000000 --- a/packages/ui/src/components/base/ErrorInformationCard.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 3057f43d..9b41b4f5 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -16,7 +16,6 @@ export { default as DoubleIcon } from './base/DoubleIcon.vue' export { default as DropArea } from './base/DropArea.vue' export { default as DropdownSelect } from './base/DropdownSelect.vue' export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue' -export { default as ErrorInformationCard } from './base/ErrorInformationCard.vue' export { default as FileInput } from './base/FileInput.vue' export { default as FilterBar } from './base/FilterBar.vue' export type { FilterBarOption } from './base/FilterBar.vue' diff --git a/packages/utils/servers/errors/modrinth-server-error.ts b/packages/utils/servers/errors/modrinth-server-error.ts index c0a548a7..ece8825c 100644 --- a/packages/utils/servers/errors/modrinth-server-error.ts +++ b/packages/utils/servers/errors/modrinth-server-error.ts @@ -54,6 +54,6 @@ export class ModrinthServerError extends Error { } super(errorMessage) - this.name = 'ModrinthServersFetchError' + this.name = 'PyroServersFetchError' } } diff --git a/packages/utils/servers/errors/modrinth-servers-fetch-error.ts b/packages/utils/servers/errors/modrinth-servers-fetch-error.ts index a2df7baa..ce01e737 100644 --- a/packages/utils/servers/errors/modrinth-servers-fetch-error.ts +++ b/packages/utils/servers/errors/modrinth-servers-fetch-error.ts @@ -5,6 +5,6 @@ export class ModrinthServersFetchError extends Error { public originalError?: Error, ) { super(message) - this.name = 'ModrinthFetchError' + this.name = 'PyroFetchError' } } -- 2.49.1 From f256ef43c06891f333ce9855bf3775c2b7184b03 Mon Sep 17 00:00:00 2001 From: Prospector Date: Mon, 7 Jul 2025 22:16:26 -0700 Subject: [PATCH 05/32] Add x-archon-request header --- apps/frontend/src/composables/servers/servers-fetch.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/frontend/src/composables/servers/servers-fetch.ts b/apps/frontend/src/composables/servers/servers-fetch.ts index 137baea5..cd403fb1 100644 --- a/apps/frontend/src/composables/servers/servers-fetch.ts +++ b/apps/frontend/src/composables/servers/servers-fetch.ts @@ -69,6 +69,7 @@ export async function useServersFetch( const headers: Record = { "User-Agent": "Modrinth/1.0 (https://modrinth.com)", + "X-Archon-Request": "true", Vary: "Accept, Origin", }; -- 2.49.1 From 7a12c4d5e2139a1bdf44747e1b5b021e057ed59f Mon Sep 17 00:00:00 2001 From: IMB11 Date: Tue, 8 Jul 2025 18:40:44 +0100 Subject: [PATCH 06/32] feat: reimplement error handling improvements w/o polling (#3942) * Reapply "fix: error handling improvements (#3797)" This reverts commit e0cde2d6ff1187b2ee2346ec6aea67a3190573cd. * fix: polling issues * fix: circuit breaker logic for spam * fix: remove polling & ping test node instead * fix: remove broken url from debugging * fix: show error information display if node access fails in fs module --- .../composables/servers/modrinth-servers.ts | 164 +++++--- .../src/composables/servers/modules/fs.ts | 33 +- .../composables/servers/modules/general.ts | 49 ++- .../src/composables/servers/servers-fetch.ts | 32 +- .../src/pages/servers/manage/[id].vue | 381 +++++++++--------- .../components/base/ErrorInformationCard.vue | 120 ++++++ packages/ui/src/components/index.ts | 1 + .../servers/errors/modrinth-server-error.ts | 2 +- .../errors/modrinth-servers-fetch-error.ts | 2 +- 9 files changed, 504 insertions(+), 280 deletions(-) create mode 100644 packages/ui/src/components/base/ErrorInformationCard.vue diff --git a/apps/frontend/src/composables/servers/modrinth-servers.ts b/apps/frontend/src/composables/servers/modrinth-servers.ts index 8c79cc62..8d07648d 100644 --- a/apps/frontend/src/composables/servers/modrinth-servers.ts +++ b/apps/frontend/src/composables/servers/modrinth-servers.ts @@ -102,7 +102,7 @@ export class ModrinthServer { try { const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, { override: auth, - retry: false, + retry: 1, // Reduce retries for optional resources }); if (fileData instanceof Blob && import.meta.client) { @@ -124,64 +124,114 @@ export class ModrinthServer { return dataURL; } } catch (error) { - if (error instanceof ModrinthServerError && error.statusCode === 404 && iconUrl) { - // Handle external icon processing - try { - const response = await fetch(iconUrl); - if (!response.ok) throw new Error("Failed to fetch icon"); - const file = await response.blob(); - const originalFile = new File([file], "server-icon-original.png", { - type: "image/png", - }); - - if (import.meta.client) { - const dataURL = await new Promise((resolve) => { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - const img = new Image(); - img.onload = () => { - canvas.width = 64; - canvas.height = 64; - ctx?.drawImage(img, 0, 0, 64, 64); - canvas.toBlob(async (blob) => { - if (blob) { - const scaledFile = new File([blob], "server-icon.png", { type: "image/png" }); - await useServersFetch(`/create?path=/server-icon.png&type=file`, { - method: "POST", - contentType: "application/octet-stream", - body: scaledFile, - override: auth, - }); - await useServersFetch(`/create?path=/server-icon-original.png&type=file`, { - method: "POST", - contentType: "application/octet-stream", - body: originalFile, - override: auth, - }); - } - }, "image/png"); - const dataURL = canvas.toDataURL("image/png"); - sharedImage.value = dataURL; - resolve(dataURL); - URL.revokeObjectURL(img.src); - }; - img.src = URL.createObjectURL(file); - }); - return dataURL; - } - } catch (error) { - console.error("Failed to process external icon:", error); + if (error instanceof ModrinthServerError) { + if (error.statusCode && error.statusCode >= 500) { + console.debug("Service unavailable, skipping icon processing"); + sharedImage.value = undefined; + return undefined; } + + if (error.statusCode === 404 && iconUrl) { + try { + const response = await fetch(iconUrl); + if (!response.ok) throw new Error("Failed to fetch icon"); + const file = await response.blob(); + const originalFile = new File([file], "server-icon-original.png", { + type: "image/png", + }); + + if (import.meta.client) { + const dataURL = await new Promise((resolve) => { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const img = new Image(); + img.onload = () => { + canvas.width = 64; + canvas.height = 64; + ctx?.drawImage(img, 0, 0, 64, 64); + canvas.toBlob(async (blob) => { + if (blob) { + const scaledFile = new File([blob], "server-icon.png", { + type: "image/png", + }); + await useServersFetch(`/create?path=/server-icon.png&type=file`, { + method: "POST", + contentType: "application/octet-stream", + body: scaledFile, + override: auth, + }); + await useServersFetch(`/create?path=/server-icon-original.png&type=file`, { + method: "POST", + contentType: "application/octet-stream", + body: originalFile, + override: auth, + }); + } + }, "image/png"); + const dataURL = canvas.toDataURL("image/png"); + sharedImage.value = dataURL; + resolve(dataURL); + URL.revokeObjectURL(img.src); + }; + img.src = URL.createObjectURL(file); + }); + return dataURL; + } + } catch (externalError: any) { + console.debug("Could not process external icon:", externalError.message); + } + } + } else { + throw error; } } - } catch (error) { - console.error("Failed to process server icon:", error); + } catch (error: any) { + console.debug("Icon processing failed:", error.message); } sharedImage.value = undefined; return undefined; } + async testNodeReachability(): Promise { + if (!this.general?.datacenter) { + console.warn("No datacenter info available for ping test"); + return false; + } + + const datacenter = this.general.datacenter; + const wsUrl = `wss://${datacenter}.nodes.modrinth.com/pingtest`; + + try { + return await new Promise((resolve) => { + const socket = new WebSocket(wsUrl); + const timeout = setTimeout(() => { + socket.close(); + resolve(false); + }, 5000); + + socket.onopen = () => { + clearTimeout(timeout); + socket.send(performance.now().toString()); + }; + + socket.onmessage = () => { + clearTimeout(timeout); + socket.close(); + resolve(true); + }; + + socket.onerror = () => { + clearTimeout(timeout); + resolve(false); + }; + }); + } catch (error) { + console.error(`Failed to ping node ${wsUrl}:`, error); + return false; + } + } + async refresh( modules: ModuleName[] = [], options?: { @@ -195,6 +245,8 @@ export class ModrinthServer { : (["general", "content", "backups", "network", "startup", "ws", "fs"] as ModuleName[]); for (const module of modulesToRefresh) { + this.errors[module] = undefined; + try { switch (module) { case "general": { @@ -239,6 +291,18 @@ export class ModrinthServer { break; } } catch (error) { + if (error instanceof ModrinthServerError) { + if (error.statusCode === 404 && ["fs", "content"].includes(module)) { + console.debug(`Optional ${module} resource not found:`, error.message); + continue; + } + + if (error.statusCode && error.statusCode >= 500) { + console.debug(`Temporary ${module} unavailable:`, error.message); + continue; + } + } + this.errors[module] = { error: error instanceof ModrinthServerError diff --git a/apps/frontend/src/composables/servers/modules/fs.ts b/apps/frontend/src/composables/servers/modules/fs.ts index 1072789e..39fe75db 100644 --- a/apps/frontend/src/composables/servers/modules/fs.ts +++ b/apps/frontend/src/composables/servers/modules/fs.ts @@ -22,26 +22,49 @@ export class FSModule extends ServerModule { this.opsQueuedForModification = []; } - private async retryWithAuth(requestFn: () => Promise): Promise { + private async retryWithAuth( + requestFn: () => Promise, + ignoreFailure: boolean = false, + ): Promise { try { return await requestFn(); } catch (error) { if (error instanceof ModrinthServerError && error.statusCode === 401) { + console.debug("Auth failed, refreshing JWT and retrying"); await this.fetch(); // Refresh auth return await requestFn(); } + + const available = await this.server.testNodeReachability(); + if (!available && !ignoreFailure) { + this.server.moduleErrors.general = { + error: new ModrinthServerError( + "Unable to reach node. FS operation failed and subsequent ping test failed.", + 500, + error as Error, + "fs", + ), + timestamp: Date.now(), + }; + } + throw error; } } - listDirContents(path: string, page: number, pageSize: number): Promise { + listDirContents( + path: string, + page: number, + pageSize: number, + ignoreFailure: boolean = false, + ): Promise { return this.retryWithAuth(async () => { const encodedPath = encodeURIComponent(path); return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, { override: this.auth, retry: false, }); - }); + }, ignoreFailure); } createFileOrFolder(path: string, type: "file" | "directory"): Promise { @@ -150,7 +173,7 @@ export class FSModule extends ServerModule { }); } - downloadFile(path: string, raw?: boolean): Promise { + downloadFile(path: string, raw: boolean = false, ignoreFailure: boolean = false): Promise { return this.retryWithAuth(async () => { const encodedPath = encodeURIComponent(path); const fileData = await useServersFetch(`/download?path=${encodedPath}`, { @@ -161,7 +184,7 @@ export class FSModule extends ServerModule { return raw ? fileData : await fileData.text(); } return fileData; - }); + }, ignoreFailure); } extractFile( diff --git a/apps/frontend/src/composables/servers/modules/general.ts b/apps/frontend/src/composables/servers/modules/general.ts index 9e8a4c4c..e46e62b4 100644 --- a/apps/frontend/src/composables/servers/modules/general.ts +++ b/apps/frontend/src/composables/servers/modules/general.ts @@ -46,13 +46,18 @@ export class GeneralModule extends ServerModule implements ServerGeneral { data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined; } - const motd = await this.getMotd(); - if (motd === "A Minecraft Server") { - await this.setMotd( - `§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`, - ); + try { + const motd = await this.getMotd(); + if (motd === "A Minecraft Server") { + await this.setMotd( + `§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`, + ); + } + data.motd = motd; + } catch { + console.error("[Modrinth Servers] [General] Failed to fetch MOTD."); + data.motd = undefined; } - data.motd = motd; // Copy data to this module Object.assign(this, data); @@ -178,7 +183,7 @@ export class GeneralModule extends ServerModule implements ServerGeneral { async getMotd(): Promise { try { - const props = await this.server.fs.downloadFile("/server.properties"); + const props = await this.server.fs.downloadFile("/server.properties", false, true); if (props) { const lines = props.split("\n"); for (const line of lines) { @@ -194,19 +199,25 @@ export class GeneralModule extends ServerModule implements ServerGeneral { } async setMotd(motd: string): Promise { - const props = (await this.server.fetchConfigFile("ServerProperties")) as any; - if (props) { - props.motd = motd; - const newProps = this.server.constructServerProperties(props); - const octetStream = new Blob([newProps], { type: "application/octet-stream" }); - const auth = await useServersFetch(`servers/${this.serverId}/fs`); + try { + const props = (await this.server.fetchConfigFile("ServerProperties")) as any; + if (props) { + props.motd = motd; + const newProps = this.server.constructServerProperties(props); + const octetStream = new Blob([newProps], { type: "application/octet-stream" }); + const auth = await useServersFetch(`servers/${this.serverId}/fs`); - await useServersFetch(`/update?path=/server.properties`, { - method: "PUT", - contentType: "application/octet-stream", - body: octetStream, - override: auth, - }); + await useServersFetch(`/update?path=/server.properties`, { + method: "PUT", + contentType: "application/octet-stream", + body: octetStream, + override: auth, + }); + } + } catch { + console.error( + "[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.", + ); } } } diff --git a/apps/frontend/src/composables/servers/servers-fetch.ts b/apps/frontend/src/composables/servers/servers-fetch.ts index cd403fb1..5b5d925b 100644 --- a/apps/frontend/src/composables/servers/servers-fetch.ts +++ b/apps/frontend/src/composables/servers/servers-fetch.ts @@ -42,6 +42,23 @@ export async function useServersFetch( retry = method === "GET" ? 3 : 0, } = options; + const circuitBreakerKey = `${module || "default"}_${path}`; + const failureCount = useState(`fetch_failures_${circuitBreakerKey}`, () => 0); + const lastFailureTime = useState(`last_failure_${circuitBreakerKey}`, () => 0); + + const now = Date.now(); + if (failureCount.value >= 3 && now - lastFailureTime.value < 30000) { + const error = new ModrinthServersFetchError( + "[Modrinth Servers] Circuit breaker open - too many recent failures", + 503, + ); + throw new ModrinthServerError("Service temporarily unavailable", 503, error, module); + } + + if (now - lastFailureTime.value > 30000) { + failureCount.value = 0; + } + const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace( /\/$/, "", @@ -99,6 +116,7 @@ export async function useServersFetch( timeout: 10000, }); + failureCount.value = 0; return response; } catch (error) { lastError = error as Error; @@ -108,6 +126,11 @@ export async function useServersFetch( const statusCode = error.response?.status; const statusText = error.response?.statusText || "Unknown error"; + if (statusCode && statusCode >= 500) { + failureCount.value++; + lastFailureTime.value = now; + } + let v1Error: V1ErrorInfo | undefined; if (error.data?.error && error.data?.description) { v1Error = { @@ -135,9 +158,11 @@ export async function useServersFetch( ? errorMessages[statusCode] : `HTTP Error: ${statusCode || "unknown"} ${statusText}`; - const isRetryable = statusCode ? [408, 429, 500, 502, 504].includes(statusCode) : true; + const isRetryable = statusCode ? [408, 429].includes(statusCode) : false; + const is5xxRetryable = + statusCode && statusCode >= 500 && statusCode < 600 && method === "GET" && attempts === 1; - if (!isRetryable || attempts >= maxAttempts) { + if (!(isRetryable || is5xxRetryable) || attempts >= maxAttempts) { console.error("Fetch error:", error); const fetchError = new ModrinthServersFetchError( @@ -148,7 +173,8 @@ export async function useServersFetch( throw new ModrinthServerError(error.message, statusCode, fetchError, module, v1Error); } - const delay = Math.min(1000 * Math.pow(2, attempts - 1) + Math.random() * 1000, 10000); + const baseDelay = statusCode && statusCode >= 500 ? 5000 : 1000; + const delay = Math.min(baseDelay * Math.pow(2, attempts - 1) + Math.random() * 1000, 15000); console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`); await new Promise((resolve) => setTimeout(resolve, delay)); continue; diff --git a/apps/frontend/src/pages/servers/manage/[id].vue b/apps/frontend/src/pages/servers/manage/[id].vue index 35fd2834..1c5d0138 100644 --- a/apps/frontend/src/pages/servers/manage/[id].vue +++ b/apps/frontend/src/pages/servers/manage/[id].vue @@ -18,48 +18,25 @@ v-if="serverData?.status === 'suspended' && serverData.suspension_reason === 'upgrading'" class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast" > -
-
-
-
- -
-

Server upgrading

-
-

- Your server's hardware is currently being upgraded and will be back online shortly! -

-
-
+
-
-
-
-
- -
-

Server suspended

-
-

- {{ - serverData.suspension_reason === "cancelled" - ? "Your subscription has been cancelled." - : serverData.suspension_reason - ? `Your server has been suspended: ${serverData.suspension_reason}` - : "Your server has been suspended." - }} -
- Contact Modrinth Support if you believe this is an error. -

-
- - - -
+
-
-
-
-
- -
-

Server not found

-
-

- You don't have permission to view this server or it no longer exists. If you believe this - is an error, please contact Modrinth Support. -

-
- - - - - -
+
-
-
-
-
- -
-

Server Node Unavailable

+ + +
-
-
-
-
-
- -
-

Connection lost

-
- {{ - formattedTime == "00" ? "Reconnecting..." : `Retrying in ${formattedTime} seconds...` - }} -
+ + + +
-->
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 9b41b4f5..3057f43d 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -16,6 +16,7 @@ export { default as DoubleIcon } from './base/DoubleIcon.vue' export { default as DropArea } from './base/DropArea.vue' export { default as DropdownSelect } from './base/DropdownSelect.vue' export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue' +export { default as ErrorInformationCard } from './base/ErrorInformationCard.vue' export { default as FileInput } from './base/FileInput.vue' export { default as FilterBar } from './base/FilterBar.vue' export type { FilterBarOption } from './base/FilterBar.vue' diff --git a/packages/utils/servers/errors/modrinth-server-error.ts b/packages/utils/servers/errors/modrinth-server-error.ts index ece8825c..c0a548a7 100644 --- a/packages/utils/servers/errors/modrinth-server-error.ts +++ b/packages/utils/servers/errors/modrinth-server-error.ts @@ -54,6 +54,6 @@ export class ModrinthServerError extends Error { } super(errorMessage) - this.name = 'PyroServersFetchError' + this.name = 'ModrinthServersFetchError' } } diff --git a/packages/utils/servers/errors/modrinth-servers-fetch-error.ts b/packages/utils/servers/errors/modrinth-servers-fetch-error.ts index ce01e737..a2df7baa 100644 --- a/packages/utils/servers/errors/modrinth-servers-fetch-error.ts +++ b/packages/utils/servers/errors/modrinth-servers-fetch-error.ts @@ -5,6 +5,6 @@ export class ModrinthServersFetchError extends Error { public originalError?: Error, ) { super(message) - this.name = 'PyroFetchError' + this.name = 'ModrinthFetchError' } } -- 2.49.1 From 242fd713ab094297581d55338eb4b5a60ecd4a05 Mon Sep 17 00:00:00 2001 From: Prospector Date: Tue, 8 Jul 2025 11:06:50 -0700 Subject: [PATCH 07/32] Update changelog --- packages/utils/changelog.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/utils/changelog.ts b/packages/utils/changelog.ts index 3c9db638..2cdf1093 100644 --- a/packages/utils/changelog.ts +++ b/packages/utils/changelog.ts @@ -10,6 +10,24 @@ export type VersionEntry = { } const VERSIONS: VersionEntry[] = [ + { + date: `2025-07-08T11:10:00-07:00`, + product: 'servers', + body: `### Improvements +- Reapplied error handling improvements, with more improvements.`, + }, + { + date: `2025-07-07T22:20:00-07:00`, + product: 'servers', + body: `### Improvements +- Fixed issue with Servers panel failing to load.`, + }, + { + date: `2025-07-07T17:45:00-07:00`, + product: 'servers', + body: `### Improvements +- Reverted error handling improvements.`, + }, { date: `2025-07-07T01:10:00-07:00`, product: 'app', @@ -73,7 +91,8 @@ const VERSIONS: VersionEntry[] = [ date: `2025-07-04T12:00:00-07:00`, product: 'web', body: `### Changed -- Changed fallback ad placeholder from promoting Modrinth+ to Modrinth Servers.`, +- Changed fallback ad placeholder from promoting Modrinth+ to Modrinth Servers. +- Fixed news section rendering incorrectly in light mode on landing page and Modrinth App page.`, }, { date: `2025-06-30T19:15:00-07:00`, -- 2.49.1 From 2692953e31d056b52889e1bdb4e5eb18cc3d42c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= <7822554+AlexTMjugador@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:03:30 +0200 Subject: [PATCH 08/32] fix(app): make Party Alex bonus default skin have slim arms (#3945) This skin was incorrectly declared as having wide arms. Resolves #3941. --- .../src/api/minecraft_skins/assets/default/default_skins.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-lib/src/api/minecraft_skins/assets/default/default_skins.rs b/packages/app-lib/src/api/minecraft_skins/assets/default/default_skins.rs index fdbee628..6c26f07d 100644 --- a/packages/app-lib/src/api/minecraft_skins/assets/default/default_skins.rs +++ b/packages/app-lib/src/api/minecraft_skins/assets/default/default_skins.rs @@ -231,7 +231,7 @@ pub static DEFAULT_SKINS: LazyLock> = LazyLock::new(|| { Skin { texture_key: Arc::from("66206c8f51d13d2d31c54696a58a3e8bcd1e5e7db9888d331d0753129324e4f1"), name: Some(Arc::from("Party Alex")), - variant: MinecraftSkinVariant::Classic, + variant: MinecraftSkinVariant::Slim, cape_id: None, texture: Arc::from(Url::try_from( "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAB3RJTUUH4QoJEjAE4f2wWAAAABd0RVh0U29mdHdhcmUAR0xEUE5HIHZlciAzLjRxhaThAAAACHRwTkdHTEQzAAAAAEqAKR8AAAAEZ0FNQQAAsY8L/GEFAAAABmJLR0QA/wD/AP+gvaeTAAAFFUlEQVR4nO1aXWhcRRS+XeMmClmbNVtTMdjUqlHrX5QSC0oMUnzxoVFBEAqSh4oRBEEEH0ooIvjggyDiSx4EkUgfLC1IEC0+iFKoQluFFNpGqLbotqtNGyVtynW/ac/s2ZN77+z9nb1NPviy586dmdxzZubMzJlxJicnnSBW3x92JX9/+34tH95xpyJPgwwi3VS/bRacFtBR6W5iZ7mk5bzDaIAwSi7W5hXzhJZ6QBAGnn1OsfTw6DIivd0R2wB5x6oBbH+Abax4A3jO8ybyOT8uc7EO4MA01/P0eO6mOz8UaO5ulVCeELasF22jpR7w6J5Oxb5X3nM6N9zlbHjtM/V8PcDTAFAUBKDo3JEfnLF3ruj3kJGGd+fOLDTlzx1o08K5NLvXXZj5QHHp6K/q2ZP1dzpf/VnWQ5sivjmS6badoCM/GoRCFy/+pxUDFz/9SKWBkCmd8nrVw5WFLHeLbWkAalHestwIXHkygJ/yZIB23i4vS7jv4yGXs7+/v4mmCu9+c8QF/Qyy6d3NgTTVzxtn/vPX9W9UA6SyEvzp8c16l8gJ3FguBtIEzEJgsacjkW/N9VL4hvKAc6U2p36jItcGSAQYdxjrNAYnzvTp8f/U93eotMqLWzUpH5WhMc+JMSlpcpTSSVJd0hETLx/8cNn/4P4hlA+4XLukDXLgy9u1XD2yztdwvIzEY4d+Ud2SODezP7ARsJgCaHmMaFJW0EOAO6DKQ39FrrBj09X1/SNf79cOywsv9A5oPn9PtzPSXdChNMAtn438DWGgDcBbNKjlvcA/trtyWhkBfOOWV50HD+zW77jSUsHyvWedoZPfaeI5CxR+3jjicB7a+LJzdHSXImSkndg+pCnzA2tqvbrCC9XGEOLDCUpT75BlgNqx4Oe0UNiy+xuHSFj87YQigeeRJHDlRrefbpJfGr5VvUfvkHm5DPCW73liNgEVg9G0moBCN5cqTRn+na8GVvDPqWtD51RX/c8lZ61Tqrd84z3vBX//OHg1/7W8jXJdOs/5w6VmuRF+SAWx1wFr+4tNBJaOlzzpld/EtOFpgGK9FxRFTwgCd2jrx45rJ7jtrVktU7rMjzQ8g+jyXM4CiawEuUPjM4jfmoLnp54BcMeXmRP0SuzpXa8YBtyZkbMD+JqCp/NewB0fl6WDTAWDg4N6qwsZHH5mTJGe5XtOuZ2V22lJ03ZYEstaLHs5Kc1rKUy/keMBkmHjAXmj9Q+wTWOGFd8DrnsDxI0H2FYgLlOJB3hh4qYZFwxVKAMkHg/4Yt2+ZUpC8Xa9UJVIPIADix0YAazu/ET9QvmFY384W//sMleQNZIeU+Q/tuzp06QYH9Jtj3nJVAwAR0rKk0xGsa1w6gYgI3jRtrKZGSBPtP4Btmn9A2yzMD097XLu++pb9+BsVRGyfC9pdQpLAtjTwxJ8jy/jAV55eHqeacwQdjMU934BnRHi/C+J838TjTHB8fHxNZwZdMpMYTTA1NSUy5nFRxGSOP83YfV+gNzrUxzAL5DJnzGGZHla9tLS16scl3lQk8Y/fuV9AHl5K1UfwHeDct9vigPww1Eu83JBdeB+QJaXLn2HAI/hAzxe0MplpijA3YDbxnamUrcffA3AWw/wa0F5dO51vB5E2+iYeGBbY2pbqjN6MCiXiD0LJHW/wBYSnQah0JO79jaxHZQMwopfB6RigLD3C2xitQekEQ+Icr/AGuLGA+LeL5Bn/Pz8X94JiHL+b+L/USxRwWwDOUgAAAAASUVORK5CYII=" -- 2.49.1 From 6caf794ae17974bf005edeca620a18d37bb535f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Gonz=C3=A1lez?= <7822554+AlexTMjugador@users.noreply.github.com> Date: Tue, 8 Jul 2025 21:22:15 +0200 Subject: [PATCH 09/32] dist(docker): add `curl` package to Labrinth image, some other minor tweaks (#3915) * dist(docker): add `.dockerignore` as symlink to `.gitignore` This ensures that no files outside of version control are transferred to the Docker build context for Labrinth and Daedalus images, which significantly improves build speed (if a `target` directory is already present) and build reproducibility. * chore(dist/docker): simplify out unneeeded statements, move `SQLX_OFFLINE` env var setting to build command itself The latter approach ensures that developers building the image locally don't forget to set `SQLX_OFFLINE`, too. * dist(docker): add `curl` package to Labrinth image --- .dockerignore | 1 + .github/workflows/labrinth-docker.yml | 5 ----- apps/daedalus_client/Dockerfile | 4 ---- apps/labrinth/Dockerfile | 10 ++-------- 4 files changed, 3 insertions(+), 17 deletions(-) create mode 120000 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 120000 index 00000000..3e4e48b0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.gitignore \ No newline at end of file diff --git a/.github/workflows/labrinth-docker.yml b/.github/workflows/labrinth-docker.yml index 1ed8e968..114c8ee4 100644 --- a/.github/workflows/labrinth-docker.yml +++ b/.github/workflows/labrinth-docker.yml @@ -18,9 +18,6 @@ on: jobs: docker: runs-on: ubuntu-latest - defaults: - run: - working-directory: ./apps/labrinth steps: - name: Checkout uses: actions/checkout@v2 @@ -38,8 +35,6 @@ jobs: - name: Build and push id: docker_build uses: docker/build-push-action@v2 - env: - SQLX_OFFLINE: true with: file: ./apps/labrinth/Dockerfile push: ${{ github.event_name != 'pull_request' }} diff --git a/apps/daedalus_client/Dockerfile b/apps/daedalus_client/Dockerfile index d33fc113..9ea70f9c 100644 --- a/apps/daedalus_client/Dockerfile +++ b/apps/daedalus_client/Dockerfile @@ -1,5 +1,4 @@ FROM rust:1.88.0 AS build -ENV PKG_CONFIG_ALLOW_CROSS=1 WORKDIR /usr/src/daedalus COPY . . @@ -10,11 +9,8 @@ FROM debian:bookworm-slim RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates openssl \ - && apt-get clean \ && rm -rf /var/lib/apt/lists/* -RUN update-ca-certificates - COPY --from=build /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client WORKDIR /daedalus_client diff --git a/apps/labrinth/Dockerfile b/apps/labrinth/Dockerfile index 2ba54754..01b73d95 100644 --- a/apps/labrinth/Dockerfile +++ b/apps/labrinth/Dockerfile @@ -1,11 +1,8 @@ FROM rust:1.88.0 AS build -ENV PKG_CONFIG_ALLOW_CROSS=1 WORKDIR /usr/src/labrinth COPY . . -COPY apps/labrinth/.sqlx/ .sqlx/ -RUN cargo build --release --package labrinth - +RUN SQLX_OFFLINE=true cargo build --release --package labrinth FROM debian:bookworm-slim @@ -14,12 +11,9 @@ LABEL org.opencontainers.image.description="Modrinth API" LABEL org.opencontainers.image.licenses=AGPL-3.0 RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates openssl dumb-init \ - && apt-get clean \ + && apt-get install -y --no-install-recommends ca-certificates openssl dumb-init curl \ && rm -rf /var/lib/apt/lists/* -RUN update-ca-certificates - COPY --from=build /usr/src/labrinth/target/release/labrinth /labrinth/labrinth COPY --from=build /usr/src/labrinth/apps/labrinth/migrations/* /labrinth/migrations/ COPY --from=build /usr/src/labrinth/apps/labrinth/assets /labrinth/assets -- 2.49.1 From 26df6f51efd4168428876475f8c5ced64c7cdc25 Mon Sep 17 00:00:00 2001 From: IMB11 Date: Tue, 8 Jul 2025 21:09:36 +0100 Subject: [PATCH 10/32] fix: composable used outside ... issue + disable cache (#3947) --- apps/frontend/src/pages/servers/index.vue | 44 +++++++++++++---------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/apps/frontend/src/pages/servers/index.vue b/apps/frontend/src/pages/servers/index.vue index 9b3c363a..60495d15 100644 --- a/apps/frontend/src/pages/servers/index.vue +++ b/apps/frontend/src/pages/servers/index.vue @@ -719,31 +719,32 @@ async function fetchCapacityStatuses(customProduct = null) { product.metadata.ram < min.metadata.ram ? product : min, ), ]; - const capacityChecks = productsToCheck.map((product) => - useServersFetch("stock", { - method: "POST", - body: { - cpu: product.metadata.cpu, - memory_mb: product.metadata.ram, - swap_mb: product.metadata.swap, - storage_mb: product.metadata.storage, - }, - bypassAuth: true, - }), - ); - - const results = await Promise.all(capacityChecks); + const capacityChecks = []; + for (const product of productsToCheck) { + capacityChecks.push( + useServersFetch("stock", { + method: "POST", + body: { + cpu: product.metadata.cpu, + memory_mb: product.metadata.ram, + swap_mb: product.metadata.swap, + storage_mb: product.metadata.storage, + }, + bypassAuth: true, + }), + ); + } if (customProduct?.metadata) { return { - custom: results[0], + custom: await capacityChecks[0], }; } else { return { - small: results[0], - medium: results[1], - large: results[2], - custom: results[3], + small: await capacityChecks[0], + medium: await capacityChecks[1], + large: await capacityChecks[2], + custom: await capacityChecks[3], }; } } catch (error) { @@ -760,6 +761,11 @@ async function fetchCapacityStatuses(customProduct = null) { const { data: capacityStatuses, refresh: refreshCapacity } = await useAsyncData( "ServerCapacityAll", fetchCapacityStatuses, + { + getCachedData() { + return null; // Dont cache stock data. + }, + }, ); const isSmallAtCapacity = computed(() => capacityStatuses.value?.small?.available === 0); -- 2.49.1 From 6da942ccbb095f4a7311396f50ac70e27f81262c Mon Sep 17 00:00:00 2001 From: didirus4 Date: Tue, 8 Jul 2025 23:32:33 +0300 Subject: [PATCH 11/32] fix: Ignore x86 windows arch --- packages/app-lib/src/state/db.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/app-lib/src/state/db.rs b/packages/app-lib/src/state/db.rs index 607a345f..bede894b 100644 --- a/packages/app-lib/src/state/db.rs +++ b/packages/app-lib/src/state/db.rs @@ -84,6 +84,11 @@ Problem files: async fn fix_modrinth_issued_migrations( pool: &Pool, ) -> crate::Result<()> { + if cfg!(target_os = "windows") && cfg!(target_arch = "x86") { + tracing::warn!("🛑 Skipping migration checksum fix on Windows x86 platform."); + return Ok(()); + } + let started = Instant::now(); tracing::info!("Fixing modrinth issued migrations"); sqlx::query( @@ -95,7 +100,7 @@ async fn fix_modrinth_issued_migrations( ) .execute(pool) .await?; - tracing::info!("⚙️ Fixed first migration"); + tracing::info!("⚙️ Fixed checksum for first migration"); sqlx::query( r#" UPDATE "_sqlx_migrations" @@ -105,7 +110,7 @@ async fn fix_modrinth_issued_migrations( ) .execute(pool) .await?; - tracing::info!("⚙️ Fixed second migration"); + tracing::info!("⚙️ Fixed checksum for second migration"); sqlx::query( r#" UPDATE "_sqlx_migrations" @@ -115,7 +120,7 @@ async fn fix_modrinth_issued_migrations( ) .execute(pool) .await?; - tracing::info!("⚙️ Fixed third migration"); + tracing::info!("⚙️ Fixed checksum for third migration"); sqlx::query( r#" UPDATE "_sqlx_migrations" @@ -125,10 +130,10 @@ async fn fix_modrinth_issued_migrations( ) .execute(pool) .await?; - tracing::info!("⚙️ Fixed fourth migration"); + tracing::info!("⚙️ Fixed checksum for fourth migration"); let elapsed = started.elapsed(); tracing::info!( - "✅ Fixed all known modrinth-issued migrations in {:.2?}", + "✅ Fixed all known Modrinth checksums for migrations in {:.2?}", elapsed ); Ok(()) -- 2.49.1 From e2e21c14964c92ad0462fc9b78ccb9b438852d92 Mon Sep 17 00:00:00 2001 From: didirus4 Date: Tue, 8 Jul 2025 23:40:55 +0300 Subject: [PATCH 12/32] fix: another try to fix x86 windows arch --- packages/app-lib/src/state/db.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/app-lib/src/state/db.rs b/packages/app-lib/src/state/db.rs index bede894b..e74b30b6 100644 --- a/packages/app-lib/src/state/db.rs +++ b/packages/app-lib/src/state/db.rs @@ -3,6 +3,7 @@ use sqlx::sqlite::{ SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, }; use sqlx::{Pool, Sqlite}; +use std::env; use tokio::time::Instant; use std::str::FromStr; use std::time::Duration; @@ -84,8 +85,13 @@ Problem files: async fn fix_modrinth_issued_migrations( pool: &Pool, ) -> crate::Result<()> { - if cfg!(target_os = "windows") && cfg!(target_arch = "x86") { - tracing::warn!("🛑 Skipping migration checksum fix on Windows x86 platform."); + let arch = env::consts::ARCH; + let os = env::consts::OS; + + tracing::info!("Running on OS: {}, ARCH: {}", os, arch); + + if os == "windows" && arch == "x86" { + tracing::warn!("🛑 Skipping migration checksum fix on Windows x86 (runtime-detected)"); return Ok(()); } -- 2.49.1 From c5e67a5c6f946ffa9893b6ae28ea56d7639bea86 Mon Sep 17 00:00:00 2001 From: didirus4 Date: Tue, 8 Jul 2025 23:43:50 +0300 Subject: [PATCH 13/32] fix: typo --- packages/app-lib/src/state/db.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-lib/src/state/db.rs b/packages/app-lib/src/state/db.rs index e74b30b6..331d9cb4 100644 --- a/packages/app-lib/src/state/db.rs +++ b/packages/app-lib/src/state/db.rs @@ -90,8 +90,8 @@ async fn fix_modrinth_issued_migrations( tracing::info!("Running on OS: {}, ARCH: {}", os, arch); - if os == "windows" && arch == "x86" { - tracing::warn!("🛑 Skipping migration checksum fix on Windows x86 (runtime-detected)"); + if os == "windows" && arch == "x86_64" { + tracing::warn!("🛑 Skipping migration checksum fix on Windows x86_64 (runtime-detected)"); return Ok(()); } -- 2.49.1 From 1fdb5ba7488431720fed0584dc3e1d8ec3235f3b Mon Sep 17 00:00:00 2001 From: Prospector <6166773+Prospector@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:48:27 -0700 Subject: [PATCH 14/32] Add authors to blog posts and shorten some summaries (#3940) --- .../src/pages/news/article/[slug].vue | 51 +++++++++++++++++-- .../src/public/news/feed/articles.json | 10 ++-- .../a-new-chapter-for-modrinth-servers.md | 1 + .../blog/articles/accelerating-development.md | 1 + .../blog/articles/becoming-sustainable.md | 1 + packages/blog/articles/capital-return.md | 1 + packages/blog/articles/carbon-ads.md | 3 +- .../blog/articles/creator-monetization.md | 3 +- packages/blog/articles/creator-update.md | 1 + .../articles/creator-updates-july-2025.md | 1 + packages/blog/articles/design-refresh.md | 1 + packages/blog/articles/download-adjustment.md | 1 + packages/blog/articles/knossos-v2.1.0.md | 3 +- packages/blog/articles/licensing-guide.md | 1 + packages/blog/articles/modpack-changes.md | 1 + packages/blog/articles/modpacks-alpha.md | 1 + packages/blog/articles/modrinth-app-beta.md | 1 + packages/blog/articles/modrinth-beta.md | 1 + .../blog/articles/modrinth-servers-beta.md | 1 + .../blog/articles/plugins-resource-packs.md | 1 + packages/blog/articles/pride-campaign-2025.md | 3 +- packages/blog/articles/redesign.md | 3 +- .../articles/skins-now-in-modrinth-app.md | 1 + .../articles/two-years-of-modrinth-history.md | 1 + .../blog/articles/two-years-of-modrinth.md | 1 + packages/blog/articles/whats-modrinth.md | 1 + .../windows-borderless-malware-disclosure.md | 1 + packages/blog/compile.ts | 5 +- .../a_new_chapter_for_modrinth_servers.ts | 1 + .../blog/compiled/accelerating_development.ts | 1 + .../blog/compiled/becoming_sustainable.ts | 1 + packages/blog/compiled/capital_return.ts | 1 + packages/blog/compiled/carbon_ads.ts | 4 +- .../blog/compiled/creator_monetization.ts | 3 +- packages/blog/compiled/creator_update.ts | 1 + .../compiled/creator_updates_july_2025.ts | 1 + packages/blog/compiled/design_refresh.ts | 1 + packages/blog/compiled/download_adjustment.ts | 1 + packages/blog/compiled/knossos_v2_1_0.ts | 3 +- packages/blog/compiled/licensing_guide.ts | 1 + packages/blog/compiled/modpack_changes.ts | 1 + packages/blog/compiled/modpacks_alpha.ts | 1 + packages/blog/compiled/modrinth_app_beta.ts | 1 + packages/blog/compiled/modrinth_beta.ts | 1 + .../blog/compiled/modrinth_servers_beta.ts | 1 + packages/blog/compiled/new_site_beta.ts | 1 + .../blog/compiled/plugins_resource_packs.ts | 1 + packages/blog/compiled/pride_campaign_2025.ts | 4 +- packages/blog/compiled/redesign.ts | 4 +- .../compiled/skins_now_in_modrinth_app.ts | 1 + .../blog/compiled/two_years_of_modrinth.ts | 1 + .../compiled/two_years_of_modrinth_history.ts | 1 + packages/blog/compiled/whats_modrinth.ts | 1 + .../windows_borderless_malware_disclosure.ts | 1 + 54 files changed, 117 insertions(+), 23 deletions(-) diff --git a/apps/frontend/src/pages/news/article/[slug].vue b/apps/frontend/src/pages/news/article/[slug].vue index fda42fae..c2e35cdf 100644 --- a/apps/frontend/src/pages/news/article/[slug].vue +++ b/apps/frontend/src/pages/news/article/[slug].vue @@ -1,9 +1,10 @@ \ No newline at end of file diff --git a/packages/assets/styles/neon-button.scss b/packages/assets/styles/neon-button.scss index 8fc13321..91e201f3 100644 --- a/packages/assets/styles/neon-button.scss +++ b/packages/assets/styles/neon-button.scss @@ -1,13 +1,8 @@ // [AR] Feature -.btn-wrapper.neon :deep(:is(button, a, .button-like):first-child), -.btn-wrapper.neon :slotted(:is(button, a, .button-like):first-child), -.btn-wrapper.neon :slotted(*) > :is(button, a, .button-like):first-child, -.btn-wrapper.neon :slotted(*) > *:first-child > :is(button, a, .button-like):first-child, -.btn-wrapper.neon - :slotted(*) - > *:first-child - > *:first-child - > :is(button, a, .button-like):first-child { +.neon-button.neon :deep(:is(button, a, .button-like)), +.neon-button.neon :slotted(:is(button, a, .button-like)), +.neon-button.neon :slotted(*) :is(button, a, .button-like) { + cursor: pointer; background-color: transparent; border: 1px solid #3e8cde; color: #3e8cde; @@ -22,20 +17,17 @@ box-shadow: 0 0 4px rgba(79, 173, 255, 0.5); } +.bordered { + border-radius: 12px; +} + /* Hover */ -.btn-wrapper.neon - :deep(:is(button, a, .button-like):first-child):hover:not([disabled]):not(.disabled), -.btn-wrapper.neon - :slotted(:is(button, a, .button-like):first-child):hover:not([disabled]):not(.disabled), -.btn-wrapper.neon - :slotted(*) > :is(button, a, .button-like):first-child:hover:not([disabled]):not(.disabled), -.btn-wrapper.neon - :slotted(*) > *:first-child > :is(button, a, .button-like):first-child:hover:not([disabled]):not(.disabled), -.btn-wrapper.neon - :slotted(*) - > *:first-child - > *:first-child - > :is(button, a, .button-like):first-child:hover:not([disabled]):not(.disabled) { +.neon-button.neon + :deep(:is(button, a, .button-like):hover):not([disabled]):not(.disabled), +.neon-button.neon + :slotted(:is(button, a, .button-like):hover):not([disabled]):not(.disabled), +.neon-button.neon + :slotted(*) :is(button, a, .button-like):hover:not([disabled]):not(.disabled) { color: #10fae5; transform: scale(1.02); box-shadow: diff --git a/packages/assets/styles/neon-icon.scss b/packages/assets/styles/neon-icon.scss new file mode 100644 index 00000000..a0e2fb62 --- /dev/null +++ b/packages/assets/styles/neon-icon.scss @@ -0,0 +1,37 @@ +// [AR] Feature +.neon-icon { + background-color: transparent; + color: #3e8cde; + text-shadow: + 0 0 4px rgba(79, 173, 255, 0.5), + 0 0 8px rgba(14, 98, 204, 0.5), + 0 0 12px rgba(122, 31, 199, 0.5); + transition: transform 0.25s ease, color 0.25s ease, text-shadow 0.25s ease; + cursor: pointer; + display: inline-block; +} + +/* Hover */ +.neon-icon:hover { + color: #10fae5; + transform: scale(1.05); + text-shadow: + 0 0 2px rgba(16, 250, 229, 0.4), + 0 0 4px rgba(16, 250, 229, 0.25); +} + +.neon-icon.pulse { + position: relative; + animation: neon-pulse 1s ease-in-out infinite; + filter: drop-shadow(0 0 6px #10fae5); + box-shadow: none; +} + +@keyframes neon-pulse { + 0%, 100% { + filter: drop-shadow(0 0 4px #10fae5); + } + 50% { + filter: drop-shadow(0 0 12px #10fae5); + } +} \ No newline at end of file diff --git a/packages/assets/styles/neon-text.scss b/packages/assets/styles/neon-text.scss new file mode 100644 index 00000000..1899f2b6 --- /dev/null +++ b/packages/assets/styles/neon-text.scss @@ -0,0 +1,28 @@ +// [AR] Feature +.neon-text { + background-color: transparent; + color: #3e8cde; + text-shadow: + 0 0 4px rgba(79, 173, 255, 0.5), + 0 0 8px rgba(14, 98, 204, 0.5), + 0 0 12px rgba(122, 31, 199, 0.5); + transition: + color 0.25s ease, + box-shadow 0.3s ease, + transform 0.15s ease; + + white-space: normal; + word-wrap: break-word; + overflow-wrap: break-word; + max-width: 100%; + display: inline-block; + padding: 4px 8px; +} + +/* Hover */ +.neon-text:hover:not([disabled]):not(.disabled) { + color: #10fae5; + text-shadow: + 0 0 2px rgba(16, 250, 229, 0.4), + 0 0 4px rgba(16, 250, 229, 0.25); +} -- 2.49.1 From b71e4cc6f92c99c5897e7ff8288697a690457cee Mon Sep 17 00:00:00 2001 From: didirus4 Date: Fri, 11 Jul 2025 02:29:05 +0300 Subject: [PATCH 27/32] refactor: update checker moved to App.vue, added new animated icons --- apps/app-frontend/src/App.vue | 25 +++++++++++++++---- .../src/components/ui/RunningAppBar.vue | 3 --- .../components/ui/modal/AppSettingsModal.vue | 5 ++-- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index a8dd1cd8..60d1f8fb 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -72,6 +72,9 @@ import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue' import { get_available_capes, get_available_skins } from './helpers/skins' import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer' +// [AR] Feature +import { getRemote, updateState } from '@/helpers/update.js' + const themeStore = useTheming() const news = ref([]) @@ -99,6 +102,7 @@ const isMaximized = ref(false) onMounted(async () => { await useCheckDisableMouseover() + await getRemote(false) // [AR] Check for updates document.querySelector('body').addEventListener('click', handleClick) document.querySelector('body').addEventListener('auxclick', handleAuxClick) @@ -465,12 +469,20 @@ function handleAuxClick(e) {
- + + + + +@import '../../../packages/assets/styles/neon-icon.scss'; +@import '../../../packages/assets/styles/neon-text.scss'; + .window-controls { z-index: 20; display: none; diff --git a/apps/app-frontend/src/components/ui/RunningAppBar.vue b/apps/app-frontend/src/components/ui/RunningAppBar.vue index b42d3320..f3a766b1 100644 --- a/apps/app-frontend/src/components/ui/RunningAppBar.vue +++ b/apps/app-frontend/src/components/ui/RunningAppBar.vue @@ -84,9 +84,6 @@ import ProgressBar from '@/components/ui/ProgressBar.vue' import { handleError } from '@/store/notifications.js' import { get_many } from '@/helpers/profile.js' import { trackEvent } from '@/helpers/analytics' -import { getRemote } from '@/helpers/update.js' - -await getRemote(false) const router = useRouter() const card = ref(null) diff --git a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue index 7869d2fd..0ce1e6c6 100644 --- a/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue +++ b/apps/app-frontend/src/components/ui/modal/AppSettingsModal.vue @@ -173,13 +173,12 @@ function devModeCount() {
-
-- 2.49.1 From 4e69cd8bde5eae066b0f3c1ba394cb3cbc14d060 Mon Sep 17 00:00:00 2001 From: didirus4 Date: Fri, 11 Jul 2025 02:38:23 +0300 Subject: [PATCH 28/32] feat: add auto application restart after migration successful fix attempt --- apps/app-frontend/src/App.vue | 2 +- apps/app-frontend/src/components/ui/ErrorModal.vue | 6 ++++++ apps/app/src/main.rs | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 60d1f8fb..c837744c 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -42,7 +42,7 @@ import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue' import { handleError, useNotifications } from '@/store/notifications.js' import { command_listener, warning_listener } from '@/helpers/events.js' import { type } from '@tauri-apps/plugin-os' -import { getOS, isDev, restartApp } from '@/helpers/utils.js' +import { getOS, isDev } from '@/helpers/utils.js' import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics' import { getCurrentWindow } from '@tauri-apps/api/window' import { getVersion } from '@tauri-apps/api/app' diff --git a/apps/app-frontend/src/components/ui/ErrorModal.vue b/apps/app-frontend/src/components/ui/ErrorModal.vue index 37c53183..808a101e 100644 --- a/apps/app-frontend/src/components/ui/ErrorModal.vue +++ b/apps/app-frontend/src/components/ui/ErrorModal.vue @@ -19,6 +19,7 @@ import { install } from '@/helpers/profile.js' import { trackEvent } from '@/helpers/analytics' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import { applyMigrationFix } from '@/helpers/utils.js' +import { restartApp } from '@/helpers/utils.js' const errorModal = ref() const error = ref() @@ -168,6 +169,11 @@ async function onApplyMigrationFix(eol) { migrationFixSuccess.value = false } finally { migrationFixCallbackModel.value?.show?.() + if (migrationFixSuccess.value === true) { + setTimeout(async () => { + await restartApp() + }, 3000) + } } } diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index dc3ee39e..94427776 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -157,7 +157,7 @@ fn main() { */ let _log_guard = theseus::start_logger(); - tracing::info!("Initialized tracing subscriber. Loading Modrinth App!"); + tracing::info!("Initialized tracing subscriber. Loading AstralRinth App!"); let mut builder = tauri::Builder::default(); -- 2.49.1 From d917bff6ef501f5723781024a2fb901866e7b447 Mon Sep 17 00:00:00 2001 From: didirus4 Date: Fri, 11 Jul 2025 03:04:37 +0300 Subject: [PATCH 29/32] feat: add ability to auto exec downloaded installer on windows; minor changes --- apps/app-frontend/src/helpers/update.js | 2 +- packages/app-lib/src/api/update.rs | 127 +++++++++++++++++++----- packages/app-lib/src/state/db.rs | 10 +- packages/app-lib/src/util/utils.rs | 4 +- 4 files changed, 108 insertions(+), 35 deletions(-) diff --git a/apps/app-frontend/src/helpers/update.js b/apps/app-frontend/src/helpers/update.js index 6f3d87a1..cea6c6bd 100644 --- a/apps/app-frontend/src/helpers/update.js +++ b/apps/app-frontend/src/helpers/update.js @@ -11,7 +11,7 @@ const releaseLink = `https://git.astralium.su/api/v1/repos/didirus/AstralRinth/r const failedFetch = [`Failed to fetch remote releases:`, `Failed to fetch remote commits:`] const osList = ['macos', 'windows', 'linux'] -const macExtensionList = ['.app', '.dmg'] +const macExtensionList = ['.dmg', '.pkg'] const windowsExtensionList = ['.exe', '.msi'] const blacklistPrefixes = [ diff --git a/packages/app-lib/src/api/update.rs b/packages/app-lib/src/api/update.rs index b1943dec..fe24ec01 100644 --- a/packages/app-lib/src/api/update.rs +++ b/packages/app-lib/src/api/update.rs @@ -1,42 +1,115 @@ use reqwest; +use std::path::PathBuf; use tokio::fs::File as AsyncFile; use tokio::io::AsyncWriteExt; use tokio::process::Command; -pub(crate) async fn download_file(download_url: &str, local_filename: &str, os_type: &str, auto_update_supported: bool) -> Result<(), Box> { - let download_dir = dirs::download_dir().ok_or("[AR] • Failed to determine download directory")?; +pub(crate) async fn get_resource( + download_url: &str, + local_filename: &str, + os_type: &str, + auto_update_supported: bool, +) -> Result<(), Box> { + let download_dir = dirs::download_dir() + .ok_or("[AR] • Failed to determine download directory")?; let full_path = download_dir.join(local_filename); + let response = reqwest::get(download_url).await?; let bytes = response.bytes().await?; let mut dest_file = AsyncFile::create(&full_path).await?; dest_file.write_all(&bytes).await?; println!("[AR] • File downloaded to: {:?}", full_path); + if auto_update_supported { - let status; - if os_type.to_lowercase() == "Windows".to_lowercase() { - status = Command::new("explorer") - .arg(download_dir.display().to_string()) - .status() - .await - .expect("[AR] • Failed to open downloads folder"); - } else if os_type.to_lowercase() == "MacOS".to_lowercase() { - status = Command::new("open") - .arg(full_path.to_str().unwrap_or_default()) - .status() - .await - .expect("[AR] • Failed to execute command"); - } else { - status = Command::new(".") - .arg(full_path.to_str().unwrap_or_default()) - .status() - .await - .expect("[AR] • Failed to execute command"); - } - if status.success() { - println!("[AR] • File opened successfully!"); - } else { - eprintln!("[AR] • Failed to open the file. Exit code: {:?}", status.code()); + let result = match os_type.to_lowercase().as_str() { + "windows" => handle_windows_file(&full_path).await, + "macos" => open_macos_file(&full_path).await, + _ => open_default(&full_path).await, + }; + + match result { + Ok(_) => println!("[AR] • File opened successfully!"), + Err(e) => eprintln!("[AR] • Failed to open file: {e}"), } } + Ok(()) -} \ No newline at end of file +} + +async fn handle_windows_file(path: &PathBuf) -> Result<(), Box> { + let filename = path + .file_name() + .and_then(|f| f.to_str()) + .unwrap_or_default() + .to_lowercase(); + + if filename.ends_with(".exe") || filename.ends_with(".msi") { + println!("[AR] • Detected installer: {}", filename); + run_windows_installer(path).await + } else { + open_windows_folder(path).await + } +} + +async fn run_windows_installer(path: &PathBuf) -> Result<(), Box> { + let installer_path = path.to_str().unwrap_or_default(); + + let status = if installer_path.ends_with(".msi") { + Command::new("msiexec") + .args(&["/i", installer_path, "/quiet"]) + .status() + .await? + } else { + Command::new("cmd") + .args(&["/C", installer_path]) + .status() + .await? + }; + + if status.success() { + println!("[AR] • Installer started successfully."); + Ok(()) + } else { + Err(format!("Installer failed. Exit code: {:?}", status.code()).into()) + } +} + +async fn open_windows_folder(path: &PathBuf) -> Result<(), Box> { + let folder = path.parent().unwrap_or(path); + let status = Command::new("explorer") + .arg(folder.display().to_string()) + .status() + .await?; + + if !status.success() { + Err(format!("Exit code: {:?}", status.code()).into()) + } else { + Ok(()) + } +} + +async fn open_macos_file(path: &PathBuf) -> Result<(), Box> { + let status = Command::new("open") + .arg(path.to_str().unwrap_or_default()) + .status() + .await?; + + if !status.success() { + Err(format!("Exit code: {:?}", status.code()).into()) + } else { + Ok(()) + } +} + +async fn open_default(path: &PathBuf) -> Result<(), Box> { + let status = Command::new(".") + .arg(path.to_str().unwrap_or_default()) + .status() + .await?; + + if !status.success() { + Err(format!("Exit code: {:?}", status.code()).into()) + } else { + Ok(()) + } +} diff --git a/packages/app-lib/src/state/db.rs b/packages/app-lib/src/state/db.rs index bebcf5fb..fe81e412 100644 --- a/packages/app-lib/src/state/db.rs +++ b/packages/app-lib/src/state/db.rs @@ -1,5 +1,5 @@ -use crate::state::DirectoryInfo; use crate::ErrorKind; +use crate::state::DirectoryInfo; use sqlx::sqlite::{ SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, }; @@ -87,14 +87,14 @@ LF -> c0de804f171b5530010edae087a6e75645c0e90177e28365f935c9fdd9a5c68e24850b8c14 CRLF -> f8c55065e2563fa4738976eb13a052ae4c28da8d33143185550f6e1cee394a3243b1dca090b3e8bc50a93a8286a78c09 LF -> c17542cb989a0466153e695bfa4717f8970feee185ca186a2caa1f2f6c5d4adb990ab97c26cacfbbe09c39ac81551704 */ -pub(crate) async fn fix_version_hash( - eol: &str, -) -> crate::Result { +pub(crate) async fn apply_migration_fix(eol: &str) -> crate::Result { let started = Instant::now(); // Create connection to the database without migrations let pool = connect_without_migrate().await?; - tracing::info!("⚙️ Patching Modrinth corrupted migration checksums using EOL standard: {eol}"); + tracing::info!( + "⚙️ Patching Modrinth corrupted migration checksums using EOL standard: {eol}" + ); // validate EOL input if eol != "lf" && eol != "crlf" { diff --git a/packages/app-lib/src/util/utils.rs b/packages/app-lib/src/util/utils.rs index 678ddb69..adebc3a6 100644 --- a/packages/app-lib/src/util/utils.rs +++ b/packages/app-lib/src/util/utils.rs @@ -26,7 +26,7 @@ pub fn read_package_json() -> io::Result { pub async fn apply_migration_fix(eol: &str) -> Result { tracing::info!("[AR] • Attempting to apply migration fix"); - let patched = db::fix_version_hash(eol).await?; + let patched = db::apply_migration_fix(eol).await?; if patched { tracing::info!("[AR] • Successfully applied migration fix"); } else { @@ -43,7 +43,7 @@ pub async fn init_download( ) -> Result<()> { println!("[AR] • Initialize downloading from • {:?}", download_url); println!("[AR] • Save local file name • {:?}", local_filename); - if let Err(e) = update::download_file( + if let Err(e) = update::get_resource( download_url, local_filename, os_type, -- 2.49.1 From 789d6665153f5df3f6d76d3e879fd4728140d632 Mon Sep 17 00:00:00 2001 From: didirus4 Date: Fri, 11 Jul 2025 03:26:04 +0300 Subject: [PATCH 30/32] refactor: windows auto updater only works with signed app --- packages/app-lib/src/api/update.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/app-lib/src/api/update.rs b/packages/app-lib/src/api/update.rs index fe24ec01..3ca0cebd 100644 --- a/packages/app-lib/src/api/update.rs +++ b/packages/app-lib/src/api/update.rs @@ -18,7 +18,7 @@ pub(crate) async fn get_resource( let bytes = response.bytes().await?; let mut dest_file = AsyncFile::create(&full_path).await?; dest_file.write_all(&bytes).await?; - println!("[AR] • File downloaded to: {:?}", full_path); + tracing::info!("[AR] • File downloaded to: {:?}", full_path); if auto_update_supported { let result = match os_type.to_lowercase().as_str() { @@ -28,8 +28,8 @@ pub(crate) async fn get_resource( }; match result { - Ok(_) => println!("[AR] • File opened successfully!"), - Err(e) => eprintln!("[AR] • Failed to open file: {e}"), + Ok(_) => tracing::info!("[AR] • File opened successfully!"), + Err(e) => tracing::info!("[AR] • Failed to open file: {e}"), } } @@ -44,7 +44,7 @@ async fn handle_windows_file(path: &PathBuf) -> Result<(), Box Result<(), Box Date: Fri, 11 Jul 2025 03:46:33 +0300 Subject: [PATCH 31/32] fix: crlf hash? --- packages/app-lib/src/state/db.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app-lib/src/state/db.rs b/packages/app-lib/src/state/db.rs index fe81e412..8db1a0f2 100644 --- a/packages/app-lib/src/state/db.rs +++ b/packages/app-lib/src/state/db.rs @@ -78,13 +78,13 @@ Problem files, view detailed information in .gitattributes: CRLF -> 4c47e326f16f2b1efca548076ce638d4c90dd610172fe48c47d6de9bc46ef1c5abeadfdea05041ddd72c3819fa10c040 LF -> e973512979feac07e415405291eefafc1ef0bd89454958ad66f5452c381db8679c20ffadab55194ecf6ba8ec4ca2db21 /packages/app-lib/migrations/20240813205023_drop-active-unique.sql !eol -CRLF -> 10f4a494df6fd791a093cc61401ecf3f9750fa6b97aa304ab06e29671e446586240910ffbf806f6ddc484a756770dde9 +CRLF -> C8FD2EFE72E66E394732599EA8D93CE1ED337F098697B3ADAD40DD37CC6367893E199A8D7113B44A3D0FFB537692F91D LF -> 5b53534a7ffd74eebede234222be47e1d37bd0cc5fee4475212491b0c0379c16e3079e08eee0af959b1fa20835eeb206 /packages/app-lib/migrations/20240930001852_disable-personalized-ads.sql !eol CRLF -> c8028ec3a2e61d15586e2f69ad6c6be5ac03b95918c2014cefb183ed6c254a52aad6f9ce98cda13ad545da3398574702 LF -> c0de804f171b5530010edae087a6e75645c0e90177e28365f935c9fdd9a5c68e24850b8c1498e386a379d525d520bc57 /packages/app-lib/migrations/20241222013857_feature-flags.sql !eol -CRLF -> f8c55065e2563fa4738976eb13a052ae4c28da8d33143185550f6e1cee394a3243b1dca090b3e8bc50a93a8286a78c09 +CRLF -> 6B6F097E5BB45A397C96C3F1DC9C2A18433564E81DB264FE08A4775198CCEAC03C9E63C3605994ECB19C281C37D8F6AE LF -> c17542cb989a0466153e695bfa4717f8970feee185ca186a2caa1f2f6c5d4adb990ab97c26cacfbbe09c39ac81551704 */ pub(crate) async fn apply_migration_fix(eol: &str) -> crate::Result { @@ -117,7 +117,7 @@ pub(crate) async fn apply_migration_fix(eol: &str) -> crate::Result { ), ( ("crlf", "20240813205023"), - "10f4a494df6fd791a093cc61401ecf3f9750fa6b97aa304ab06e29671e446586240910ffbf806f6ddc484a756770dde9", + "C8FD2EFE72E66E394732599EA8D93CE1ED337F098697B3ADAD40DD37CC6367893E199A8D7113B44A3D0FFB537692F91D", ), ( ("lf", "20240930001852"), @@ -133,7 +133,7 @@ pub(crate) async fn apply_migration_fix(eol: &str) -> crate::Result { ), ( ("crlf", "20241222013857"), - "f8c55065e2563fa4738976eb13a052ae4c28da8d33143185550f6e1cee394a3243b1dca090b3e8bc50a93a8286a78c09", + "6B6F097E5BB45A397C96C3F1DC9C2A18433564E81DB264FE08A4775198CCEAC03C9E63C3605994ECB19C281C37D8F6AE", ), ]); -- 2.49.1 From 591d98a9ebdafeae1bc6351ec6a50358cbb751f3 Mon Sep 17 00:00:00 2001 From: didirus4 Date: Fri, 11 Jul 2025 03:56:11 +0300 Subject: [PATCH 32/32] fix: crlf hash? --- packages/app-lib/src/state/db.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app-lib/src/state/db.rs b/packages/app-lib/src/state/db.rs index 8db1a0f2..c939fd64 100644 --- a/packages/app-lib/src/state/db.rs +++ b/packages/app-lib/src/state/db.rs @@ -81,7 +81,7 @@ LF -> e973512979feac07e415405291eefafc1ef0bd89454958ad66f5452c381db8679c20ffadab CRLF -> C8FD2EFE72E66E394732599EA8D93CE1ED337F098697B3ADAD40DD37CC6367893E199A8D7113B44A3D0FFB537692F91D LF -> 5b53534a7ffd74eebede234222be47e1d37bd0cc5fee4475212491b0c0379c16e3079e08eee0af959b1fa20835eeb206 /packages/app-lib/migrations/20240930001852_disable-personalized-ads.sql !eol -CRLF -> c8028ec3a2e61d15586e2f69ad6c6be5ac03b95918c2014cefb183ed6c254a52aad6f9ce98cda13ad545da3398574702 +CRLF -> C0DE804F171B5530010EDAE087A6E75645C0E90177E28365F935C9FDD9A5C68E24850B8C1498E386A379D525D520BC57 LF -> c0de804f171b5530010edae087a6e75645c0e90177e28365f935c9fdd9a5c68e24850b8c1498e386a379d525d520bc57 /packages/app-lib/migrations/20241222013857_feature-flags.sql !eol CRLF -> 6B6F097E5BB45A397C96C3F1DC9C2A18433564E81DB264FE08A4775198CCEAC03C9E63C3605994ECB19C281C37D8F6AE @@ -125,7 +125,7 @@ pub(crate) async fn apply_migration_fix(eol: &str) -> crate::Result { ), ( ("crlf", "20240930001852"), - "c8028ec3a2e61d15586e2f69ad6c6be5ac03b95918c2014cefb183ed6c254a52aad6f9ce98cda13ad545da3398574702", + "C0DE804F171B5530010EDAE087A6E75645C0E90177E28365F935C9FDD9A5C68E24850B8C1498E386A379D525D520BC57", ), ( ("lf", "20241222013857"), -- 2.49.1