Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Docfx.Common/Docfx.Common.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Spectre.Console" />
</ItemGroup>
Expand Down
40 changes: 28 additions & 12 deletions src/Docfx.Common/FileAbstractLayer/ManifestFileWriter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Docfx.Plugins;
Expand Down Expand Up @@ -41,21 +41,37 @@ public override Stream Create(RelativePath file)
{
throw new InvalidOperationException("File entry not found.");
}
if (_noRandomFile)

string path = _noRandomFile
? Path.Combine(_manifestFolder, file.RemoveWorkingFolder())
: Path.Combine(OutputFolder, file.RemoveWorkingFolder());
path = Path.GetFullPath(path);
Directory.CreateDirectory(Path.GetDirectoryName(path));

int retryCount = 0;
Retry:
try
{
Directory.CreateDirectory(
Path.Combine(_manifestFolder, file.RemoveWorkingFolder().GetDirectoryPath()));
var result = File.Create(Path.Combine(_manifestFolder, file.RemoveWorkingFolder()));
var fileStream = File.Create(path);
entry.LinkToPath = null;
return result;
return fileStream;
}
else
catch (IOException e) when ((e.HResult & 0x0000FFFF) == 32) // ERROR_SHARING_VIOLATION: 0x80070020
{
var path = Path.Combine(OutputFolder, file.RemoveWorkingFolder());
Directory.CreateDirectory(Path.GetDirectoryName(path));
var result = File.Create(path);
entry.LinkToPath = path;
return result;
// If retry failed 3 times. throw exception
if (retryCount > 3)
throw;

var message = FileLockCheck.GetLockingProcessNames(path);
if (string.IsNullOrEmpty(message))
message = "File is locked by other process";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
message = "File is locked by other process";
message = "File is locked by another process";


var sleepDelay = 500 * retryCount; // Retry immediately on first exception.
Logger.LogWarning($"{message}. Retry after {sleepDelay}[ms]", file: path, code: WarningCodes.Build.LockedFile);
Thread.Sleep(sleepDelay);

++retryCount;
goto Retry;
}
}
}
Expand Down
231 changes: 231 additions & 0 deletions src/Docfx.Common/FileLockCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Runtime.InteropServices;

#nullable enable

namespace Docfx.Common;

// Based on https://github.com/dotnet/roslyn/blob/main/src/Compilers/Core/Portable/InternalUtilities/FileLockCheck.cs

/// <summary>
/// This class implements checking what processes are locking a file on Windows.
/// It uses the Restart Manager API to do this. Other platforms are skipped.
/// </summary>
internal static class FileLockCheck
{
[StructLayout(LayoutKind.Sequential)]
private struct FILETIME
{
public uint dwLowDateTime;
public uint dwHighDateTime;
}

[StructLayout(LayoutKind.Sequential)]
private struct RM_UNIQUE_PROCESS
{
public uint dwProcessId;
public FILETIME ProcessStartTime;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct RM_PROCESS_INFO
{
private const int CCH_RM_MAX_APP_NAME = 255;
private const int CCH_RM_MAX_SVC_NAME = 63;

internal RM_UNIQUE_PROCESS Process;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)]
public string strAppName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)]
public string strServiceShortName;
internal int ApplicationType;
public uint AppStatus;
public uint TSSessionId;
[MarshalAs(UnmanagedType.Bool)]
public bool bRestartable;
}

private const string RestartManagerDll = "rstrtmgr.dll";

[DllImport(RestartManagerDll, CharSet = CharSet.Unicode)]
private static extern int RmRegisterResources(uint pSessionHandle,
uint nFiles,
string[] rgsFilenames,
uint nApplications,
[In] RM_UNIQUE_PROCESS[]? rgApplications,
uint nServices,
string[]? rgsServiceNames);

/// <summary>
/// Starts a new Restart Manager session.
/// A maximum of 64 Restart Manager sessions per user session
/// can be open on the system at the same time. When this
/// function starts a session, it returns a session handle
/// and session key that can be used in subsequent calls to
/// the Restart Manager API.
/// </summary>
/// <param name="pSessionHandle">
/// A pointer to the handle of a Restart Manager session.
/// The session handle can be passed in subsequent calls
/// to the Restart Manager API.
/// </param>
/// <param name="dwSessionFlags">
/// Reserved. This parameter should be 0.
/// </param>
/// <param name="strSessionKey">
/// A null-terminated string that contains the session key
/// to the new session. The string must be allocated before
/// calling the RmStartSession function.
/// </param>
/// <returns>System error codes that are defined in Winerror.h.</returns>
/// <remarks>
/// The RmStartSession function doesn't properly null-terminate
/// the session key, even though the function is documented as
/// returning a null-terminated string. To work around this bug,
/// we pre-fill the buffer with null characters so that whatever
/// ends gets written will have a null terminator (namely, one of
/// the null characters we placed ahead of time).
/// <para>
/// see <see href="http://blogs.msdn.com/b/oldnewthing/archive/2012/02/17/10268840.aspx"/>.
/// </para>
/// </remarks>
[DllImport(RestartManagerDll, CharSet = CharSet.Unicode)]
private static extern unsafe int RmStartSession(
out uint pSessionHandle,
int dwSessionFlags,
char* strSessionKey);

/// <summary>
/// Ends the Restart Manager session.
/// This function should be called by the primary installer that
/// has previously started the session by calling the <see cref="RmStartSession"/>
/// function. The RmEndSession function can be called by a secondary installer
/// that is joined to the session once no more resources need to be registered
/// by the secondary installer.
/// </summary>
/// <param name="pSessionHandle">A handle to an existing Restart Manager session.</param>
/// <returns>
/// The function can return one of the system error codes that are defined in Winerror.h.
/// </returns>
[DllImport(RestartManagerDll)]
private static extern int RmEndSession(uint pSessionHandle);

[DllImport(RestartManagerDll, CharSet = CharSet.Unicode)]
private static extern int RmGetList(uint dwSessionHandle,
out uint pnProcInfoNeeded,
ref uint pnProcInfo,
[In, Out] RM_PROCESS_INFO[]? rgAffectedApps,
ref uint lpdwRebootReasons);

public static string GetLockingProcessNames(string filePath)
{
if (!OperatingSystem.IsWindows())
return "";

try
{
var processes = GetLockingProcessNames([filePath]);
if (processes.Length == 0)
return "";

var isFileLockedBySelf = processes.Any(x => x.processId == Environment.ProcessId);
return isFileLockedBySelf
? $"File is locked by this process"
: $"File is locked by external process: {string.Join(',', processes)}";
}
catch (Exception)
{
// Never throw if we can't get the processes locking the file.
return "";
}
}

private static (int processId, string processName)[] GetLockingProcessNames(string[] paths)
{
const int MaxRetries = 6;
const int ERROR_MORE_DATA = 234;
const uint RM_REBOOT_REASON_NONE = 0;

uint handle;
int res;

unsafe
{
char* key = stackalloc char[sizeof(Guid) * 2 + 1];
res = RmStartSession(out handle, 0, key);
}

if (res != 0)
{
return [];
}

try
{
res = RmRegisterResources(handle, (uint)paths.Length, paths, 0, null, 0, null);
if (res != 0)
{
return [];
}

//
// Obtain the list of affected applications/services.
//
// NOTE: Restart Manager returns the results into the buffer allocated by the caller. The first call to
// RmGetList() will return the size of the buffer (i.e. nProcInfoNeeded) the caller needs to allocate.
// The caller then needs to allocate the buffer (i.e. rgAffectedApps) and make another RmGetList()
// call to ask Restart Manager to write the results into the buffer. However, since Restart Manager
// refreshes the list every time RmGetList()is called, it is possible that the size returned by the first
// RmGetList()call is not sufficient to hold the results discovered by the second RmGetList() call. Therefore,
// it is recommended that the caller follows the following practice to handle this race condition:
//
// Use a loop to call RmGetList() in case the buffer allocated according to the size returned in previous
// call is not enough.
//
uint pnProcInfo = 0;
RM_PROCESS_INFO[]? rgAffectedApps = null;
int retry = 0;
do
{
uint lpdwRebootReasons = RM_REBOOT_REASON_NONE;
res = RmGetList(handle, out uint pnProcInfoNeeded, ref pnProcInfo, rgAffectedApps, ref lpdwRebootReasons);
if (res == 0)
{
// If pnProcInfo == 0, then there is simply no locking process (found), in this case rgAffectedApps is "null".
if (pnProcInfo == 0)
{
return [];
}

Debug.Assert(rgAffectedApps != null);

var lockInfos = new List<(int, string)>((int)pnProcInfo);
for (int i = 0; i < pnProcInfo; i++)
{
lockInfos.Add(((int)rgAffectedApps[i].Process.dwProcessId, rgAffectedApps[i].strAppName));
}

return lockInfos.ToArray();
}

if (res != ERROR_MORE_DATA)
{
return [];
}

pnProcInfo = pnProcInfoNeeded;
rgAffectedApps = new RM_PROCESS_INFO[pnProcInfo];
}
while (res == ERROR_MORE_DATA && retry++ < MaxRetries);
}
finally
{
_ = RmEndSession(handle);
}

return [];
}
}
1 change: 1 addition & 0 deletions src/Docfx.Common/Loggers/WarningCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static class Build
public const string UnknownContentType = "UnknownContentType";
public const string UnknownContentTypeForTemplate = "UnknownContentTypeForTemplate";
public const string InvalidTocInclude = "InvalidTocInclude";
public const string LockedFile = "LockedFile";
}

public static class Metadata
Expand Down