File: System\Configuration\Internal\WriteFileContext.cs
Project: ndp\fx\src\Configuration\System.Configuration.csproj (System.Configuration)
//------------------------------------------------------------------------------
// <copyright file="WriteFileContext.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------
 
namespace System.Configuration.Internal {
    using System.Configuration;
    using System.IO;
    using System.Security.Permissions;
    using System.Reflection;
    using System.Threading;
    using System.Security;
    using System.CodeDom.Compiler;
    using Microsoft.Win32;	
#if !FEATURE_PAL
    using System.Security.AccessControl;
#endif
 
    internal class WriteFileContext {
        private const  int          SAVING_TIMEOUT        = 10000;  // 10 seconds
        private const  int          SAVING_RETRY_INTERVAL =   100;  // 100 milliseconds
        private static volatile bool _osPlatformDetermined;
        private static volatile PlatformID _osPlatform;
        
        private TempFileCollection  _tempFiles;
        private string              _tempNewFilename;
        private string              _templateFilename;
 
        internal WriteFileContext(string filename, string templateFilename) {
            string directoryname = UrlPath.GetDirectoryOrRootName(filename);
 
            _templateFilename = templateFilename;
            _tempFiles = new TempFileCollection(directoryname);
            try {
                _tempNewFilename = _tempFiles.AddExtension("newcfg");
            }
            catch {
                ((IDisposable)_tempFiles).Dispose();
                _tempFiles = null;
                throw;
            }
        }
 
        static WriteFileContext() {
            _osPlatformDetermined = false;
        }
 
        internal string TempNewFilename {
            get {return _tempNewFilename;}
        }
 
        // Complete
        //
        // Cleanup the WriteFileContext object based on either success
        // or failure
        //
        // Note: The current algorithm guarantess
        //         1) The file we are saving to will always be present 
        //            on the file system (ie. there will be no window
        //            during saving in which there won't be a file there)
        //         2) It will always be available for reading from a 
        //            client and it will be complete and accurate.
        //
        // ... This means that writing is a bit more complicated, and may
        // have to be retried (because of reading lock), but I don't see 
        // anyway to get around this given 1 and 2.
        //
        internal void Complete(string filename, bool success) {
            try {
                if (success) {
                    if ( File.Exists( filename ) ) {
                        // Test that we can write to the file
                        ValidateWriteAccess( filename );
 
                        // Copy Attributes from original
                        DuplicateFileAttributes( filename, _tempNewFilename );
                    } 
                    else {
                        if ( _templateFilename != null ) {
                            // Copy Acl from template file
                            DuplicateTemplateAttributes( _templateFilename, _tempNewFilename );
                        }
                    }
 
                    ReplaceFile(_tempNewFilename, filename);
 
                    // Don't delete, since we just moved it.
                    _tempFiles.KeepFiles = true;
                }
            }
            finally {
                ((IDisposable)_tempFiles).Dispose();
                _tempFiles = null;
            }
        }
 
        // DuplicateFileAttributes
        //
        // Copy all the files attributes that we care about from the source
        // file to the destination file
        //
        private void DuplicateFileAttributes( string source, string destination )
        {
#if !FEATURE_PAL
            FileAttributes      attributes;
            DateTime            creationTime;
 
            // Copy File Attributes, ie. Hidden, Readonly, etc.
            attributes = File.GetAttributes( source );
            File.SetAttributes( destination, attributes );
 
            // Copy Creation Time
            creationTime = File.GetCreationTimeUtc( source );
            File.SetCreationTimeUtc( destination, creationTime );
 
            // Copy ACL's
            DuplicateTemplateAttributes( source, destination );
#endif	// FEATURE_PAL
        }
 
        // DuplicateTemplateAttributes
        //
        // Copy over all the attributes you would want copied from a template file.
        // As of right now this is just acl's
        //
        private void DuplicateTemplateAttributes( string source, string destination ) {
#if !FEATURE_PAL
            if (IsWinNT) {
                FileSecurity        fileSecurity;
 
                // Copy Security information
                fileSecurity = File.GetAccessControl( source, AccessControlSections.Access );
 
                // Mark dirty, so effective for write
                fileSecurity.SetAccessRuleProtection( fileSecurity.AreAccessRulesProtected, true );
                File.SetAccessControl( destination, fileSecurity );
            }
            else {
                FileAttributes  fileAttributes;
 
                fileAttributes = File.GetAttributes( source );
                File.SetAttributes( destination, fileAttributes );
            }
#endif	// FEATURE_PAL
        }
 
        // ValidateWriteAccess
        //
        // Validate that we can write to the file.  This will enforce the ACL's
        // on the file.  Since we do our moving of files to replace, this is 
        // nice to ensure we are not by-passing some security permission
        // that someone set (although that could bypass this via move themselves)
        //
        // Note: 1) This is really just a nice to have, since with directory permissions
        //          they could do the same thing we are doing
        //
        //       2) We are depending on the current behavior that if the file is locked 
        //          and we can not open it, that we will get an UnauthorizedAccessException
        //          and not the IOException.
        //
        private void ValidateWriteAccess( string filename ) {
            FileStream fs = null;
 
            try {
                // Try to open file for write access
                fs = new FileStream( filename,
                                     FileMode.Open,
                                     FileAccess.Write,
                                     FileShare.ReadWrite );
            }
            catch ( UnauthorizedAccessException ) {
                // Access was denied, make sure we throw this
                throw;
            }
            catch ( IOException ) {
                // Someone else was using the file.  Since we did not get
                // the unauthorizedAccessException we have access to the file
            }
            catch ( Exception ) {
                // Unexpected, so just throw for safety sake
                throw;
            }
            finally {
                if ( fs != null ) {
                    fs.Close();
                }
            }
        }
        
        // ReplaceFile
        //
        // Replace one file with another using MoveFileEx.  This will
        // retry the operation if the file is locked because someone
        // is reading it
        //
        private void ReplaceFile( string Source, string Target )
        {
            bool WriteSucceeded = false;
            int  Duration       = 0;
 
            WriteSucceeded = AttemptMove( Source, Target );
 
            // The file may be open for read, if it is then 
            // lets try again because maybe they will finish
            // soon, and we will be able to replace
            while ( !WriteSucceeded                &&
                    ( Duration < SAVING_TIMEOUT )  &&
                    File.Exists( Target )          &&
                    !FileIsWriteLocked( Target ) ) {
                    
                Thread.Sleep( SAVING_RETRY_INTERVAL );
 
                Duration += SAVING_RETRY_INTERVAL;                
 
                WriteSucceeded = AttemptMove( Source, Target );
            }
 
            if ( !WriteSucceeded ) {
                
                throw new ConfigurationErrorsException(
                              SR.GetString(SR.Config_write_failed, Target) );
            }
        }
 
        // AttemptMove
        //
        // Attempt to move a file from one location to another
        //
        // Return Values:
        //   TRUE  - Move Successful
        //   FALSE - Move Failed
        private bool AttemptMove( string Source, string Target ) {
            bool MoveSuccessful = false;
 
            if ( IsWinNT ) {
 
                // We can only call this when we have kernel32.dll
                MoveSuccessful = UnsafeNativeMethods.MoveFileEx( 
                                     Source,
                                     Target,
                                     UnsafeNativeMethods.MOVEFILE_REPLACE_EXISTING );
            }
            else {
 
                try {
                    // VSWhidbey 548017:
                    // File.Move isn't supported on Win9x.  We'll use File.Copy
                    // instead.  Please note that Source is a temporary file which 
                    // will be deleted when _tempFiles is disposed.
                    File.Copy(Source, Target, true);
                    MoveSuccessful = true;
                }
                catch {
                    
                    MoveSuccessful = false;
                }
                
            }
 
            return MoveSuccessful;
        }
        
        // FileIsWriteLocked
        //
        // Is the file write locked or not?
        //
        private bool FileIsWriteLocked( string FileName ) {
            Stream FileStream  = null;
            bool   WriteLocked = true;
 
            if (!FileUtil.FileExists(FileName, true)) {
                // It can't be locked if it doesn't exist
                return false;
            }
 
            try {
                FileShare fileShare = FileShare.Read;
 
                if (IsWinNT) {
                    fileShare |= FileShare.Delete;
                }
                
                // Try to open for shared reading
                FileStream  = new FileStream( FileName, 
                                              FileMode.Open, 
                                              FileAccess.Read, 
                                              fileShare);
 
                // If we can open it for shared reading, it is not 
                // write locked
                WriteLocked = false;
            }
            finally {
                if ( FileStream != null ) {
                    
                    FileStream.Close();
                    FileStream = null;
                }
            }
            
            return WriteLocked;
        }
 
        // IsWinNT
        //
        // Are we running in WinNT or not?
        //
        private bool IsWinNT {
            get {
                if ( !_osPlatformDetermined ) {
                    
                    _osPlatform = Environment.OSVersion.Platform;
                    _osPlatformDetermined = true;
                }
 
                return ( _osPlatform == System.PlatformID.Win32NT );
            }
        }
    }
}