diff --git a/src/backend/main.ts b/src/backend/main.ts index e03ea4c9cd..cf5e188e0b 100644 --- a/src/backend/main.ts +++ b/src/backend/main.ts @@ -24,13 +24,7 @@ import { import 'backend/updater' import { autoUpdater } from 'electron-updater' import { cpus } from 'os' -import { - existsSync, - rmSync, - watch, - readdirSync, - readFileSync -} from 'graceful-fs' +import { existsSync, watch, readdirSync, readFileSync } from 'graceful-fs' import 'source-map-support/register' import Backend from 'i18next-fs-backend' @@ -60,6 +54,7 @@ import { downloadDefaultWine, sendGameStatusUpdate } from './utils' +import { uninstallGameCallback } from './utils/uninstaller' import { configStore, discordLink, @@ -87,7 +82,6 @@ import { createNecessaryFolders, fixAsarPath, isSnap, - fixesPath, isWindows, isMac } from './constants' @@ -144,7 +138,6 @@ import { getGameOverride, getGameSdl } from 'backend/storeManagers/legendary/library' -import { storeMap } from 'common/utils' app.commandLine?.appendSwitch('ozone-platform-hint', 'auto') @@ -954,72 +947,7 @@ ipcMain.handle('openDialog', async (e, args) => { ipcMain.on('showItemInFolder', async (e, item) => showItemInFolder(item)) -ipcMain.handle( - 'uninstall', - async (event, appName, runner, shouldRemovePrefix, shouldRemoveSetting) => { - sendGameStatusUpdate({ - appName, - runner, - status: 'uninstalling' - }) - - const { title } = gameManagerMap[runner].getGameInfo(appName) - - let uninstalled = false - - try { - await gameManagerMap[runner].uninstall({ appName, shouldRemovePrefix }) - uninstalled = true - } catch (error) { - notify({ - title, - body: i18next.t('notify.uninstalled.error', 'Error uninstalling') - }) - logError(error, LogPrefix.Backend) - } - - if (uninstalled) { - if (shouldRemovePrefix) { - const { winePrefix } = await gameManagerMap[runner].getSettings(appName) - logInfo(`Removing prefix ${winePrefix}`, LogPrefix.Backend) - // remove prefix if exists - if (existsSync(winePrefix)) { - rmSync(winePrefix, { recursive: true }) - } - } - if (shouldRemoveSetting) { - const removeIfExists = (filename: string) => { - logInfo(`Removing ${filename}`, LogPrefix.Backend) - const gameSettingsFile = join(gamesConfigPath, filename) - if (existsSync(gameSettingsFile)) { - rmSync(gameSettingsFile) - } - } - - removeIfExists(appName.concat('.json')) - removeIfExists(appName.concat('.log')) - removeIfExists(appName.concat('-lastPlay.log')) - } - - const fixFilePath = path.join( - fixesPath, - `${appName}-${storeMap[runner]}.json` - ) - if (existsSync(fixFilePath)) { - rmSync(fixFilePath) - } - - notify({ title, body: i18next.t('notify.uninstalled') }) - logInfo('Finished uninstalling', LogPrefix.Backend) - } - - sendGameStatusUpdate({ - appName, - runner, - status: 'done' - }) - } -) +ipcMain.handle('uninstall', uninstallGameCallback) ipcMain.handle('repair', async (event, appName, runner) => { if (!isOnline()) { diff --git a/src/backend/storeManagers/sideload/games.ts b/src/backend/storeManagers/sideload/games.ts index ca256f71d8..a7b5adefc4 100644 --- a/src/backend/storeManagers/sideload/games.ts +++ b/src/backend/storeManagers/sideload/games.ts @@ -23,6 +23,7 @@ import { notify } from '../../dialog/dialog' import { launchGame } from 'backend/storeManagers/storeManagerCommon/games' import { GOGCloudSavesLocation } from 'common/types/gog' import { InstallResult, RemoveArgs } from 'common/types/game_manager' +import { removePrefix } from 'backend/utils/uninstaller' export function getGameInfo(appName: string): GameInfo { const store = libraryStore.get('games', []) @@ -110,14 +111,9 @@ export async function uninstall({ title, install: { executable } } = gameInfo - const { winePrefix } = await getSettings(appName) if (shouldRemovePrefix) { - logInfo(`Removing prefix ${winePrefix}`, LogPrefix.Backend) - if (existsSync(winePrefix)) { - // remove prefix if exists - rmSync(winePrefix, { recursive: true }) - } + removePrefix(appName, 'sideload') } libraryStore.set('games', current) diff --git a/src/backend/utils/uninstaller.ts b/src/backend/utils/uninstaller.ts new file mode 100644 index 0000000000..d5897738b6 --- /dev/null +++ b/src/backend/utils/uninstaller.ts @@ -0,0 +1,126 @@ +import { GlobalConfig } from 'backend/config' +import { fixesPath, gamesConfigPath } from 'backend/constants' +import { notify } from 'backend/dialog/dialog' +import { logError, logInfo, LogPrefix } from 'backend/logger/logger' +import { gameManagerMap } from 'backend/storeManagers' +import { sendGameStatusUpdate } from 'backend/utils' +import { Runner } from 'common/types' +import { storeMap } from 'common/utils' +import { Event } from 'electron' +import { existsSync, readdirSync, rmSync } from 'graceful-fs' +import i18next from 'i18next' +import { join } from 'path' + +export const removePrefix = async (appName: string, runner: Runner) => { + const { winePrefix } = await gameManagerMap[runner].getSettings(appName) + logInfo(`Removing prefix ${winePrefix}`, LogPrefix.Backend) + + if (!existsSync(winePrefix)) { + logInfo(`Prefix folder ${winePrefix} doesn't exist, ignoring removal`) + return + } + + // folder exists, do some sanity checks before deleting it + const { defaultInstallPath, defaultWinePrefix } = + GlobalConfig.get().getSettings() + + if (winePrefix === defaultInstallPath) { + logInfo( + `Can't delete folder ${winePrefix}, prefix folder is the default install directory ${defaultInstallPath}` + ) + return + } + + if (winePrefix === defaultWinePrefix) { + logInfo( + `Can't delete folder ${winePrefix}, prefix folder is the default prefix directory ${defaultWinePrefix}` + ) + return + } + + const dirContent = readdirSync(winePrefix) + + if (dirContent.length > 0) { + const driveCPath = join(winePrefix, 'drive_c') + const pfxPath = join(winePrefix, 'pfx') + + if (!existsSync(driveCPath) && !existsSync(pfxPath)) { + logInfo( + `Can't delete folder ${winePrefix}, folder does not contain a drive_c/pfx folder. If this is the correct prefix folder, delete it manually.` + ) + return + } + } + + // if we got here, we are safe to delete this folder + rmSync(winePrefix, { recursive: true }) +} + +const removeFixFile = (appName: string, runner: Runner) => { + const fixFilePath = join(fixesPath, `${appName}-${storeMap[runner]}.json`) + if (existsSync(fixFilePath)) { + rmSync(fixFilePath) + } +} + +const removeSettingsAndLogs = (appName: string) => { + const removeIfExists = (filename: string) => { + logInfo(`Removing ${filename}`, LogPrefix.Backend) + const gameSettingsFile = join(gamesConfigPath, filename) + if (existsSync(gameSettingsFile)) { + rmSync(gameSettingsFile) + } + } + + removeIfExists(appName.concat('.json')) + removeIfExists(appName.concat('.log')) + removeIfExists(appName.concat('-lastPlay.log')) +} + +export const uninstallGameCallback = async ( + event: Event, + appName: string, + runner: Runner, + shouldRemovePrefix: boolean, + shouldRemoveSetting: boolean +) => { + sendGameStatusUpdate({ + appName, + runner, + status: 'uninstalling' + }) + + const { title } = gameManagerMap[runner].getGameInfo(appName) + + let uninstalled = false + + try { + await gameManagerMap[runner].uninstall({ appName, shouldRemovePrefix }) + uninstalled = true + } catch (error) { + notify({ + title, + body: i18next.t('notify.uninstalled.error', 'Error uninstalling') + }) + logError(error, LogPrefix.Backend) + } + + if (uninstalled) { + if (shouldRemovePrefix) { + removePrefix(appName, runner) + } + if (shouldRemoveSetting) { + removeSettingsAndLogs(appName) + } + removeFixFile(appName, runner) + + notify({ title, body: i18next.t('notify.uninstalled') }) + logInfo('Finished uninstalling', LogPrefix.Backend) + } + + sendGameStatusUpdate({ + appName, + runner, + status: 'done' + }) +}