diff options
| author | Rob Mensching <rob@firegiant.com> | 2024-04-04 15:24:34 -0700 |
|---|---|---|
| committer | Rob Mensching <rob@firegiant.com> | 2024-04-05 09:46:43 -0700 |
| commit | 681cf4a9eb6be7e4092c6e5b690773fbd8469e63 (patch) | |
| tree | 1ff50c7856c83bf930a0cbb4f52d5e1b5cbc016f /src | |
| parent | 6ed045ee1fe69be037a999df2e57122a25f0dedf (diff) | |
| download | wix-681cf4a9eb6be7e4092c6e5b690773fbd8469e63.tar.gz wix-681cf4a9eb6be7e4092c6e5b690773fbd8469e63.tar.bz2 wix-681cf4a9eb6be7e4092c6e5b690773fbd8469e63.zip | |
Ensure elevated SFXCA uses Windows Installer cache and unelevated uses Temp folder
Fixes 8078
Diffstat (limited to 'src')
| -rw-r--r-- | src/dtf/SfxCA/SfxCA.vcxproj | 2 | ||||
| -rw-r--r-- | src/dtf/SfxCA/SfxUtil.cpp | 169 | ||||
| -rw-r--r-- | src/dtf/dtf.cmd | 23 | ||||
| -rw-r--r-- | src/test/msi/TestData/CustomActionTests/ManagedCustomActions/Package.wxs | 6 | ||||
| -rw-r--r-- | src/test/msi/TestData/CustomActionTests/TestCA/CustomAction.cs | 38 |
5 files changed, 203 insertions, 35 deletions
diff --git a/src/dtf/SfxCA/SfxCA.vcxproj b/src/dtf/SfxCA/SfxCA.vcxproj index 5c4f674f..ee591619 100644 --- a/src/dtf/SfxCA/SfxCA.vcxproj +++ b/src/dtf/SfxCA/SfxCA.vcxproj | |||
| @@ -41,7 +41,7 @@ | |||
| 41 | <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> | 41 | <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> |
| 42 | 42 | ||
| 43 | <PropertyGroup> | 43 | <PropertyGroup> |
| 44 | <ProjectAdditionalLinkLibraries>msi.lib;cabinet.lib;shlwapi.lib</ProjectAdditionalLinkLibraries> | 44 | <ProjectAdditionalLinkLibraries>msi.lib;cabinet.lib;rpcrt4.lib;shlwapi.lib</ProjectAdditionalLinkLibraries> |
| 45 | </PropertyGroup> | 45 | </PropertyGroup> |
| 46 | 46 | ||
| 47 | <ItemGroup> | 47 | <ItemGroup> |
diff --git a/src/dtf/SfxCA/SfxUtil.cpp b/src/dtf/SfxCA/SfxUtil.cpp index 32dc6e04..079f1617 100644 --- a/src/dtf/SfxCA/SfxUtil.cpp +++ b/src/dtf/SfxCA/SfxUtil.cpp | |||
| @@ -3,6 +3,8 @@ | |||
| 3 | #include "precomp.h" | 3 | #include "precomp.h" |
| 4 | #include "SfxUtil.h" | 4 | #include "SfxUtil.h" |
| 5 | 5 | ||
| 6 | #define GUID_STRING_LENGTH 39 | ||
| 7 | |||
| 6 | /// <summary> | 8 | /// <summary> |
| 7 | /// Writes a formatted message to the MSI log. | 9 | /// Writes a formatted message to the MSI log. |
| 8 | /// Does out-of-proc MSI calls if necessary. | 10 | /// Does out-of-proc MSI calls if necessary. |
| @@ -110,20 +112,75 @@ bool DeleteDirectory(const wchar_t* szDir) | |||
| 110 | hSearch = INVALID_HANDLE_VALUE; | 112 | hSearch = INVALID_HANDLE_VALUE; |
| 111 | } | 113 | } |
| 112 | } | 114 | } |
| 113 | return RemoveDirectory(szDir) != 0; | 115 | |
| 116 | for (int i = 0; i < 3; i++) | ||
| 117 | { | ||
| 118 | if (::RemoveDirectory(szDir)) | ||
| 119 | { | ||
| 120 | return true; | ||
| 121 | } | ||
| 122 | |||
| 123 | ::Sleep(100); | ||
| 124 | } | ||
| 125 | |||
| 126 | return false; | ||
| 114 | } | 127 | } |
| 115 | 128 | ||
| 116 | bool DirectoryExists(const wchar_t* szDir) | 129 | static HRESULT CreateGuid( |
| 130 | _Out_z_cap_c_(GUID_STRING_LENGTH) wchar_t* wzGuid) | ||
| 117 | { | 131 | { |
| 118 | if (szDir != NULL) | 132 | HRESULT hr = S_OK; |
| 133 | RPC_STATUS rs = RPC_S_OK; | ||
| 134 | UUID guid = {}; | ||
| 135 | |||
| 136 | rs = ::UuidCreate(&guid); | ||
| 137 | if (rs != RPC_S_OK) | ||
| 119 | { | 138 | { |
| 120 | DWORD dwAttrs = GetFileAttributes(szDir); | 139 | hr = (HRESULT)(rs | FACILITY_RPC); |
| 121 | if (dwAttrs != -1 && (dwAttrs & FILE_ATTRIBUTE_DIRECTORY) != 0) | ||
| 122 | { | ||
| 123 | return true; | ||
| 124 | } | ||
| 125 | } | 140 | } |
| 126 | return false; | 141 | else if (!::StringFromGUID2(guid, wzGuid, GUID_STRING_LENGTH)) |
| 142 | { | ||
| 143 | hr = E_OUTOFMEMORY; | ||
| 144 | } | ||
| 145 | else // make the temp directory more recognizable for easy deletion. | ||
| 146 | { | ||
| 147 | // Copy the first four hex chars of the GUID over the dashes in the GUID and trim the, so | ||
| 148 | // '{1234ABCD-ABCD-ABCD-ABCD-ABCDABCDABCD}' turns into '{1234ABCD1ABCD2ABCD3ABCD4ABCDABCDABCD}' | ||
| 149 | wzGuid[9] = wzGuid[1]; | ||
| 150 | wzGuid[14] = wzGuid[2]; | ||
| 151 | wzGuid[19] = wzGuid[3]; | ||
| 152 | wzGuid[24] = wzGuid[4]; | ||
| 153 | |||
| 154 | // Now '{1234ABCD1ABCD2ABCD3ABCD4ABCDABCDABCD}' turns into 'SFXCAABCD1ABCD2ABCD3ABCD4ABCDABCDABCD' | ||
| 155 | wzGuid[0] = L'S'; | ||
| 156 | wzGuid[1] = L'F'; | ||
| 157 | wzGuid[2] = L'X'; | ||
| 158 | wzGuid[3] = L'C'; | ||
| 159 | wzGuid[4] = L'A'; | ||
| 160 | wzGuid[GUID_STRING_LENGTH - 2] = L'\0'; | ||
| 161 | } | ||
| 162 | |||
| 163 | return hr; | ||
| 164 | } | ||
| 165 | |||
| 166 | static HRESULT ProcessElevated() | ||
| 167 | { | ||
| 168 | HRESULT hr = S_OK; | ||
| 169 | HANDLE hToken = NULL; | ||
| 170 | TOKEN_ELEVATION tokenElevated = {}; | ||
| 171 | DWORD cbToken = 0; | ||
| 172 | |||
| 173 | if (::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY, &hToken) && | ||
| 174 | ::GetTokenInformation(hToken, TokenElevation, &tokenElevated, sizeof(TOKEN_ELEVATION), &cbToken)) | ||
| 175 | { | ||
| 176 | hr = (0 != tokenElevated.TokenIsElevated) ? S_OK : S_FALSE; | ||
| 177 | } | ||
| 178 | else | ||
| 179 | { | ||
| 180 | hr = HRESULT_FROM_WIN32(::GetLastError()); | ||
| 181 | } | ||
| 182 | |||
| 183 | return hr; | ||
| 127 | } | 184 | } |
| 128 | 185 | ||
| 129 | /// <summary> | 186 | /// <summary> |
| @@ -143,49 +200,95 @@ __success(return != false) | |||
| 143 | bool ExtractToTempDirectory(__in MSIHANDLE hSession, __in HMODULE hModule, | 200 | bool ExtractToTempDirectory(__in MSIHANDLE hSession, __in HMODULE hModule, |
| 144 | __out_ecount_z(cchTempDirBuf) wchar_t* szTempDir, DWORD cchTempDirBuf) | 201 | __out_ecount_z(cchTempDirBuf) wchar_t* szTempDir, DWORD cchTempDirBuf) |
| 145 | { | 202 | { |
| 146 | wchar_t szModule[MAX_PATH]; | 203 | HRESULT hr = S_OK; |
| 147 | DWORD cchCopied = GetModuleFileName(hModule, szModule, MAX_PATH - 1); | 204 | wchar_t szModule[MAX_PATH] = {}; |
| 148 | if (cchCopied == 0) | 205 | wchar_t szGuid[GUID_STRING_LENGTH] = {}; |
| 206 | |||
| 207 | DWORD cchCopied = ::GetModuleFileName(hModule, szModule, MAX_PATH - 1); | ||
| 208 | if (cchCopied == 0 || cchCopied == MAX_PATH - 1) | ||
| 149 | { | 209 | { |
| 150 | Log(hSession, L"Failed to get module path. Error code %d.", GetLastError()); | 210 | hr = HRESULT_FROM_WIN32(::GetLastError()); |
| 151 | return false; | 211 | if (SUCCEEDED(hr)) |
| 212 | { | ||
| 213 | hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER); | ||
| 214 | } | ||
| 215 | |||
| 216 | Log(hSession, L"Failed to get module path. Error code 0x%x.", hr); | ||
| 217 | goto LExit; | ||
| 152 | } | 218 | } |
| 153 | else if (cchCopied == MAX_PATH - 1) | 219 | |
| 220 | hr = CreateGuid(szGuid); | ||
| 221 | if (FAILED(hr)) | ||
| 154 | { | 222 | { |
| 155 | Log(hSession, L"Failed to get module path -- path is too long."); | 223 | Log(hSession, L"Failed to create a GUID. Error code 0x%x", hr); |
| 156 | return false; | 224 | goto LExit; |
| 157 | } | 225 | } |
| 158 | 226 | ||
| 159 | if (szTempDir == NULL || cchTempDirBuf < wcslen(szModule) + 1) | 227 | // Unelevated we use the user's temp directory. |
| 228 | hr = ProcessElevated(); | ||
| 229 | if (S_FALSE == hr) | ||
| 230 | { | ||
| 231 | // Temp path is documented to be returned with a trailing backslash. | ||
| 232 | cchCopied = ::GetTempPath(cchTempDirBuf, szTempDir); | ||
| 233 | if (cchCopied == 0 || cchCopied >= cchTempDirBuf) | ||
| 234 | { | ||
| 235 | hr = HRESULT_FROM_WIN32(::GetLastError()); | ||
| 236 | if (SUCCEEDED(hr)) | ||
| 237 | { | ||
| 238 | hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER); | ||
| 239 | } | ||
| 240 | |||
| 241 | Log(hSession, L"Failed to get user temp directory. Error code 0x%x", hr); | ||
| 242 | goto LExit; | ||
| 243 | } | ||
| 244 | } | ||
| 245 | else // elevated or we couldn't check (in the latter case, assume we're elevated since it's safer to use) | ||
| 160 | { | 246 | { |
| 161 | Log(hSession, L"Temp directory buffer is NULL or too small."); | 247 | // Windows directory will not contain a trailing backslash, so we add it next. |
| 162 | return false; | 248 | cchCopied = ::GetWindowsDirectoryW(szTempDir, cchTempDirBuf); |
| 249 | if (cchCopied == 0 || cchCopied >= cchTempDirBuf) | ||
| 250 | { | ||
| 251 | hr = HRESULT_FROM_WIN32(::GetLastError()); | ||
| 252 | if (SUCCEEDED(hr)) | ||
| 253 | { | ||
| 254 | hr = HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER); | ||
| 255 | } | ||
| 256 | |||
| 257 | Log(hSession, L"Failed to get Windows directory. Error code 0x%x", hr); | ||
| 258 | goto LExit; | ||
| 259 | } | ||
| 260 | |||
| 261 | hr = ::StringCchCat(szTempDir, cchTempDirBuf, L"\\Installer\\"); | ||
| 262 | if (FAILED(hr)) | ||
| 263 | { | ||
| 264 | Log(hSession, L"Failed append 'Installer' to Windows directory. Error code 0x%x", hr); | ||
| 265 | goto LExit; | ||
| 266 | } | ||
| 163 | } | 267 | } |
| 164 | StringCchCopy(szTempDir, cchTempDirBuf, szModule); | ||
| 165 | StringCchCat(szTempDir, cchTempDirBuf, L"-"); | ||
| 166 | 268 | ||
| 167 | BOOL fCreatedDirectory = FALSE; | 269 | hr = ::StringCchCat(szTempDir, cchTempDirBuf, szGuid); |
| 168 | DWORD cchTempDir = (DWORD) wcslen(szTempDir); | 270 | if (FAILED(hr)) |
| 169 | for (int i = 0; i < 10000 && !fCreatedDirectory; i++) | ||
| 170 | { | 271 | { |
| 171 | swprintf_s(szTempDir + cchTempDir, cchTempDirBuf - cchTempDir, L"%d", i); | 272 | Log(hSession, L"Failed append GUID to temp path. Error code 0x%x", hr); |
| 172 | fCreatedDirectory = ::CreateDirectory(szTempDir, NULL); | 273 | goto LExit; |
| 173 | } | 274 | } |
| 174 | 275 | ||
| 175 | if (!fCreatedDirectory) | 276 | if (!::CreateDirectory(szTempDir, NULL)) |
| 176 | { | 277 | { |
| 177 | Log(hSession, L"Failed to create temp directory. Error code %d", ::GetLastError()); | 278 | hr = HRESULT_FROM_WIN32(::GetLastError()); |
| 178 | return false; | 279 | Log(hSession, L"Failed to create temp directory. Error code 0x%x", hr); |
| 280 | goto LExit; | ||
| 179 | } | 281 | } |
| 180 | 282 | ||
| 181 | Log(hSession, L"Extracting custom action to temporary directory: %s\\", szTempDir); | 283 | Log(hSession, L"Extracting custom action to temporary directory: %s\\", szTempDir); |
| 182 | int err = ExtractCabinet(szModule, szTempDir); | 284 | int err = ExtractCabinet(szModule, szTempDir); |
| 183 | if (err != 0) | 285 | if (err != 0) |
| 184 | { | 286 | { |
| 287 | hr = E_FAIL; | ||
| 185 | Log(hSession, L"Failed to extract to temporary directory. Cabinet error code %d.", err); | 288 | Log(hSession, L"Failed to extract to temporary directory. Cabinet error code %d.", err); |
| 186 | DeleteDirectory(szTempDir); | 289 | DeleteDirectory(szTempDir); |
| 187 | return false; | ||
| 188 | } | 290 | } |
| 189 | return true; | ||
| 190 | } | ||
| 191 | 291 | ||
| 292 | LExit: | ||
| 293 | return SUCCEEDED(hr); | ||
| 294 | } | ||
diff --git a/src/dtf/dtf.cmd b/src/dtf/dtf.cmd index d72c803b..530ee432 100644 --- a/src/dtf/dtf.cmd +++ b/src/dtf/dtf.cmd | |||
| @@ -4,13 +4,36 @@ | |||
| 4 | @set _C=Debug | 4 | @set _C=Debug |
| 5 | :parse_args | 5 | :parse_args |
| 6 | @if /i "%1"=="release" set _C=Release | 6 | @if /i "%1"=="release" set _C=Release |
| 7 | @if /i "%1"=="inc" set _INC=1 | ||
| 8 | @if /i "%1"=="clean" set _CLEAN=1 | ||
| 7 | @if not "%1"=="" shift & goto parse_args | 9 | @if not "%1"=="" shift & goto parse_args |
| 8 | 10 | ||
| 11 | :: Clean | ||
| 12 | |||
| 13 | @if "%_INC%"=="" call :clean | ||
| 14 | @if NOT "%_CLEAN%"=="" goto :end | ||
| 15 | |||
| 9 | @echo Building dtf %_C% | 16 | @echo Building dtf %_C% |
| 10 | 17 | ||
| 11 | msbuild -Restore SfxCA\sfxca_t.proj -p:Configuration=%_C% -tl -nologo -m -warnaserror -bl:..\..\build\logs\dtf_sfxca.binlog || exit /b | 18 | msbuild -Restore SfxCA\sfxca_t.proj -p:Configuration=%_C% -tl -nologo -m -warnaserror -bl:..\..\build\logs\dtf_sfxca.binlog || exit /b |
| 12 | 19 | ||
| 13 | msbuild -Restore -t:Pack dtf.sln -p:Configuration=%_C% -tl -nologo -m -warnaserror -bl:..\..\build\logs\dtf_build.binlog || exit /b | 20 | msbuild -Restore -t:Pack dtf.sln -p:Configuration=%_C% -tl -nologo -m -warnaserror -bl:..\..\build\logs\dtf_build.binlog || exit /b |
| 14 | 21 | ||
| 22 | @goto :end | ||
| 23 | |||
| 24 | :clean | ||
| 25 | @rd /s/q "..\..\build\dtf" 2> nul | ||
| 26 | @del "..\..\build\artifacts\WixToolset.Dtf.*.nupkg" 2> nul | ||
| 27 | @rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.compression" 2> nul | ||
| 28 | @rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.compression.cab" 2> nul | ||
| 29 | @rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.compression.zip" 2> nul | ||
| 30 | @rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.customaction" 2> nul | ||
| 31 | @rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.resources" 2> nul | ||
| 32 | @rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.windowsinstaller" 2> nul | ||
| 33 | @rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.windowsinstaller.linq" 2> nul | ||
| 34 | @rd /s/q "%USERPROFILE%\.nuget\packages\wixtoolset.dtf.windowsinstaller.package" 2> nul | ||
| 35 | @exit /b | ||
| 36 | |||
| 37 | :end | ||
| 15 | @popd | 38 | @popd |
| 16 | @endlocal | 39 | @endlocal |
diff --git a/src/test/msi/TestData/CustomActionTests/ManagedCustomActions/Package.wxs b/src/test/msi/TestData/CustomActionTests/ManagedCustomActions/Package.wxs index 9b1d224a..4159cd3e 100644 --- a/src/test/msi/TestData/CustomActionTests/ManagedCustomActions/Package.wxs +++ b/src/test/msi/TestData/CustomActionTests/ManagedCustomActions/Package.wxs | |||
| @@ -13,13 +13,17 @@ | |||
| 13 | <Binary Id="ManagedCA" SourceFile="TestCA.CA.dll" /> | 13 | <Binary Id="ManagedCA" SourceFile="TestCA.CA.dll" /> |
| 14 | 14 | ||
| 15 | <CustomAction Id="ImmediateCA" BinaryRef="ManagedCA" DllEntry="ImmediateCA" Execute="immediate" Return="check" /> | 15 | <CustomAction Id="ImmediateCA" BinaryRef="ManagedCA" DllEntry="ImmediateCA" Execute="immediate" Return="check" /> |
| 16 | <CustomAction Id="DeferredCA" BinaryRef="ManagedCA" DllEntry="DeferredCA" Execute="deferred" Return="check" /> | 16 | <CustomAction Id="ExecuteImmediateCA" BinaryRef="ManagedCA" DllEntry="ExecuteImmediateCA" Execute="immediate" Return="check" /> |
| 17 | <CustomAction Id="ImpersonatedDeferredCA" BinaryRef="ManagedCA" DllEntry="ImpersonatedDeferredCA" Execute="deferred" Impersonate="yes" Return="check" /> | ||
| 18 | <CustomAction Id="DeferredCA" BinaryRef="ManagedCA" DllEntry="DeferredCA" Execute="deferred" Impersonate="no" Return="check" /> | ||
| 17 | 19 | ||
| 18 | <InstallUISequence> | 20 | <InstallUISequence> |
| 19 | <Custom Action="ImmediateCA" Before="CostFinalize" /> | 21 | <Custom Action="ImmediateCA" Before="CostFinalize" /> |
| 20 | </InstallUISequence> | 22 | </InstallUISequence> |
| 21 | 23 | ||
| 22 | <InstallExecuteSequence> | 24 | <InstallExecuteSequence> |
| 25 | <Custom Action="ExecuteImmediateCA" Before="CostFinalize" /> | ||
| 26 | <Custom Action="ImpersonatedDeferredCA" Before="InstallFiles" /> | ||
| 23 | <Custom Action="DeferredCA" After="InstallFiles" /> | 27 | <Custom Action="DeferredCA" After="InstallFiles" /> |
| 24 | </InstallExecuteSequence> | 28 | </InstallExecuteSequence> |
| 25 | </Package> | 29 | </Package> |
diff --git a/src/test/msi/TestData/CustomActionTests/TestCA/CustomAction.cs b/src/test/msi/TestData/CustomActionTests/TestCA/CustomAction.cs index 6891f8da..54afc2cf 100644 --- a/src/test/msi/TestData/CustomActionTests/TestCA/CustomAction.cs +++ b/src/test/msi/TestData/CustomActionTests/TestCA/CustomAction.cs | |||
| @@ -4,7 +4,9 @@ namespace WixToolsetTest.TestCA | |||
| 4 | { | 4 | { |
| 5 | using System; | 5 | using System; |
| 6 | using System.Collections.Generic; | 6 | using System.Collections.Generic; |
| 7 | using System.IO; | ||
| 7 | using System.Linq; | 8 | using System.Linq; |
| 9 | using System.Text; | ||
| 8 | using WixToolset.Dtf.WindowsInstaller; | 10 | using WixToolset.Dtf.WindowsInstaller; |
| 9 | 11 | ||
| 10 | public class CustomActions | 12 | public class CustomActions |
| @@ -14,6 +16,31 @@ namespace WixToolsetTest.TestCA | |||
| 14 | { | 16 | { |
| 15 | session.Log("Begin ImmediateCA"); | 17 | session.Log("Begin ImmediateCA"); |
| 16 | 18 | ||
| 19 | var path = Path.Combine(Path.GetTempPath(), "ImmediateCA.txt"); | ||
| 20 | WriteNow(path); | ||
| 21 | |||
| 22 | return ActionResult.Success; | ||
| 23 | } | ||
| 24 | |||
| 25 | [CustomAction] | ||
| 26 | public static ActionResult ExecuteImmediateCA(Session session) | ||
| 27 | { | ||
| 28 | session.Log("Begin ExecuteImmediateCA"); | ||
| 29 | |||
| 30 | var path = Path.Combine(Path.GetTempPath(), "ExecuteImmediateCA.txt"); | ||
| 31 | WriteNow(path); | ||
| 32 | |||
| 33 | return ActionResult.Success; | ||
| 34 | } | ||
| 35 | |||
| 36 | [CustomAction] | ||
| 37 | public static ActionResult ImpersonatedDeferredCA(Session session) | ||
| 38 | { | ||
| 39 | session.Log("Begin ImpersonatedDeferredCA"); | ||
| 40 | |||
| 41 | var path = Path.Combine(Path.GetTempPath(), "ImpersonatedDeferredCA.txt"); | ||
| 42 | WriteNow(path); | ||
| 43 | |||
| 17 | return ActionResult.Success; | 44 | return ActionResult.Success; |
| 18 | } | 45 | } |
| 19 | 46 | ||
| @@ -22,7 +49,18 @@ namespace WixToolsetTest.TestCA | |||
| 22 | { | 49 | { |
| 23 | session.Log("Begin DeferredCA"); | 50 | session.Log("Begin DeferredCA"); |
| 24 | 51 | ||
| 52 | WriteNow(@"C:\Windows\Installer\DeferredCA.txt"); | ||
| 53 | |||
| 25 | return ActionResult.Success; | 54 | return ActionResult.Success; |
| 26 | } | 55 | } |
| 56 | |||
| 57 | private static void WriteNow(string path) | ||
| 58 | { | ||
| 59 | using (var file = File.Create(path)) | ||
| 60 | { | ||
| 61 | var now = Encoding.UTF8.GetBytes(DateTime.Now.ToString("o")); | ||
| 62 | file.Write(now, 0, now.Length); | ||
| 63 | } | ||
| 64 | } | ||
| 27 | } | 65 | } |
| 28 | } | 66 | } |
