1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using Microsoft.Win32.SafeHandles; 6 using System.Collections.Generic; 7 using System.IO.Pipes; 8 using System.Runtime.InteropServices; 9 using System.Threading; 10 using System.Threading.Tasks; 11 using Xunit; 12 13 namespace System.IO.Tests 14 { 15 public class FileStream_CopyToAsync : FileSystemTest 16 { 17 [Theory] 18 [InlineData(false)] 19 [InlineData(true)] InvalidArgs_Throws(bool useAsync)20 public void InvalidArgs_Throws(bool useAsync) 21 { 22 using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync)) 23 { 24 AssertExtensions.Throws<ArgumentNullException>("destination", () => { fs.CopyToAsync(null); }); 25 AssertExtensions.Throws<ArgumentOutOfRangeException>("bufferSize", () => { fs.CopyToAsync(new MemoryStream(), 0); }); 26 Assert.Throws<NotSupportedException>(() => { fs.CopyToAsync(new MemoryStream(new byte[1], writable: false)); }); 27 fs.Dispose(); 28 Assert.Throws<ObjectDisposedException>(() => { fs.CopyToAsync(new MemoryStream()); }); 29 } 30 using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.Write)) 31 { 32 Assert.Throws<NotSupportedException>(() => { fs.CopyToAsync(new MemoryStream()); }); 33 } 34 using (FileStream src = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync)) 35 using (FileStream dst = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync)) 36 { 37 dst.Dispose(); 38 Assert.Throws<ObjectDisposedException>(() => { src.CopyToAsync(dst); }); 39 } 40 } 41 42 [Theory] 43 [InlineData(false)] 44 [InlineData(true)] 45 [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "The Stream CopyToAsync fails on netcoreapp because it calls Length which checks the validity of the underlying handle. On NetFX the operation no-ops for no input or delays failure to execution for input. See /dotnet/coreclr/pull/4540.")] DisposeHandleThenUseFileStream_CopyToAsync(bool useAsync)46 public void DisposeHandleThenUseFileStream_CopyToAsync(bool useAsync) 47 { 48 using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync)) 49 { 50 fs.SafeFileHandle.Dispose(); 51 Assert.Throws<ObjectDisposedException>(() => { fs.CopyToAsync(new MemoryStream()); }); 52 } 53 54 using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync)) 55 { 56 fs.Write(TestBuffer, 0, TestBuffer.Length); 57 fs.SafeFileHandle.Dispose(); 58 Assert.Throws<ObjectDisposedException>(() => { fs.CopyToAsync(new MemoryStream()).Wait(); }); 59 } 60 } 61 62 [Theory] 63 [InlineData(false)] 64 [InlineData(true)] 65 [SkipOnTargetFramework(~TargetFrameworkMonikers.NetFramework, "The Stream CopyToAsync fails on netcoreapp because it calls Length which checks the validity of the underlying handle. On NetFX the operation no-ops for no input or delays failure to execution for input. See /dotnet/coreclr/pull/4540.")] DisposeHandleThenUseFileStream_CopyToAsync_netfx(bool useAsync)66 public void DisposeHandleThenUseFileStream_CopyToAsync_netfx(bool useAsync) 67 { 68 using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync)) 69 { 70 fs.SafeFileHandle.Dispose(); 71 fs.CopyToAsync(new MemoryStream()); 72 } 73 74 using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync)) 75 { 76 fs.Write(TestBuffer, 0, TestBuffer.Length); 77 fs.SafeFileHandle.Dispose(); 78 try 79 { 80 fs.CopyToAsync(new MemoryStream()).Wait(); 81 } 82 catch (AggregateException e) 83 { 84 Assert.Equal(typeof(ObjectDisposedException), e.InnerException.GetType()); 85 } 86 } 87 } 88 89 [Theory] 90 [InlineData(false)] 91 [InlineData(true)] AlreadyCanceled_ReturnsCanceledTask(bool useAsync)92 public async Task AlreadyCanceled_ReturnsCanceledTask(bool useAsync) 93 { 94 using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, 0x100, useAsync)) 95 { 96 await Assert.ThrowsAnyAsync<OperationCanceledException>(() => fs.CopyToAsync(fs, 0x1000, new CancellationToken(canceled: true))); 97 } 98 } 99 100 [Theory] // inner loop, just a few cases 101 [InlineData(false, false)] 102 [InlineData(true, false)] 103 [InlineData(true, true)] File_AllDataCopied_InnerLoop(bool useAsync, bool preWrite)104 public Task File_AllDataCopied_InnerLoop(bool useAsync, bool preWrite) 105 { 106 return File_AllDataCopied( 107 _ => new MemoryStream(), useAsync, preRead: false, preWrite: preWrite, exposeHandle: false, cancelable: true, 108 bufferSize: 4096, writeSize: 1024, numWrites: 10); 109 } 110 111 [Theory] // outer loop, many combinations 112 [OuterLoop] 113 [MemberData(nameof(File_AllDataCopied_MemberData))] File_AllDataCopied( Func<string, Stream> createDestinationStream, bool useAsync, bool preRead, bool preWrite, bool exposeHandle, bool cancelable, int bufferSize, int writeSize, int numWrites)114 public async Task File_AllDataCopied( 115 Func<string, Stream> createDestinationStream, 116 bool useAsync, bool preRead, bool preWrite, bool exposeHandle, bool cancelable, 117 int bufferSize, int writeSize, int numWrites) 118 { 119 // Create the expected data 120 long totalLength = writeSize * numWrites; 121 var expectedData = new byte[totalLength]; 122 new Random(42).NextBytes(expectedData); 123 124 // Write it out into the source file 125 string srcPath = GetTestFilePath(); 126 File.WriteAllBytes(srcPath, expectedData); 127 128 string dstPath = GetTestFilePath(); 129 using (FileStream src = new FileStream(srcPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None, bufferSize, useAsync)) 130 using (Stream dst = createDestinationStream(dstPath)) 131 { 132 // If configured to expose the handle, do so. This influences the stream's need to ensure the position is in sync. 133 if (exposeHandle) 134 { 135 var ignored = src.SafeFileHandle; 136 } 137 138 // If configured to "preWrite", do a write before we start reading. 139 if (preWrite) 140 { 141 src.Write(new byte[] { 42 }, 0, 1); 142 dst.Write(new byte[] { 42 }, 0, 1); 143 expectedData[0] = 42; 144 } 145 146 // If configured to "preRead", read one byte from the source prior to the CopyToAsync. 147 // This helps test what happens when there's already data in the buffer, when the position 148 // isn't starting at zero, etc. 149 if (preRead) 150 { 151 int initialByte = src.ReadByte(); 152 if (initialByte >= 0) 153 { 154 dst.WriteByte((byte)initialByte); 155 } 156 } 157 158 // Do the copy 159 await src.CopyToAsync(dst, writeSize, cancelable ? new CancellationTokenSource().Token : CancellationToken.None); 160 dst.Flush(); 161 162 // Make sure we're at the end of the source file 163 Assert.Equal(src.Length, src.Position); 164 165 // Verify the copied data 166 dst.Position = 0; 167 var result = new MemoryStream(); 168 dst.CopyTo(result); 169 byte[] actualData = result.ToArray(); 170 Assert.Equal(expectedData.Length, actualData.Length); 171 Assert.Equal<byte>(expectedData, actualData); 172 } 173 } 174 File_AllDataCopied_MemberData()175 public static IEnumerable<object[]> File_AllDataCopied_MemberData() 176 { 177 bool[] bools = new[] { true, false }; 178 foreach (bool useAsync in bools) // sync or async mode 179 { 180 foreach (bool preRead in bools) // whether to do a read before the CopyToAsync 181 { 182 foreach (bool cancelable in bools) // whether to use a cancelable token 183 { 184 for (int streamType = 0; streamType < 2; streamType++) // kind of stream to use 185 { 186 Func<string, Stream> createDestinationStream; 187 switch (streamType) 188 { 189 case 0: createDestinationStream = _ => new MemoryStream(); break; 190 default: createDestinationStream = s => File.Create(s); break; 191 } 192 193 // Various exposeHandle (whether the SafeFileHandle was publicly accessed), 194 // preWrite, bufferSize, writeSize, and numWrites combinations 195 yield return new object[] { createDestinationStream, useAsync, preRead, false, false, cancelable, 0x1000, 0x100, 100 }; 196 yield return new object[] { createDestinationStream, useAsync, preRead, false, false, cancelable, 0x1, 0x1, 1000 }; 197 yield return new object[] { createDestinationStream, useAsync, preRead, false, true, cancelable, 0x2, 0x100, 100 }; 198 yield return new object[] { createDestinationStream, useAsync, preRead, false, false, cancelable, 0x4000, 0x10, 100 }; 199 yield return new object[] { createDestinationStream, useAsync, preRead, false, true, cancelable, 0x1000, 99999, 10 }; 200 } 201 } 202 } 203 } 204 } 205 206 [Theory] 207 [InlineData(10, 1024)] AnonymousPipeViaFileStream_AllDataCopied(int writeSize, int numWrites)208 public async Task AnonymousPipeViaFileStream_AllDataCopied(int writeSize, int numWrites) 209 { 210 long totalLength = writeSize * numWrites; 211 var expectedData = new byte[totalLength]; 212 new Random(42).NextBytes(expectedData); 213 214 var results = new MemoryStream(); 215 216 using (var server = new AnonymousPipeServerStream(PipeDirection.Out)) 217 { 218 Task serverTask = Task.Run(async () => 219 { 220 for (int i = 0; i < numWrites; i++) 221 { 222 await server.WriteAsync(expectedData, i * writeSize, writeSize); 223 } 224 }); 225 226 using (var client = new FileStream(new SafeFileHandle(server.ClientSafePipeHandle.DangerousGetHandle(), false), FileAccess.Read, bufferSize: 3)) 227 { 228 Task copyTask = client.CopyToAsync(results, writeSize); 229 await await Task.WhenAny(serverTask, copyTask); 230 231 server.Dispose(); 232 await copyTask; 233 } 234 } 235 236 byte[] actualData = results.ToArray(); 237 Assert.Equal(expectedData.Length, actualData.Length); 238 Assert.Equal<byte>(expectedData, actualData); 239 } 240 241 [PlatformSpecific(TestPlatforms.Windows)] // Uses P/Invokes to create async pipe handle 242 [Theory] 243 [InlineData(false, 10, 1024)] 244 [InlineData(true, 10, 1024)] 245 [ActiveIssue(22271, TargetFrameworkMonikers.UapNotUapAot)] NamedPipeViaFileStream_AllDataCopied(bool useAsync, int writeSize, int numWrites)246 public async Task NamedPipeViaFileStream_AllDataCopied(bool useAsync, int writeSize, int numWrites) 247 { 248 long totalLength = writeSize * numWrites; 249 var expectedData = new byte[totalLength]; 250 new Random(42).NextBytes(expectedData); 251 252 var results = new MemoryStream(); 253 var pipeOptions = useAsync ? PipeOptions.Asynchronous : PipeOptions.None; 254 255 string name = GetNamedPipeServerStreamName(); 256 using (var server = new NamedPipeServerStream(name, PipeDirection.Out, 1, PipeTransmissionMode.Byte, pipeOptions)) 257 { 258 Task serverTask = Task.Run(async () => 259 { 260 await server.WaitForConnectionAsync(); 261 for (int i = 0; i < numWrites; i++) 262 { 263 await server.WriteAsync(expectedData, i * writeSize, writeSize); 264 } 265 server.Dispose(); 266 }); 267 268 Assert.True(WaitNamedPipeW(@"\\.\pipe\" + name, -1)); 269 using (SafeFileHandle clientHandle = CreateFileW(@"\\.\pipe\" + name, GENERIC_READ, FileShare.None, IntPtr.Zero, FileMode.Open, (int)pipeOptions, IntPtr.Zero)) 270 using (var client = new FileStream(clientHandle, FileAccess.Read, bufferSize: 3, isAsync: useAsync)) 271 { 272 Task copyTask = client.CopyToAsync(results, (int)totalLength); 273 await await Task.WhenAny(serverTask, copyTask); 274 await copyTask; 275 } 276 } 277 278 byte[] actualData = results.ToArray(); 279 Assert.Equal(expectedData.Length, actualData.Length); 280 Assert.Equal<byte>(expectedData, actualData); 281 } 282 283 [PlatformSpecific(TestPlatforms.Windows)] // Uses P/Invokes to create async pipe handle 284 [Theory] NamedPipeViaFileStream_CancellationRequested_OperationCanceled()285 public async Task NamedPipeViaFileStream_CancellationRequested_OperationCanceled() 286 { 287 string name = Guid.NewGuid().ToString("N"); 288 using (var server = new NamedPipeServerStream(name, PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous)) 289 { 290 Task serverTask = server.WaitForConnectionAsync(); 291 292 Assert.True(WaitNamedPipeW(@"\\.\pipe\" + name, -1)); 293 using (SafeFileHandle clientHandle = CreateFileW(@"\\.\pipe\" + name, GENERIC_READ, FileShare.None, IntPtr.Zero, FileMode.Open, (int)PipeOptions.Asynchronous, IntPtr.Zero)) 294 using (var client = new FileStream(clientHandle, FileAccess.Read, bufferSize: 3, isAsync: true)) 295 { 296 await serverTask; 297 298 var cts = new CancellationTokenSource(); 299 Task clientTask = client.CopyToAsync(new MemoryStream(), 0x1000, cts.Token); 300 Assert.False(clientTask.IsCompleted); 301 302 cts.Cancel(); 303 await Assert.ThrowsAsync<OperationCanceledException>(() => clientTask); 304 } 305 } 306 } 307 308 [Theory] 309 [InlineData(false)] 310 [InlineData(true)] DerivedFileStream_ReadAsyncInvoked(bool useAsync)311 public async Task DerivedFileStream_ReadAsyncInvoked(bool useAsync) 312 { 313 var expectedData = new byte[100]; 314 new Random(42).NextBytes(expectedData); 315 316 string srcPath = GetTestFilePath(); 317 File.WriteAllBytes(srcPath, expectedData); 318 319 bool readAsyncInvoked = false; 320 using (var fs = new FileStreamThatOverridesReadAsync(srcPath, useAsync, () => readAsyncInvoked = true)) 321 { 322 await fs.CopyToAsync(new MemoryStream()); 323 Assert.True(readAsyncInvoked); 324 } 325 } 326 327 private class FileStreamThatOverridesReadAsync : FileStream 328 { 329 private readonly Action _readAsyncInvoked; 330 FileStreamThatOverridesReadAsync(string path, bool useAsync, Action readAsyncInvoked)331 internal FileStreamThatOverridesReadAsync(string path, bool useAsync, Action readAsyncInvoked) : 332 base(path, FileMode.Open, FileAccess.Read, FileShare.Read, 0x1000, useAsync) 333 { 334 _readAsyncInvoked = readAsyncInvoked; 335 } 336 ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)337 public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 338 { 339 _readAsyncInvoked(); 340 return base.ReadAsync(buffer, offset, count, cancellationToken); 341 } 342 } 343 344 #region Windows P/Invokes 345 // We need to P/Invoke to test the named pipe async behavior with FileStream 346 // because NamedPipeClientStream internally binds the created handle, 347 // and that then prevents FileStream's constructor from working with the handle 348 // when trying to set isAsync to true. 349 350 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] 351 [return: MarshalAs(UnmanagedType.Bool)] WaitNamedPipeW(string name, int timeout)352 public static extern bool WaitNamedPipeW(string name, int timeout); 353 354 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] CreateFileW( string lpFileName, int dwDesiredAccess, FileShare dwShareMode, IntPtr securityAttrs, FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile)355 internal static extern SafeFileHandle CreateFileW( 356 string lpFileName, int dwDesiredAccess, FileShare dwShareMode, 357 IntPtr securityAttrs, FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile); 358 359 internal const int GENERIC_READ = unchecked((int)0x80000000); 360 #endregion 361 } 362 } 363