Skip to content

Commit d80c7bc

Browse files
committed
fix: IoException thrown issue at HtmlPostProcessor
1 parent 38bfe3f commit d80c7bc

File tree

4 files changed

+260
-11
lines changed

4 files changed

+260
-11
lines changed

src/Docfx.Common/Docfx.Common.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
4+
</PropertyGroup>
5+
26
<ItemGroup>
37
<PackageReference Include="Spectre.Console" />
48
</ItemGroup>

src/Docfx.Common/FileAbstractLayer/ManifestFileWriter.cs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,37 @@ public override Stream Create(RelativePath file)
4141
{
4242
throw new InvalidOperationException("File entry not found.");
4343
}
44-
if (_noRandomFile)
44+
45+
string path = _noRandomFile
46+
? Path.Combine(_manifestFolder, file.RemoveWorkingFolder())
47+
: Path.Combine(OutputFolder, file.RemoveWorkingFolder());
48+
path = Path.GetFullPath(path);
49+
Directory.CreateDirectory(Path.GetDirectoryName(path));
50+
51+
int retryCount = 0;
52+
53+
Retry:
54+
try
4555
{
46-
Directory.CreateDirectory(
47-
Path.Combine(_manifestFolder, file.RemoveWorkingFolder().GetDirectoryPath()));
48-
var result = File.Create(Path.Combine(_manifestFolder, file.RemoveWorkingFolder()));
56+
var fileStream = File.Create(path);
4957
entry.LinkToPath = null;
50-
return result;
58+
return fileStream;
5159
}
52-
else
60+
catch (IOException e) when ((e.HResult & 0x0000FFFF) == 32) // ERROR_SHARING_VIOLATION: 0x80070020
5361
{
54-
var path = Path.Combine(OutputFolder, file.RemoveWorkingFolder());
55-
Directory.CreateDirectory(Path.GetDirectoryName(path));
56-
var result = File.Create(path);
57-
entry.LinkToPath = path;
58-
return result;
62+
var message = FileLockCheck.GetLockingProcessNames(path);
63+
if (!string.IsNullOrEmpty(message))
64+
Logger.LogWarning(message, file: path, code: WarningCodes.Build.LockedFile);
65+
66+
++retryCount;
67+
if (retryCount <= 3)
68+
{
69+
Thread.Sleep(500 * retryCount);
70+
goto Retry;
71+
}
72+
73+
// If retry failed 3 times. throw original exception
74+
throw;
5975
}
6076
}
6177
}

src/Docfx.Common/FileLockCheck.cs

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Runtime.InteropServices;
6+
7+
#nullable enable
8+
9+
namespace Docfx.Common;
10+
11+
// Based on https://github.com/dotnet/roslyn/blob/main/src/Compilers/Core/Portable/InternalUtilities/FileLockCheck.cs
12+
13+
/// <summary>
14+
/// This class implements checking what processes are locking a file on Windows.
15+
/// It uses the Restart Manager API to do this. Other platforms are skipped.
16+
/// </summary>
17+
internal static class FileLockCheck
18+
{
19+
[StructLayout(LayoutKind.Sequential)]
20+
private struct FILETIME
21+
{
22+
public uint dwLowDateTime;
23+
public uint dwHighDateTime;
24+
}
25+
26+
[StructLayout(LayoutKind.Sequential)]
27+
private struct RM_UNIQUE_PROCESS
28+
{
29+
public uint dwProcessId;
30+
public FILETIME ProcessStartTime;
31+
}
32+
33+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
34+
private struct RM_PROCESS_INFO
35+
{
36+
private const int CCH_RM_MAX_APP_NAME = 255;
37+
private const int CCH_RM_MAX_SVC_NAME = 63;
38+
39+
internal RM_UNIQUE_PROCESS Process;
40+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)]
41+
public string strAppName;
42+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)]
43+
public string strServiceShortName;
44+
internal int ApplicationType;
45+
public uint AppStatus;
46+
public uint TSSessionId;
47+
[MarshalAs(UnmanagedType.Bool)]
48+
public bool bRestartable;
49+
}
50+
51+
private const string RestartManagerDll = "rstrtmgr.dll";
52+
53+
[DllImport(RestartManagerDll, CharSet = CharSet.Unicode)]
54+
private static extern int RmRegisterResources(uint pSessionHandle,
55+
uint nFiles,
56+
string[] rgsFilenames,
57+
uint nApplications,
58+
[In] RM_UNIQUE_PROCESS[]? rgApplications,
59+
uint nServices,
60+
string[]? rgsServiceNames);
61+
62+
/// <summary>
63+
/// Starts a new Restart Manager session.
64+
/// A maximum of 64 Restart Manager sessions per user session
65+
/// can be open on the system at the same time. When this
66+
/// function starts a session, it returns a session handle
67+
/// and session key that can be used in subsequent calls to
68+
/// the Restart Manager API.
69+
/// </summary>
70+
/// <param name="pSessionHandle">
71+
/// A pointer to the handle of a Restart Manager session.
72+
/// The session handle can be passed in subsequent calls
73+
/// to the Restart Manager API.
74+
/// </param>
75+
/// <param name="dwSessionFlags">
76+
/// Reserved. This parameter should be 0.
77+
/// </param>
78+
/// <param name="strSessionKey">
79+
/// A null-terminated string that contains the session key
80+
/// to the new session. The string must be allocated before
81+
/// calling the RmStartSession function.
82+
/// </param>
83+
/// <returns>System error codes that are defined in Winerror.h.</returns>
84+
/// <remarks>
85+
/// The RmツュツュStartSession function doesn窶冲 properly null-terminate
86+
/// the session key, even though the function is documented as
87+
/// returning a null-terminated string. To work around this bug,
88+
/// we pre-fill the buffer with null characters so that whatever
89+
/// ends gets written will have a null terminator (namely, one of
90+
/// the null characters we placed ahead of time).
91+
/// <para>
92+
/// see <see href="http://blogs.msdn.com/b/oldnewthing/archive/2012/02/17/10268840.aspx"/>.
93+
/// </para>
94+
/// </remarks>
95+
[DllImport(RestartManagerDll, CharSet = CharSet.Unicode)]
96+
private static extern unsafe int RmStartSession(
97+
out uint pSessionHandle,
98+
int dwSessionFlags,
99+
char* strSessionKey);
100+
101+
/// <summary>
102+
/// Ends the Restart Manager session.
103+
/// This function should be called by the primary installer that
104+
/// has previously started the session by calling the <see cref="RmStartSession"/>
105+
/// function. The RmEndSession function can be called by a secondary installer
106+
/// that is joined to the session once no more resources need to be registered
107+
/// by the secondary installer.
108+
/// </summary>
109+
/// <param name="pSessionHandle">A handle to an existing Restart Manager session.</param>
110+
/// <returns>
111+
/// The function can return one of the system error codes that are defined in Winerror.h.
112+
/// </returns>
113+
[DllImport(RestartManagerDll)]
114+
private static extern int RmEndSession(uint pSessionHandle);
115+
116+
[DllImport(RestartManagerDll, CharSet = CharSet.Unicode)]
117+
private static extern int RmGetList(uint dwSessionHandle,
118+
out uint pnProcInfoNeeded,
119+
ref uint pnProcInfo,
120+
[In, Out] RM_PROCESS_INFO[]? rgAffectedApps,
121+
ref uint lpdwRebootReasons);
122+
123+
public static string GetLockingProcessNames(string filePath)
124+
{
125+
if (!OperatingSystem.IsWindows())
126+
return "";
127+
128+
try
129+
{
130+
var processes = GetLockingProcessNames([filePath]);
131+
if (processes.Length == 0)
132+
return "";
133+
134+
return $"File is locked by process: {string.Join(',', processes)}";
135+
}
136+
catch (Exception)
137+
{
138+
// Never throw if we can't get the processes locking the file.
139+
return "";
140+
}
141+
}
142+
143+
private static string[] GetLockingProcessNames(string[] paths)
144+
{
145+
const int MaxRetries = 6;
146+
const int ERROR_MORE_DATA = 234;
147+
const uint RM_REBOOT_REASON_NONE = 0;
148+
149+
uint handle;
150+
int res;
151+
152+
unsafe
153+
{
154+
char* key = stackalloc char[sizeof(Guid) * 2 + 1];
155+
res = RmStartSession(out handle, 0, key);
156+
}
157+
158+
if (res != 0)
159+
{
160+
return [];
161+
}
162+
163+
try
164+
{
165+
res = RmRegisterResources(handle, (uint)paths.Length, paths, 0, null, 0, null);
166+
if (res != 0)
167+
{
168+
return [];
169+
}
170+
171+
//
172+
// Obtain the list of affected applications/services.
173+
//
174+
// NOTE: Restart Manager returns the results into the buffer allocated by the caller. The first call to
175+
// RmGetList() will return the size of the buffer (i.e. nProcInfoNeeded) the caller needs to allocate.
176+
// The caller then needs to allocate the buffer (i.e. rgAffectedApps) and make another RmGetList()
177+
// call to ask Restart Manager to write the results into the buffer. However, since Restart Manager
178+
// refreshes the list every time RmGetList()is called, it is possible that the size returned by the first
179+
// RmGetList()call is not sufficient to hold the results discovered by the second RmGetList() call. Therefore,
180+
// it is recommended that the caller follows the following practice to handle this race condition:
181+
//
182+
// Use a loop to call RmGetList() in case the buffer allocated according to the size returned in previous
183+
// call is not enough.
184+
//
185+
uint pnProcInfo = 0;
186+
RM_PROCESS_INFO[]? rgAffectedApps = null;
187+
int retry = 0;
188+
do
189+
{
190+
uint lpdwRebootReasons = RM_REBOOT_REASON_NONE;
191+
res = RmGetList(handle, out uint pnProcInfoNeeded, ref pnProcInfo, rgAffectedApps, ref lpdwRebootReasons);
192+
if (res == 0)
193+
{
194+
// If pnProcInfo == 0, then there is simply no locking process (found), in this case rgAffectedApps is "null".
195+
if (pnProcInfo == 0)
196+
{
197+
return [];
198+
}
199+
200+
Debug.Assert(rgAffectedApps != null);
201+
202+
var lockInfos = new List<string>((int)pnProcInfo);
203+
for (int i = 0; i < pnProcInfo; i++)
204+
{
205+
lockInfos.Add(rgAffectedApps[i].strAppName);
206+
}
207+
208+
return lockInfos.ToArray();
209+
}
210+
211+
if (res != ERROR_MORE_DATA)
212+
{
213+
return [];
214+
}
215+
216+
pnProcInfo = pnProcInfoNeeded;
217+
rgAffectedApps = new RM_PROCESS_INFO[pnProcInfo];
218+
}
219+
while (res == ERROR_MORE_DATA && retry++ < MaxRetries);
220+
}
221+
finally
222+
{
223+
_ = RmEndSession(handle);
224+
}
225+
226+
return [];
227+
}
228+
}

src/Docfx.Common/Loggers/WarningCodes.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public static class Build
2323
public const string UnknownContentType = "UnknownContentType";
2424
public const string UnknownContentTypeForTemplate = "UnknownContentTypeForTemplate";
2525
public const string InvalidTocInclude = "InvalidTocInclude";
26+
public const string LockedFile = "LockedFile";
2627
}
2728

2829
public static class Metadata

0 commit comments

Comments
 (0)