Rewrite custom actions in C (#253)

* Rewrite custom actions in C

* Add language setting file

* Improve setup

* Improve build script

* Skip directory selection on upgrade

* Fix dll symbols error on x86

* Validate install path

* Hide language selection dialog on upgrade

* Improve macros

* Add multi-language support to the setup

* Remove old files
This commit is contained in:
Gerhard Tan
2025-07-03 22:05:26 +08:00
committed by GitHub
parent a443bf41ba
commit 8219d4b7e5
19 changed files with 848 additions and 678 deletions

View File

@ -72,7 +72,7 @@ VERSIONINFO_TEMPLATE(
"FRP 관리자" "FRP 관리자"
) )
LANGUAGE LANG_SPANISH, SUBLANG_SPANISH LANGUAGE LANG_SPANISH, SUBLANG_SPANISH_MODERN
VERSIONINFO_TEMPLATE( VERSIONINFO_TEMPLATE(
"0C0A04B0", 0x0C0A, 1200, "0C0A04B0", 0x0C0A, 1200,
"Administrador de FRP" "Administrador de FRP"

View File

@ -42,7 +42,14 @@ func GetLanguage() string {
// langInConfig returns the UI language code in config file // langInConfig returns the UI language code in config file
func langInConfig() string { func langInConfig() string {
b, err := os.ReadFile(config.DefaultAppFile) b, err := os.ReadFile(config.LangFile)
if err == nil {
id := string(b)
if _, ok := IDToName[id]; ok {
return id
}
}
b, err = os.ReadFile(config.DefaultAppFile)
if err != nil { if err != nil {
return "" return ""
} }
@ -52,7 +59,10 @@ func langInConfig() string {
if err = json.Unmarshal(b, &s); err != nil { if err = json.Unmarshal(b, &s); err != nil {
return "" return ""
} }
return s.Lang if _, ok := IDToName[s.Lang]; ok {
return s.Lang
}
return ""
} }
// lang returns the user preferred UI language. // lang returns the user preferred UI language.

View File

@ -1,3 +0,0 @@
.vs
bin
obj

397
installer/actions/actions.c Normal file
View File

@ -0,0 +1,397 @@
#include <windows.h>
#include <msi.h>
#include <msidefs.h>
#include <msiquery.h>
#include <tlhelp32.h>
#include <shlwapi.h>
#define LEGACY_SERVICE_PREFIX L"FRPC$"
#define SERVICE_PREFIX L"frpmgr_"
static void Log(MSIHANDLE installer, INSTALLMESSAGE messageType, const WCHAR* format, ...)
{
MSIHANDLE record = MsiCreateRecord(0);
if (!record)
return;
LPWSTR pBuffer = NULL;
va_list args = NULL;
va_start(args, format);
FormatMessageW(FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ALLOCATE_BUFFER, format,
0, 0, (LPWSTR)&pBuffer, 0, &args);
va_end(args);
if (pBuffer)
{
MsiRecordSetStringW(record, 0, pBuffer);
MsiProcessMessage(installer, messageType, record);
LocalFree(pBuffer);
}
MsiCloseHandle(record);
}
static BOOL GetFileInformation(const LPWSTR path, BY_HANDLE_FILE_INFORMATION* fileInfo)
{
HANDLE file = CreateFileW(path, 0, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (file == INVALID_HANDLE_VALUE)
return FALSE;
BOOL ret = GetFileInformationByHandle(file, fileInfo);
CloseHandle(file);
return ret;
}
static void KillProcessesEx(LPWSTR path, BOOL uiOnly)
{
HANDLE snapshot, process;
BY_HANDLE_FILE_INFORMATION fileInfo = { 0 }, procFileInfo = { 0 };
WCHAR procPath[MAX_PATH];
PROCESSENTRY32W entry;
entry.dwSize = sizeof(PROCESSENTRY32W);
LPWSTR filename = PathFindFileNameW(path);
if (!GetFileInformation(path, &fileInfo))
return;
snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE)
return;
for (BOOL ret = Process32FirstW(snapshot, &entry); ret; ret = Process32NextW(snapshot, &entry))
{
if (_wcsicmp(entry.szExeFile, filename))
continue;
process = OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID);
if (!process)
continue;
DWORD procPathLen = _countof(procPath);
DWORD sessionId = 0;
if (!QueryFullProcessImageNameW(process, 0, procPath, &procPathLen))
goto next;
if (!GetFileInformation(procPath, &procFileInfo))
goto next;
if (fileInfo.dwVolumeSerialNumber != procFileInfo.dwVolumeSerialNumber ||
fileInfo.nFileIndexHigh != procFileInfo.nFileIndexHigh ||
fileInfo.nFileIndexLow != procFileInfo.nFileIndexLow)
goto next;
if (!ProcessIdToSessionId(entry.th32ProcessID, &sessionId))
goto next;
if (uiOnly && sessionId == 0)
goto next;
if (TerminateProcess(process, 1))
WaitForSingleObject(process, INFINITE);
next:
CloseHandle(process);
}
CloseHandle(snapshot);
return;
}
__declspec(dllexport) UINT __stdcall KillFrpProcesses(MSIHANDLE installer)
{
WCHAR path[MAX_PATH];
DWORD pathLen = _countof(path);
UINT ret = MsiGetPropertyW(installer, L"CustomActionData", path, &pathLen);
if (ret != ERROR_SUCCESS)
{
Log(installer, INSTALLMESSAGE_ERROR, L"Failed to load CustomActionData");
return ERROR_SUCCESS;
}
if (path[0])
KillProcessesEx(path, FALSE);
return ERROR_SUCCESS;
}
__declspec(dllexport) UINT __stdcall KillFrpGUIProcesses(MSIHANDLE installer)
{
WCHAR path[MAX_PATH];
DWORD pathLen = _countof(path);
MSIHANDLE record = MsiCreateRecord(0);
if (!record)
return ERROR_SUCCESS;
MsiRecordSetStringW(record, 0, L"[#frpmgr.exe]");
UINT ret = MsiFormatRecordW(installer, record, path, &pathLen);
MsiCloseHandle(record);
if (ret != ERROR_SUCCESS)
{
Log(installer, INSTALLMESSAGE_ERROR, L"Failed to load application path");
return ERROR_SUCCESS;
}
if (path[0])
KillProcessesEx(path, TRUE);
return ERROR_SUCCESS;
}
__declspec(dllexport) UINT __stdcall EvaluateFrpServices(MSIHANDLE installer)
{
SC_HANDLE scm = NULL;
LPENUM_SERVICE_STATUS_PROCESSW services = NULL;
DWORD SERVICE_STATUS_PROCESS_SIZE = 0x10000;
DWORD resume = 0;
LPQUERY_SERVICE_CONFIGW cfg = NULL;
DWORD cfgSize = 0;
MSIHANDLE db = 0, view = 0;
WCHAR path[MAX_PATH];
DWORD pathLen = _countof(path);
BY_HANDLE_FILE_INFORMATION fileInfo = { 0 }, svcFileInfo = { 0 };
BOOL fileInfoExists = FALSE;
MSIHANDLE record = MsiCreateRecord(0);
if (!record)
goto out;
MsiRecordSetStringW(record, 0, L"[#frpmgr.exe]");
UINT ret = MsiFormatRecordW(installer, record, path, &pathLen);
MsiCloseHandle(record);
if (ret != ERROR_SUCCESS)
{
Log(installer, INSTALLMESSAGE_ERROR, L"Failed to load application path");
goto out;
}
if (!path[0])
goto out;
fileInfoExists = GetFileInformation(path, &fileInfo);
db = MsiGetActiveDatabase(installer);
if (!db)
{
Log(installer, INSTALLMESSAGE_ERROR, L"MsiGetActiveDatabase failed");
goto out;
}
ret = MsiDatabaseOpenViewW(db,
L"INSERT INTO `ServiceControl` (`ServiceControl`, `Name`, `Event`, `Component_`, `Wait`) VALUES(?, ?, ?, ?, ?) TEMPORARY",
&view);
if (ret != ERROR_SUCCESS)
{
Log(installer, INSTALLMESSAGE_ERROR, L"MsiDatabaseOpenView failed (%1)", ret);
goto out;
}
scm = OpenSCManagerW(NULL, SERVICES_ACTIVE_DATABASEW, SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE);
if (!scm)
{
Log(installer, INSTALLMESSAGE_ERROR, L"OpenSCManager failed (%1)", GetLastError());
goto out;
}
services = (LPENUM_SERVICE_STATUS_PROCESSW)LocalAlloc(LMEM_FIXED, SERVICE_STATUS_PROCESS_SIZE);
if (!services)
{
Log(installer, INSTALLMESSAGE_ERROR, L"LocalAlloc failed (%1)", GetLastError());
goto out;
}
for (BOOL more = TRUE; more;)
{
DWORD bytesNeeded = 0, count = 0;
if (EnumServicesStatusExW(scm, SC_ENUM_PROCESS_INFO, SERVICE_WIN32, SERVICE_STATE_ALL, (LPBYTE)services,
SERVICE_STATUS_PROCESS_SIZE, &bytesNeeded, &count, &resume, NULL))
more = FALSE;
else
{
ret = GetLastError();
if (ret != ERROR_MORE_DATA)
{
Log(installer, INSTALLMESSAGE_ERROR, L"EnumServicesStatusEx failed (%1)", ret);
break;
}
}
for (DWORD i = 0; i < count; ++i)
{
INT legacy;
if ((legacy = _wcsnicmp(services[i].lpServiceName, LEGACY_SERVICE_PREFIX, _countof(LEGACY_SERVICE_PREFIX) - 1)) &&
_wcsnicmp(services[i].lpServiceName, SERVICE_PREFIX, _countof(SERVICE_PREFIX) - 1))
continue;
SC_HANDLE service = OpenServiceW(scm, services[i].lpServiceName, SERVICE_QUERY_CONFIG);
if (!service)
continue;
BOOL ok = FALSE;
while (!(ok = QueryServiceConfigW(service, cfg, cfgSize, &bytesNeeded)) && GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
if (cfg)
LocalFree(cfg);
else
bytesNeeded += sizeof(path) + 256 * sizeof(WCHAR); // Additional size for path and display name.
cfgSize = bytesNeeded;
cfg = (LPQUERY_SERVICE_CONFIGW)LocalAlloc(LMEM_FIXED, cfgSize);
if (!cfg)
{
Log(installer, INSTALLMESSAGE_ERROR, L"LocalAlloc failed (%1)", GetLastError());
break;
}
}
CloseServiceHandle(service);
if (!ok || cfg == NULL)
continue;
INT nArgs = 0;
LPWSTR* args = CommandLineToArgvW(cfg->lpBinaryPathName, &nArgs);
if (!args)
continue;
ok = nArgs >= 1 && (fileInfoExists ?
(GetFileInformation(args[0], &svcFileInfo) &&
fileInfo.dwVolumeSerialNumber == svcFileInfo.dwVolumeSerialNumber &&
fileInfo.nFileIndexHigh == svcFileInfo.nFileIndexHigh &&
fileInfo.nFileIndexLow == svcFileInfo.nFileIndexLow) : _wcsicmp(args[0], path) == 0);
LocalFree(args);
if (!ok)
continue;
Log(installer, INSTALLMESSAGE_INFO, L"Scheduling stop on upgrade or removal on uninstall of service %1", services[i].lpServiceName);
GUID guid;
if (FAILED(CoCreateGuid(&guid)))
continue;
WCHAR identifier[40];
if (StringFromGUID2(&guid, identifier, _countof(identifier)) == 0)
continue;
record = MsiCreateRecord(5);
if (!record)
continue;
MsiRecordSetStringW(record, 1, identifier);
MsiRecordSetStringW(record, 2, services[i].lpServiceName);
MsiRecordSetInteger(record, 3, msidbServiceControlEventStop | msidbServiceControlEventUninstallStop | (legacy == 0 ? msidbServiceControlEventDelete : 0) | msidbServiceControlEventUninstallDelete);
MsiRecordSetStringW(record, 4, L"frpmgr.exe");
MsiRecordSetInteger(record, 5, 1);
ret = MsiViewExecute(view, record);
MsiCloseHandle(record);
if (ret != ERROR_SUCCESS)
Log(installer, INSTALLMESSAGE_ERROR, L"MsiViewExecute failed for service %1 (%2)", services[i].lpServiceName, ret);
}
}
LocalFree(services);
if (cfg)
LocalFree(cfg);
out:
if (scm)
CloseServiceHandle(scm);
if (view)
MsiCloseHandle(view);
if (db)
MsiCloseHandle(db);
return ERROR_SUCCESS;
}
__declspec(dllexport) UINT __stdcall SetLangConfig(MSIHANDLE installer)
{
WCHAR path[MAX_PATH];
DWORD pathLen = _countof(path);
UINT ret = MsiGetPropertyW(installer, L"CustomActionData", path, &pathLen);
if (ret != ERROR_SUCCESS)
{
Log(installer, INSTALLMESSAGE_ERROR, L"Failed to load CustomActionData");
goto out;
}
if (!path[0] || !PathAppendW(path, L"lang.config"))
goto out;
WCHAR localeName[LOCALE_NAME_MAX_LENGTH];
if (LCIDToLocaleName(MsiGetLanguage(installer), localeName, _countof(localeName), 0) == 0)
goto out;
HANDLE langFile = CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (langFile == INVALID_HANDLE_VALUE)
goto out;
CHAR buf[LOCALE_NAME_MAX_LENGTH];
DWORD bytesWritten = WideCharToMultiByte(CP_UTF8, 0, localeName, -1, buf, sizeof(buf), NULL, NULL);
if (bytesWritten > 0)
WriteFile(langFile, buf, bytesWritten - 1, &bytesWritten, NULL);
CloseHandle(langFile);
out:
return ERROR_SUCCESS;
}
__declspec(dllexport) UINT __stdcall MoveFrpProfiles(MSIHANDLE installer)
{
WIN32_FIND_DATAW findData;
const WCHAR* dirs[] = { NULL, L"profiles" };
WCHAR path[MAX_PATH], newPath[MAX_PATH];
DWORD pathLen = _countof(path), newPathLen = _countof(newPath);
UINT ret = MsiGetPropertyW(installer, L"CustomActionData", path, &pathLen);
if (ret != ERROR_SUCCESS)
{
Log(installer, INSTALLMESSAGE_ERROR, L"Failed to load CustomActionData");
goto out;
}
if (!path[0] || !PathCombineW(newPath, path, L"profiles"))
goto out;
if (CreateDirectoryW(newPath, NULL) == 0 && GetLastError() != ERROR_ALREADY_EXISTS)
goto out;
newPathLen = wcsnlen_s(newPath, _countof(newPath) - 1);
for (size_t i = 0; i < _countof(dirs); i++)
{
path[pathLen] = L'\0';
if (dirs[i] && !PathAppendW(path, dirs[i]))
continue;
pathLen = wcsnlen_s(path, _countof(path) - 1);
if (!PathAppendW(path, L"*.ini"))
continue;
HANDLE hFind = FindFirstFileExW(path, FindExInfoBasic, &findData, FindExSearchNameMatch, NULL, 0);
if (hFind != INVALID_HANDLE_VALUE)
{
do
{
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY || !PathMatchSpecW(findData.cFileName, L"*.ini"))
continue;
path[pathLen] = L'\0';
newPath[newPathLen] = L'\0';
if (!PathAppendW(path, findData.cFileName) ||
!PathRenameExtensionW(findData.cFileName, L".conf") ||
!PathAppendW(newPath, findData.cFileName))
continue;
MoveFileW(path, newPath);
} while (FindNextFileW(hFind, &findData));
FindClose(hFind);
}
}
out:
return ERROR_SUCCESS;
}
__declspec(dllexport) UINT __stdcall RemoveFrpFiles(MSIHANDLE installer)
{
WCHAR path[MAX_PATH];
DWORD pathLen = _countof(path);
UINT ret = MsiGetPropertyW(installer, L"CustomActionData", path, &pathLen);
if (ret != ERROR_SUCCESS)
{
Log(installer, INSTALLMESSAGE_ERROR, L"Failed to load CustomActionData");
return ERROR_SUCCESS;
}
const WCHAR* appFiles[] = { L"app.json", L"lang.config" };
for (size_t i = 0; i < _countof(appFiles); i++)
{
path[pathLen] = L'\0';
if (!PathAppendW(path, appFiles[i]))
return ERROR_SUCCESS;
DeleteFileW(path);
}
WIN32_FIND_DATAW findData;
const WCHAR* files[][2] = { {L"profiles", L"*.conf"}, {L"logs", L"*.log"} };
for (size_t i = 0; i < _countof(files); i++)
{
path[pathLen] = L'\0';
if (!PathAppendW(path, files[i][0]))
continue;
SIZE_T dirLen = wcsnlen_s(path, _countof(path) - 1);
if (!PathAppendW(path, files[i][1]))
continue;
HANDLE hFind = FindFirstFileExW(path, FindExInfoBasic, &findData, FindExSearchNameMatch, NULL, 0);
if (hFind != INVALID_HANDLE_VALUE)
{
do
{
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY || !PathMatchSpecW(findData.cFileName, files[i][1]))
continue;
path[dirLen] = L'\0';
if (!PathAppendW(path, findData.cFileName))
continue;
DeleteFileW(path);
} while (FindNextFileW(hFind, &findData));
FindClose(hFind);
}
path[dirLen] = L'\0';
RemoveDirectoryW(path);
}
return ERROR_SUCCESS;
}

View File

@ -0,0 +1,8 @@
LIBRARY actions
EXPORTS
EvaluateFrpServices
KillFrpGUIProcesses
KillFrpProcesses
MoveFrpProfiles
RemoveFrpFiles
SetLangConfig

View File

@ -1,31 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30717.126
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "actions", "actions\actions.csproj", "{DC9743DA-8782-4A7C-8B46-B2D4EEA19D4E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DC9743DA-8782-4A7C-8B46-B2D4EEA19D4E}.Debug|x64.ActiveCfg = Debug|x64
{DC9743DA-8782-4A7C-8B46-B2D4EEA19D4E}.Debug|x64.Build.0 = Debug|x64
{DC9743DA-8782-4A7C-8B46-B2D4EEA19D4E}.Debug|x86.ActiveCfg = Debug|x86
{DC9743DA-8782-4A7C-8B46-B2D4EEA19D4E}.Debug|x86.Build.0 = Debug|x86
{DC9743DA-8782-4A7C-8B46-B2D4EEA19D4E}.Release|x64.ActiveCfg = Release|x64
{DC9743DA-8782-4A7C-8B46-B2D4EEA19D4E}.Release|x64.Build.0 = Release|x64
{DC9743DA-8782-4A7C-8B46-B2D4EEA19D4E}.Release|x86.ActiveCfg = Release|x86
{DC9743DA-8782-4A7C-8B46-B2D4EEA19D4E}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9B84C910-F22D-4D3F-9BD6-6C2134E26EE8}
EndGlobalSection
EndGlobal

View File

@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<!--
Use supportedRuntime tags to explicitly specify the version(s) of the .NET Framework runtime that
the custom action should run on. If no versions are specified, the chosen version of the runtime
will be the "best" match to what Microsoft.Deployment.WindowsInstaller.dll was built against.
WARNING: leaving the version unspecified is dangerous as it introduces a risk of compatibility
problems with future versions of the .NET Framework runtime. It is highly recommended that you specify
only the version(s) of the .NET Framework runtime that you have tested against.
Note for .NET Framework v3.0 and v3.5, the runtime version is still v2.0.
In order to enable .NET Framework version 2.0 runtime activation policy, which is to load all assemblies
by using the latest supported runtime, @useLegacyV2RuntimeActivationPolicy="true".
For more information, see http://msdn.microsoft.com/en-us/library/bbx34a2h.aspx
-->
<supportedRuntime version="v4.0" />
<supportedRuntime version="v2.0.50727"/>
</startup>
<!--
Add additional configuration settings here. For more information on application config files,
see http://msdn.microsoft.com/en-us/library/kza1yk3a.aspx
-->
</configuration>

View File

@ -1,290 +0,0 @@
using Microsoft.Deployment.WindowsInstaller;
using System;
using System.Configuration.Install;
using System.Diagnostics;
using System.IO;
using System.Management;
using System.Runtime.InteropServices;
using System.ServiceProcess;
using System.Text;
namespace actions
{
public class CustomActions
{
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern int MessageBox(int hWnd, String text, String caption, uint type);
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, int dwShareMode, IntPtr lpSECURITY_ATTRIBUTES, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool GetFileInformationByHandle(IntPtr handle, ref BY_HANDLE_FILE_INFORMATION hfi);
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern int LCIDToLocaleName(uint Locale, StringBuilder lpName, int cchName, int dwFlags);
[StructLayout(LayoutKind.Sequential)]
public struct BY_HANDLE_FILE_INFORMATION
{
public uint dwFileAttributes;
public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
public uint dwVolumeSerialNumber;
public uint nFileSizeHigh;
public uint nFileSizeLow;
public uint nNumberOfLinks;
public uint nFileIndexHigh;
public uint nFileIndexLow;
}
public const int OPEN_EXISTING = 3;
public const int INVALID_HANDLE_VALUE = -1;
public const int FILE_ATTRIBUTE_NORMAL = 0x80;
public const int MB_OK = 0;
public const int MB_YESNO = 0x4;
public const int MB_RETRYCANCEL = 0x5;
public const int MB_ICONQUESTION = 0x20;
public const int MB_ICONWARNING = 0x30;
public const int IDYES = 6;
public const int IDRETRY = 4;
public static bool CalculateFileId(string path, out BY_HANDLE_FILE_INFORMATION hfi)
{
hfi = new BY_HANDLE_FILE_INFORMATION { };
IntPtr file = CreateFile(path, 0, 0, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero);
if (file.ToInt32() == INVALID_HANDLE_VALUE)
return false;
bool ret = GetFileInformationByHandle(file, ref hfi);
CloseHandle(file);
if (!ret)
return false;
return true;
}
public static bool CompareFile(BY_HANDLE_FILE_INFORMATION f1, BY_HANDLE_FILE_INFORMATION f2)
{
return f1.dwVolumeSerialNumber == f2.dwVolumeSerialNumber && f1.nFileIndexHigh == f2.nFileIndexHigh && f1.nFileIndexLow == f2.nFileIndexLow;
}
public static int ForceDeleteDirectory(string path)
{
if (!Directory.Exists(path))
{
return -1;
}
using (ManagementObject dirObject = new ManagementObject("Win32_Directory.Name='" + path + "'"))
{
dirObject.Get();
ManagementBaseObject outParams = dirObject.InvokeMethod("Delete", null, null);
return Convert.ToInt32(outParams.Properties["ReturnValue"].Value);
}
}
public static void RemoveServices(Session session, string prefix, string binPath)
{
ServiceController[] services = ServiceController.GetServices();
foreach (ServiceController controller in services)
{
ManagementObject wmiService = new ManagementObject("Win32_Service.Name='" + controller.ServiceName + "'");
wmiService.Get();
string pathName = wmiService.GetPropertyValue("PathName").ToString();
string path1 = pathName.Substring(0, Math.Min(binPath.Length, pathName.Length));
string path2 = pathName.Substring(1, Math.Min(binPath.Length, pathName.Length - 1));
if ((binPath.ToLower().Equals(path1.ToLower()) || binPath.ToLower().Equals(path2.ToLower())) && controller.ServiceName.StartsWith(prefix))
{
try
{
controller.Stop();
controller.WaitForStatus(ServiceControllerStatus.Stopped);
}
catch (Exception)
{
session.Log("Failed to stop " + controller.ServiceName);
}
ServiceInstaller installer = new ServiceInstaller
{
Context = new InstallContext(),
ServiceName = controller.ServiceName
};
try
{
installer.Uninstall(null);
}
catch (Exception)
{
session.Log("Failed to uninstall " + controller.ServiceName);
}
}
}
}
[CustomAction]
public static ActionResult KillProcesses(Session session)
{
session.Log("Killing FRP processes");
string binPath = session["CustomActionData"];
if (string.IsNullOrEmpty(binPath) || !CalculateFileId(binPath, out BY_HANDLE_FILE_INFORMATION binInfo))
{
return ActionResult.Success;
}
Process[] processes = Process.GetProcesses();
foreach (Process p in processes)
{
try
{
if (!CalculateFileId(p.MainModule.FileName, out BY_HANDLE_FILE_INFORMATION info))
continue;
if (CompareFile(binInfo, info))
{
p.Kill();
p.WaitForExit();
}
}
catch (Exception)
{
continue;
}
}
return ActionResult.Success;
}
[CustomAction]
public static ActionResult RemoveFrpFiles(Session session)
{
session.Log("Removing files");
string installPath = session["CustomActionData"];
if (!string.IsNullOrEmpty(installPath))
{
ForceDeleteDirectory(Path.Combine(installPath, "profiles"));
ForceDeleteDirectory(Path.Combine(installPath, "logs"));
try
{
File.Delete(Path.Combine(installPath, "app.json"));
}
catch (Exception e)
{
session.Log(e.Message);
}
}
return ActionResult.Success;
}
[CustomAction]
public static ActionResult EvaluateFrpServices(Session session)
{
session.Log("Evaluate FRP Services");
string binPath = session["CustomActionData"];
if (string.IsNullOrEmpty(binPath))
{
return ActionResult.Success;
}
RemoveServices(session, "", binPath);
return ActionResult.Success;
}
[CustomAction]
public static ActionResult KillGUIProcesses(Session session)
{
session.Log("Killing FRP GUI processes");
string binPath = session.Format(session["WixShellExecTarget"]);
if (string.IsNullOrEmpty(binPath))
{
return ActionResult.Success;
}
SelectQuery q = new SelectQuery("Win32_Process", "ExecutablePath = '" + binPath.Replace(@"\", @"\\") + "' AND SessionId != 0");
ManagementObjectSearcher s = new ManagementObjectSearcher(q);
foreach (ManagementObject process in s.Get())
{
process.Delete();
}
return ActionResult.Success;
}
[CustomAction]
public static ActionResult SetLangConfig(Session session)
{
session.Log("Set language config");
string installPath = session["CustomActionData"];
if (string.IsNullOrEmpty(installPath))
{
return ActionResult.Failure;
}
StringBuilder name = new StringBuilder(500);
if (LCIDToLocaleName((uint)session.Language, name, name.Capacity, 0) == 0)
{
return ActionResult.Failure;
}
string cfgPath = Path.Combine(installPath, "app.json");
if (!File.Exists(cfgPath))
{
try
{
File.WriteAllText(cfgPath, "{\n \"lang\": \"" + name.ToString() + "\"\n}");
}
catch (Exception e)
{
session.Log(e.Message);
}
}
return ActionResult.Success;
}
[CustomAction]
public static ActionResult MoveFrpProfiles(Session session)
{
session.Log("Moving FRP profiles");
string installPath = session["CustomActionData"];
if (string.IsNullOrEmpty(installPath))
{
return ActionResult.Failure;
}
string profilePath = Path.Combine(installPath, "profiles");
Directory.CreateDirectory(profilePath);
foreach (string profile in Directory.GetFiles(installPath, "*.ini"))
{
try
{
File.Move(profile, Path.Combine(profilePath, Path.GetFileName(profile)));
}
catch (Exception e)
{
session.Log(e.Message);
}
}
foreach (string profile in Directory.GetFiles(profilePath, "*.ini"))
{
try
{
File.Move(profile, Path.Combine(profilePath, Path.GetFileNameWithoutExtension(profile) + ".conf"));
}
catch (Exception e)
{
session.Log(e.Message);
}
}
return ActionResult.Success;
}
[CustomAction]
public static ActionResult RemoveOldFrpServices(Session session)
{
session.Log("Remove old FRP Services");
string binPath = session["CustomActionData"];
if (string.IsNullOrEmpty(binPath))
{
return ActionResult.Success;
}
RemoveServices(session, "FRPC$", binPath);
return ActionResult.Success;
}
}
}

View File

@ -1,35 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("FRP Manager Setup Custom Actions")]
[assembly: AssemblyDescription("FRP Manager Setup Custom Actions")]
[assembly: AssemblyCompany("FRP Manager Project")]
[assembly: AssemblyProduct("FRP Manager")]
[assembly: AssemblyCopyright("Copyright © FRP Manager Project")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("dc9743da-8782-4a7c-8b46-b2d4eea19d4e")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.21.1.0")]
[assembly: AssemblyFileVersion("1.21.1.0")]

View File

@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" InitialTargets="EnsureWixToolsetInstalled" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{DC9743DA-8782-4A7C-8B46-B2D4EEA19D4E}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>actions</RootNamespace>
<AssemblyName>actions</AssemblyName>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>7.3</LangVersion>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Configuration.Install" />
<Reference Include="System.Core" />
<Reference Include="System.Management" />
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.Deployment.WindowsInstaller">
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="CustomAction.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Content Include="CustomAction.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(WixCATargetsPath)" Condition=" '$(WixCATargetsPath)' != '' " />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.CA.targets" Condition=" '$(WixCATargetsPath)' == '' AND Exists('$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.CA.targets') " />
<Target Name="EnsureWixToolsetInstalled" Condition=" '$(WixCATargetsImported)' != 'true' ">
<Error Text="The WiX Toolset v3.11 (or newer) build tools must be installed to build this project. To download the WiX Toolset, see http://wixtoolset.org/releases/" />
</Target>
</Project>

View File

@ -0,0 +1,10 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level='asInvoker' uiAccess='false' />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

View File

@ -0,0 +1,38 @@
#include <windows.h>
#pragma code_page(65001) // UTF-8
#define STRINGIZE(x) #x
#define EXPAND(x) STRINGIZE(x)
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_ARRAY
PRODUCTVERSION VERSION_ARRAY
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0x0
FILEOS VOS__WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "FRP Manager Project"
VALUE "FileDescription", "FRP Manager Setup Custom Actions"
VALUE "FileVersion", EXPAND(VERSION_STR)
VALUE "InternalName", "frpmgr-actions"
VALUE "LegalCopyright", "Copyright © FRP Manager Project"
VALUE "OriginalFilename", "actions.dll"
VALUE "ProductName", "FRP Manager"
VALUE "ProductVersion", EXPAND(VERSION_STR)
VALUE "Comments", "https://github.com/koho/frpmgr"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
ISOLATIONAWARE_MANIFEST_RESOURCE_ID RT_MANIFEST manifest.xml

View File

@ -16,11 +16,6 @@ if "%ARCH%" == "" (
exit /b 1 exit /b 1
) )
if not defined WIX (
echo ERROR: WIX was not found.
exit /b 1
)
:build :build
if not exist build md build if not exist build md build
set PLAT_DIR=build\%ARCH% set PLAT_DIR=build\%ARCH%
@ -38,11 +33,15 @@ if not defined WIX (
exit /b 0 exit /b 0
:build_actions :build_actions
msbuild actions\actions.sln /t:Rebuild /p:Configuration=Release /p:Platform="%ARCH%" || goto :error rc /DVERSION_ARRAY=%VERSION:.=,% /DVERSION_STR=%VERSION% /Fo %PLAT_DIR%\actions.res actions\version.rc || goto :error
copy actions\actions\bin\%ARCH%\Release\actions.CA.dll %PLAT_DIR%\actions.dll /y || goto :error cl /O2 /LD /MD /DNDEBUG /Fe%PLAT_DIR%\actions.dll /Fo%PLAT_DIR%\actions.obj actions\actions.c /link /DEF:actions\actions.def %PLAT_DIR%\actions.res msi.lib shell32.lib advapi32.lib shlwapi.lib ole32.lib || goto :error
goto :eof goto :eof
:build_msi :build_msi
if not defined WIX (
echo ERROR: WIX was not found.
exit /b 1
)
set WIX_CANDLE_FLAGS=-dVERSION=%VERSION% set WIX_CANDLE_FLAGS=-dVERSION=%VERSION%
set WIX_LIGHT_FLAGS=-ext "%WIX%bin\WixUtilExtension.dll" -ext "%WIX%bin\WixUIExtension.dll" -sval set WIX_LIGHT_FLAGS=-ext "%WIX%bin\WixUtilExtension.dll" -ext "%WIX%bin\WixUIExtension.dll" -sval
set WIX_OBJ=%PLAT_DIR%\frpmgr.wixobj set WIX_OBJ=%PLAT_DIR%\frpmgr.wixobj
@ -58,8 +57,21 @@ if not defined WIX (
goto :eof goto :eof
:build_setup :build_setup
rc /DFILENAME=%SETUP_FILENAME% /DVERSION_ARRAY=%VERSION:.=,% /DVERSION_STR=%VERSION% /DMSI_FILE=%MSI_FILE:\=\\% /Fo %PLAT_DIR%\rsrc.res setup\resource.rc || goto :error rc /DFILENAME=%SETUP_FILENAME% /DVERSION_ARRAY=%VERSION:.=,% /DVERSION_STR=%VERSION% /DMSI_FILE=%MSI_FILE:\=\\% /Fo %PLAT_DIR%\setup.res setup\resource.rc || goto :error
cl /Fe%PLAT_DIR%\setup.exe /Fo%PLAT_DIR%\setup.obj /utf-8 setup\setup.c /link /subsystem:windows %PLAT_DIR%\rsrc.res shlwapi.lib msi.lib user32.lib advapi32.lib || goto :error set ARCH_LINE=-1
for /f "tokens=1 delims=:" %%a in ('findstr /n /r ".*=.*\"%ARCH%\"" msi\frpmgr.wxs') do set ARCH_LINE=%%a
if %ARCH_LINE% lss 0 (
echo ERROR: unsupported architecture.
exit /b 1
)
for /f "tokens=1,5 delims=: " %%a in ('findstr /n /r "UpgradeCode.*=.*\"[0-9a-fA-F-]*\"" msi\frpmgr.wxs') do (
if %%a gtr %ARCH_LINE% if not defined UPGRADE_CODE set UPGRADE_CODE=%%b
)
if not defined UPGRADE_CODE (
echo ERROR: UpgradeCode was not found.
exit /b 1
)
cl /O2 /MD /DUPGRADE_CODE=L\"{%UPGRADE_CODE%}\" /DVERSION=L\"%VERSION%\" /DNDEBUG /Fe%PLAT_DIR%\setup.exe /Fo%PLAT_DIR%\setup.obj setup\setup.c /link /subsystem:windows %PLAT_DIR%\setup.res shlwapi.lib msi.lib user32.lib advapi32.lib ole32.lib || goto :error
goto :eof goto :eof
:dist :dist

View File

@ -1,19 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?if $(sys.BUILDARCH) = x64 ?> <?if $(sys.BUILDARCH) = "x64" ?>
<?define Win64 = "yes" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?> <?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?define UpgradeCode = "c9f7c2b3-291a-454a-9871-150d98dc2645" ?> <?define UpgradeCode = "C9F7C2B3-291A-454A-9871-150D98DC2645" ?>
<?else?> <?elseif $(sys.BUILDARCH) = "x86" ?>
<?define Win64 = "no" ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?> <?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?define UpgradeCode = "46E3AA36-10BB-4CD9-92E3-5F990AB5FC88" ?> <?define UpgradeCode = "46E3AA36-10BB-4CD9-92E3-5F990AB5FC88" ?>
<?else?>
<?error Unknown platform ?>
<?endif?> <?endif?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"> <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Id="*" Name="!(loc.ApplicationName)" Language="!(loc.Language)" Version="$(var.VERSION)" Manufacturer="FRP Manager Project" UpgradeCode="$(var.UpgradeCode)"> <Product Id="*" Name="!(loc.ApplicationName)" Language="!(loc.Language)" Version="$(var.VERSION)" Manufacturer="FRP Manager Project" UpgradeCode="$(var.UpgradeCode)">
<Package InstallerVersion="400" Compressed="yes" InstallScope="perMachine" Languages="1033,1041,1042,2052,1028,3082" Description="!(loc.ApplicationName)" /> <Package InstallerVersion="400" Compressed="yes" InstallScope="perMachine" Languages="1033,1041,1042,2052,1028,3082" Description="!(loc.ApplicationName)" ReadOnly="yes" />
<MediaTemplate EmbedCab="yes" CompressionLevel="high" /> <MediaTemplate EmbedCab="yes" CompressionLevel="high" />
@ -26,10 +26,10 @@
--> -->
<MajorUpgrade AllowDowngrades="yes" /> <MajorUpgrade AllowDowngrades="yes" />
<Icon Id="ProductIcon" SourceFile="..\icon\app.ico" /> <Icon Id="app.ico" SourceFile="..\icon\app.ico" />
<Binary Id="actions.dll" SourceFile="build\$(sys.BUILDARCH)\actions.dll" /> <Binary Id="actions.dll" SourceFile="build\$(sys.BUILDARCH)\actions.dll" />
<Property Id="ARPPRODUCTICON" Value="ProductIcon" /> <Property Id="ARPPRODUCTICON" Value="app.ico" />
<Property Id="ARPURLINFOABOUT" Value="https://github.com/koho/frpmgr" /> <Property Id="ARPURLINFOABOUT" Value="https://github.com/koho/frpmgr" />
<Property Id="ARPNOREPAIR" Value="yes" /> <Property Id="ARPNOREPAIR" Value="yes" />
<Property Id="DISABLEADVTSHORTCUTS" Value="yes" /> <Property Id="DISABLEADVTSHORTCUTS" Value="yes" />
@ -45,8 +45,8 @@
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" /> <Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
<Property Id="LicenseAccepted" Value="1" /> <Property Id="LicenseAccepted" Value="1" />
<Feature Id="ProductFeature" Title="FRP" Level="1"> <Feature Id="CoreFeature" Title="!(loc.ApplicationName)" Level="1">
<ComponentGroupRef Id="ProductComponents" /> <ComponentGroupRef Id="CoreComponents" />
</Feature> </Feature>
<UI> <UI>
@ -54,8 +54,12 @@
<UIRef Id="WixUI_ErrorProgressText" /> <UIRef Id="WixUI_ErrorProgressText" />
<!-- Skip license dialog --> <!-- Skip license dialog -->
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2">1</Publish> <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2">NOT WIX_UPGRADE_DETECTED</Publish>
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">1</Publish> <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">NOT WIX_UPGRADE_DETECTED</Publish>
<!-- Skip directory selection on upgrade -->
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="2">WIX_UPGRADE_DETECTED</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">WIX_UPGRADE_DETECTED</Publish>
<!-- Launch application after installation --> <!-- Launch application after installation -->
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">NOT Installed</Publish> <Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">NOT Installed</Publish>
@ -78,9 +82,9 @@
Components Components
--> -->
<Fragment> <Fragment>
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER"> <ComponentGroup Id="CoreComponents" Directory="INSTALLFOLDER">
<Component Guid="{E39EABEF-A7EB-4EAF-AD3E-A1254450BBE1}" Id="MainApplication" Win64="$(var.Win64)"> <Component Id="frpmgr.exe">
<File Id="MainApplication" Source="..\bin\$(sys.BUILDARCH)\frpmgr.exe" KeyPath="yes"> <File Id="frpmgr.exe" Source="..\bin\$(sys.BUILDARCH)\frpmgr.exe" KeyPath="yes">
<Shortcut Id="StartMenuShortcut" Name="!(loc.ApplicationName)" Directory="ProgramMenuFolder" WorkingDirectory="INSTALLFOLDER" Advertise="yes"/> <Shortcut Id="StartMenuShortcut" Name="!(loc.ApplicationName)" Directory="ProgramMenuFolder" WorkingDirectory="INSTALLFOLDER" Advertise="yes"/>
</File> </File>
<!-- A dummy to make WiX create ServiceControl table for us. --> <!-- A dummy to make WiX create ServiceControl table for us. -->
@ -94,43 +98,40 @@
--> -->
<Fragment> <Fragment>
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" /> <SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />
<CustomAction Id="KillProcesses.SetProperty" Return="check" Property="KillProcesses" Value="[#MainApplication]" /> <CustomAction Id="KillFrpProcesses.SetProperty" Return="check" Property="KillFrpProcesses" Value="[#frpmgr.exe]" />
<CustomAction Id="EvaluateFrpServices.SetProperty" Return="check" Property="EvaluateFrpServices" Value="[#MainApplication]" />
<CustomAction Id="RemoveFrpFiles.SetProperty" Return="check" Property="RemoveFrpFiles" Value="[INSTALLFOLDER]" /> <CustomAction Id="RemoveFrpFiles.SetProperty" Return="check" Property="RemoveFrpFiles" Value="[INSTALLFOLDER]" />
<CustomAction Id="SetLangConfig.SetProperty" Return="check" Property="SetLangConfig" Value="[INSTALLFOLDER]" /> <CustomAction Id="SetLangConfig.SetProperty" Return="check" Property="SetLangConfig" Value="[INSTALLFOLDER]" />
<CustomAction Id="MoveFrpProfiles.SetProperty" Return="check" Property="MoveFrpProfiles" Value="[INSTALLFOLDER]" /> <CustomAction Id="MoveFrpProfiles.SetProperty" Return="check" Property="MoveFrpProfiles" Value="[INSTALLFOLDER]" />
<CustomAction Id="RemoveOldFrpServices.SetProperty" Return="check" Property="RemoveOldFrpServices" Value="[#MainApplication]" />
<!-- <!--
Launch application Launch application
--> -->
<Property Id="WixShellExecTarget" Value="[#MainApplication]" /> <Property Id="WixShellExecTarget" Value="[#frpmgr.exe]" />
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" /> <CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
<!-- <!--
Close GUI windows Close GUI windows
--> -->
<CustomAction Id="KillGUIProcesses" BinaryKey="actions.dll" DllEntry="KillGUIProcesses" Impersonate="yes" Execute="immediate" /> <CustomAction Id="KillFrpGUIProcesses" BinaryKey="actions.dll" DllEntry="KillFrpGUIProcesses" Impersonate="yes" Execute="immediate" />
<InstallExecuteSequence> <InstallExecuteSequence>
<Custom Action="KillGUIProcesses" Before="InstallValidate">(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom> <Custom Action="KillFrpGUIProcesses" Before="InstallValidate">(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom>
</InstallExecuteSequence> </InstallExecuteSequence>
<!-- <!--
Evaluate FRP services Evaluate FRP services
--> -->
<CustomAction Id="EvaluateFrpServices" BinaryKey="actions.dll" DllEntry="EvaluateFrpServices" Impersonate="no" Execute="deferred" /> <CustomAction Id="EvaluateFrpServices" BinaryKey="actions.dll" DllEntry="EvaluateFrpServices" />
<InstallExecuteSequence> <InstallExecuteSequence>
<Custom Action="EvaluateFrpServices.SetProperty" After="InstallInitialize" /> <Custom Action="EvaluateFrpServices" After="InstallInitialize">NOT (UPGRADINGPRODUCTCODE AND (REMOVE="ALL"))</Custom>
<Custom Action="EvaluateFrpServices" After="EvaluateFrpServices.SetProperty">(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom>
</InstallExecuteSequence> </InstallExecuteSequence>
<!-- <!--
Kill lingering processes Kill lingering processes
--> -->
<CustomAction Id="KillProcesses" BinaryKey="actions.dll" DllEntry="KillProcesses" Impersonate="no" Execute="deferred" /> <CustomAction Id="KillFrpProcesses" BinaryKey="actions.dll" DllEntry="KillFrpProcesses" Impersonate="no" Execute="deferred" />
<InstallExecuteSequence> <InstallExecuteSequence>
<Custom Action="KillProcesses.SetProperty" After="StopServices" /> <Custom Action="KillFrpProcesses.SetProperty" After="StopServices" />
<Custom Action="KillProcesses" After="KillProcesses.SetProperty">REMOVE="ALL"</Custom> <Custom Action="KillFrpProcesses" After="KillFrpProcesses.SetProperty">REMOVE="ALL"</Custom>
</InstallExecuteSequence> </InstallExecuteSequence>
<!-- <!--
@ -159,14 +160,5 @@
<Custom Action="MoveFrpProfiles.SetProperty" After="InstallFiles" /> <Custom Action="MoveFrpProfiles.SetProperty" After="InstallFiles" />
<Custom Action="MoveFrpProfiles" After="MoveFrpProfiles.SetProperty">NOT (REMOVE="ALL")</Custom> <Custom Action="MoveFrpProfiles" After="MoveFrpProfiles.SetProperty">NOT (REMOVE="ALL")</Custom>
</InstallExecuteSequence> </InstallExecuteSequence>
<!--
Delete old version frp services
-->
<CustomAction Id="RemoveOldFrpServices" BinaryKey="actions.dll" DllEntry="RemoveOldFrpServices" Impersonate="no" Execute="deferred" />
<InstallExecuteSequence>
<Custom Action="RemoveOldFrpServices.SetProperty" After="InstallFiles" />
<Custom Action="RemoveOldFrpServices" After="RemoveOldFrpServices.SetProperty">NOT (REMOVE="ALL")</Custom>
</InstallExecuteSequence>
</Fragment> </Fragment>
</Wix> </Wix>

View File

@ -5,7 +5,13 @@
#define STRINGIZE(x) #x #define STRINGIZE(x) #x
#define EXPAND(x) STRINGIZE(x) #define EXPAND(x) STRINGIZE(x)
#define TITLE "FRP Manager Setup"
#define TITLE_EN_US "FRP Manager Setup"
#define TITLE_ZH_CN "FRP 管理器安装程序"
#define TITLE_ZH_TW "FRP 管理器安裝程式"
#define TITLE_JA_JP "FRP マネージャーインストーラー"
#define TITLE_KO_KR "FRP 관리자 설치 프로그램"
#define TITLE_ES_ES "Instalación de Administrador de FRP"
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST manifest.xml CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST manifest.xml
@ -46,54 +52,84 @@ END
LANGUAGE LANG_ENGLISH, SUBLANG_DEFAULT LANGUAGE LANG_ENGLISH, SUBLANG_DEFAULT
VERSIONINFO_TEMPLATE( VERSIONINFO_TEMPLATE(
"040904B0", 0x0409, 1200, "040904B0", 0x0409, 1200,
TITLE, TITLE_EN_US,
"FRP Manager" "FRP Manager"
) )
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
VERSIONINFO_TEMPLATE( VERSIONINFO_TEMPLATE(
"080404B0", 0x0804, 1200, "080404B0", 0x0804, 1200,
"FRP 管理器安装程序", TITLE_ZH_CN,
"FRP 管理器" "FRP 管理器"
) )
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL
VERSIONINFO_TEMPLATE( VERSIONINFO_TEMPLATE(
"040404B0", 0x0404, 1200, "040404B0", 0x0404, 1200,
"FRP 管理器安裝程式", TITLE_ZH_TW,
"FRP 管理器" "FRP 管理器"
) )
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
VERSIONINFO_TEMPLATE( VERSIONINFO_TEMPLATE(
"041104B0", 0x0411, 1200, "041104B0", 0x0411, 1200,
"FRP マネージャーインストーラー", TITLE_JA_JP,
"FRP マネージャ" "FRP マネージャ"
) )
LANGUAGE LANG_KOREAN, SUBLANG_DEFAULT LANGUAGE LANG_KOREAN, SUBLANG_DEFAULT
VERSIONINFO_TEMPLATE( VERSIONINFO_TEMPLATE(
"041204B0", 0x0412, 1200, "041204B0", 0x0412, 1200,
"FRP 관리자 설치 프로그램", TITLE_KO_KR,
"FRP 관리자" "FRP 관리자"
) )
LANGUAGE LANG_SPANISH, SUBLANG_SPANISH LANGUAGE LANG_SPANISH, SUBLANG_SPANISH_MODERN
VERSIONINFO_TEMPLATE( VERSIONINFO_TEMPLATE(
"0C0A04B0", 0x0C0A, 1200, "0C0A04B0", 0x0C0A, 1200,
"Instalación de Administrador de FRP", TITLE_ES_ES,
"Administrador de FRP" "Administrador de FRP"
) )
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL #define LANG_DIALOG_TEMPLATE(title, description, ok, cancel) \
IDD_LANG_DIALOG DIALOGEX 0, 0, 252, 79 IDD_LANG_DIALOG DIALOGEX 0, 0, 252, 69 \
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU \
CAPTION TITLE CAPTION title \
FONT 8, "Tahoma" FONT 9, "Segoe UI" \
BEGIN BEGIN \
COMBOBOX IDC_LANG_COMBO, 34, 40, 211, 374, CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP COMBOBOX IDC_LANG_COMBO, 34, 30, 211, 374, CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP \
DEFPUSHBUTTON "OK", IDOK, 141, 58, 50, 14, BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP DEFPUSHBUTTON ok, IDOK, 141, 48, 50, 14, BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP \
PUSHBUTTON "Cancel", IDCANCEL, 195, 58, 50, 14, BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP PUSHBUTTON cancel, IDCANCEL, 195, 48, 50, 14, BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP \
LTEXT "Select the language for the installation from the choices below.", IDC_STATIC, 34, 8, 211, 25, SS_LEFT | SS_NOPREFIX | WS_CHILD | WS_VISIBLE | WS_GROUP LTEXT description, IDC_STATIC, 34, 8, 211, 20, SS_LEFT | SS_NOPREFIX | WS_CHILD | WS_VISIBLE | WS_GROUP \
ICON IDI_ICON, IDC_STATIC, 7, 7, 21, 20, SS_ICON | WS_CHILD | WS_VISIBLE ICON IDI_ICON, IDC_STATIC, 7, 7, 21, 20, SS_ICON | WS_CHILD | WS_VISIBLE \
END END
LANGUAGE LANG_ENGLISH, SUBLANG_DEFAULT
LANG_DIALOG_TEMPLATE(
TITLE_EN_US, "Select the language for the installation from the choices below.", "OK", "Cancel"
)
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
LANG_DIALOG_TEMPLATE(
TITLE_ZH_CN, "从下列选项中选择安装语言。", "确定", "取消"
)
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL
LANG_DIALOG_TEMPLATE(
TITLE_ZH_TW, "從下列選項中選擇安裝語言。", "確定", "取消"
)
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
LANG_DIALOG_TEMPLATE(
TITLE_JA_JP, "以下のオプションからインストール言語を選択してください。", "OK", "キャンセル"
)
LANGUAGE LANG_KOREAN, SUBLANG_DEFAULT
LANG_DIALOG_TEMPLATE(
TITLE_KO_KR, "아래 옵션에서 설치 언어를 선택하세요.", "확인", "취소"
)
LANGUAGE LANG_SPANISH, SUBLANG_SPANISH_MODERN
LANG_DIALOG_TEMPLATE(
TITLE_ES_ES, "Seleccione el idioma para la instalación entre las opciones siguientes.", "Aceptar", "Cancelar"
)

View File

@ -1,169 +1,273 @@
#ifndef UNICODE #define UNICODE
#define UNICODE #define _UNICODE
#endif
#include <windows.h> #include <windows.h>
#include <shlwapi.h>
#include <ntsecapi.h>
#include <stdint.h>
#include <stdio.h>
#include <msi.h> #include <msi.h>
#include <shlwapi.h>
#include <sddl.h>
#include <stdio.h>
#include "resource.h" #include "resource.h"
static WCHAR msiPath[MAX_PATH];
static HANDLE msiFile = INVALID_HANDLE_VALUE;
typedef struct { typedef struct {
LCID id; WCHAR id[5];
TCHAR name[16]; WCHAR name[16];
char code[10]; WCHAR code[10];
} Language; } Language;
static TCHAR msiFile[MAX_PATH]; typedef struct {
static HANDLE hFile = INVALID_HANDLE_VALUE; WCHAR path[MAX_PATH];
DWORD pathLen;
WCHAR lang[10];
DWORD langLen;
WCHAR version[20];
DWORD versionLen;
} Product;
static Language languages[] = { static Language languages[] = {
{2052, TEXT("简体中文"), "zh-CN"}, {L"2052", L"简体中文", L"zh-CN"},
{1028, TEXT("繁體中文"), "zh-TW"}, {L"1028", L"繁體中文", L"zh-TW"},
{1033, TEXT("English"), "en-US"}, {L"1033", L"English", L"en-US"},
{1041, TEXT("日本語"), "ja-JP"}, {L"1041", L"日本語", L"ja-JP"},
{1042, TEXT("한국어"), "ko-KR"}, {L"1042", L"한국어", L"ko-KR"},
{3082, TEXT("Español"), "es-ES"}, {L"3082", L"Español", L"es-ES"},
}; };
static BOOL RandomString(TCHAR ss[32]) { static INT MatchLanguageCode(LPWSTR langCode)
uint8_t bytes[32]; {
if (!RtlGenRandom(bytes, sizeof(bytes))) for (size_t i = 0; i < _countof(languages); i++)
return FALSE; {
for (int i = 0; i < 31; ++i) { if (wcscmp(languages[i].code, langCode) == 0)
ss[i] = (TCHAR) (bytes[i] % 26 + 97); return i;
} }
ss[31] = '\0'; return -1;
return TRUE;
} }
static int Cleanup(void) { static INT GetApplicationLanguage(LPWSTR path, DWORD pathLen)
if (hFile != INVALID_HANDLE_VALUE) { {
for (int i = 0; i < 200 && !DeleteFile(msiFile) && GetLastError() != ERROR_FILE_NOT_FOUND; ++i) if (!PathAppendW(path, L"lang.config"))
Sleep(200); return -1;
DWORD bytesRead = 0;
HANDLE hFile = CreateFileW(path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
path[pathLen] = L'\0';
if (hFile != INVALID_HANDLE_VALUE)
{
CHAR buf[LOCALE_NAME_MAX_LENGTH];
WCHAR localeName[LOCALE_NAME_MAX_LENGTH];
BOOL ok = ReadFile(hFile, buf, sizeof(buf) - 1, &bytesRead, NULL);
CloseHandle(hFile);
if (ok && bytesRead != 0)
{
buf[bytesRead] = 0;
if (MultiByteToWideChar(CP_UTF8, 0, buf, -1, localeName, _countof(localeName)) > 0)
{
INT i = MatchLanguageCode(localeName);
if (i >= 0)
return i;
}
}
} }
if (!PathAppendW(path, L"app.json"))
return -1;
hFile = CreateFileW(path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
path[pathLen] = L'\0';
if (hFile == INVALID_HANDLE_VALUE)
return -1;
CHAR buf[100];
// To avoid JSON dependency, we require the first field to be the language setting.
static const CHAR* langKey = "{\"lang\":\"*\"";
WCHAR langCode[10];
DWORD langCodeLen = 0;
INT j = 0;
while (ReadFile(hFile, buf, sizeof(buf), &bytesRead, NULL) && bytesRead != 0)
{
for (DWORD i = 0; i < bytesRead; i++)
{
if (langKey[j] == '*')
{
if (buf[i] == '"')
j++;
else
{
langCode[langCodeLen++] = buf[i];
if (langCodeLen >= sizeof(langCode) - 1)
goto out;
continue;
}
}
if (buf[i] == langKey[j])
{
j++;
if (langKey[j] == 0)
goto out;
}
else if (buf[i] != '\t' && buf[i] != ' ' && buf[i] != '\r' && buf[i] != '\n')
goto out;
else if (langKey[j] != '{' && langKey[j] != ':' && j > 0 && langKey[j - 1] != '{' && langKey[j - 1] != ':')
goto out;
}
}
out:
CloseHandle(hFile);
if (langKey[j] != 0 || langCodeLen == 0)
return -1;
langCode[langCodeLen] = 0;
return MatchLanguageCode(langCode);
}
INT_PTR CALLBACK LanguageDialog(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) {
case WM_INITDIALOG:
for (size_t i = 0; i < _countof(languages); i++)
SendDlgItemMessageW(hDlg, IDC_LANG_COMBO, CB_ADDSTRING, 0, (LPARAM)languages[i].name);
SendDlgItemMessageW(hDlg, IDC_LANG_COMBO, CB_SETCURSEL, lParam, 0);
return (INT_PTR)TRUE;
case WM_COMMAND:
INT_PTR nResult = LOWORD(wParam);
if (nResult == IDOK || nResult == IDCANCEL)
{
if (nResult == IDOK)
{
LRESULT i = SendDlgItemMessageW(hDlg, IDC_LANG_COMBO, CB_GETCURSEL, 0, 0);
nResult = (i >= 0 && i < _countof(languages)) ? (INT_PTR)&languages[i] : 0;
}
else
nResult = 0;
EndDialog(hDlg, nResult);
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
static int cleanup(void)
{
if (msiFile != INVALID_HANDLE_VALUE)
{
CloseHandle(msiFile);
msiFile = INVALID_HANDLE_VALUE;
}
for (INT i = 0; i < 200 && !DeleteFileW(msiPath) && GetLastError() != ERROR_FILE_NOT_FOUND; i++)
Sleep(200);
return 0; return 0;
} }
static Language *GetPreferredLang(TCHAR *folder) { int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
TCHAR langPath[MAX_PATH]; {
if (PathCombine(langPath, folder, L"app.json") == NULL) { INT langIndex = -1;
return NULL; BOOL installed = FALSE, showDlg = TRUE;
} Product product = {
FILE *file; .path = { 0 },
if (_wfopen_s(&file, langPath, L"rb") != 0) { .pathLen = _countof(product.path),
return NULL; .lang = { 0 },
} .langLen = _countof(product.lang),
fseek(file, 0L, SEEK_END); .version = { 0 },
long fileSize = ftell(file); .versionLen = _countof(product.version)
fseek(file, 0L, SEEK_SET); };
char *buf = malloc(fileSize + 1);
size_t size = fread(buf, 1, fileSize, file);
buf[size] = 0;
fclose(file);
const char *p1 = strstr(buf, "\"lang\"");
if (p1 == NULL) {
goto cleanup;
}
const char *p2 = strstr(p1, ":");
if (p2 == NULL) {
goto cleanup;
}
const char *p3 = strstr(p2, "\"");
if (p3 == NULL) {
goto cleanup;
}
for (int i = 0; i < sizeof(languages) / sizeof(languages[0]); i++) {
if (strncmp(p3 + 1, languages[i].code, strlen(languages[i].code)) == 0) {
free(buf);
return &languages[i];
}
}
cleanup:
free(buf);
return NULL;
}
INT_PTR CALLBACK LangDialog(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { #ifdef UPGRADE_CODE
switch (message) { WCHAR productCode[39];
case WM_INITDIALOG: if (MsiEnumRelatedProductsW(UPGRADE_CODE, 0, 0, productCode) == ERROR_SUCCESS)
for (int i = 0; i < sizeof(languages) / sizeof(languages[0]); i++) { {
SendDlgItemMessage(hDlg, IDC_LANG_COMBO, CB_ADDSTRING, 0, (LPARAM) languages[i].name); MsiGetProductInfo(productCode, INSTALLPROPERTY_VERSIONSTRING, product.version, &product.versionLen);
} if (MsiGetProductInfo(productCode, INSTALLPROPERTY_INSTALLLOCATION, product.path, &product.pathLen) == ERROR_SUCCESS && product.path[0])
SendDlgItemMessage(hDlg, IDC_LANG_COMBO, CB_SETCURSEL, 0, 0); langIndex = GetApplicationLanguage(product.path, product.pathLen);
return (INT_PTR) TRUE; if (MsiGetProductInfo(productCode, INSTALLPROPERTY_INSTALLEDLANGUAGE, product.lang, &product.langLen) == ERROR_SUCCESS && langIndex < 0)
{
case WM_COMMAND: for (size_t i = 0; i < _countof(languages); i++)
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { {
INT_PTR nResult = LOWORD(wParam); if (wcscmp(languages[i].id, product.lang) == 0)
if (LOWORD(wParam) == IDOK) { {
int idx = SendDlgItemMessage(hDlg, IDC_LANG_COMBO, CB_GETCURSEL, 0, 0); langIndex = i;
nResult = (INT_PTR) &languages[idx]; break;
} }
EndDialog(hDlg, nResult);
return (INT_PTR) TRUE;
} }
break;
}
return (INT_PTR) FALSE;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pCmdLine, int nCmdShow) {
_onexit(Cleanup);
// Retrieve install location
TCHAR installPath[MAX_PATH];
DWORD dwSize = MAX_PATH;
memset(installPath, 0, dwSize);
Language *lang = NULL;
if (MsiLocateComponent(L"{E39EABEF-A7EB-4EAF-AD3E-A1254450BBE1}", installPath, &dwSize) >= 0 && wcslen(installPath) > 0) {
PathRemoveFileSpec(installPath);
lang = GetPreferredLang(installPath);
}
if (lang == NULL) {
INT_PTR nResult = DialogBox(hInstance, MAKEINTRESOURCE(IDD_LANG_DIALOG), NULL, LangDialog);
if (nResult == IDCANCEL) {
return 0;
} }
lang = (Language *) nResult;
} }
TCHAR randFile[32]; #ifdef VERSION
if (!GetWindowsDirectory(msiFile, sizeof(msiFile)) || !PathAppend(msiFile, L"Temp")) installed = wcscmp(VERSION, product.version) == 0;
return 1; showDlg = !product.path[0] || installed || langIndex < 0;
if (!RandomString(randFile)) #endif
return 1; #endif
if (!PathAppend(msiFile, randFile))
return 1; if (langIndex < 0)
HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(IDR_MSI), RT_RCDATA); {
if (hRes == NULL) { PZZWSTR langList = NULL;
return 1; ULONG langNum, langLen = 0;
if (GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &langNum, NULL, &langLen))
{
langList = (PZZWSTR)LocalAlloc(LMEM_FIXED, langLen * sizeof(WCHAR));
if (langList)
{
if (GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &langNum, langList, &langLen) && langNum > 0)
{
for (size_t i = 0; i < langLen && langList[i] != L'\0'; i += wcsnlen_s(&langList[i], langLen - i) + 1)
{
langIndex = MatchLanguageCode(&langList[i]);
if (langIndex >= 0)
break;
}
}
LocalFree(langList);
}
}
} }
HGLOBAL msiData = LoadResource(NULL, hRes);
if (msiData == NULL) { Language* lang = showDlg ? (Language*)DialogBoxParamW(
hInstance, MAKEINTRESOURCE(IDD_LANG_DIALOG),
NULL, LanguageDialog, langIndex
) : &languages[langIndex];
if (lang == NULL)
return 0;
if (!GetWindowsDirectoryW(msiPath, _countof(msiPath)) || !PathAppendW(msiPath, L"Temp"))
return 1; return 1;
} GUID guid;
DWORD msiSize = SizeofResource(NULL, hRes); if (FAILED(CoCreateGuid(&guid)))
if (msiSize == 0) {
return 1; return 1;
} WCHAR identifier[40];
LPVOID pMsiData = LockResource(msiData); if (StringFromGUID2(&guid, identifier, _countof(identifier)) == 0 || !PathAppendW(msiPath, identifier))
if (pMsiData == NULL) {
return 1; return 1;
}
SECURITY_ATTRIBUTES security_attributes = {.nLength = sizeof(security_attributes)}; HRSRC hRes = FindResourceW(NULL, MAKEINTRESOURCE(IDR_MSI), RT_RCDATA);
hFile = CreateFile(msiFile, GENERIC_WRITE | DELETE, 0, &security_attributes, CREATE_NEW, if (hRes == NULL)
FILE_ATTRIBUTE_TEMPORARY, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
return 1; return 1;
} HGLOBAL hResData = LoadResource(NULL, hRes);
if (hResData == NULL)
return 1;
DWORD resSize = SizeofResource(NULL, hRes);
if (resSize == 0)
return 1;
LPVOID pResData = LockResource(hResData);
if (pResData == NULL)
return 1;
SECURITY_ATTRIBUTES sa = { .nLength = sizeof(sa) };
if (!ConvertStringSecurityDescriptorToSecurityDescriptorA("O:BAD:PAI(A;;FA;;;BA)", SDDL_REVISION_1, &sa.lpSecurityDescriptor, NULL))
return 1;
msiFile = CreateFileW(msiPath, GENERIC_WRITE, 0, &sa, CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY, NULL);
if (sa.lpSecurityDescriptor)
LocalFree(sa.lpSecurityDescriptor);
if (msiFile == INVALID_HANDLE_VALUE)
return 1;
_onexit(cleanup);
DWORD bytesWritten; DWORD bytesWritten;
if (!WriteFile(hFile, pMsiData, msiSize, &bytesWritten, NULL) || bytesWritten != msiSize) { BOOL ok = WriteFile(msiFile, pResData, resSize, &bytesWritten, NULL);
CloseHandle(hFile); CloseHandle(msiFile);
msiFile = INVALID_HANDLE_VALUE;
if (!ok || bytesWritten != resSize)
return 1; return 1;
}
CloseHandle(hFile);
MsiSetInternalUI(INSTALLUILEVEL_FULL, NULL); MsiSetInternalUI(INSTALLUILEVEL_FULL, NULL);
TCHAR cmd[500]; #define CMD_FORMAT L"ProductLanguage=%s PREVINSTALLFOLDER=\"%s\""
wsprintf(cmd, L"ProductLanguage=%d PREVINSTALLFOLDER=\"%s\"", lang->id, installPath); WCHAR cmd[_countof(CMD_FORMAT) + _countof(product.path)];
return MsiInstallProduct(msiFile, cmd); if (swprintf_s(cmd, _countof(cmd), CMD_FORMAT, lang->id, product.path) < 0)
return 1;
return MsiInstallProductW(msiPath, cmd);
} }

View File

@ -7,7 +7,10 @@ import (
"github.com/koho/frpmgr/pkg/consts" "github.com/koho/frpmgr/pkg/consts"
) )
const DefaultAppFile = "app.json" const (
DefaultAppFile = "app.json"
LangFile = "lang.config"
)
type App struct { type App struct {
Lang string `json:"lang,omitempty"` Lang string `json:"lang,omitempty"`
@ -50,12 +53,18 @@ func (dv *DefaultValue) AsClientConfig() ClientCommon {
} }
} }
func UnmarshalAppConf(path string, dst *App) error { func UnmarshalAppConf(path string, dst *App) (lang *string, err error) {
b, err := os.ReadFile(path) b, err := os.ReadFile(LangFile)
if err != nil { if err == nil {
return err s := string(b)
lang = &s
} }
return json.Unmarshal(b, dst) b, err = os.ReadFile(path)
if err != nil {
return
}
err = json.Unmarshal(b, dst)
return
} }
func (conf *App) Save(path string) error { func (conf *App) Save(path string) error {

View File

@ -35,11 +35,19 @@ func TestUnmarshalAppConfFromIni(t *testing.T) {
LegacyFormat: true, LegacyFormat: true,
}, },
} }
expectedLang := "en-US"
if err := os.WriteFile(LangFile, []byte(expectedLang), 0666); err != nil {
t.Fatal(err)
}
var actual App var actual App
if err := UnmarshalAppConf(DefaultAppFile, &actual); err != nil { lang, err := UnmarshalAppConf(DefaultAppFile, &actual)
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !reflect.DeepEqual(actual, expected) { if !reflect.DeepEqual(actual, expected) {
t.Errorf("Expected: %v, got: %v", expected, actual) t.Errorf("Expected: %v, got: %v", expected, actual)
} }
if lang == nil || *lang != expectedLang {
t.Errorf("Expected: %v, got: %v", expectedLang, lang)
}
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/lxn/walk" "github.com/lxn/walk"
"github.com/samber/lo" "github.com/samber/lo"
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/config" "github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts" "github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/util" "github.com/koho/frpmgr/pkg/util"
@ -101,8 +102,18 @@ var (
) )
func loadAllConfs() ([]*Conf, error) { func loadAllConfs() ([]*Conf, error) {
_ = config.UnmarshalAppConf(config.DefaultAppFile, &appConf) // Load and migrate application configuration.
// Find all config files in `profiles` directory if lang, _ := config.UnmarshalAppConf(config.DefaultAppFile, &appConf); lang != nil {
if _, ok := i18n.IDToName[*lang]; ok {
appConf.Lang = *lang
if saveAppConfig() == nil {
os.Remove(config.LangFile)
}
} else {
os.Remove(config.LangFile)
}
}
// Find all config files in `profiles` directory.
files, err := filepath.Glob(PathOfConf("*.conf")) files, err := filepath.Glob(PathOfConf("*.conf"))
if err != nil { if err != nil {
return nil, err return nil, err