PageRenderTime 52ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/src/CFNetwork/WebResponseStream.cs

https://github.com/kjpou1/maccore
C# | 699 lines | 487 code | 109 blank | 103 comment | 73 complexity | 565ad39516e8bb0389062b6699cbdac1 MD5 | raw file
Possible License(s): Apache-2.0
  1. //
  2. // MonoMac.CFNetwork.WebResponseStream
  3. //
  4. // Authors:
  5. // Martin Baulig (martin.baulig@gmail.com)
  6. //
  7. // Copyright 2012 Xamarin Inc. (http://www.xamarin.com)
  8. //
  9. //
  10. // Permission is hereby granted, free of charge, to any person obtaining
  11. // a copy of this software and associated documentation files (the
  12. // "Software"), to deal in the Software without restriction, including
  13. // without limitation the rights to use, copy, modify, merge, publish,
  14. // distribute, sublicense, and/or sell copies of the Software, and to
  15. // permit persons to whom the Software is furnished to do so, subject to
  16. // the following conditions:
  17. //
  18. // The above copyright notice and this permission notice shall be
  19. // included in all copies or substantial portions of the Software.
  20. //
  21. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  22. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  23. // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  24. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  25. // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  26. // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  27. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  28. //
  29. using System;
  30. using System.IO;
  31. using System.Net.Http;
  32. using System.Threading;
  33. using System.Threading.Tasks;
  34. using MonoMac.CoreFoundation;
  35. using MonoMac.CoreServices;
  36. using MonoMac.Foundation;
  37. using MonoMac.CFNetwork;
  38. namespace MonoMac.CFNetwork {
  39. /*
  40. * For optimal performance and reliability, either only access the
  41. * public System.IO.Stream methods from the application's main thread
  42. * or schedule the MessageHandler on a WorkerThread.
  43. *
  44. * If is permitted to use the public Stream methods from a ThreadPool
  45. * thread, though this scenario hasn't been very carefully tested for
  46. * race conditions yet.
  47. */
  48. class WebResponseStream : Stream, IDisposable {
  49. CFHTTPStream stream;
  50. WorkerThread worker;
  51. WebRequestStream body;
  52. CancellationTokenSource openCts;
  53. TaskCompletionSource<CFHTTPMessage> openTcs;
  54. IOperation currentOperation;
  55. bool bytesAvailable;
  56. bool busy;
  57. Thread mainThread;
  58. Thread workerThread;
  59. volatile bool crossThreadAccess;
  60. object syncRoot;
  61. bool open;
  62. bool canceled;
  63. bool completed;
  64. Exception lastError;
  65. WebResponseStream (CFHTTPStream stream, WebRequestStream body)
  66. {
  67. this.stream = stream;
  68. this.body = body;
  69. syncRoot = new object ();
  70. }
  71. public static WebResponseStream Create (CFHTTPMessage request)
  72. {
  73. var stream = CFStream.CreateForHTTPRequest (request);
  74. if (stream == null)
  75. return null;
  76. return new WebResponseStream (stream, null);
  77. }
  78. public static WebResponseStream Create (CFHTTPMessage request, WebRequestStream body)
  79. {
  80. var stream = CFStream.CreateForStreamedHTTPRequest (request, body.ReadStream);
  81. if (stream == null)
  82. return null;
  83. return new WebResponseStream (stream, body);
  84. }
  85. public static WebResponseStream Create (Uri uri, HttpMethod method, Version version)
  86. {
  87. using (var req = CFHTTPMessage.CreateRequest (uri, method.Method, version))
  88. return Create (req);
  89. }
  90. public CFHTTPStream Stream {
  91. get { return stream; }
  92. }
  93. ~WebResponseStream ()
  94. {
  95. Dispose (false);
  96. }
  97. protected override void Dispose (bool disposing)
  98. {
  99. if (disposing) {
  100. OnCanceled ();
  101. if (stream != null) {
  102. stream.Dispose ();
  103. stream = null;
  104. }
  105. if (openCts != null) {
  106. openCts.Dispose ();
  107. openCts = null;
  108. }
  109. }
  110. base.Dispose (disposing);
  111. }
  112. void OnError (Exception error)
  113. {
  114. if (error == null)
  115. error = new InvalidOperationException ("Unknown error.");
  116. if (completed)
  117. return;
  118. lastError = error;
  119. completed = true;
  120. stream.Close ();
  121. if (!open)
  122. openTcs.SetException (error);
  123. var operation = Interlocked.Exchange (ref currentOperation, null);
  124. if (operation != null)
  125. operation.SetException (error);
  126. }
  127. void OnCanceled ()
  128. {
  129. if (completed)
  130. return;
  131. completed = canceled = true;
  132. stream.Close ();
  133. if (!open)
  134. openTcs.SetCanceled ();
  135. var operation = Interlocked.Exchange (ref currentOperation, null);
  136. if (operation != null)
  137. operation.SetCanceled ();
  138. }
  139. void OnCompleted ()
  140. {
  141. if (completed)
  142. return;
  143. completed = true;
  144. stream.Close ();
  145. if (!open) {
  146. openTcs.SetException (new InvalidOperationException ());
  147. return;
  148. }
  149. var operation = Interlocked.Exchange (ref currentOperation, null);
  150. if (operation != null)
  151. operation.SetCompleted ();
  152. }
  153. /*
  154. * Under normal circumstances, we're running on the main thread,
  155. * so we could do without all the locking.
  156. *
  157. * However, we're exposing a System.IO.Stream to the user, who
  158. * might be using ConfigureAwait (false).
  159. *
  160. * Ideally, consumers of the API should either only access the
  161. * stream from the main thread or use a 'WorkerThread'.
  162. *
  163. */
  164. void StartOperation (IOperation operation)
  165. {
  166. bool isCrossThread;
  167. lock (syncRoot) {
  168. if (!open || (currentOperation != null))
  169. throw new InvalidOperationException ();
  170. if (canceled) {
  171. operation.SetCanceled ();
  172. return;
  173. }
  174. if (lastError != null) {
  175. operation.SetException (lastError);
  176. return;
  177. }
  178. if (completed) {
  179. operation.SetCompleted ();
  180. return;
  181. }
  182. currentOperation = operation;
  183. isCrossThread = CheckCrossThreadAccess ();
  184. if (!bytesAvailable)
  185. return;
  186. }
  187. /*
  188. * The server already sent us the OnBytesAvailable() event
  189. * before the operation started.
  190. *
  191. * If we have a worker thread, we simply handle it there and
  192. * don't have to worry about any locking or anything.
  193. *
  194. */
  195. if ((worker != null) && !Thread.CurrentThread.Equals (workerThread)) {
  196. worker.Post (() => {
  197. if (bytesAvailable)
  198. OnBytesAvailable (false);
  199. }
  200. );
  201. return;
  202. }
  203. /*
  204. * We're on the main / worker thread, so we don't need any locking.
  205. */
  206. if (!isCrossThread) {
  207. OnBytesAvailable (false);
  208. return;
  209. }
  210. /*
  211. * Ok, now it's getting complicated: we're neither on the main nor on
  212. * the worker thread, so we need to do some locking here.
  213. *
  214. */
  215. Monitor.Enter (syncRoot);
  216. if (!bytesAvailable) {
  217. Monitor.Exit (syncRoot);
  218. return;
  219. }
  220. OnBytesAvailable (true);
  221. }
  222. bool CheckCrossThreadAccess ()
  223. {
  224. if (crossThreadAccess)
  225. return true;
  226. if (Thread.CurrentThread.Equals (mainThread))
  227. return false;
  228. if (Thread.CurrentThread.Equals (workerThread))
  229. return false;
  230. crossThreadAccess = true;
  231. return true;
  232. }
  233. void HasBytesAvailable ()
  234. {
  235. /*
  236. * We're always on the main / worker thread here.
  237. *
  238. * As long as nobody accesses the Stream API from another thread,
  239. * we don't need any of the locking.
  240. *
  241. */
  242. if (!crossThreadAccess) {
  243. if ((currentOperation == null) || busy) {
  244. bytesAvailable = true;
  245. return;
  246. }
  247. if (!crossThreadAccess) {
  248. OnBytesAvailable (false);
  249. return;
  250. }
  251. }
  252. /*
  253. * Acquire and keep the lock until OnBytesAvailable()
  254. * releases it.
  255. */
  256. Monitor.Enter (syncRoot);
  257. if ((currentOperation == null) || busy) {
  258. bytesAvailable = true;
  259. Monitor.Exit (syncRoot);
  260. return;
  261. }
  262. OnBytesAvailable (true);
  263. }
  264. async Task OnBytesAvailable (bool exitContext)
  265. {
  266. bool keepGoing;
  267. do {
  268. bytesAvailable = false;
  269. try {
  270. keepGoing = await ReadFromServer (exitContext);
  271. } catch (Exception ex) {
  272. OnError (ex);
  273. break;
  274. }
  275. /*
  276. * 'bytesAvailable' is true here if the server sent us another
  277. * OnBytesAvailable event while we were sending the data to
  278. * the client.
  279. *
  280. */
  281. } while (bytesAvailable && keepGoing);
  282. if (exitContext)
  283. Monitor.Exit (syncRoot);
  284. }
  285. async Task<bool> ReadFromServer (bool exitContext)
  286. {
  287. int index, count;
  288. var buffer = currentOperation.GetBuffer (out index, out count);
  289. int ret;
  290. try {
  291. ret = stream.Read (buffer, index, count);
  292. } catch (Exception ex) {
  293. OnError (ex);
  294. return false;
  295. }
  296. /*
  297. * If there are still bytes available to be read, then we'll immediately
  298. * get another BytesAvailable event, whereas calling stream.Read() again
  299. * could block.
  300. */
  301. if (ret < 0) {
  302. OnError (stream.GetError ());
  303. return false;
  304. } else if (ret == 0) {
  305. OnCompleted ();
  306. return false;
  307. }
  308. /*
  309. * We're normally called from the CFReadStream's OnBytesAvailableEvent
  310. * on the main thread, though OperationStarted() may also call us from
  311. * a ThreadPool thread.
  312. *
  313. * Release the lock while we're writing the data and re-acquire it when
  314. * done with that. The server may send us a OnBytesAvailableEvent while
  315. * we're await'ing - if that happens, 'onBytesAvailable' will be set.
  316. */
  317. busy = true;
  318. if (exitContext)
  319. Monitor.Exit (syncRoot);
  320. bool keepGoing;
  321. try {
  322. keepGoing = await currentOperation.Write (ret);
  323. } finally {
  324. if (exitContext)
  325. Monitor.Enter (syncRoot);
  326. busy = false;
  327. }
  328. /*
  329. * 'keepGoing' specifies whether the client wants more data from us.
  330. */
  331. if (keepGoing)
  332. return true;
  333. var operation = Interlocked.Exchange (ref currentOperation, null);
  334. operation.SetCompleted ();
  335. return false;
  336. }
  337. interface IOperation : IDisposable {
  338. bool IsCompleted {
  339. get;
  340. }
  341. void SetCompleted ();
  342. void SetCanceled ();
  343. void SetException (Exception error);
  344. byte[] GetBuffer (out int index, out int count);
  345. Task<bool> Write (int count);
  346. }
  347. abstract class Operation<T> : IOperation, IDisposable {
  348. CancellationTokenSource cts;
  349. TaskCompletionSource<T> tcs;
  350. bool completed;
  351. protected Operation (WebResponseStream parent,
  352. CancellationToken cancellationToken)
  353. {
  354. cts = CancellationTokenSource.CreateLinkedTokenSource (
  355. cancellationToken);
  356. cts.Token.Register (() => parent.OnCanceled ());
  357. tcs = new TaskCompletionSource<T> ();
  358. }
  359. public Task<T> Task {
  360. get { return tcs.Task; }
  361. }
  362. public bool IsCompleted {
  363. get { return completed; }
  364. }
  365. protected TaskCompletionSource<T> TaskCompletionSource {
  366. get { return tcs; }
  367. }
  368. protected CancellationToken CancellationToken {
  369. get { return cts.Token; }
  370. }
  371. public void SetCanceled ()
  372. {
  373. if (completed)
  374. return;
  375. completed = true;
  376. tcs.SetCanceled ();
  377. }
  378. public void SetException (Exception error)
  379. {
  380. if (completed)
  381. return;
  382. completed = true;
  383. tcs.SetException (error);
  384. }
  385. public void SetCompleted ()
  386. {
  387. if (completed)
  388. return;
  389. completed = true;
  390. OnCompleted ();
  391. }
  392. protected abstract void OnCompleted ();
  393. public abstract byte[] GetBuffer (out int index, out int count);
  394. public abstract Task<bool> Write (int count);
  395. ~Operation ()
  396. {
  397. Dispose (false);
  398. }
  399. public void Dispose ()
  400. {
  401. Dispose (true);
  402. GC.SuppressFinalize (this);
  403. }
  404. protected virtual void Dispose (bool disposing)
  405. {
  406. if (disposing) {
  407. SetCanceled ();
  408. if (cts != null) {
  409. cts.Dispose ();
  410. cts = null;
  411. }
  412. }
  413. }
  414. }
  415. class CopyToAsyncOperation : Operation<object> {
  416. Stream destination;
  417. byte[] buffer;
  418. public CopyToAsyncOperation (WebResponseStream parent,
  419. Stream destination, int bufferSize,
  420. CancellationToken cancellationToken)
  421. : base (parent, cancellationToken)
  422. {
  423. this.destination = destination;
  424. buffer = new byte [bufferSize];
  425. }
  426. public override byte[] GetBuffer (out int index, out int count)
  427. {
  428. index = 0;
  429. count = buffer.Length;
  430. return buffer;
  431. }
  432. public override async Task<bool> Write (int count)
  433. {
  434. await destination.WriteAsync (buffer, 0, count, CancellationToken);
  435. return true;
  436. }
  437. protected override void OnCompleted ()
  438. {
  439. TaskCompletionSource.SetResult (null);
  440. }
  441. }
  442. class ReadAsyncOperation : Operation<int> {
  443. byte[] buffer;
  444. int bufferIndex;
  445. int bufferCount;
  446. int successfullyWritten;
  447. public ReadAsyncOperation (WebResponseStream parent,
  448. byte[] buffer, int offset, int count,
  449. CancellationToken cancellationToken)
  450. : base (parent, cancellationToken)
  451. {
  452. this.buffer = buffer;
  453. this.bufferIndex = offset;
  454. this.bufferCount = count;
  455. }
  456. public override byte[] GetBuffer (out int index, out int count)
  457. {
  458. index = bufferIndex;
  459. count = bufferCount;
  460. return buffer;
  461. }
  462. public override Task<bool> Write (int count)
  463. {
  464. bufferIndex += count;
  465. successfullyWritten += count;
  466. bool keepGoing = bufferIndex < bufferCount;
  467. return System.Threading.Tasks.Task.FromResult (keepGoing);
  468. }
  469. protected override void OnCompleted ()
  470. {
  471. TaskCompletionSource.SetResult (successfullyWritten);
  472. }
  473. }
  474. public async Task<CFHTTPMessage> Open (WorkerThread worker,
  475. CancellationToken cancellationToken)
  476. {
  477. this.worker = worker;
  478. openTcs = new TaskCompletionSource<CFHTTPMessage> ();
  479. openCts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken);
  480. openCts.Token.Register (() => OnCanceled ());
  481. mainThread = Thread.CurrentThread;
  482. try {
  483. if (worker != null)
  484. await worker.Post (c => DoOpen (), openCts.Token);
  485. else
  486. DoOpen ();
  487. var result = await openTcs.Task;
  488. return result;
  489. } finally {
  490. openCts.Dispose ();
  491. openCts = null;
  492. }
  493. }
  494. void DoOpen ()
  495. {
  496. if (lastError != null) {
  497. openTcs.SetException (lastError);
  498. return;
  499. }
  500. /*
  501. * We must wait until the HasBytesAvailableEvent has been fired
  502. * before we can access the result.
  503. *
  504. */
  505. stream.ErrorEvent += (sender, e) => {
  506. OnError (stream.GetError ());
  507. };
  508. stream.ClosedEvent += (sender, e) => {
  509. if (!open) {
  510. open = true;
  511. openTcs.SetResult (stream.GetResponseHeader ());
  512. }
  513. OnCompleted ();
  514. };
  515. stream.HasBytesAvailableEvent += (sender, e) => {
  516. if (!open) {
  517. open = true;
  518. openTcs.SetResult (stream.GetResponseHeader ());
  519. }
  520. HasBytesAvailable ();
  521. };
  522. stream.OpenCompletedEvent += (sender, e) => {
  523. if (body == null)
  524. return;
  525. body.Open ();
  526. };
  527. workerThread = Thread.CurrentThread;
  528. stream.EnableEvents (CFRunLoop.Current, CFRunLoop.CFRunLoopCommonModes);
  529. stream.Open ();
  530. }
  531. #region implemented abstract members of System.IO.Stream
  532. public override Task CopyToAsync (Stream destination, int bufferSize,
  533. CancellationToken cancellationToken)
  534. {
  535. var operation = new CopyToAsyncOperation (
  536. this, destination, bufferSize, cancellationToken);
  537. StartOperation (operation);
  538. return operation.Task;
  539. }
  540. public override void Flush ()
  541. {
  542. ;
  543. }
  544. public override Task<int> ReadAsync (byte[] buffer, int offset, int count,
  545. CancellationToken cancellationToken)
  546. {
  547. var operation = new ReadAsyncOperation (
  548. this, buffer, offset, count, cancellationToken);
  549. StartOperation (operation);
  550. return operation.Task;
  551. }
  552. public override int Read (byte[] buffer, int offset, int count)
  553. {
  554. if (Thread.CurrentThread.Equals (mainThread) ||
  555. Thread.CurrentThread.Equals (workerThread))
  556. throw new InvalidOperationException (
  557. "You must not use synchronous Read() from the main thread.");
  558. return ReadAsync (buffer, offset, count, CancellationToken.None).Result;
  559. }
  560. public override long Seek (long offset, SeekOrigin origin)
  561. {
  562. throw new NotSupportedException ();
  563. }
  564. public override void SetLength (long value)
  565. {
  566. throw new NotSupportedException ();
  567. }
  568. public override void Write (byte[] buffer, int offset, int count)
  569. {
  570. throw new NotSupportedException ();
  571. }
  572. public override bool CanRead {
  573. get { return true; }
  574. }
  575. public override bool CanSeek {
  576. get { return false; }
  577. }
  578. public override bool CanWrite {
  579. get { return false; }
  580. }
  581. public override long Length {
  582. get { throw new NotSupportedException (); }
  583. }
  584. public override long Position {
  585. get { throw new NotSupportedException (); }
  586. set { throw new NotSupportedException (); }
  587. }
  588. #endregion
  589. }
  590. }