File: CommandLine\CommonCompiler.CompilerEmitStreamProvider.cs
Web Access
Project: src\src\Compilers\Core\Portable\Microsoft.CodeAnalysis.csproj (Microsoft.CodeAnalysis)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis
{
    internal abstract partial class CommonCompiler
    {
        /// <summary>
        /// This implementation of <see cref="Compilation.EmitStreamProvider"/> will delay the creation
        /// of the PE / PDB file until the compiler determines the compilation has succeeded.  This prevents
        /// the compiler from deleting output from the previous compilation when a new compilation 
        /// fails. The <see cref="Close"/> method must be called to retrieve all diagnostics.
        /// </summary>
        private sealed class CompilerEmitStreamProvider : Compilation.EmitStreamProvider
        {
            private readonly CommonCompiler _compiler;
            private readonly string _filePath;
            private Stream? _streamToDispose;
 
            internal CompilerEmitStreamProvider(
                CommonCompiler compiler,
                string filePath)
            {
                _compiler = compiler;
                _filePath = filePath;
            }
 
            public void Close(DiagnosticBag diagnostics)
            {
                try
                {
                    _streamToDispose?.Dispose();
                }
                catch (Exception e)
                {
                    var messageProvider = _compiler.MessageProvider;
                    var diagnosticInfo = new DiagnosticInfo(messageProvider, messageProvider.ERR_OutputWriteFailed, _filePath, e.Message);
                    diagnostics.Add(messageProvider.CreateDiagnostic(diagnosticInfo));
                }
            }
 
            protected override Stream? CreateStream(DiagnosticBag diagnostics)
            {
                Debug.Assert(_streamToDispose == null);
 
                try
                {
                    try
                    {
                        return OpenFileStream();
                    }
                    catch (IOException e)
                    {
                        // Other process is reading the file preventing us to write to it.
                        // We attempt to rename and delete the file in case the reader opened it with FileShare.Delete flag that
                        // allows the file to be deleted by other processes.
                        //
                        // Note that if the file is marked "readonly" or the current user doesn't have sufficient privileges
                        // the exception thrown is UnauthorizedAccessException, not IOException, so we won't attempt to delete the file.
 
                        try
                        {
                            const int eWin32SharingViolation = unchecked((int)0x80070020);
 
                            if (PathUtilities.IsUnixLikePlatform)
                            {
                                // Unix & Mac are simple: just delete the file in the directory. 
                                // The memory mapped content remains available for the reader.
                                File.Delete(_filePath);
                            }
                            else if (e.HResult == eWin32SharingViolation)
                            {
                                // On Windows File.Delete only marks the file for deletion, but doesn't remove it from the directory.
                                var newFilePath = Path.Combine(Path.GetDirectoryName(_filePath)!, Guid.NewGuid().ToString() + "_" + Path.GetFileName(_filePath));
 
                                // Try to rename the existing file. This fails unless the file is open with FileShare.Delete.
                                File.Move(_filePath, newFilePath);
 
                                // hide the renamed file:
                                File.SetAttributes(newFilePath, FileAttributes.Hidden);
 
                                // Mark the renamed file for deletion, so that it's deleted as soon as the current reader is finished reading it
                                File.Delete(newFilePath);
                            }
                        }
                        catch
                        {
                            // report the original exception
                            ReportOpenFileDiagnostic(diagnostics, e);
                            return null;
                        }
 
                        return OpenFileStream();
                    }
                }
                catch (Exception e)
                {
                    ReportOpenFileDiagnostic(diagnostics, e);
                    return null;
                }
            }
 
            private Stream OpenFileStream()
            {
                return _streamToDispose = _compiler.FileSystem.OpenFile(_filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
            }
 
            private void ReportOpenFileDiagnostic(DiagnosticBag diagnostics, Exception e)
            {
                var messageProvider = _compiler.MessageProvider;
                var lockedBy = FileLockCheck.TryGetLockingProcessInfos(_filePath);
 
                var message = lockedBy.IsEmpty
                    ? (object)e.Message
                    : new LocalizableResourceString(
                        nameof(CodeAnalysisResources.ExceptionMessage_FileMayBeLockedBy),
                        CodeAnalysisResources.ResourceManager,
                        typeof(CodeAnalysisResources),
                        e.Message,
                        string.Join(", ", lockedBy.Select(info => $"'{info.applicationName}' ({info.processId})")));
 
                diagnostics.Add(messageProvider.CreateDiagnostic(messageProvider.ERR_CantOpenFileWrite, Location.None, _filePath, message));
            }
        }
    }
}