PowerShell/src/System.Management.Automation/namespaces/FileSystemContentStream.cs
2021-08-17 09:01:04 +05:00

1572 lines
59 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Management.Automation;
using System.Management.Automation.Internal;
using System.Management.Automation.Provider;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Dbg = System.Management.Automation;
namespace Microsoft.PowerShell.Commands
{
/// <summary>
/// The content stream class for the file system provider. It implements both
/// the IContentReader and IContentWriter interfaces.
/// </summary>
/// <remarks>
/// Note, this class does no specific error handling. All errors are allowed to
/// propagate to the caller so that they can be written to the error pipeline
/// if necessary.
/// </remarks>
internal class FileSystemContentReaderWriter : IContentReader, IContentWriter
{
#region tracer
/// <summary>
/// An instance of the PSTraceSource class used for trace output
/// using "FileSystemContentStream" as the category.
/// </summary>
[Dbg.TraceSourceAttribute(
"FileSystemContentStream",
"The provider content reader and writer for the file system")]
private static readonly Dbg.PSTraceSource s_tracer =
Dbg.PSTraceSource.GetTracer("FileSystemContentStream",
"The provider content reader and writer for the file system");
#endregion tracer
private readonly string _path;
private readonly string _streamName;
private readonly FileMode _mode;
private readonly FileAccess _access;
private readonly FileShare _share;
private readonly Encoding _encoding;
private readonly CmdletProvider _provider;
private FileStream _stream;
private StreamReader _reader;
private StreamWriter _writer;
private readonly bool _usingByteEncoding;
private const char DefaultDelimiter = '\n';
private readonly string _delimiter = $"{DefaultDelimiter}";
private readonly int[] _offsetDictionary;
private readonly bool _usingDelimiter;
private readonly StringBuilder _currentLineContent;
private bool _waitForChanges;
private readonly bool _isRawStream;
private long _fileOffset;
private FileAttributes _oldAttributes;
private bool _haveOldAttributes;
// The reader to read file content backward
private FileStreamBackReader _backReader;
private bool _alreadyDetectEncoding = false;
// False to add a newline to the end of the output string, true if not.
private readonly bool _suppressNewline = false;
/// <summary>
/// Constructor for the content stream.
/// </summary>
/// <param name="path">
/// The path to the file to get the content from.
/// </param>
/// <param name="mode">
/// The file mode to open the file with.
/// </param>
/// <param name="access">
/// The file access requested in the file.
/// </param>
/// <param name="share">
/// The file share to open the file with
/// </param>
/// <param name="encoding">
/// The encoding of the file to be read or written.
/// </param>
/// <param name="usingByteEncoding">
/// If true, bytes will be read from the file. If false, the specified encoding
/// will be used to read the file.
/// </param>
/// <param name="waitForChanges">
/// If true, we will perform blocking reads on the file, waiting for new content to be appended
/// </param>
/// <param name="provider">
/// The CmdletProvider invoking this stream
/// </param>
/// <param name="isRawStream">
/// Indicates raw stream.
/// </param>
public FileSystemContentReaderWriter(
string path,
FileMode mode,
FileAccess access,
FileShare share,
Encoding encoding,
bool usingByteEncoding,
bool waitForChanges,
CmdletProvider provider,
bool isRawStream)
: this(
path,
streamName: null,
mode,
access,
share,
encoding,
usingByteEncoding,
waitForChanges,
provider,
isRawStream)
{
}
/// <summary>
/// Constructor for the content stream.
/// </summary>
/// <param name="path">
/// The path to the file to get the content from.
/// </param>
/// <param name="streamName">
/// The name of the Alternate Data Stream to get the content from. If null or empty, returns
/// the file's primary content.
/// </param>
/// <param name="mode">
/// The file mode to open the file with.
/// </param>
/// <param name="access">
/// The file access requested in the file.
/// </param>
/// <param name="share">
/// The file share to open the file with
/// </param>
/// <param name="encoding">
/// The encoding of the file to be read or written.
/// </param>
/// <param name="usingByteEncoding">
/// If true, bytes will be read from the file. If false, the specified encoding
/// will be used to read the file.
/// </param>
/// <param name="waitForChanges">
/// If true, we will perform blocking reads on the file, waiting for new content to be appended
/// </param>
/// <param name="provider">
/// The CmdletProvider invoking this stream
/// </param>
/// <param name="isRawStream">
/// Indicates raw stream.
/// </param>
public FileSystemContentReaderWriter(
string path, string streamName, FileMode mode, FileAccess access, FileShare share,
Encoding encoding, bool usingByteEncoding, bool waitForChanges, CmdletProvider provider,
bool isRawStream)
{
if (string.IsNullOrEmpty(path))
{
throw PSTraceSource.NewArgumentNullException(nameof(path));
}
if (s_tracer.IsEnabled)
{
s_tracer.WriteLine("path = {0}", path);
s_tracer.WriteLine("mode = {0}", mode);
s_tracer.WriteLine("access = {0}", access);
}
_path = path;
_streamName = streamName;
_mode = mode;
_access = access;
_share = share;
_encoding = encoding;
_usingByteEncoding = usingByteEncoding;
_waitForChanges = waitForChanges;
_provider = provider;
_isRawStream = isRawStream;
CreateStreams(path, streamName, mode, access, share, encoding);
}
/// <summary>
/// Constructor for the content stream.
/// </summary>
/// <param name="path">
/// The path to the file to get the content from.
/// </param>
/// <param name="streamName">
/// The name of the Alternate Data Stream to get the content from. If null or empty, returns
/// the file's primary content.
/// </param>
/// <param name="mode">
/// The file mode to open the file with.
/// </param>
/// <param name="access">
/// The file access requested in the file.
/// </param>
/// <param name="share">
/// The file share to open the file with
/// </param>
/// <param name="encoding">
/// The encoding of the file to be read or written.
/// </param>
/// <param name="usingByteEncoding">
/// If true, bytes will be read from the file. If false, the specified encoding
/// will be used to read the file.
/// </param>
/// <param name="waitForChanges">
/// If true, we will perform blocking reads on the file, waiting for new content to be appended
/// </param>
/// <param name="provider">
/// The CmdletProvider invoking this stream
/// </param>
/// <param name="isRawStream">
/// Indicates raw stream.
/// </param>
/// <param name="suppressNewline">
/// False to add a newline to the end of the output string, true if not.
/// </param>
public FileSystemContentReaderWriter(
string path, string streamName, FileMode mode, FileAccess access, FileShare share,
Encoding encoding, bool usingByteEncoding, bool waitForChanges, CmdletProvider provider,
bool isRawStream, bool suppressNewline)
: this(path, streamName, mode, access, share, encoding, usingByteEncoding, waitForChanges, provider, isRawStream)
{
_suppressNewline = suppressNewline;
}
/// <summary>
/// Constructor for the content stream.
/// </summary>
/// <param name="path">
/// The path to the file to get the content from.
/// </param>
/// <param name="streamName">
/// The name of the Alternate Data Stream to get the content from. If null or empty, returns
/// the file's primary content.
/// </param>
/// <param name="mode">
/// The file mode to open the file with.
/// </param>
/// <param name="access">
/// The file access requested in the file.
/// </param>
/// <param name="share">
/// The file share to open the file with
/// </param>
/// <param name="delimiter">
/// The delimiter to use when reading strings. Each time read is called, all contents up to an including
/// the delimiter is read.
/// </param>
/// <param name="encoding">
/// The encoding of the file to be read or written.
/// </param>
/// <param name="waitForChanges">
/// If true, we will perform blocking reads on the file, waiting for new content to be appended
/// </param>
/// <param name="provider">
/// The CmdletProvider invoking this stream
/// </param>
/// <param name="isRawStream">
/// Indicates raw stream.
/// </param>
public FileSystemContentReaderWriter(
string path,
string streamName,
FileMode mode,
FileAccess access,
FileShare share,
string delimiter,
Encoding encoding,
bool waitForChanges,
CmdletProvider provider,
bool isRawStream)
: this(path, streamName, mode, access, share, encoding, false, waitForChanges, provider, isRawStream)
{
// If the delimiter is default ('\n') we'll use ReadLine() method.
// Otherwise allocate temporary structures for ReadDelimited() method.
if (!(delimiter.Length == 1 && delimiter[0] == DefaultDelimiter))
{
_delimiter = delimiter;
_usingDelimiter = true;
// We expect that we are parsing files where line lengths can be relatively long.
const int DefaultLineLength = 256;
_currentLineContent = new StringBuilder(DefaultLineLength);
// For Boyer-Moore string search algorithm.
// Populate the offset lookups.
// These will tell us the maximum number of characters
// we can read to generate another possible match (safe shift).
// If we read more characters than this, we risk consuming
// more of the stream than we need.
//
// Because an unicode character size is 2 byte we would to have use
// very large array with 65535 size to keep this safe offsets.
// One solution is to pack unicode character to byte.
// The workaround is to use low byte from unicode character.
// This allow us to use small array with size 256.
// This workaround is the fastest and provides excellent results
// in regular search scenarios when the file contains
// mostly characters from the same alphabet.
_offsetDictionary = new int[256];
// If next char from file is not in search pattern safe shift is the search pattern length.
for (var n = 0; n < _offsetDictionary.Length; n++)
{
_offsetDictionary[n] = _delimiter.Length;
}
// If next char from file is in search pattern we should calculate a safe shift.
char currentChar;
byte lowByte;
for (var i = 0; i < _delimiter.Length; i++)
{
currentChar = _delimiter[i];
lowByte = Unsafe.As<char, byte>(ref currentChar);
_offsetDictionary[lowByte] = _delimiter.Length - i - 1;
}
}
}
/// <summary>
/// Reads the specified number of characters or a lines from the file.
/// </summary>
/// <param name="readCount">
/// If less than 1, then the entire file is read at once. If 1 or greater, then
/// readCount is used to determine how many items (ie: lines, bytes, delimited tokens)
/// to read per call.
/// </param>
/// <returns>
/// An array of strings representing the character(s) or line(s) read from
/// the file.
/// </returns>
public IList Read(long readCount)
{
if (_isRawStream && _waitForChanges)
{
throw PSTraceSource.NewInvalidOperationException(FileSystemProviderStrings.RawAndWaitCannotCoexist);
}
bool waitChanges = _waitForChanges;
s_tracer.WriteLine("blocks requested = {0}", readCount);
var blocks = new List<object>();
bool readToEnd = (readCount <= 0);
if (_alreadyDetectEncoding && _reader.BaseStream.Position == 0)
{
Encoding curEncoding = _reader.CurrentEncoding;
// Close the stream, and reopen the stream to make the BOM correctly processed.
// The reader has already detected encoding, so if we don't reopen the stream, the BOM (if there is any)
// will be treated as a regular character.
_stream.Dispose();
CreateStreams(_path, null, _mode, _access, _share, curEncoding);
_alreadyDetectEncoding = false;
}
try
{
for (long currentBlock = 0; (currentBlock < readCount) || (readToEnd); ++currentBlock)
{
if (waitChanges && _provider.Stopping)
waitChanges = false;
if (_usingByteEncoding)
{
if (!ReadByteEncoded(waitChanges, blocks, readBackward: false))
break;
}
else
{
if (_usingDelimiter || _isRawStream)
{
if (!ReadDelimited(waitChanges, blocks, readBackward: false, _delimiter))
break;
}
else
{
if (!ReadByLine(waitChanges, blocks, readBackward: false))
break;
}
}
}
s_tracer.WriteLine("blocks read = {0}", blocks.Count);
}
catch (Exception e)
{
if ((e is IOException) ||
(e is ArgumentException) ||
(e is System.Security.SecurityException) ||
(e is UnauthorizedAccessException) ||
(e is ArgumentNullException))
{
// Exception contains specific message about the error occured and so no need for errordetails.
_provider.WriteError(new ErrorRecord(e, "GetContentReaderIOError", ErrorCategory.ReadError, _path));
return null;
}
else
throw;
}
return blocks.ToArray();
}
/// <summary>
/// Read the content regardless of the 'waitForChanges' flag.
/// </summary>
/// <param name="readCount"></param>
/// <returns></returns>
internal IList ReadWithoutWaitingChanges(long readCount)
{
bool oldWaitChanges = _waitForChanges;
_waitForChanges = false;
try
{
return Read(readCount);
}
finally
{
_waitForChanges = oldWaitChanges;
}
}
/// <summary>
/// Move the pointer of the stream to the position where there are 'backCount' number
/// of items (depends on what we are using: delimiter? line? byts?) to the end of the file.
/// </summary>
/// <param name="backCount"></param>
internal void SeekItemsBackward(int backCount)
{
if (backCount < 0)
{
// The caller needs to guarantee that 'backCount' is greater or equals to 0
throw PSTraceSource.NewArgumentException(nameof(backCount));
}
if (_isRawStream && _waitForChanges)
{
throw PSTraceSource.NewInvalidOperationException(FileSystemProviderStrings.RawAndWaitCannotCoexist);
}
s_tracer.WriteLine("blocks seek backwards = {0}", backCount);
var blocks = new List<object>();
if (_reader != null)
{
// Make the reader automatically detect the encoding
Seek(0, SeekOrigin.Begin);
_reader.Peek();
_alreadyDetectEncoding = true;
}
Seek(0, SeekOrigin.End);
if (backCount == 0)
{
// If backCount is 0, we should move the position to the end of the file.
// Maybe the "waitForChanges" is true in this case, which means that we are waiting for new inputs.
return;
}
string actualDelimiter = string.Create(
_delimiter.Length,
_delimiter,
(chars, buf) =>
{
for (int i = 0, j = buf.Length - 1; i < chars.Length; i++, j--)
{
chars[i] = buf[j];
}
});
long currentBlock = 0;
string lastDelimiterMatch = null;
try
{
if (_isRawStream)
{
// We always read to the end for the raw data.
// If it's indicated as RawStream, we move the pointer to the
// beginning of the file
Seek(0, SeekOrigin.Begin);
return;
}
for (; currentBlock < backCount; ++currentBlock)
{
if (_usingByteEncoding)
{
if (!ReadByteEncoded(waitChanges: false, blocks, readBackward: true))
break;
}
else
{
if (_usingDelimiter)
{
if (!ReadDelimited(waitChanges: false, blocks, readBackward: true, actualDelimiter))
break;
// If the delimiter is at the end of the file, we need to read one more
// to get to the right position. For example:
// ua123ua456ua -- -Tail 1
// If we read backward only once, we get 'ua', and cannot get to the right position
// So we read one more time, get 'ua456ua', and then we can get the right position
lastDelimiterMatch = (string)blocks[0];
if (currentBlock == 0 && lastDelimiterMatch.Equals(actualDelimiter, StringComparison.Ordinal))
backCount++;
}
else
{
if (!ReadByLine(waitChanges: false, blocks, readBackward: true))
break;
}
}
blocks.Clear();
}
// If usingByteEncoding is true, we don't create the reader and _backReader
if (!_usingByteEncoding)
{
long curStreamPosition = _backReader.GetCurrentPosition();
if (_usingDelimiter)
{
if (currentBlock == backCount)
{
Dbg.Diagnostics.Assert(lastDelimiterMatch != null, "lastDelimiterMatch should not be null when currentBlock == backCount");
if (lastDelimiterMatch.EndsWith(actualDelimiter, StringComparison.Ordinal))
{
curStreamPosition += _backReader.GetByteCount(_delimiter);
}
}
}
Seek(curStreamPosition, SeekOrigin.Begin);
}
s_tracer.WriteLine("blocks seek position = {0}", _stream.Position);
}
catch (Exception e)
{
if ((e is IOException) ||
(e is ArgumentException) ||
(e is System.Security.SecurityException) ||
(e is UnauthorizedAccessException) ||
(e is ArgumentNullException))
{
// Exception contains specific message about the error occurred and so no need for errordetails.
_provider.WriteError(new ErrorRecord(e, "GetContentReaderIOError", ErrorCategory.ReadError, _path));
}
else
throw;
}
}
private bool ReadByLine(bool waitChanges, List<object> blocks, bool readBackward)
{
// Reading lines as strings
string line = readBackward ? _backReader.ReadLine() : _reader.ReadLine();
if (line == null)
{
if (waitChanges)
{
// We only wait for changes when read forwards. So here we use reader, instead of 'localReader'
do
{
WaitForChanges(_path, _mode, _access, _share, _reader.CurrentEncoding);
line = _reader.ReadLine();
}
while ((line == null) && (!_provider.Stopping));
}
}
if (line != null)
{
blocks.Add(line);
}
int peekResult = readBackward ? _backReader.Peek() : _reader.Peek();
if (peekResult == -1)
{
return false;
}
else
{
return true;
}
}
private bool ReadDelimited(bool waitChanges, List<object> blocks, bool readBackward, string actualDelimiter)
{
if (_isRawStream)
{
// when -Raw is used we want to anyway read the whole thing
// so avoiding the while loop by reading the entire content.
string contentRead = _reader.ReadToEnd();
if (contentRead.Length > 0)
{
blocks.Add(contentRead);
}
// We already read whole file so return EOF.
return false;
}
// Since the delimiter is a string, we're essentially
// dealing with a "find the substring" algorithm, but with
// the additional restriction that we cannot read past the
// end of the delimiter. If we read past the end of the delimiter,
// then we'll eat up bytes that we need from the filestream.
// The solution is a modified Boyer-Moore string search algorithm.
// This version retains the sub-linear search performance (via the
// lookup tables).
int numRead = 0;
int currentOffset = actualDelimiter.Length;
Span<char> readBuffer = stackalloc char[currentOffset];
bool delimiterNotFound = true;
_currentLineContent.Clear();
do
{
// Read in the required batch of characters
numRead = readBackward
? _backReader.Read(readBuffer.Slice(0, currentOffset))
: _reader.Read(readBuffer.Slice(0, currentOffset));
// If we want to wait for changes, then we'll keep on attempting to read
// until we fill the buffer.
if (numRead == 0)
{
if (waitChanges)
{
// But stop reading if the provider is stopping
while ((numRead < currentOffset) && (!_provider.Stopping))
{
// Get the change, and try to read more characters
// We only wait for changes when read forwards, so here we don't need to check if 'readBackward' is
// true or false, we only use 'reader'. The member 'reader' will be updated by WaitForChanges.
WaitForChanges(_path, _mode, _access, _share, _reader.CurrentEncoding);
numRead += _reader.Read(readBuffer.Slice(0, currentOffset - numRead));
}
}
}
if (numRead > 0)
{
_currentLineContent.Append(readBuffer.Slice(0, numRead));
// Look up the final character in our offset table.
// If the character doesn't exist in the lookup table, then it's not in
// our search key. That means the match must happen strictly /after/ the
// current position. Because of that, we can feel confident reading in the
// number of characters in the search key, without the risk of reading too many.
var currentChar = _currentLineContent[_currentLineContent.Length - 1];
currentOffset = _offsetDictionary[Unsafe.As<char, byte>(ref currentChar)];
// We want to keep reading if delimiter not found and we haven't hit the end of file
delimiterNotFound = true;
// If the final letters matched, then we will get an offset of "0".
// In that case, we'll either have a match (and break from the while loop,)
// or we need to move the scan forward one position.
if (currentOffset == 0)
{
currentOffset = 1;
if (actualDelimiter.Length <= _currentLineContent.Length)
{
delimiterNotFound = false;
int i = 0;
int j = _currentLineContent.Length - actualDelimiter.Length;
for (; i < actualDelimiter.Length; i++, j++)
{
if (actualDelimiter[i] != _currentLineContent[j])
{
delimiterNotFound = true;
break;
}
}
}
}
}
}
while (delimiterNotFound && (numRead != 0));
// We've reached the end of file or end of line.
if (_currentLineContent.Length > 0)
{
// Add the block read to the ouptut array list, trimming a trailing delimiter, if present.
// Note: If -Tail was specified, we get here in the course of 2 distinct passes:
// - Once while reading backward simply to determine the appropriate *start position* for later forward reading, ignoring the content of the blocks read (in reverse).
// - Then again during forward reading, for regular output processing; it is only then that trimming the delimiter is necessary.
// (Trimming it during backward reading would not only be unnecessary, but could interfere with determining the correct start position.)
blocks.Add(
!readBackward && !delimiterNotFound
? _currentLineContent.ToString(0, _currentLineContent.Length - actualDelimiter.Length)
: _currentLineContent.ToString());
}
int peekResult = readBackward ? _backReader.Peek() : _reader.Peek();
if (peekResult != -1)
{
return true;
}
else
{
if (readBackward && _currentLineContent.Length > 0)
{
return true;
}
return false;
}
}
private bool ReadByteEncoded(bool waitChanges, List<object> blocks, bool readBackward)
{
if (_isRawStream)
{
// if RawSteam, read all bytes and return. When RawStream is used, we dont
// support -first, -last
byte[] bytes = new byte[_stream.Length];
int numBytesToRead = (int)_stream.Length;
int numBytesRead = 0;
while (numBytesToRead > 0)
{
// Read may return anything from 0 to numBytesToRead.
int n = _stream.Read(bytes, numBytesRead, numBytesToRead);
// Break when the end of the file is reached.
if (n == 0)
{
break;
}
numBytesRead += n;
numBytesToRead -= n;
}
if (numBytesRead == 0)
{
return false;
}
else
{
blocks.Add(bytes);
return true;
}
}
if (readBackward)
{
if (_stream.Position == 0)
{
return false;
}
_stream.Position--;
blocks.Add((byte)_stream.ReadByte());
_stream.Position--;
return true;
}
// Reading bytes not strings
int byteRead = _stream.ReadByte();
// We've found the end of the file.
if (byteRead == -1)
{
// If we want to tail the file, wait for
// the changes
if (waitChanges)
{
WaitForChanges(_path, _mode, _access, _share, ClrFacade.GetDefaultEncoding());
byteRead = _stream.ReadByte();
}
}
// Add the byte we read to the list of blocks
if (byteRead != -1)
{
blocks.Add((byte)byteRead);
return true;
}
else
{
return false;
}
}
private void CreateStreams(string filePath, string streamName, FileMode fileMode, FileAccess fileAccess, FileShare fileShare, Encoding fileEncoding)
{
// Try to mask off the ReadOnly, and Hidden attributes
// if they've specified Force.
if (File.Exists(filePath) && _provider.Force)
{
// Store the old attributes so that we can recover them
// in the Close();
_oldAttributes = File.GetAttributes(filePath);
_haveOldAttributes = true;
// Clear the hidden attribute, and if we're writing, also clear readonly.
var attributesToClear = FileAttributes.Hidden;
if ((fileAccess & (FileAccess.Write)) != 0)
{
attributesToClear |= FileAttributes.ReadOnly;
}
File.SetAttributes(_path, (File.GetAttributes(filePath) & ~attributesToClear));
}
// If we want to write to the stream, attempt to open it for reading as well
// so that we can determine the file encoding as we append to it
FileAccess requestedAccess = fileAccess;
if ((fileAccess & (FileAccess.Write)) != 0)
{
fileAccess = FileAccess.ReadWrite;
}
try
{
#if !UNIX
if (!string.IsNullOrEmpty(streamName))
{
_stream = AlternateDataStreamUtilities.CreateFileStream(filePath, streamName, fileMode, fileAccess, fileShare);
}
else
#endif
{
_stream = new FileStream(filePath, fileMode, fileAccess, fileShare);
}
}
catch (IOException)
{
#if !UNIX
if (!string.IsNullOrEmpty(streamName))
{
_stream = AlternateDataStreamUtilities.CreateFileStream(filePath, streamName, fileMode, requestedAccess, fileShare);
}
else
#endif
{
_stream = new FileStream(filePath, fileMode, requestedAccess, fileShare);
}
}
if (!_usingByteEncoding)
{
// Open the reader stream
if ((fileAccess & (FileAccess.Read)) != 0)
{
_reader = new StreamReader(_stream, fileEncoding);
_backReader = new FileStreamBackReader(_stream, fileEncoding);
}
// Open the writer stream
if ((fileAccess & (FileAccess.Write)) != 0)
{
// Ensure we are using the proper encoding
if ((_reader != null) &&
((fileAccess & (FileAccess.Read)) != 0))
{
_reader.Peek();
fileEncoding = _reader.CurrentEncoding;
}
_writer = new StreamWriter(_stream, fileEncoding);
}
}
}
/// <summary>
/// Waits for changes to the specified file. To do this, it closes the file
/// and then monitors for changes. Once a change appears, it reopens the streams
/// and seeks to the last read position.
/// </summary>
/// <param name="filePath">The path of the file to read / monitor.</param>
/// <param name="fileMode">The FileMode of the file (ie: Open / Append).</param>
/// <param name="fileAccess">The access properties of the file (ie: Read / Write).</param>
/// <param name="fileShare">The sharing properties of the file (ie: Read / ReadWrite).</param>
/// <param name="fileEncoding">The encoding of the file.</param>
private void WaitForChanges(string filePath, FileMode fileMode, FileAccess fileAccess, FileShare fileShare, Encoding fileEncoding)
{
// Close the old stream, and store our current position.
if (_stream != null)
{
_fileOffset = _stream.Position;
_stream.Dispose();
}
// Watch for changes, as a blocking call.
FileInfo watchFile = new FileInfo(filePath);
long originalLength = watchFile.Length;
using (FileSystemWatcher watcher = new FileSystemWatcher(watchFile.DirectoryName, watchFile.Name))
{
ErrorEventArgs errorEventArgs = null;
var tcs = new TaskCompletionSource<FileSystemEventArgs>();
FileSystemEventHandler onChangedHandler = (object source, FileSystemEventArgs e) => tcs.TrySetResult(e);
RenamedEventHandler onRenamedHandler = (object source, RenamedEventArgs e) => tcs.TrySetResult(e);
ErrorEventHandler onErrorHandler = (object source, ErrorEventArgs e) =>
{
errorEventArgs = e;
tcs.TrySetResult(new FileSystemEventArgs(WatcherChangeTypes.All, watchFile.DirectoryName, watchFile.Name));
};
// With WaitForChanged, we registered for all change types, so we do the same here.
watcher.Changed += onChangedHandler;
watcher.Created += onChangedHandler;
watcher.Deleted += onChangedHandler;
watcher.Renamed += onRenamedHandler;
watcher.Error += onErrorHandler;
try
{
watcher.EnableRaisingEvents = true;
while (!_provider.Stopping)
{
bool isTaskCompleted = tcs.Task.Wait(500);
if (errorEventArgs != null)
{
throw errorEventArgs.GetException();
}
if (isTaskCompleted)
break;
// If a process is still writing, .NET doesn't generate a change notification.
// So do a simple comparison on file size
watchFile.Refresh();
if (originalLength != watchFile.Length)
{
break;
}
}
}
finally
{
// Done here to guarantee that the handlers are removed prior
// to the call to ManualResetEvent's Dispose() method.
watcher.EnableRaisingEvents = false;
watcher.Changed -= onChangedHandler;
watcher.Created -= onChangedHandler;
watcher.Deleted -= onChangedHandler;
watcher.Renamed -= onRenamedHandler;
watcher.Error -= onErrorHandler;
}
}
// Let the change complete.
// This is a fairly arbitrary number. Without it, though,
// some of the filesystem streams cannot be reopened.
System.Threading.Thread.Sleep(100);
// Reopen the streams.
CreateStreams(filePath, null, fileMode, fileAccess, fileShare, fileEncoding);
// If the file has been shortened, restart from zero.
// That will let us catch log roll-overs.
if (_fileOffset > _stream.Length)
_fileOffset = 0;
// Seek to the place we last left off.
_stream.Seek(_fileOffset, SeekOrigin.Begin);
if (_reader != null) { _reader.DiscardBufferedData(); }
if (_backReader != null) { _backReader.DiscardBufferedData(); }
}
/// <summary>
/// Moves the current stream position in the file.
/// </summary>
/// <param name="offset">
/// The offset from the origin to move the position to.
/// </param>
/// <param name="origin">
/// The origin from which the offset is calculated.
/// </param>
public void Seek(long offset, SeekOrigin origin)
{
if (_writer != null) { _writer.Flush(); }
_stream.Seek(offset, origin);
if (_writer != null) { _writer.Flush(); }
if (_reader != null) { _reader.DiscardBufferedData(); }
if (_backReader != null) { _backReader.DiscardBufferedData(); }
}
/// <summary>
/// Closes the file.
/// </summary>
public void Close()
{
bool streamClosed = false;
if (_writer != null)
{
try
{
_writer.Flush();
_writer.Dispose();
}
finally
{
streamClosed = true;
}
}
if (_reader != null)
{
_reader.Dispose();
streamClosed = true;
}
if (_backReader != null)
{
_backReader.Dispose();
streamClosed = true;
}
if (!streamClosed)
{
_stream.Flush();
_stream.Dispose();
}
// Reset the attributes
if (_haveOldAttributes && _provider.Force)
{
File.SetAttributes(_path, _oldAttributes);
}
}
/// <summary>
/// Writes the specified object to the file.
/// </summary>
/// <param name="content">
/// The objects to write to the file
/// </param>
/// <returns>
/// The objects written to the file.
/// </returns>
public IList Write(IList content)
{
foreach (object line in content)
{
object[] contentArray = line as object[];
if (contentArray != null)
{
foreach (object obj in contentArray)
{
WriteObject(obj);
}
}
else
{
WriteObject(line);
}
}
return content;
}
private void WriteObject(object content)
{
if (content == null)
{
return;
}
if (_usingByteEncoding)
{
try
{
byte byteToWrite = (byte)content;
_stream.WriteByte(byteToWrite);
}
catch (InvalidCastException)
{
throw PSTraceSource.NewArgumentException(nameof(content), FileSystemProviderStrings.ByteEncodingError);
}
}
else
{
if (_suppressNewline)
{
_writer.Write(content.ToString());
}
else
{
_writer.WriteLine(content.ToString());
}
}
}
/// <summary>
/// Closes the file stream.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
internal void Dispose(bool isDisposing)
{
if (isDisposing)
{
if (_stream != null)
_stream.Dispose();
if (_reader != null)
_reader.Dispose();
if (_backReader != null)
_backReader.Dispose();
if (_writer != null)
_writer.Dispose();
}
}
}
internal sealed class FileStreamBackReader : StreamReader
{
internal FileStreamBackReader(FileStream fileStream, Encoding encoding)
: base(fileStream, encoding)
{
_stream = fileStream;
if (_stream.Length > 0)
{
long curPosition = _stream.Position;
_stream.Seek(0, SeekOrigin.Begin);
base.Peek();
_stream.Position = curPosition;
_currentEncoding = base.CurrentEncoding;
_currentPosition = _stream.Position;
// Get the oem encoding and system current ANSI code page
_oemEncoding = EncodingConversion.Convert(null, EncodingConversion.OEM);
_defaultAnsiEncoding = EncodingConversion.Convert(null, EncodingConversion.Default);
}
}
private readonly FileStream _stream;
private readonly Encoding _currentEncoding;
private readonly Encoding _oemEncoding;
private readonly Encoding _defaultAnsiEncoding;
private const int BuffSize = 4096;
private readonly byte[] _byteBuff = new byte[BuffSize];
private readonly char[] _charBuff = new char[BuffSize];
private int _byteCount = 0;
private int _charCount = 0;
private long _currentPosition = 0;
private bool? _singleByteCharSet = null;
private const byte BothTopBitsSet = 0xC0;
private const byte TopBitUnset = 0x80;
/// <summary>
/// If the given encoding is OEM or Default, check to see if the code page
/// is SBCS(single byte character set).
/// </summary>
/// <returns></returns>
private bool IsSingleByteCharacterSet()
{
if (_singleByteCharSet != null)
return (bool)_singleByteCharSet;
// Porting note: only UTF-8 is supported on Linux, which is not an SBCS
if ((_currentEncoding.Equals(_oemEncoding) ||
_currentEncoding.Equals(_defaultAnsiEncoding))
&& Platform.IsWindows)
{
NativeMethods.CPINFO cpInfo;
if (NativeMethods.GetCPInfo((uint)_currentEncoding.CodePage, out cpInfo) &&
cpInfo.MaxCharSize == 1)
{
_singleByteCharSet = true;
return true;
}
}
_singleByteCharSet = false;
return false;
}
/// <summary>
/// We don't support this method because it is not used by the ReadBackward method in FileStreamContentReaderWriter.
/// </summary>
/// <param name="buffer"></param>
/// <param name="index"></param>
/// <param name="count"></param>
/// <returns></returns>
public override int ReadBlock(char[] buffer, int index, int count)
{
// This method is not supposed to be used
throw PSTraceSource.NewNotSupportedException();
}
/// <summary>
/// We don't support this method because it is not used by the ReadBackward method in FileStreamContentReaderWriter.
/// </summary>
/// <returns></returns>
public override string ReadToEnd()
{
// This method is not supposed to be used
throw PSTraceSource.NewNotSupportedException();
}
/// <summary>
/// Reset the internal character buffer. Use it only when the position of the internal buffer and
/// the base stream do not match. These positions can become mismatch when the user read the data
/// into the buffer and then seek a new position in the underlying stream.
/// </summary>
internal new void DiscardBufferedData()
{
base.DiscardBufferedData();
_currentPosition = _stream.Position;
_charCount = 0;
_byteCount = 0;
}
/// <summary>
/// Return the current actual stream position.
/// </summary>
/// <returns></returns>
internal long GetCurrentPosition()
{
if (_charCount == 0)
return _currentPosition;
// _charCount > 0
int byteCount = _currentEncoding.GetByteCount(_charBuff, 0, _charCount);
return (_currentPosition + byteCount);
}
/// <summary>
/// Get the number of bytes the delimiter will
/// be encoded to.
/// </summary>
/// <param name="delimiter"></param>
/// <returns></returns>
internal int GetByteCount(string delimiter)
{
char[] chars = delimiter.ToCharArray();
return _currentEncoding.GetByteCount(chars, 0, chars.Length);
}
/// <summary>
/// Peek the next character.
/// </summary>
/// <returns>Return -1 if we reach the head of the file.</returns>
public override int Peek()
{
if (_charCount == 0)
{
if (RefillCharBuffer() == -1)
{
return -1;
}
}
// Return the next available character, but DONT consume it (don't advance the _charCount)
return (int)_charBuff[_charCount - 1];
}
/// <summary>
/// Read the next character.
/// </summary>
/// <returns>Return -1 if we reach the head of the file.</returns>
public override int Read()
{
if (_charCount == 0)
{
if (RefillCharBuffer() == -1)
{
return -1;
}
}
_charCount--;
return _charBuff[_charCount];
}
/// <summary>
/// Read a specific maximum of characters from the current stream into a buffer.
/// </summary>
/// <param name="buffer">Output buffer.</param>
/// <param name="index">Start position to write with.</param>
/// <param name="count">Number of bytes to read.</param>
/// <returns>Return the number of characters read, or -1 if we reach the head of the file.</returns>
/// <returns>Return the number of characters read, or -1 if we reach the head of the file.</returns>
public override int Read(char[] buffer, int index, int count)
{
return ReadSpan(new Span<char>(buffer, index, count));
}
/// <summary>
/// Read characters from the current stream into a Span buffer.
/// </summary>
/// <param name="buffer">Output buffer.</param>
/// <returns>Return the number of characters read, or -1 if we reach the head of the file.</returns>
public override int Read(Span<char> buffer)
{
return ReadSpan(buffer);
}
private int ReadSpan(Span<char> buffer)
{
// deal with the argument validation
int charRead = 0;
int index = 0;
int count = buffer.Length;
do
{
if (_charCount == 0)
{
if (RefillCharBuffer() == -1)
{
return charRead;
}
}
int toRead = _charCount > count ? count : _charCount;
for (; toRead > 0; toRead--, count--, charRead++)
{
buffer[index++] = _charBuff[--_charCount];
}
}
while (count > 0);
return charRead;
}
/// <summary>
/// Read a line from the current stream.
/// </summary>
/// <returns>Return null if we reach the head of the file.</returns>
public override string ReadLine()
{
if (_charCount == 0 && RefillCharBuffer() == -1)
{
return null;
}
int charsToRemove = 0;
StringBuilder line = new StringBuilder();
if (_charBuff[_charCount - 1] == '\r' ||
_charBuff[_charCount - 1] == '\n')
{
charsToRemove++;
line.Insert(0, _charBuff[--_charCount]);
if (_charBuff[_charCount] == '\n')
{
if (_charCount == 0 && RefillCharBuffer() == -1)
{
return string.Empty;
}
if (_charCount > 0 && _charBuff[_charCount - 1] == '\r')
{
charsToRemove++;
line.Insert(0, _charBuff[--_charCount]);
}
}
}
while (true)
{
while (_charCount > 0)
{
if (_charBuff[_charCount - 1] == '\r' ||
_charBuff[_charCount - 1] == '\n')
{
line.Remove(line.Length - charsToRemove, charsToRemove);
return line.ToString();
}
else
{
line.Insert(0, _charBuff[--_charCount]);
}
}
if (RefillCharBuffer() == -1)
{
line.Remove(line.Length - charsToRemove, charsToRemove);
return line.ToString();
}
}
}
/// <summary>
/// Refill the internal character buffer.
/// </summary>
/// <returns></returns>
private int RefillCharBuffer()
{
if ((RefillByteBuff()) == -1)
{
return -1;
}
_charCount = _currentEncoding.GetChars(_byteBuff, 0, _byteCount, _charBuff, 0);
return _charCount;
}
/// <summary>
/// Refill the internal byte buffer.
/// </summary>
/// <returns></returns>
private int RefillByteBuff()
{
long lengthLeft = _stream.Position;
if (lengthLeft == 0)
{
return -1;
}
int toRead = lengthLeft > BuffSize ? BuffSize : (int)lengthLeft;
_stream.Seek(-toRead, SeekOrigin.Current);
if (_currentEncoding is UTF8Encoding)
{
// It's UTF-8, we need to detect the starting byte of a character
do
{
_currentPosition = _stream.Position;
byte curByte = (byte)_stream.ReadByte();
if ((curByte & BothTopBitsSet) == BothTopBitsSet ||
(curByte & TopBitUnset) == 0x00)
{
_byteBuff[0] = curByte;
_byteCount = 1;
break;
}
} while (lengthLeft > _stream.Position);
if (lengthLeft == _stream.Position)
{
// Cannot find a starting byte. The file is NOT UTF-8 format. Read 'toRead' number of bytes
_stream.Seek(-toRead, SeekOrigin.Current);
_byteCount = 0;
}
_byteCount += _stream.Read(_byteBuff, _byteCount, (int)(lengthLeft - _stream.Position));
_stream.Position = _currentPosition;
}
else if (_currentEncoding is UnicodeEncoding ||
_currentEncoding is UTF32Encoding ||
_currentEncoding is ASCIIEncoding ||
IsSingleByteCharacterSet())
{
// Unicode -- two bytes per character
// UTF-32 -- four bytes per character
// ASCII -- one byte per character
// The BufferSize will be a multiple of 4, so we can just read toRead number of bytes
// if the current file is encoded by any of these formatting
// If IsSingleByteCharacterSet() returns true, we are sure that the given encoding is OEM
// or Default, and it is SBCS(single byte character set) code page -- one byte per character
_currentPosition = _stream.Position;
_byteCount = _stream.Read(_byteBuff, 0, toRead);
_stream.Position = _currentPosition;
}
else
{
// OEM and ANSI code pages include multibyte CJK code pages. If the current code page
// is MBCS(multibyte character set), we cannot detect a starting byte.
// UTF-7 has some characters encoded into UTF-16 and then in Modified Base64,
// the start of these characters is indicated by a '+' sign, and the end is
// indicated by a character that is not in Modified Base64 set.
// For these encodings, we cannot detect a starting byte with confidence when
// reading bytes backward. Throw out exception in these cases.
string errMsg = StringUtil.Format(
FileSystemProviderStrings.ReadBackward_Encoding_NotSupport,
_currentEncoding.EncodingName);
throw new BackReaderEncodingNotSupportedException(errMsg, _currentEncoding.EncodingName);
}
return _byteCount;
}
private static class NativeMethods
{
// Default values
private const int MAX_DEFAULTCHAR = 2;
private const int MAX_LEADBYTES = 12;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct CPINFO
{
[MarshalAs(UnmanagedType.U4)]
internal int MaxCharSize;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_DEFAULTCHAR)]
public byte[] DefaultChar;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_LEADBYTES)]
public byte[] LeadBytes;
}
/// <summary>
/// Get information on a named code page.
/// </summary>
/// <param name="codePage"></param>
/// <param name="lpCpInfo"></param>
/// <returns></returns>
[DllImport(PinvokeDllNames.GetCPInfoDllName, CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetCPInfo(uint codePage, out CPINFO lpCpInfo);
}
}
/// <summary>
/// The exception that indicates the encoding is not supported when reading backward.
/// </summary>
[SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Justification = "This exception is internal and never thrown by any public API")]
[SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors", Justification = "This exception is internal and never thrown by any public API")]
internal sealed class BackReaderEncodingNotSupportedException : NotSupportedException
{
internal BackReaderEncodingNotSupportedException(string message, string encodingName)
: base(message)
{
EncodingName = encodingName;
}
internal BackReaderEncodingNotSupportedException(string encodingName)
{
EncodingName = encodingName;
}
/// <summary>
/// Get the encoding name.
/// </summary>
internal string EncodingName { get; }
}
}