IClipboardHelper.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Collections; 7 using System.Collections.Specialized; 8 using System.Diagnostics; 9 using System.Drawing; 10 using System.IO; 11 using System.IO.Pipes; 12 using System.Reflection; 13 using System.Runtime.InteropServices; 14 using System.Security.AccessControl; 15 using System.Security.Principal; 16 using System.Threading; 17 using System.Threading.Tasks; 18 using System.Windows.Forms; 19 20 using Microsoft.VisualStudio.Threading; 21 using MouseWithoutBorders.Core; 22 using Newtonsoft.Json; 23 using StreamJsonRpc; 24 25 #if !MM_HELPER 26 using MouseWithoutBorders.Class; 27 #endif 28 29 using SystemClipboard = System.Windows.Forms.Clipboard; 30 #if !MM_HELPER 31 using Clipboard = MouseWithoutBorders.Core.Clipboard; 32 using Thread = MouseWithoutBorders.Core.Thread; 33 #endif 34 35 namespace MouseWithoutBorders 36 { 37 [JsonObject(MemberSerialization.OptIn)] 38 public struct ByteArrayOrString 39 { 40 private enum ValueType 41 { 42 ByteArray, 43 String, 44 } 45 46 [JsonProperty] 47 private ValueType _type; 48 49 private byte[] _byteArrayValue; 50 private string _stringValue; 51 52 public ByteArrayOrString(byte[] byteArray) 53 { 54 _type = ValueType.ByteArray; 55 _byteArrayValue = byteArray; 56 _stringValue = null; 57 } 58 59 public ByteArrayOrString(string str) 60 { 61 _type = ValueType.String; 62 _byteArrayValue = null; 63 _stringValue = str; 64 } 65 66 public bool IsByteArray => _type == ValueType.ByteArray; 67 68 public bool IsString => _type == ValueType.String; 69 70 [JsonProperty("byteArray")] 71 public byte[] ByteArray 72 { 73 get => _byteArrayValue; 74 set 75 { 76 _byteArrayValue = value; 77 _type = ValueType.ByteArray; 78 } 79 } 80 81 [JsonProperty("string")] 82 public string TheString 83 { 84 get => _stringValue; 85 set 86 { 87 _stringValue = value; 88 _type = ValueType.String; 89 } 90 } 91 92 public bool ShouldSerializeByteArray() => IsByteArray; 93 94 public bool ShouldSerializeString() => IsString; 95 96 public byte[] GetByteArray() 97 { 98 if (IsByteArray) 99 { 100 return _byteArrayValue; 101 } 102 103 throw new InvalidOperationException("The value is not a byte array."); 104 } 105 106 public string GetString() 107 { 108 if (IsString) 109 { 110 return _stringValue; 111 } 112 113 throw new InvalidOperationException("The value is not a string."); 114 } 115 116 public static implicit operator ByteArrayOrString(byte[] byteArray) => new ByteArrayOrString(byteArray); 117 118 public static implicit operator ByteArrayOrString(string str) => new ByteArrayOrString(str); 119 } 120 121 public interface IClipboardHelper 122 { 123 void SendLog(string log); 124 125 void SendDragFile(string fileName); 126 127 void SendClipboardData(ByteArrayOrString data, bool isFilePath); 128 } 129 130 #if !MM_HELPER 131 public class ClipboardHelper : IClipboardHelper 132 { 133 public void SendLog(string log) 134 { 135 Logger.LogDebug("FROM HELPER: " + log); 136 137 if (!string.IsNullOrEmpty(log)) 138 { 139 if (log.StartsWith("Screen capture ended", StringComparison.InvariantCulture)) 140 { 141 /* TODO: Telemetry for screen capture. */ 142 143 if (Setting.Values.FirstCtrlShiftS) 144 { 145 Setting.Values.FirstCtrlShiftS = false; 146 Common.ShowToolTip("Selective screen capture has been triggered, you can change the hotkey on the Settings form.", 10000); 147 } 148 } 149 else if (log.StartsWith("Trace:", StringComparison.InvariantCulture)) 150 { 151 Logger.TelemetryLogTrace(log, SeverityLevel.Information); 152 } 153 } 154 } 155 156 public void SendDragFile(string fileName) 157 { 158 DragDrop.DragDropStep05Ex(fileName); 159 } 160 161 public void SendClipboardData(ByteArrayOrString data, bool isFilePath) 162 { 163 _ = Clipboard.CheckClipboardEx(data, isFilePath); 164 } 165 } 166 #endif 167 168 internal sealed class IpcChannel<T> 169 where T : new() 170 { 171 public static T StartIpcServer(string pipeName, CancellationToken cancellationToken) 172 { 173 SecurityIdentifier securityIdentifier = new SecurityIdentifier( 174 WellKnownSidType.AuthenticatedUserSid, null); 175 176 PipeSecurity pipeSecurity = new PipeSecurity(); 177 pipeSecurity.AddAccessRule(new PipeAccessRule( 178 securityIdentifier, 179 PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance, 180 AccessControlType.Allow)); 181 182 _ = Task.Factory.StartNew( 183 async () => 184 { 185 try 186 { 187 while (!cancellationToken.IsCancellationRequested) 188 { 189 using (var serverChannel = NamedPipeServerStreamAcl.Create(pipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, 0, 0, pipeSecurity)) 190 { 191 await serverChannel.WaitForConnectionAsync(); 192 var taskRpc = JsonRpc.Attach(serverChannel, new T()); 193 await taskRpc.Completion; 194 } 195 } 196 } 197 catch (OperationCanceledException) 198 { 199 } 200 catch (Exception e) 201 { 202 #if MM_HELPER 203 _ = e; 204 #else 205 Logger.Log(e); 206 #endif 207 } 208 }, 209 cancellationToken, 210 TaskCreationOptions.None, 211 TaskScheduler.Default); 212 213 return default(T); 214 } 215 } 216 217 internal sealed class IpcHelper 218 { 219 private const string ChannelName = "MouseWithoutBorders"; 220 private const string RemoteObjectName = "ClipboardHelper"; 221 222 #if !MM_HELPER 223 private static void CleanupStream() 224 { 225 if (_serverTaskCancellationSource != null) 226 { 227 _serverTaskCancellationSource.Cancel(); 228 _serverTaskCancellationSource.Dispose(); 229 _serverTaskCancellationSource = null; 230 } 231 } 232 233 private static CancellationTokenSource _serverTaskCancellationSource; 234 235 internal static void CreateIpcServer(bool cleanup) 236 { 237 try 238 { 239 if (cleanup) 240 { 241 CleanupStream(); 242 return; 243 } 244 245 _serverTaskCancellationSource = new CancellationTokenSource(); 246 CancellationToken cancellationToken = _serverTaskCancellationSource.Token; 247 248 IpcChannel<ClipboardHelper>.StartIpcServer(ChannelName + "/" + RemoteObjectName, cancellationToken); 249 IpcChannelHelper.IpcChannelCreated = true; 250 } 251 catch (Exception e) 252 { 253 IpcChannelHelper.IpcChannelCreated = false; 254 Common.ShowToolTip("Error setting up clipboard sharing, clipboard sharing will not work!", 5000, ToolTipIcon.Error); 255 Logger.Log(e); 256 } 257 } 258 #else 259 internal static IClipboardHelper CreateIpcClient() 260 { 261 try 262 { 263 var stream = new NamedPipeClientStream(".", ChannelName + "/" + RemoteObjectName, PipeDirection.InOut, PipeOptions.Asynchronous); 264 265 stream.Connect(); 266 267 return JsonRpc.Attach<IClipboardHelper>(stream); 268 } 269 catch (Exception e) 270 { 271 EventLogger.LogEvent(e.Message, EventLogEntryType.Error); 272 } 273 274 return null; 275 } 276 #endif 277 278 } 279 280 internal static class EventLogger 281 { 282 #if MM_HELPER 283 private const string EventSourceName = "MouseWithoutBordersHelper"; 284 #else 285 private const string EventSourceName = "MouseWithoutBorders"; 286 #endif 287 288 internal static void LogEvent(string message, EventLogEntryType logType = EventLogEntryType.Information) 289 { 290 try 291 { 292 if (!EventLog.SourceExists(EventSourceName)) 293 { 294 EventLog.CreateEventSource(EventSourceName, "Application"); 295 } 296 297 EventLog.WriteEntry(EventSourceName, message, logType); 298 } 299 catch (Exception e) 300 { 301 Debug.WriteLine(message + ": " + e.Message); 302 } 303 } 304 } 305 #if MM_HELPER 306 307 internal static class ClipboardMMHelper 308 { 309 internal static IntPtr NextClipboardViewer = IntPtr.Zero; 310 private static FormHelper helperForm; 311 private static bool addClipboardFormatListenerResult; 312 313 private static void Log(string log) 314 { 315 helperForm.SendLog(log); 316 } 317 318 private static void Log(Exception e) 319 { 320 Log($"Trace: {e}"); 321 } 322 323 internal static void HookClipboard(FormHelper f) 324 { 325 helperForm = f; 326 327 try 328 { 329 addClipboardFormatListenerResult = NativeMethods.AddClipboardFormatListener(f.Handle); 330 331 int err = addClipboardFormatListenerResult ? 0 : Marshal.GetLastWin32Error(); 332 333 if (err != 0) 334 { 335 Log($"Trace: {nameof(NativeMethods.AddClipboardFormatListener)}: GetLastError = {err}"); 336 } 337 } 338 catch (EntryPointNotFoundException e) 339 { 340 Log($"{nameof(NativeMethods.AddClipboardFormatListener)} is unavailable in this version of Windows."); 341 Log(e); 342 } 343 catch (Exception e) 344 { 345 Log(e); 346 } 347 348 // Fallback 349 if (!addClipboardFormatListenerResult) 350 { 351 NextClipboardViewer = NativeMethods.SetClipboardViewer(f.Handle); 352 int err = NextClipboardViewer == IntPtr.Zero ? Marshal.GetLastWin32Error() : 0; 353 354 if (err != 0) 355 { 356 Log($"Trace: {nameof(NativeMethods.SetClipboardViewer)}: GetLastError = {err}"); 357 } 358 } 359 360 Log($"Trace: Clipboard monitor method {(addClipboardFormatListenerResult ? nameof(NativeMethods.AddClipboardFormatListener) : NextClipboardViewer != IntPtr.Zero ? nameof(NativeMethods.SetClipboardViewer) : "(none)")} is used."); 361 } 362 363 internal static void UnhookClipboard() 364 { 365 if (addClipboardFormatListenerResult) 366 { 367 addClipboardFormatListenerResult = false; 368 _ = NativeMethods.RemoveClipboardFormatListener(helperForm.Handle); 369 } 370 else 371 { 372 _ = NativeMethods.ChangeClipboardChain(helperForm.Handle, NextClipboardViewer); 373 NextClipboardViewer = IntPtr.Zero; 374 } 375 } 376 377 private static void ReHookClipboard() 378 { 379 UnhookClipboard(); 380 HookClipboard(helperForm); 381 } 382 383 internal static bool UpdateNextClipboardViewer(Message m) 384 { 385 if (m.WParam == NextClipboardViewer) 386 { 387 NextClipboardViewer = m.LParam; 388 return true; 389 } 390 391 return false; 392 } 393 394 internal static void PassMessageToTheNextViewer(Message m) 395 { 396 if (NextClipboardViewer != IntPtr.Zero && NextClipboardViewer != helperForm.Handle) 397 { 398 _ = NativeMethods.SendMessage(NextClipboardViewer, m.Msg, m.WParam, m.LParam); 399 } 400 } 401 402 public static bool ContainsFileDropList() 403 { 404 bool rv = false; 405 406 try 407 { 408 rv = IpcChannelHelper.Retry(nameof(SystemClipboard.ContainsFileDropList), () => { return SystemClipboard.ContainsFileDropList(); }, (log) => Log(log)); 409 } 410 catch (ExternalException e) 411 { 412 Log(e); 413 ReHookClipboard(); 414 } 415 catch (ThreadStateException e) 416 { 417 Log(e); 418 ReHookClipboard(); 419 } 420 421 return rv; 422 } 423 424 public static bool ContainsImage() 425 { 426 bool rv = false; 427 428 try 429 { 430 rv = IpcChannelHelper.Retry(nameof(SystemClipboard.ContainsImage), () => { return SystemClipboard.ContainsImage(); }, (log) => Log(log)); 431 } 432 catch (ExternalException e) 433 { 434 Log(e); 435 ReHookClipboard(); 436 } 437 catch (ThreadStateException e) 438 { 439 Log(e); 440 ReHookClipboard(); 441 } 442 443 return rv; 444 } 445 446 public static bool ContainsText() 447 { 448 bool rv = false; 449 450 try 451 { 452 rv = IpcChannelHelper.Retry(nameof(SystemClipboard.ContainsText), () => { return SystemClipboard.ContainsText(); }, (log) => Log(log)); 453 } 454 catch (ExternalException e) 455 { 456 Log(e); 457 ReHookClipboard(); 458 } 459 catch (ThreadStateException e) 460 { 461 Log(e); 462 ReHookClipboard(); 463 } 464 465 return rv; 466 } 467 468 public static StringCollection GetFileDropList() 469 { 470 StringCollection rv = null; 471 472 try 473 { 474 rv = IpcChannelHelper.Retry(nameof(SystemClipboard.GetFileDropList), () => { return SystemClipboard.GetFileDropList(); }, (log) => Log(log)); 475 } 476 catch (ExternalException e) 477 { 478 Log(e); 479 ReHookClipboard(); 480 } 481 catch (ThreadStateException e) 482 { 483 Log(e); 484 ReHookClipboard(); 485 } 486 487 return rv; 488 } 489 490 public static Image GetImage() 491 { 492 Image rv = null; 493 494 try 495 { 496 rv = IpcChannelHelper.Retry(nameof(SystemClipboard.GetImage), () => { return SystemClipboard.GetImage(); }, (log) => Log(log)); 497 } 498 catch (ExternalException e) 499 { 500 Log(e); 501 ReHookClipboard(); 502 } 503 catch (ThreadStateException e) 504 { 505 Log(e); 506 ReHookClipboard(); 507 } 508 509 return rv; 510 } 511 512 public static string GetText(TextDataFormat format) 513 { 514 string rv = null; 515 516 try 517 { 518 rv = IpcChannelHelper.Retry(nameof(SystemClipboard.GetText), () => { return SystemClipboard.GetText(format); }, (log) => Log(log)); 519 } 520 catch (ExternalException e) 521 { 522 Log(e); 523 ReHookClipboard(); 524 } 525 catch (ThreadStateException e) 526 { 527 Log(e); 528 ReHookClipboard(); 529 } 530 catch (System.ComponentModel.InvalidEnumArgumentException e) 531 { 532 Log(e); 533 } 534 535 return rv; 536 } 537 538 public static void SetImage(Image image) 539 { 540 try 541 { 542 _ = IpcChannelHelper.Retry( 543 nameof(SystemClipboard.SetImage), 544 () => 545 { 546 SystemClipboard.SetImage(image); 547 return true; 548 }, 549 (log) => Log(log)); 550 } 551 catch (ExternalException e) 552 { 553 Log(e); 554 ReHookClipboard(); 555 } 556 catch (ThreadStateException e) 557 { 558 Log(e); 559 ReHookClipboard(); 560 } 561 catch (ArgumentNullException e) 562 { 563 Log(e); 564 } 565 } 566 567 public static void SetText(string text) 568 { 569 try 570 { 571 _ = IpcChannelHelper.Retry( 572 nameof(SystemClipboard.SetText), 573 () => 574 { 575 SystemClipboard.SetText(text); 576 return true; 577 }, 578 (log) => Log(log)); 579 } 580 catch (ExternalException e) 581 { 582 Log(e); 583 ReHookClipboard(); 584 } 585 catch (ThreadStateException e) 586 { 587 Log(e); 588 ReHookClipboard(); 589 } 590 catch (ArgumentNullException e) 591 { 592 Log(e); 593 } 594 } 595 } 596 597 #endif 598 599 internal sealed class SharedConst 600 { 601 internal const int QUIT_CMD = 0x409; 602 } 603 }