private/UI4ME.Transport.cs
|
// Runtime-compiled helper for the UI4ME.Core PowerShell module. No compiled // assembly is shipped; Add-Type builds this source once per process. // // The source must compile as C# 5 against .NET Framework 4.x (Windows // PowerShell 5.1, CodeDOM csc) and with Roslyn against modern .NET // (PowerShell 7.2+). UI4ME_NETFX is defined by the module on .NET Framework. // // Threading contract: the reader thread only parses frames and queues them. // It never invokes PowerShell. All application callbacks run when the // PowerShell runspace pumps Receive(). using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; using System.IO.MemoryMappedFiles; using Microsoft.Win32.SafeHandles; using System.IO.Pipes; using System.Management.Automation; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; namespace UI4ME { /// <summary>A malformed or unexpected peer message. Fatal to the connection.</summary> public sealed class ProtocolException : Exception { public ProtocolException(string message) : base(message) { } } /// <summary>The host answered a request with a JSON-RPC error.</summary> public sealed class RpcException : Exception { public RpcException(string method, long code, string message, object data) : base("UI4ME " + method + " failed (" + code.ToString(CultureInfo.InvariantCulture) + "): " + message) { Method = method; Code = code; RpcMessage = message; RpcData = data; Data["RpcCode"] = code; Data["RpcData"] = data; } public string Method { get; private set; } public long Code { get; private set; } public string RpcMessage { get; private set; } public object RpcData { get; private set; } } /// <summary>The host connection is closed; no further requests can succeed.</summary> public sealed class DisconnectedException : IOException { public DisconnectedException(string message) : base(message) { } public DisconnectedException(string message, Exception inner) : base(message, inner) { } } /// <summary>A wait expired. The host may still complete the operation.</summary> public sealed class RpcTimeoutException : TimeoutException { public RpcTimeoutException(string message) : base(message) { } } /// <summary>A window, document, node or bitmap handle is no longer current.</summary> public sealed class StaleObjectException : InvalidOperationException { public StaleObjectException(string message) : base(message) { } } /// <summary> /// Strict JSON for the host protocol. Objects become PSObjects with note /// properties (no date coercion, no PowerShell 5.1 array wrapping quirks). /// </summary> public static class Json { private const int MaximumDepth = 128; public static object Parse(string text) { if (text == null) throw new ArgumentNullException("text"); Parser parser = new Parser(text); parser.SkipWhitespace(); object value = parser.ReadValue(0); parser.SkipWhitespace(); if (parser.Position != text.Length) throw new ProtocolException("Trailing data after JSON value"); return value; } private sealed class Parser { private readonly string text; public int Position; public Parser(string text) { this.text = text; } public void SkipWhitespace() { while (Position < text.Length) { char c = text[Position]; if (c == ' ' || c == '\t' || c == '\r' || c == '\n') Position++; else break; } } private char Peek() { if (Position >= text.Length) throw new ProtocolException("Unexpected end of JSON"); return text[Position]; } private void Expect(string literal) { if (string.CompareOrdinal(text, Position, literal, 0, literal.Length) != 0) throw new ProtocolException("Invalid JSON literal"); Position += literal.Length; } public object ReadValue(int depth) { if (depth > MaximumDepth) throw new ProtocolException("JSON nesting is too deep"); char c = Peek(); switch (c) { case '{': return ReadObject(depth); case '[': return ReadArray(depth); case '"': return ReadString(); case 't': Expect("true"); return true; case 'f': Expect("false"); return false; case 'n': Expect("null"); return null; default: if (c == '-' || (c >= '0' && c <= '9')) return ReadNumber(); throw new ProtocolException("Invalid JSON value"); } } private object ReadObject(int depth) { Position++; List<KeyValuePair<string, object>> members = new List<KeyValuePair<string, object>>(); HashSet<string> exact = new HashSet<string>(StringComparer.Ordinal); HashSet<string> folded = new HashSet<string>(StringComparer.OrdinalIgnoreCase); bool caseCollision = false; SkipWhitespace(); if (Peek() == '}') { Position++; return new PSObject(); } while (true) { SkipWhitespace(); if (Peek() != '"') throw new ProtocolException("Expected JSON member name"); string name = ReadString(); if (!exact.Add(name)) throw new ProtocolException("Duplicate JSON member: " + name); if (!folded.Add(name)) caseCollision = true; SkipWhitespace(); if (Peek() != ':') throw new ProtocolException("Expected ':' in JSON object"); Position++; SkipWhitespace(); members.Add(new KeyValuePair<string, object>(name, ReadValue(depth + 1))); SkipWhitespace(); char next = Peek(); Position++; if (next == '}') break; if (next != ',') throw new ProtocolException("Expected ',' or '}' in JSON object"); } if (caseCollision) { // PowerShell member names are case-insensitive. Preserve // distinct keys such as {"A":1,"a":2} in an ordinal table. Hashtable table = new Hashtable(StringComparer.Ordinal); foreach (KeyValuePair<string, object> member in members) table[member.Key] = member.Value; return table; } PSObject result = new PSObject(); foreach (KeyValuePair<string, object> member in members) result.Properties.Add(new PSNoteProperty(member.Key, member.Value)); return result; } private object ReadArray(int depth) { Position++; List<object> items = new List<object>(); SkipWhitespace(); if (Peek() == ']') { Position++; return items.ToArray(); } while (true) { SkipWhitespace(); items.Add(ReadValue(depth + 1)); SkipWhitespace(); char next = Peek(); Position++; if (next == ']') break; if (next != ',') throw new ProtocolException("Expected ',' or ']' in JSON array"); } return items.ToArray(); } private static int Hex(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; throw new ProtocolException("Invalid JSON unicode escape"); } private string ReadString() { Position++; StringBuilder builder = null; int start = Position; while (true) { if (Position >= text.Length) throw new ProtocolException("Unterminated JSON string"); char c = text[Position]; if (c == '"') { string tail = text.Substring(start, Position - start); Position++; if (builder == null) return tail; builder.Append(tail); return builder.ToString(); } if (c < ' ') throw new ProtocolException("Control character in JSON string"); if (c != '\\') { Position++; continue; } if (builder == null) builder = new StringBuilder(); builder.Append(text, start, Position - start); Position++; char e = Peek(); Position++; switch (e) { case '"': builder.Append('"'); break; case '\\': builder.Append('\\'); break; case '/': builder.Append('/'); break; case 'b': builder.Append('\b'); break; case 'f': builder.Append('\f'); break; case 'n': builder.Append('\n'); break; case 'r': builder.Append('\r'); break; case 't': builder.Append('\t'); break; case 'u': if (Position + 4 > text.Length) throw new ProtocolException("Truncated JSON unicode escape"); int code = (Hex(text[Position]) << 12) | (Hex(text[Position + 1]) << 8) | (Hex(text[Position + 2]) << 4) | Hex(text[Position + 3]); builder.Append((char)code); Position += 4; break; default: throw new ProtocolException("Invalid JSON escape"); } start = Position; } } private object ReadNumber() { int start = Position; if (text[Position] == '-') Position++; if (Position >= text.Length) throw new ProtocolException("Invalid JSON number"); if (text[Position] == '0') Position++; else if (text[Position] >= '1' && text[Position] <= '9') { while (Position < text.Length && char.IsDigit(text[Position]) && text[Position] < 128) Position++; } else throw new ProtocolException("Invalid JSON number"); bool integral = true; if (Position < text.Length && text[Position] == '.') { integral = false; Position++; int digits = Position; while (Position < text.Length && text[Position] >= '0' && text[Position] <= '9') Position++; if (Position == digits) throw new ProtocolException("Invalid JSON fraction"); } if (Position < text.Length && (text[Position] == 'e' || text[Position] == 'E')) { integral = false; Position++; if (Position < text.Length && (text[Position] == '+' || text[Position] == '-')) Position++; int digits = Position; while (Position < text.Length && text[Position] >= '0' && text[Position] <= '9') Position++; if (Position == digits) throw new ProtocolException("Invalid JSON exponent"); } string token = text.Substring(start, Position - start); if (integral) { int small; if (int.TryParse(token, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out small)) return small; long large; if (long.TryParse(token, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out large)) return large; ulong huge; if (ulong.TryParse(token, NumberStyles.None, CultureInfo.InvariantCulture, out huge)) return huge; } double value = double.Parse(token, NumberStyles.Float, CultureInfo.InvariantCulture); if (double.IsInfinity(value) || double.IsNaN(value)) throw new ProtocolException("Non-finite JSON number"); return value; } } public static string Serialize(object value) { StringBuilder builder = new StringBuilder(); Write(builder, value, 0); return builder.ToString(); } private static void WriteString(StringBuilder builder, string text) { builder.Append('"'); foreach (char c in text) { switch (c) { case '"': builder.Append("\\\""); break; case '\\': builder.Append("\\\\"); break; case '\n': builder.Append("\\n"); break; case '\r': builder.Append("\\r"); break; case '\t': builder.Append("\\t"); break; case '\b': builder.Append("\\b"); break; case '\f': builder.Append("\\f"); break; default: if (c < ' ') builder.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture)); else builder.Append(c); break; } } builder.Append('"'); } private static void Write(StringBuilder builder, object value, int depth) { if (depth > MaximumDepth) throw new ArgumentException("Value is nested too deeply for JSON"); PSObject wrapped = value as PSObject; if (wrapped != null) { if (wrapped.BaseObject is PSCustomObject) { builder.Append('{'); bool firstMember = true; foreach (PSPropertyInfo property in wrapped.Properties) { if (!firstMember) builder.Append(','); firstMember = false; WriteString(builder, property.Name); builder.Append(':'); Write(builder, property.Value, depth + 1); } builder.Append('}'); return; } value = wrapped.BaseObject; } if (value == null || value is DBNull) { builder.Append("null"); return; } string text = value as string; if (text != null) { WriteString(builder, text); return; } if (value is char) { WriteString(builder, value.ToString()); return; } if (value is bool) { builder.Append((bool)value ? "true" : "false"); return; } if (value is int || value is long || value is short || value is sbyte || value is byte || value is uint || value is ulong || value is ushort) { builder.Append(Convert.ToString(value, CultureInfo.InvariantCulture)); return; } if (value is double || value is float) { double number = Convert.ToDouble(value, CultureInfo.InvariantCulture); if (double.IsNaN(number) || double.IsInfinity(number)) throw new ArgumentException("JSON cannot represent NaN or infinity"); builder.Append(number.ToString("R", CultureInfo.InvariantCulture)); return; } if (value is decimal) { builder.Append(((decimal)value).ToString(CultureInfo.InvariantCulture)); return; } IDictionary dictionary = value as IDictionary; if (dictionary != null) { builder.Append('{'); bool firstEntry = true; foreach (DictionaryEntry entry in dictionary) { object key = entry.Key; PSObject wrappedKey = key as PSObject; if (wrappedKey != null) key = wrappedKey.BaseObject; if (!(key is string) && !(key is char)) throw new ArgumentException("JSON object keys must be strings"); if (!firstEntry) builder.Append(','); firstEntry = false; WriteString(builder, key.ToString()); builder.Append(':'); Write(builder, entry.Value, depth + 1); } builder.Append('}'); return; } if (value is ScriptBlock || value is Enum || value is DateTime || value is Delegate) throw new ArgumentException("Cannot send a " + value.GetType().FullName + " to the native host; convert it to a string or number"); IEnumerable sequence = value as IEnumerable; if (sequence != null) { builder.Append('['); bool firstItem = true; foreach (object item in sequence) { if (!firstItem) builder.Append(','); firstItem = false; Write(builder, item, depth + 1); } builder.Append(']'); return; } throw new ArgumentException("Cannot send a " + value.GetType().FullName + " to the native host as JSON"); } /// <summary>Returns a member of a parsed object, or null.</summary> public static object Field(object container, string name) { if (container == null) return null; PSObject wrapped = container as PSObject; if (wrapped != null) { if (wrapped.BaseObject is PSCustomObject) { PSPropertyInfo property = wrapped.Properties[name]; return property == null ? null : property.Value; } container = wrapped.BaseObject; } IDictionary dictionary = container as IDictionary; if (dictionary != null) return dictionary.Contains(name) ? dictionary[name] : null; return null; } public static bool HasField(object container, string name) { if (container == null) return false; PSObject wrapped = container as PSObject; if (wrapped != null) { if (wrapped.BaseObject is PSCustomObject) return wrapped.Properties[name] != null; container = wrapped.BaseObject; } IDictionary dictionary = container as IDictionary; return dictionary != null && dictionary.Contains(name); } public static bool IsObject(object value) { PSObject wrapped = value as PSObject; if (wrapped != null) return wrapped.BaseObject is PSCustomObject || wrapped.BaseObject is IDictionary; return value is IDictionary; } } /// <summary>One validated JSON-RPC message received from the host.</summary> public sealed class Message { public string Kind { get; internal set; } // response | request | notification public string Id { get; internal set; } public string Method { get; internal set; } public object Params { get; internal set; } public object Result { get; internal set; } public bool IsError { get; internal set; } public long ErrorCode { get; internal set; } public string ErrorMessage { get; internal set; } public object ErrorData { get; internal set; } internal int Size; } public sealed class Transport : IDisposable { public const int MaximumFrame = 2 * 1024 * 1024; public const int MaximumQueuedBytes = 8 * 1024 * 1024; public const int MaximumQueuedMessages = 128; public const int MaximumPending = 128; private readonly BlockingCollection<Message> incoming = new BlockingCollection<Message>(); private readonly HashSet<string> pending = new HashSet<string>(StringComparer.Ordinal); private readonly object stateLock = new object(); private readonly object writeLock = new object(); private readonly UTF8Encoding utf8 = new UTF8Encoding(false, true); private readonly Stream stream; private readonly Thread reader; // Rejections of host requests that arrive while the callback queue is // full. The reader thread must never block on a write: a peer that is // itself busy writing would fill the pipe and deadlock both sides. private readonly BlockingCollection<Hashtable> rejections = new BlockingCollection<Hashtable>(MaximumPending * 2); private readonly Thread rejecter; private volatile bool disposed; private volatile bool disconnected; private volatile string failure; private int queuedMessages; private int queuedBytes; private long droppedNotifications; private long rejectedRequests; private long receivedMessages; public Transport(string endpoint, int connectTimeoutMs) { if (string.IsNullOrEmpty(endpoint)) throw new ArgumentException("Endpoint must not be empty."); if (connectTimeoutMs < 1 || connectTimeoutMs > 120000) throw new ArgumentOutOfRangeException("connectTimeoutMs"); if (endpoint.StartsWith(@"\\.\pipe\", StringComparison.OrdinalIgnoreCase)) { string pipeName = endpoint.Substring(9); if (pipeName.Length == 0 || pipeName.IndexOf('\\') >= 0 || pipeName.IndexOf('/') >= 0) throw new ArgumentException("Expected a private local Windows named pipe."); // Asynchronous (overlapped) so a blocked read on the reader // thread never serializes writes from the PowerShell thread. NamedPipeClientStream pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); try { pipe.Connect(connectTimeoutMs); stream = pipe; } catch { pipe.Dispose(); throw; } } else { // .NET Framework has no UnixDomainSocketEndPoint. Resolve it at // runtime so this source also compiles for Windows PowerShell. Type unixEndpointType = Type.GetType("System.Net.Sockets.UnixDomainSocketEndPoint, System.Net.Sockets", false); if (unixEndpointType == null) unixEndpointType = typeof(Socket).Assembly.GetType("System.Net.Sockets.UnixDomainSocketEndPoint", false); if (unixEndpointType == null) throw new PlatformNotSupportedException("Unix-domain sockets require PowerShell 7.2+."); if (!Path.IsPathRooted(endpoint)) throw new ArgumentException("Unix socket path must be absolute."); object resolved = Activator.CreateInstance(unixEndpointType, new object[] { endpoint }); Socket socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); try { // EndConnect still uses the wait handle; never dispose it first. IAsyncResult connecting = socket.BeginConnect((EndPoint)resolved, null, null); if (!connecting.AsyncWaitHandle.WaitOne(connectTimeoutMs)) throw new TimeoutException("Timed out connecting to the native host."); socket.EndConnect(connecting); stream = new NetworkStream(socket, true); } catch { socket.Dispose(); throw; } } reader = new Thread(ReadLoop); reader.Name = "ui4me-powershell-reader"; reader.IsBackground = true; rejecter = new Thread(RejectLoop); rejecter.Name = "ui4me-powershell-rejecter"; rejecter.IsBackground = true; rejecter.Start(); reader.Start(); } private void RejectLoop() { try { foreach (Hashtable response in rejections.GetConsumingEnumerable()) { if (disposed || disconnected) return; try { Send(response); } catch (DisconnectedException) { return; } } } catch (ObjectDisposedException) { } } public bool IsDisconnected { get { return disconnected || disposed; } } public string Failure { get { return failure ?? "The native host disconnected"; } } public long DroppedNotifications { get { return Interlocked.Read(ref droppedNotifications); } } public long RejectedRequests { get { return Interlocked.Read(ref rejectedRequests); } } public long ReceivedMessages { get { return Interlocked.Read(ref receivedMessages); } } public int QueuedMessages { get { return Volatile.Read(ref queuedMessages); } } public int PendingCount { get { lock (stateLock) return pending.Count; } } /// <summary>Registers an outgoing request ID before its frame is sent.</summary> public void Register(string id) { if (string.IsNullOrEmpty(id)) throw new ArgumentException("Request ID must not be empty"); lock (stateLock) { if (disposed || disconnected) throw new DisconnectedException(Failure); if (pending.Count >= MaximumPending) throw new InvalidOperationException("UI4ME has 128 outstanding host requests"); if (!pending.Add(id)) throw new InvalidOperationException("Duplicate request ID"); } } /// <summary>Forgets a request; a late response for it is discarded.</summary> public void Forget(string id) { if (id == null) return; lock (stateLock) pending.Remove(id); } private static bool IsLifecycle(string method, object parameters) { if (method == "app.shutdown" || method == "window.before_close" || method == "app.before_quit") return true; return method == "window.event" && (Json.Field(parameters, "type") as string) == "closed"; } private byte[] ReadExact(int count, bool frameStart) { byte[] data = new byte[count]; int consumed = 0; while (consumed < count) { int n = stream.Read(data, consumed, count - consumed); if (n <= 0) { if (frameStart && consumed == 0) return null; throw new EndOfStreamException("The native host closed the connection inside a frame."); } consumed += n; } return data; } private Message Decode(string json, int size) { object value = Json.Parse(json); if (!Json.IsObject(value) || !"2.0".Equals(Json.Field(value, "jsonrpc"))) throw new ProtocolException("Expected a JSON-RPC 2.0 object"); Message message = new Message(); message.Size = size; object id = Json.Field(value, "id"); bool hasId = Json.HasField(value, "id"); if (Json.HasField(value, "method")) { string method = Json.Field(value, "method") as string; if (method == null) throw new ProtocolException("Host method must be a string"); object parameters = Json.Field(value, "params"); if (parameters == null) parameters = new PSObject(); else if (!Json.IsObject(parameters)) throw new ProtocolException("Host params must be an object"); if (hasId && !(id is string)) throw new ProtocolException("Host request IDs must be strings"); message.Kind = hasId ? "request" : "notification"; message.Id = id as string; message.Method = method; message.Params = parameters; return message; } if (!(id is string)) throw new ProtocolException("Host response has an invalid request ID"); bool hasResult = Json.HasField(value, "result"); bool hasError = Json.HasField(value, "error"); if (hasResult == hasError) throw new ProtocolException("Host response must contain result or error"); message.Kind = "response"; message.Id = (string)id; if (hasError) { object error = Json.Field(value, "error"); object code = Json.Field(error, "code"); string text = Json.Field(error, "message") as string; if (!Json.IsObject(error) || !(code is int || code is long) || text == null) throw new ProtocolException("Invalid JSON-RPC error object"); message.IsError = true; message.ErrorCode = Convert.ToInt64(code, CultureInfo.InvariantCulture); message.ErrorMessage = text; message.ErrorData = Json.Field(error, "data"); } else { message.Result = Json.Field(value, "result"); } return message; } private void ReadLoop() { try { while (!disposed) { byte[] header = ReadExact(4, true); if (header == null) throw new EndOfStreamException("The native host closed the connection."); long size = ((long)header[0] << 24) | ((long)header[1] << 16) | ((long)header[2] << 8) | header[3]; if (size < 1 || size > MaximumFrame) throw new ProtocolException("Invalid frame length"); byte[] body = ReadExact((int)size, false); string json; try { json = utf8.GetString(body); } catch (DecoderFallbackException) { throw new ProtocolException("Frame is not valid UTF-8"); } Message message = Decode(json, (int)size); Interlocked.Increment(ref receivedMessages); if (message.Kind == "response") { bool expected; lock (stateLock) expected = pending.Remove(message.Id); if (expected) incoming.Add(message); // bounded by MaximumPending continue; // late reply to a cancelled request } bool admit; lock (stateLock) { admit = IsLifecycle(message.Method, message.Params) || (queuedMessages < MaximumQueuedMessages && queuedBytes + message.Size <= MaximumQueuedBytes); if (admit) { queuedMessages++; queuedBytes += message.Size; } } if (admit) { incoming.Add(message); continue; } if (message.Kind == "request") { Interlocked.Increment(ref rejectedRequests); Hashtable error = new Hashtable(); error["code"] = -32005; error["message"] = "PowerShell callback queue is full"; Hashtable response = new Hashtable(); response["jsonrpc"] = "2.0"; response["id"] = message.Id; response["error"] = error; if (!rejections.TryAdd(response)) throw new ProtocolException("The host keeps sending requests that the backend cannot accept"); } else { Interlocked.Increment(ref droppedNotifications); } } } catch (Exception ex) { if (!disposed) failure = ex.GetType().Name + ": " + ex.Message; } finally { disconnected = true; try { incoming.CompleteAdding(); } catch (ObjectDisposedException) { } try { rejections.CompleteAdding(); } catch (ObjectDisposedException) { } } } /// <summary> /// Returns the next host message, or null after the timeout. Throws an /// IOException once the host disconnected and every message was consumed. /// </summary> public Message Receive(int timeoutMs) { if (timeoutMs < -1) throw new ArgumentOutOfRangeException("timeoutMs"); Message message; bool taken; try { taken = incoming.TryTake(out message, timeoutMs); } catch (ObjectDisposedException) { throw new DisconnectedException(Failure); } if (taken) { if (message.Kind != "response") { lock (stateLock) { queuedMessages--; queuedBytes -= message.Size; } } return message; } if (incoming.IsCompleted) throw new DisconnectedException(Failure); return null; } /// <summary>Serializes and writes one frame. Thread-safe.</summary> public void Send(object envelope) { if (disposed || disconnected) throw new DisconnectedException(Failure); byte[] body = utf8.GetBytes(Json.Serialize(envelope)); if (body.Length == 0 || body.Length > MaximumFrame) throw new InvalidDataException("A JSON-RPC frame must be 1 byte to 2 MiB"); byte[] frame = new byte[body.Length + 4]; frame[0] = (byte)(body.Length >> 24); frame[1] = (byte)(body.Length >> 16); frame[2] = (byte)(body.Length >> 8); frame[3] = (byte)body.Length; Buffer.BlockCopy(body, 0, frame, 4, body.Length); lock (writeLock) { if (disposed || disconnected) throw new DisconnectedException(Failure); try { stream.Write(frame, 0, frame.Length); stream.Flush(); } catch (Exception ex) { if (ex is IOException || ex is ObjectDisposedException || ex is SocketException) { failure = failure ?? (ex.GetType().Name + ": " + ex.Message); disconnected = true; throw new DisconnectedException(Failure, ex); } throw; } } } public void Dispose() { lock (stateLock) { if (disposed) return; disposed = true; pending.Clear(); } try { stream.Dispose(); } catch (IOException) { } catch (ObjectDisposedException) { } if (Thread.CurrentThread != reader) reader.Join(2000); try { incoming.CompleteAdding(); } catch (ObjectDisposedException) { } try { rejections.CompleteAdding(); } catch (ObjectDisposedException) { } if (Thread.CurrentThread != rejecter) rejecter.Join(2000); } } /// <summary>A writable view of one host-created live-bitmap file.</summary> public sealed class MappedBitmap : IDisposable { private FileStream file; private MemoryMappedFile map; private MemoryMappedViewAccessor view; private readonly long capacity; public MappedBitmap(string path, long capacity, bool deleteOnClose) { if (string.IsNullOrEmpty(path) || !Path.IsPathRooted(path)) throw new ArgumentException("Bitmap path must be absolute"); if (capacity < 1) throw new ArgumentOutOfRangeException("capacity"); this.capacity = capacity; // Delete sharing lets the host unlink the transport after the first // present; Windows delete-on-close removes it if both sides die. FileOptions options = deleteOnClose ? FileOptions.DeleteOnClose : FileOptions.None; file = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete, 4096, options); try { if (file.Length < capacity) throw new InvalidDataException("Host bitmap file is smaller than its geometry"); #if UI4ME_NETFX map = MemoryMappedFile.CreateFromFile(file, null, 0, MemoryMappedFileAccess.ReadWrite, null, HandleInheritability.None, true); #else map = MemoryMappedFile.CreateFromFile(file, null, 0, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true); #endif view = map.CreateViewAccessor(0, capacity, MemoryMappedFileAccess.ReadWrite); } catch { Dispose(); throw; } } public long Capacity { get { return capacity; } } public bool IsClosed { get { return view == null; } } public void Write(long offset, byte[] data) { if (data == null) throw new ArgumentNullException("data"); if (view == null) throw new ObjectDisposedException("MappedBitmap"); if (offset < 0 || offset + data.LongLength > capacity) throw new ArgumentOutOfRangeException("offset"); // One bulk copy into the mapped view. UnmanagedMemoryAccessor.WriteArray // copies element by element on .NET Framework (Windows PowerShell 5.1), // which made a 1024x768 frame take ~150 ms there. SafeMemoryMappedViewHandle handle = view.SafeMemoryMappedViewHandle; bool added = false; try { handle.DangerousAddRef(ref added); long address = handle.DangerousGetHandle().ToInt64() + view.PointerOffset + offset; System.Runtime.InteropServices.Marshal.Copy(data, 0, new IntPtr(address), data.Length); } finally { if (added) handle.DangerousRelease(); } } public void Dispose() { if (view != null) { view.Dispose(); view = null; } if (map != null) { map.Dispose(); map = null; } if (file != null) { file.Dispose(); file = null; } } } } |