/ src / modules / MouseWithoutBorders / App / Class / IClipboardHelper.cs
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  }