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 관리자"
)
LANGUAGE LANG_SPANISH, SUBLANG_SPANISH
LANGUAGE LANG_SPANISH, SUBLANG_SPANISH_MODERN
VERSIONINFO_TEMPLATE(
"0C0A04B0", 0x0C0A, 1200,
"Administrador de FRP"

View File

@ -42,7 +42,14 @@ func GetLanguage() string {
// langInConfig returns the UI language code in config file
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 {
return ""
}
@ -52,7 +59,10 @@ func langInConfig() string {
if err = json.Unmarshal(b, &s); err != nil {
return ""
}
if _, ok := IDToName[s.Lang]; ok {
return s.Lang
}
return ""
}
// 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
)
if not defined WIX (
echo ERROR: WIX was not found.
exit /b 1
)
:build
if not exist build md build
set PLAT_DIR=build\%ARCH%
@ -38,11 +33,15 @@ if not defined WIX (
exit /b 0
:build_actions
msbuild actions\actions.sln /t:Rebuild /p:Configuration=Release /p:Platform="%ARCH%" || goto :error
copy actions\actions\bin\%ARCH%\Release\actions.CA.dll %PLAT_DIR%\actions.dll /y || goto :error
rc /DVERSION_ARRAY=%VERSION:.=,% /DVERSION_STR=%VERSION% /Fo %PLAT_DIR%\actions.res actions\version.rc || 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
:build_msi
if not defined WIX (
echo ERROR: WIX was not found.
exit /b 1
)
set WIX_CANDLE_FLAGS=-dVERSION=%VERSION%
set WIX_LIGHT_FLAGS=-ext "%WIX%bin\WixUtilExtension.dll" -ext "%WIX%bin\WixUIExtension.dll" -sval
set WIX_OBJ=%PLAT_DIR%\frpmgr.wixobj
@ -58,8 +57,21 @@ if not defined WIX (
goto :eof
: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
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
rc /DFILENAME=%SETUP_FILENAME% /DVERSION_ARRAY=%VERSION:.=,% /DVERSION_STR=%VERSION% /DMSI_FILE=%MSI_FILE:\=\\% /Fo %PLAT_DIR%\setup.res setup\resource.rc || 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
:dist

View File

@ -1,19 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<?if $(sys.BUILDARCH) = x64 ?>
<?define Win64 = "yes" ?>
<?if $(sys.BUILDARCH) = "x64" ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?define UpgradeCode = "c9f7c2b3-291a-454a-9871-150d98dc2645" ?>
<?else?>
<?define Win64 = "no" ?>
<?define UpgradeCode = "C9F7C2B3-291A-454A-9871-150D98DC2645" ?>
<?elseif $(sys.BUILDARCH) = "x86" ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?define UpgradeCode = "46E3AA36-10BB-4CD9-92E3-5F990AB5FC88" ?>
<?else?>
<?error Unknown platform ?>
<?endif?>
<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)">
<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" />
@ -26,10 +26,10 @@
-->
<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" />
<Property Id="ARPPRODUCTICON" Value="ProductIcon" />
<Property Id="ARPPRODUCTICON" Value="app.ico" />
<Property Id="ARPURLINFOABOUT" Value="https://github.com/koho/frpmgr" />
<Property Id="ARPNOREPAIR" Value="yes" />
<Property Id="DISABLEADVTSHORTCUTS" Value="yes" />
@ -45,8 +45,8 @@
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
<Property Id="LicenseAccepted" Value="1" />
<Feature Id="ProductFeature" Title="FRP" Level="1">
<ComponentGroupRef Id="ProductComponents" />
<Feature Id="CoreFeature" Title="!(loc.ApplicationName)" Level="1">
<ComponentGroupRef Id="CoreComponents" />
</Feature>
<UI>
@ -54,8 +54,12 @@
<UIRef Id="WixUI_ErrorProgressText" />
<!-- Skip license dialog -->
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" 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">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 -->
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">NOT Installed</Publish>
@ -78,9 +82,9 @@
Components
-->
<Fragment>
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
<Component Guid="{E39EABEF-A7EB-4EAF-AD3E-A1254450BBE1}" Id="MainApplication" Win64="$(var.Win64)">
<File Id="MainApplication" Source="..\bin\$(sys.BUILDARCH)\frpmgr.exe" KeyPath="yes">
<ComponentGroup Id="CoreComponents" Directory="INSTALLFOLDER">
<Component Id="frpmgr.exe">
<File Id="frpmgr.exe" Source="..\bin\$(sys.BUILDARCH)\frpmgr.exe" KeyPath="yes">
<Shortcut Id="StartMenuShortcut" Name="!(loc.ApplicationName)" Directory="ProgramMenuFolder" WorkingDirectory="INSTALLFOLDER" Advertise="yes"/>
</File>
<!-- A dummy to make WiX create ServiceControl table for us. -->
@ -94,43 +98,40 @@
-->
<Fragment>
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />
<CustomAction Id="KillProcesses.SetProperty" Return="check" Property="KillProcesses" Value="[#MainApplication]" />
<CustomAction Id="EvaluateFrpServices.SetProperty" Return="check" Property="EvaluateFrpServices" Value="[#MainApplication]" />
<CustomAction Id="KillFrpProcesses.SetProperty" Return="check" Property="KillFrpProcesses" Value="[#frpmgr.exe]" />
<CustomAction Id="RemoveFrpFiles.SetProperty" Return="check" Property="RemoveFrpFiles" Value="[INSTALLFOLDER]" />
<CustomAction Id="SetLangConfig.SetProperty" Return="check" Property="SetLangConfig" Value="[INSTALLFOLDER]" />
<CustomAction Id="MoveFrpProfiles.SetProperty" Return="check" Property="MoveFrpProfiles" Value="[INSTALLFOLDER]" />
<CustomAction Id="RemoveOldFrpServices.SetProperty" Return="check" Property="RemoveOldFrpServices" Value="[#MainApplication]" />
<!--
Launch application
-->
<Property Id="WixShellExecTarget" Value="[#MainApplication]" />
<Property Id="WixShellExecTarget" Value="[#frpmgr.exe]" />
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
<!--
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>
<Custom Action="KillGUIProcesses" Before="InstallValidate">(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom>
<Custom Action="KillFrpGUIProcesses" Before="InstallValidate">(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom>
</InstallExecuteSequence>
<!--
Evaluate FRP services
-->
<CustomAction Id="EvaluateFrpServices" BinaryKey="actions.dll" DllEntry="EvaluateFrpServices" Impersonate="no" Execute="deferred" />
<CustomAction Id="EvaluateFrpServices" BinaryKey="actions.dll" DllEntry="EvaluateFrpServices" />
<InstallExecuteSequence>
<Custom Action="EvaluateFrpServices.SetProperty" After="InstallInitialize" />
<Custom Action="EvaluateFrpServices" After="EvaluateFrpServices.SetProperty">(NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")</Custom>
<Custom Action="EvaluateFrpServices" After="InstallInitialize">NOT (UPGRADINGPRODUCTCODE AND (REMOVE="ALL"))</Custom>
</InstallExecuteSequence>
<!--
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>
<Custom Action="KillProcesses.SetProperty" After="StopServices" />
<Custom Action="KillProcesses" After="KillProcesses.SetProperty">REMOVE="ALL"</Custom>
<Custom Action="KillFrpProcesses.SetProperty" After="StopServices" />
<Custom Action="KillFrpProcesses" After="KillFrpProcesses.SetProperty">REMOVE="ALL"</Custom>
</InstallExecuteSequence>
<!--
@ -159,14 +160,5 @@
<Custom Action="MoveFrpProfiles.SetProperty" After="InstallFiles" />
<Custom Action="MoveFrpProfiles" After="MoveFrpProfiles.SetProperty">NOT (REMOVE="ALL")</Custom>
</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>
</Wix>

View File

@ -5,7 +5,13 @@
#define STRINGIZE(x) #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
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST manifest.xml
@ -46,54 +52,84 @@ END
LANGUAGE LANG_ENGLISH, SUBLANG_DEFAULT
VERSIONINFO_TEMPLATE(
"040904B0", 0x0409, 1200,
TITLE,
TITLE_EN_US,
"FRP Manager"
)
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
VERSIONINFO_TEMPLATE(
"080404B0", 0x0804, 1200,
"FRP 管理器安装程序",
TITLE_ZH_CN,
"FRP 管理器"
)
LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL
VERSIONINFO_TEMPLATE(
"040404B0", 0x0404, 1200,
"FRP 管理器安裝程式",
TITLE_ZH_TW,
"FRP 管理器"
)
LANGUAGE LANG_JAPANESE, SUBLANG_DEFAULT
VERSIONINFO_TEMPLATE(
"041104B0", 0x0411, 1200,
"FRP マネージャーインストーラー",
TITLE_JA_JP,
"FRP マネージャ"
)
LANGUAGE LANG_KOREAN, SUBLANG_DEFAULT
VERSIONINFO_TEMPLATE(
"041204B0", 0x0412, 1200,
"FRP 관리자 설치 프로그램",
TITLE_KO_KR,
"FRP 관리자"
)
LANGUAGE LANG_SPANISH, SUBLANG_SPANISH
LANGUAGE LANG_SPANISH, SUBLANG_SPANISH_MODERN
VERSIONINFO_TEMPLATE(
"0C0A04B0", 0x0C0A, 1200,
"Instalación de Administrador de FRP",
TITLE_ES_ES,
"Administrador de FRP"
)
LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
IDD_LANG_DIALOG DIALOGEX 0, 0, 252, 79
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION TITLE
FONT 8, "Tahoma"
BEGIN
COMBOBOX IDC_LANG_COMBO, 34, 40, 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
PUSHBUTTON "Cancel", IDCANCEL, 195, 58, 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
ICON IDI_ICON, IDC_STATIC, 7, 7, 21, 20, SS_ICON | WS_CHILD | WS_VISIBLE
#define LANG_DIALOG_TEMPLATE(title, description, ok, cancel) \
IDD_LANG_DIALOG DIALOGEX 0, 0, 252, 69 \
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU \
CAPTION title \
FONT 9, "Segoe UI" \
BEGIN \
COMBOBOX IDC_LANG_COMBO, 34, 30, 211, 374, CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_TABSTOP \
DEFPUSHBUTTON ok, IDOK, 141, 48, 50, 14, BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP \
PUSHBUTTON cancel, IDCANCEL, 195, 48, 50, 14, BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP \
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 \
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
#endif
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <shlwapi.h>
#include <ntsecapi.h>
#include <stdint.h>
#include <stdio.h>
#include <msi.h>
#include <shlwapi.h>
#include <sddl.h>
#include <stdio.h>
#include "resource.h"
static WCHAR msiPath[MAX_PATH];
static HANDLE msiFile = INVALID_HANDLE_VALUE;
typedef struct {
LCID id;
TCHAR name[16];
char code[10];
WCHAR id[5];
WCHAR name[16];
WCHAR code[10];
} Language;
static TCHAR msiFile[MAX_PATH];
static HANDLE hFile = INVALID_HANDLE_VALUE;
typedef struct {
WCHAR path[MAX_PATH];
DWORD pathLen;
WCHAR lang[10];
DWORD langLen;
WCHAR version[20];
DWORD versionLen;
} Product;
static Language languages[] = {
{2052, TEXT("简体中文"), "zh-CN"},
{1028, TEXT("繁體中文"), "zh-TW"},
{1033, TEXT("English"), "en-US"},
{1041, TEXT("日本語"), "ja-JP"},
{1042, TEXT("한국어"), "ko-KR"},
{3082, TEXT("Español"), "es-ES"},
{L"2052", L"简体中文", L"zh-CN"},
{L"1028", L"繁體中文", L"zh-TW"},
{L"1033", L"English", L"en-US"},
{L"1041", L"日本語", L"ja-JP"},
{L"1042", L"한국어", L"ko-KR"},
{L"3082", L"Español", L"es-ES"},
};
static BOOL RandomString(TCHAR ss[32]) {
uint8_t bytes[32];
if (!RtlGenRandom(bytes, sizeof(bytes)))
return FALSE;
for (int i = 0; i < 31; ++i) {
ss[i] = (TCHAR) (bytes[i] % 26 + 97);
static INT MatchLanguageCode(LPWSTR langCode)
{
for (size_t i = 0; i < _countof(languages); i++)
{
if (wcscmp(languages[i].code, langCode) == 0)
return i;
}
ss[31] = '\0';
return TRUE;
return -1;
}
static int Cleanup(void) {
if (hFile != INVALID_HANDLE_VALUE) {
for (int i = 0; i < 200 && !DeleteFile(msiFile) && GetLastError() != ERROR_FILE_NOT_FOUND; ++i)
Sleep(200);
static INT GetApplicationLanguage(LPWSTR path, DWORD pathLen)
{
if (!PathAppendW(path, L"lang.config"))
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;
}
return 0;
}
}
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);
}
static Language *GetPreferredLang(TCHAR *folder) {
TCHAR langPath[MAX_PATH];
if (PathCombine(langPath, folder, L"app.json") == NULL) {
return NULL;
}
FILE *file;
if (_wfopen_s(&file, langPath, L"rb") != 0) {
return NULL;
}
fseek(file, 0L, SEEK_END);
long fileSize = ftell(file);
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) {
INT_PTR CALLBACK LanguageDialog(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) {
case WM_INITDIALOG:
for (int i = 0; i < sizeof(languages) / sizeof(languages[0]); i++) {
SendDlgItemMessage(hDlg, IDC_LANG_COMBO, CB_ADDSTRING, 0, (LPARAM) languages[i].name);
}
SendDlgItemMessage(hDlg, IDC_LANG_COMBO, CB_SETCURSEL, 0, 0);
return (INT_PTR) TRUE;
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:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) {
INT_PTR nResult = LOWORD(wParam);
if (LOWORD(wParam) == IDOK) {
int idx = SendDlgItemMessage(hDlg, IDC_LANG_COMBO, CB_GETCURSEL, 0, 0);
nResult = (INT_PTR) &languages[idx];
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;
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR) FALSE;
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);
static int cleanup(void)
{
if (msiFile != INVALID_HANDLE_VALUE)
{
CloseHandle(msiFile);
msiFile = INVALID_HANDLE_VALUE;
}
if (lang == NULL) {
INT_PTR nResult = DialogBox(hInstance, MAKEINTRESOURCE(IDD_LANG_DIALOG), NULL, LangDialog);
if (nResult == IDCANCEL) {
for (INT i = 0; i < 200 && !DeleteFileW(msiPath) && GetLastError() != ERROR_FILE_NOT_FOUND; i++)
Sleep(200);
return 0;
}
lang = (Language *) nResult;
}
TCHAR randFile[32];
if (!GetWindowsDirectory(msiFile, sizeof(msiFile)) || !PathAppend(msiFile, L"Temp"))
return 1;
if (!RandomString(randFile))
return 1;
if (!PathAppend(msiFile, randFile))
return 1;
HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(IDR_MSI), RT_RCDATA);
if (hRes == NULL) {
return 1;
}
HGLOBAL msiData = LoadResource(NULL, hRes);
if (msiData == NULL) {
return 1;
}
DWORD msiSize = SizeofResource(NULL, hRes);
if (msiSize == 0) {
return 1;
}
LPVOID pMsiData = LockResource(msiData);
if (pMsiData == NULL) {
return 1;
}
SECURITY_ATTRIBUTES security_attributes = {.nLength = sizeof(security_attributes)};
hFile = CreateFile(msiFile, GENERIC_WRITE | DELETE, 0, &security_attributes, CREATE_NEW,
FILE_ATTRIBUTE_TEMPORARY, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
return 1;
}
DWORD bytesWritten;
if (!WriteFile(hFile, pMsiData, msiSize, &bytesWritten, NULL) || bytesWritten != msiSize) {
CloseHandle(hFile);
return 1;
}
CloseHandle(hFile);
MsiSetInternalUI(INSTALLUILEVEL_FULL, NULL);
TCHAR cmd[500];
wsprintf(cmd, L"ProductLanguage=%d PREVINSTALLFOLDER=\"%s\"", lang->id, installPath);
return MsiInstallProduct(msiFile, cmd);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
INT langIndex = -1;
BOOL installed = FALSE, showDlg = TRUE;
Product product = {
.path = { 0 },
.pathLen = _countof(product.path),
.lang = { 0 },
.langLen = _countof(product.lang),
.version = { 0 },
.versionLen = _countof(product.version)
};
#ifdef UPGRADE_CODE
WCHAR productCode[39];
if (MsiEnumRelatedProductsW(UPGRADE_CODE, 0, 0, productCode) == ERROR_SUCCESS)
{
MsiGetProductInfo(productCode, INSTALLPROPERTY_VERSIONSTRING, product.version, &product.versionLen);
if (MsiGetProductInfo(productCode, INSTALLPROPERTY_INSTALLLOCATION, product.path, &product.pathLen) == ERROR_SUCCESS && product.path[0])
langIndex = GetApplicationLanguage(product.path, product.pathLen);
if (MsiGetProductInfo(productCode, INSTALLPROPERTY_INSTALLEDLANGUAGE, product.lang, &product.langLen) == ERROR_SUCCESS && langIndex < 0)
{
for (size_t i = 0; i < _countof(languages); i++)
{
if (wcscmp(languages[i].id, product.lang) == 0)
{
langIndex = i;
break;
}
}
}
}
#ifdef VERSION
installed = wcscmp(VERSION, product.version) == 0;
showDlg = !product.path[0] || installed || langIndex < 0;
#endif
#endif
if (langIndex < 0)
{
PZZWSTR langList = NULL;
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);
}
}
}
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;
GUID guid;
if (FAILED(CoCreateGuid(&guid)))
return 1;
WCHAR identifier[40];
if (StringFromGUID2(&guid, identifier, _countof(identifier)) == 0 || !PathAppendW(msiPath, identifier))
return 1;
HRSRC hRes = FindResourceW(NULL, MAKEINTRESOURCE(IDR_MSI), RT_RCDATA);
if (hRes == NULL)
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;
BOOL ok = WriteFile(msiFile, pResData, resSize, &bytesWritten, NULL);
CloseHandle(msiFile);
msiFile = INVALID_HANDLE_VALUE;
if (!ok || bytesWritten != resSize)
return 1;
MsiSetInternalUI(INSTALLUILEVEL_FULL, NULL);
#define CMD_FORMAT L"ProductLanguage=%s PREVINSTALLFOLDER=\"%s\""
WCHAR cmd[_countof(CMD_FORMAT) + _countof(product.path)];
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"
)
const DefaultAppFile = "app.json"
const (
DefaultAppFile = "app.json"
LangFile = "lang.config"
)
type App struct {
Lang string `json:"lang,omitempty"`
@ -50,12 +53,18 @@ func (dv *DefaultValue) AsClientConfig() ClientCommon {
}
}
func UnmarshalAppConf(path string, dst *App) error {
b, err := os.ReadFile(path)
if err != nil {
return err
func UnmarshalAppConf(path string, dst *App) (lang *string, err error) {
b, err := os.ReadFile(LangFile)
if err == nil {
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 {

View File

@ -35,11 +35,19 @@ func TestUnmarshalAppConfFromIni(t *testing.T) {
LegacyFormat: true,
},
}
expectedLang := "en-US"
if err := os.WriteFile(LangFile, []byte(expectedLang), 0666); err != nil {
t.Fatal(err)
}
var actual App
if err := UnmarshalAppConf(DefaultAppFile, &actual); err != nil {
lang, err := UnmarshalAppConf(DefaultAppFile, &actual)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(actual, expected) {
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/samber/lo"
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/util"
@ -101,8 +102,18 @@ var (
)
func loadAllConfs() ([]*Conf, error) {
_ = config.UnmarshalAppConf(config.DefaultAppFile, &appConf)
// Find all config files in `profiles` directory
// Load and migrate application configuration.
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"))
if err != nil {
return nil, err