Welcome to mirror list, hosted at ThFree Co, Russian Federation.

FileUtilities.cs « TextModel « Impl « Text « src - github.com/microsoft/vs-editor-api.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: b6fbd822d1426abcf8d9c116387ad2ec25315abc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
//
//  Copyright (c) Microsoft Corporation. All rights reserved.
//  Licensed under the MIT License. See License.txt in the project root for license information.
//
// This file contain implementations details that are subject to change without notice.
// Use at your own risk.
//
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace Microsoft.VisualStudio.Text.Implementation
{
    internal class FileUtilities
    {
        public static void SaveSnapshot(ITextSnapshot snapshot,
                                        FileMode fileMode,
                                        Encoding encoding,
                                        string filePath)
        {
            Debug.Assert((fileMode == FileMode.Create) || (fileMode == FileMode.CreateNew));

            //Save the contents of the text buffer to disk.

            string temporaryFilePath = null;
            try
            {
                FileStream originalFileStream = null;
                FileStream temporaryFileStream = FileUtilities.CreateFileStream(filePath, fileMode, out temporaryFilePath, out originalFileStream);
                if (originalFileStream == null)
                {
                    //The "normal" scenario: save the snapshot directly to disk. Either:
                    // there are no hard links to the target file so we can write the snapshot to the temporary and use File.Replace.
                    // we're creating a new file (in which case, temporaryFileStream is a misnomer: it is the stream for the file we are creating).
                    try
                    {
                        using (StreamWriter streamWriter = new StreamWriter(temporaryFileStream, encoding))
                        {
                            snapshot.Write(streamWriter);
                        }
                    }
                    finally
                    {
                        //This is somewhat redundant: disposing of streamWriter had the side-effect of disposing of temporaryFileStream
                        temporaryFileStream.Dispose();
                        temporaryFileStream = null;
                    }

                    if (temporaryFilePath != null)
                    {
                        //We were saving to the original file and already have a copy of the file on disk.
                        int remainingAttempts = 3;
                        do
                        {
                            try
                            {
                                //Replace the contents of filePath with the contents of the temporary using File.Replace to
                                //preserve the various attributes of the original file.
                                File.Replace(temporaryFilePath, filePath, null, true);
                                temporaryFilePath = null;

                                return;
                            }
                            catch (FileNotFoundException)
                            {
                                // The target file doesn't exist (someone deleted it after we detected it earlier).
                                // This is an acceptable condition so don't throw.
                                File.Move(temporaryFilePath, filePath);
                                temporaryFilePath = null;

                                return;
                            }
                            catch (IOException)
                            {
                                //There was some other exception when trying to replace the contents of the file
                                //(probably because some other process had the file locked).
                                //Wait a few ms and try again.
                                System.Threading.Thread.Sleep(5);
                            }
                        }
                        while (--remainingAttempts > 0);

                        //We're giving up on replacing the file. Try overwriting it directly (this is essentially the old Dev11 behavior).
                        //Do not try approach we are using for hard links (copying the original & restoring it if there is a failure) since
                        //getting here implies something strange is going on with the file system (Git or the like locking files) so we
                        //want the simplest possible fallback.

                        //Failing here causes the exception to be passed to the calling code.
                        using (FileStream stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read))
                        {
                            using (StreamWriter streamWriter = new StreamWriter(stream, encoding))
                            {
                                snapshot.Write(streamWriter);
                            }
                        }
                    }
                }
                else
                {
                    //filePath has hard links so we need to use a different approach to save the file:
                    // copy the original file to the temporary
                    // write directly to the original
                    // restore the original in the event of errors (which could be encoding errors and not disk issues) if there's a problem.
                    try
                    {
                        // Copy the contents of the original file to the temporary.
                        originalFileStream.CopyTo(temporaryFileStream);

                        //We've got a clean copy, try writing the snapshot directly to the original file
                        try
                        {
                            originalFileStream.Seek(0, SeekOrigin.Begin);
                            originalFileStream.SetLength(0);

                            //Make sure the StreamWriter is flagged leaveOpen == true. Otherwise disposing of the StreamWriter will dispose of originalFileStream and we need to
                            //leave originalFileStream open so we can use it to restore the original from the temporary copy we made.
                            using (var streamWriter = new StreamWriter(originalFileStream, encoding, bufferSize: 1024, leaveOpen: true))        //1024 == the default buffer size for a StreamWriter.
                            {
                                snapshot.Write(streamWriter);
                            }
                        }
                        catch
                        {
                            //Restore the original from the temporary copy we made (but rethrow the original exception since we didn't save the file).
                            temporaryFileStream.Seek(0, SeekOrigin.Begin);

                            originalFileStream.Seek(0, SeekOrigin.Begin);
                            originalFileStream.SetLength(0);

                            temporaryFileStream.CopyTo(originalFileStream);

                            throw;
                        }
                    }
                    finally
                    {
                        originalFileStream.Dispose();
                        originalFileStream = null;

                        temporaryFileStream.Dispose();
                        temporaryFileStream = null;
                    }
                }
            }
            finally
            {
                if (temporaryFilePath != null)
                {
                    try
                    {
                        //We do not need the temporary any longer.
                        if (File.Exists(temporaryFilePath))
                        {
                            File.Delete(temporaryFilePath);
                        }
                    }
                    catch
                    {
                        //Failing to clean up the temporary is an ignorable exception.
                    }
                }
            }
        }

        private static FileStream CreateFileStream(string filePath, FileMode fileMode, out string temporaryPath, out FileStream originalFileStream)
        {
            originalFileStream = null;

            if (File.Exists(filePath))
            {
                // We're writing to a file that already exists. This is an error if we're trying to do a CreateNew.
                if (fileMode == FileMode.CreateNew)
                {
                    throw new IOException(filePath + " exists");
                }

                try
                {
                    originalFileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);

                    //Even thoug SafeFileHandle is an IDisposable, we don't dispose of it since that closes the strem.
                    var safeHandle = originalFileStream.SafeFileHandle;
                    if (!(safeHandle.IsClosed || safeHandle.IsInvalid))
                    {
                        BY_HANDLE_FILE_INFORMATION fi;
                        if (NativeMethods.GetFileInformationByHandle(safeHandle, out fi))
                        {
                            if (fi.NumberOfLinks <= 1)
                            {
                                // The file we're trying to write to doesn't have any hard links ... clear out the originalFileStream
                                // as a clue.
                                originalFileStream.Dispose();
                                originalFileStream = null;
                            }
                        }
                    }
                }
                catch
                {
                    if (originalFileStream != null)
                    {
                        originalFileStream.Dispose();
                        originalFileStream = null;
                    }

                    //We were not able to determine whether or not the file had hard links so throw here (aborting the save)
                    //since we don't know how to do it safely.
                    throw;
                }

                string root = Path.GetDirectoryName(filePath);

                int count = 0;
                while (++count < 20)
                {
                    try
                    {
                        temporaryPath = Path.Combine(root, Path.GetRandomFileName() + "~");   //The ~ suffix hides the temporary file from GIT.
                        return new FileStream(temporaryPath, FileMode.CreateNew, (originalFileStream != null) ? FileAccess.ReadWrite : FileAccess.Write, FileShare.None);
                    }
                    catch (IOException)
                    {
                        //Ignore IOExceptions ... GetRandomFileName() came up with a duplicate so we need to try again.
                    }
                }

                Debug.Fail("Unable to create a temporary file");
            }

            temporaryPath = null;
            return new FileStream(filePath, fileMode, FileAccess.Write, FileShare.Read);
        }
    }
}