251 lines
8.9 KiB
C++
251 lines
8.9 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "UninstallHelper.h"
|
|
|
|
#include "RequiredProgramMainCPPInclude.h"
|
|
|
|
IMPLEMENT_APPLICATION(UninstallHelper, "UninstallHelper");
|
|
DEFINE_LOG_CATEGORY(LogUninstallHelper);
|
|
|
|
namespace UninstallHelper
|
|
{
|
|
|
|
struct FExecActionInfo
|
|
{
|
|
FString URL;
|
|
FString Params;
|
|
};
|
|
|
|
struct FUninstallOptions
|
|
{
|
|
bool bShouldExecute = true;
|
|
bool bAllowHarshReturnCodes = false;
|
|
FString AppName = "";
|
|
FString InstallDir = "";
|
|
bool bDeletePersistentDownloadDir = true;
|
|
bool bDeleteConfig = false;
|
|
bool bInstallDirRelativeToExecutableDir = false;
|
|
TArray<FString> AdditionalDeleteArtifacts = {};
|
|
TArray<FExecActionInfo> AdditionalActions = {};
|
|
};
|
|
|
|
inline FString GetUserSavedDir(const FUninstallOptions& Options)
|
|
{
|
|
return FPaths::Combine(FPlatformProcess::UserSettingsDir(), Options.AppName, "Saved");
|
|
}
|
|
|
|
inline FString GetPersistentDownloadDir(const FUninstallOptions& Options)
|
|
{
|
|
return FPaths::Combine(GetUserSavedDir(Options), "PersistentDownloadDir", "");
|
|
}
|
|
|
|
inline FString GetConfigDir(const FUninstallOptions& Options)
|
|
{
|
|
return FPaths::Combine(GetUserSavedDir(Options), "Config", "");
|
|
}
|
|
|
|
FString ReplacePathTokens(const FString& Path, const FUninstallOptions& Options)
|
|
{
|
|
FString Result = Path;
|
|
Result.ReplaceInline(TEXT("%LocalDataDir%"), FPlatformProcess::UserSettingsDir());
|
|
Result.ReplaceInline(TEXT("%LocalSavedDir%"), *GetUserSavedDir(Options));
|
|
Result.ReplaceInline(TEXT("%InstallDir%"), *Options.InstallDir);
|
|
Result.ReplaceInline(TEXT("%GameName%"), *Options.AppName);
|
|
return Result;
|
|
}
|
|
|
|
int32 ExecProcess(const FExecActionInfo& Action)
|
|
{
|
|
int32 Result;
|
|
FPlatformProcess::ExecElevatedProcess(*Action.URL, *Action.Params, &Result);
|
|
if (Result)
|
|
{
|
|
UE_LOG(LogUninstallHelper, Error, TEXT("Action %s %s failed with exit code %d"), *Action.URL, *Action.Params, Result);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
/**
|
|
* Runs the requested uninstall actions.
|
|
* @param Options The options to apply to this run.
|
|
* @return Whether or not all of the attempted actions were successful.
|
|
*/
|
|
EReturnCode Execute(const FUninstallOptions& Options)
|
|
{
|
|
// TODO: Decide whether an error should cause a graceful or hard fail
|
|
EReturnCode Result = EReturnCode::Success;
|
|
IFileManager& FileManager = IFileManager::Get();
|
|
if (Options.bDeletePersistentDownloadDir)
|
|
{
|
|
const FString PersistentDownloadDir = GetPersistentDownloadDir(Options);
|
|
if (FileManager.DirectoryExists(*PersistentDownloadDir))
|
|
{
|
|
UE_LOG(LogUninstallHelper, Log, TEXT("Deleting the PersistentDownloadDir located at %s"), *PersistentDownloadDir);
|
|
if (Options.bShouldExecute && !FileManager.DeleteDirectory(*PersistentDownloadDir, true, true))
|
|
{
|
|
Result = EReturnCode::DiskOperationFailed;
|
|
}
|
|
}
|
|
}
|
|
if (Options.bDeleteConfig)
|
|
{
|
|
const FString ConfigDir = GetConfigDir(Options);
|
|
if (FileManager.DirectoryExists(*ConfigDir))
|
|
{
|
|
UE_LOG(LogUninstallHelper, Log, TEXT("Deleting the Config directory located at %s"), *ConfigDir);
|
|
if (Options.bShouldExecute && !FileManager.DeleteDirectory(*ConfigDir, true, true))
|
|
{
|
|
Result = EReturnCode::DiskOperationFailed;
|
|
}
|
|
}
|
|
}
|
|
// Delete artifacts
|
|
for (const FString& Artifact : Options.AdditionalDeleteArtifacts)
|
|
{
|
|
FString CleanArtifactPath = ReplacePathTokens(Artifact, Options);
|
|
if (FileManager.DirectoryExists(*CleanArtifactPath))
|
|
{
|
|
UE_LOG(LogUninstallHelper, Log, TEXT("Deleting directory %s"), *CleanArtifactPath);
|
|
if (Options.bShouldExecute && !FileManager.DeleteDirectory(*CleanArtifactPath, true, true))
|
|
{
|
|
Result = EReturnCode::DiskOperationFailed;
|
|
}
|
|
}
|
|
else if (FileManager.FileExists(*CleanArtifactPath))
|
|
{
|
|
UE_LOG(LogUninstallHelper, Log, TEXT("Deleting file %s"), *CleanArtifactPath);
|
|
if (Options.bShouldExecute && !FileManager.Delete(*CleanArtifactPath, true, true))
|
|
{
|
|
Result = EReturnCode::DiskOperationFailed;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogUninstallHelper, Log, TEXT("Unable to find artifact '%s' on the disk, skipping."), *CleanArtifactPath);
|
|
}
|
|
}
|
|
// Execute other actions
|
|
int32 LastProcessResult = 0;
|
|
for (const FExecActionInfo& Action : Options.AdditionalActions)
|
|
{
|
|
UE_LOG(LogUninstallHelper, Log, TEXT("Executing process %s %s..."), *Action.URL, *Action.Params);
|
|
if (Options.bShouldExecute && (LastProcessResult = ExecProcess(Action)) != 0)
|
|
{
|
|
UE_LOG(LogUninstallHelper, Log, TEXT("Process %s %s failed with exit code %d"), *Action.URL, *Action.Params, LastProcessResult);
|
|
Result = EReturnCode::ExecActionFailed;
|
|
}
|
|
}
|
|
|
|
if (Result != EReturnCode::Success)
|
|
{
|
|
UE_LOG(LogUninstallHelper, Error, TEXT("UninstallHelper exited with code %d"), static_cast<int32>(Result));
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
}
|
|
|
|
INT32_MAIN_INT32_ARGC_TCHAR_ARGV()
|
|
{
|
|
const FString CmdLine = FCommandLine::BuildFromArgV(nullptr, ArgC, ArgV, nullptr);
|
|
if (!FCommandLine::Set(*CmdLine))
|
|
{
|
|
UE_LOG(LogUninstallHelper, Error, TEXT("Failed to initialize command line. Exiting."));
|
|
return static_cast<int32>(UninstallHelper::EReturnCode::ArgumentError);
|
|
}
|
|
|
|
UninstallHelper::FUninstallOptions Options;
|
|
|
|
if (!FParse::Value(*CmdLine, TEXT("GameName="), Options.AppName))
|
|
{
|
|
UE_LOG(LogUninstallHelper, Error, TEXT("An app name must be provided in order for this tool to run. Exiting."));
|
|
return static_cast<int32>(UninstallHelper::EReturnCode::UnknownAppName);
|
|
}
|
|
|
|
if (!FParse::Value(*CmdLine, TEXT("InstallDir="), Options.InstallDir))
|
|
{
|
|
UE_LOG(LogUninstallHelper, Error, TEXT("InstallDir must be provided in order for this tool to run. Exiting."));
|
|
return static_cast<int32>(UninstallHelper::EReturnCode::InvalidInstallDir);
|
|
}
|
|
|
|
// InstallDir can optionally be treated as relative to the UninstallHelper executable directory instead of the current working directory.
|
|
FParse::Bool(*CmdLine, TEXT("InstallDirRelativeToExecutableDir="), Options.bInstallDirRelativeToExecutableDir);
|
|
if (Options.bInstallDirRelativeToExecutableDir && FPaths::IsRelative(Options.InstallDir))
|
|
{
|
|
const FString ExecutableDir = FPaths::GetPath(FPlatformProcess::ExecutablePath());
|
|
Options.InstallDir = FPaths::ConvertRelativePathToFull(ExecutableDir, Options.InstallDir);
|
|
}
|
|
|
|
if (!FPaths::DirectoryExists(Options.InstallDir))
|
|
{
|
|
UE_LOG(LogUninstallHelper, Error, TEXT("InstallDir %s does not exist. Exiting."), *Options.InstallDir);
|
|
return static_cast<int32>(UninstallHelper::EReturnCode::InvalidInstallDir);
|
|
}
|
|
FParse::Bool(*CmdLine, TEXT("AllowHarshReturnCodes="), Options.bAllowHarshReturnCodes);
|
|
FParse::Bool(*CmdLine, TEXT("DeletePersistentDownloadDir="), Options.bDeletePersistentDownloadDir);
|
|
FParse::Bool(*CmdLine, TEXT("DeleteConfig="), Options.bDeleteConfig);
|
|
Options.bShouldExecute = !FParse::Param(*CmdLine, TEXT("NoExecute")); // An option to just log with this application, do not actually do anything
|
|
|
|
FString ListDelimiter = ";";
|
|
FParse::Value(*CmdLine, TEXT("ArtifactDelimiter="), ListDelimiter);
|
|
|
|
FString DeleteDirsCmd;
|
|
if (FParse::Value(*CmdLine, TEXT("DeleteArtifacts="), DeleteDirsCmd))
|
|
{
|
|
DeleteDirsCmd.ParseIntoArray(Options.AdditionalDeleteArtifacts, *ListDelimiter);
|
|
}
|
|
|
|
FString UninstallArtifactsManifestFilename;
|
|
if (FParse::Value(*CmdLine, TEXT("UninstallArtifactsManifest="), UninstallArtifactsManifestFilename))
|
|
{
|
|
TArray<FString> UninstallArtifactsManifest;
|
|
const FString ManifestPath = UninstallHelper::ReplacePathTokens(UninstallArtifactsManifestFilename, Options);
|
|
if (!FFileHelper::LoadFileToStringArray(UninstallArtifactsManifest, *ManifestPath))
|
|
{
|
|
UE_LOG(LogUninstallHelper, Warning, TEXT("Failed to find uninstall artifacts manifest file '%s', skipping."), *ManifestPath);
|
|
}
|
|
Options.AdditionalDeleteArtifacts.Append(UninstallArtifactsManifest);
|
|
}
|
|
|
|
FString UninstallActionsManifestFilename;
|
|
if (FParse::Value(*CmdLine, TEXT("UninstallActionsManifest="), UninstallActionsManifestFilename))
|
|
{
|
|
TArray<FString> UninstallActionsManifest;
|
|
const FString ManifestPath = UninstallHelper::ReplacePathTokens(UninstallActionsManifestFilename, Options);
|
|
if (FFileHelper::LoadFileToStringArray(UninstallActionsManifest, *ManifestPath))
|
|
{
|
|
for (const FString& Action : UninstallActionsManifest)
|
|
{
|
|
FString Url;
|
|
FString Args;
|
|
if (Action.StartsWith(TEXT("\"")))
|
|
{
|
|
int Size;
|
|
FParse::QuotedString(*Action, Url, &Size);
|
|
Url = Url.TrimQuotes();
|
|
Args = Action.RightChop(Size).TrimStart();
|
|
}
|
|
else
|
|
{
|
|
Action.Split(TEXT(" "), &Url, &Args);
|
|
}
|
|
Url = ReplacePathTokens(Url, Options);
|
|
Args = ReplacePathTokens(Args, Options);
|
|
Options.AdditionalActions.Add({ Url, Args });
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogUninstallHelper, Warning, TEXT("Failed to find uninstall actions manifest file '%s', skipping."), *ManifestPath);
|
|
}
|
|
}
|
|
|
|
const UninstallHelper::EReturnCode Result = UninstallHelper::Execute(Options);
|
|
if (Result != UninstallHelper::EReturnCode::Success)
|
|
{
|
|
UE_LOG(LogUninstallHelper, Error, TEXT("UninstallHelper failed to execute with exit code %d"), static_cast<int32>(Result));
|
|
}
|
|
return Options.bAllowHarshReturnCodes ? static_cast<int32>(Result) : 0;
|
|
}
|