2008-03-25

Clipboard Chaos!

Note: this content originally from http://mygreenpaste.blogspot.com. If you are reading it from some other site, please take the time to visit My Green Paste, Inc. Thank you.

OK, so perhaps chaos is a bit of a harsh word here. But the clipboard was recently driving me nuts! All I was trying to do was copy some text to it, and the operation was failing. Of course, as it was an ad hoc app, I didn't have any kind of error handling. The app worked just fine on one system, but running the app on another system (a virtual machine) consistently resulted in failure to copy the text to the clipboard.

Ultimately, I was able to determine what process was preventing my app from putting data in the clipboard, but I haven't yet found a decent workaround for when the problem happens. It's not critical for me, as the act of copying the text to the clipboard is more of a nicety than a requirement.

Anyway, using P/Invoke and System.Diagnostics, I found that vmusrvc.exe - the Virtual PC "Virtual Machine User Services" - had the clipboard open. Using the timestamps from Process Monitor's Profiling Events (generated at 100 ms intervals), and the timestamp of the failed operation from my app, I was able to determine the stack of vmusrvc.exe:

ntdll.dllKiFastSystemCallRet
vmusrvc.exevmusrvc.exe + 0x9a17
vmusrvc.exevmusrvc.exe + 0x9c24
vmusrvc.exevmusrvc.exe + 0x91f8
vmusrvc.exevmusrvc.exe + 0x907f
USER32.dllInternalCallWinProc + 0x28
USER32.dllUserCallWinProcCheckWow + 0x150
USER32.dllDispatchClientMessage + 0xa3
USER32.dll__fnDWORD + 0x24
ntdll.dllKiUserCallbackDispatcher + 0x13
vmusrvc.exevmusrvc.exe + 0x2d29
vmusrvc.exevmusrvc.exe + 0xdba6
kernel32.dllBaseProcessStart + 0x23

No parameters, of course, and symbol information for vmusrvc.exe does not appear to be available, but obviously user32.dll is processing some message. I may look into this more at a later point.

To find the process that was interfering with my clipboard work, I used P/Invoke to call GetOpenClipboardWindow() and then GetWindowThreadProcessId(), passing in the handle returned by GetOpenClipboardWindow(). Then, finding the process' executable name was just a matter of using the Modules collection of the Process instance returned by passing in the process id retrieved by GetWindowThreadProcessId() to System.Diagnostics.Process.GetProcessById().

The following code:

using System.Runtime.InteropServices;
using System.Diagnostics;
...
string data = "aasdlkjasdlk alkjsdl kajsdlkj al";
try
{
Clipboard.SetData( System.Windows.Forms.DataFormats.Text, data );
}
catch( ExternalException ee )
{
LogIt( ee.ToString() );
IntPtr hWnd = GetOpenClipboardWindow();
if( IntPtr.Zero != hWnd )
{
uint pid = 0;
uint tid = GetWindowThreadProcessId( hWnd, out pid );
LogIt( "Process with hWnd {0}, PID {1} ({1:x}), TID {2} ({2:x}), " +
"name {3} has the clipboard", hWnd, pid, tid,
Process.GetProcessById( (int)pid ).Modules[0].FileName );
}
}

Resulted in the following output:


2008-03-25 00:54:45.4938864--> System.Runtime.InteropServices.ExternalException: Requested Clipboard operation did not succeed.
at System.Windows.Forms.Clipboard.ThrowIfFailed(Int32 hr)
at System.Windows.Forms.Clipboard.SetDataObject(Object data, Boolean copy, Int32 retryTimes, Int32 retryDelay)
at System.Windows.Forms.Clipboard.SetData(String format, Object data)
at Clippy.Form1.button1_Click(Object sender, EventArgs e)
2008-03-25 00:54:45.5339440--> Process with hWnd 65716 (65716), PID 1492 (5d4), TID 1496 (5d8), name C:\Program Files\Virtual Machine Additions\vmusrvc.exe has the clipboard

Interestingly, trying an alternative method of the Clipboard to set the content also failed. The Clipboard.SetDataObject() overload that takes a retryTimes and retryDelay parameter failed in the same fashion after roughly ten seconds when invoked as follows:


Clipboard.SetDataObject( data, false, 100, 100 );

I tried variations on retryTimes and retryDelay, to no avail.

Not sure what vmusrvc.exe is doing with the clipboard (probably has to do with monitoring it for host / guest VM interaction), but the act of setting the contents of the clipboard didn't fail 100% of the time in the VM. Often enough to make it extremely unreliable, though. During "normal" system usage, I was not able to cause a failure when running the app on a non-virtual (actual?) system.

11 comments:

Anonymous said...

This may be exactly what i'm looking for. I have an application which uses the clipboard to copy items from Excel to Powerpoint. I've issues when testing this on a virtual machine where i get spurious errors about copies failing. Its not necessarily the same copies that fail either. I am wondering if your post is perhaps the answer. I'd very much like to try what you have implemented in the catch block but i cannot find the functions to do this. Can you provider a little more on what is exactly required in visual studio to get these functions.

Thanks in advance,

G

«/\/\Ø|ö±ò\/»®© said...

Hi G,

The LogIt function is left for you to implement, however you wish to do it. In order to use the code in the post, you should put the namespace declarations from the post at the top of the .cs file. You also need to declare the P/Invoke for GetOpenClipboardWindow and GetWindowThreadProcessId. Something like the following (the formatting of which will probably get all messed up...)

[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr GetOpenClipboardWindow();

[DllImport("user32.dll", SetLastError = true)]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

Then, you should be able to use the code from the post (again, LogIt is left to you to implement or whatever) to get things working.

If you have problems, post back with any details (complier errors, etc.).

Hope this helps!

Kind regards,

--molotov

Anonymous said...

that helps a lot.

many thanks

Anonymous said...

This call always returns IntPtr.Zero for me. Am I forgetting something?

IntPtr hWnd = GetOpenClipboardWindow();




I do have this

[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr GetOpenClipboardWindow();

[DllImport("user32.dll", SetLastError = true)]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

Thanks

Pete

«/\/\Ø|ö±ò\/»®© said...

Hi Pete,

As MSDN states for GetOpenClipboardWindow:
==
If no window has the clipboard open, the return value is NULL. To get extended error information, call GetLastError.
==
So, you could consider calling Marshal.GetLastWin32Error to see what the error may be. Note the following from the doc for Marshal.GetLastWin32Error:
==
You can only use this method to obtain error codes if you apply the System.Runtime.InteropServices.DllImportAttribute to the method signature and set the SetLastError field to true.
==

Hope this helps!

Kind regards,

--molotov

Colin said...

Excellent post on a frustrating issue.

It's a bit of a hack, but you can get around vmusrvc locking the clipboard by trying to copy, recycling vmusrvc on failure, and trying to copy again:

try
{

// try copying.
Clipboard.SetDataObject(data, true);

}
catch (ExternalException)
{

// recycle the vmusrvc process.
foreach (Process oldProcess in Process.GetProcessesByName("vmusrvc"))
{

string path = oldProcess.MainModule.FileName;

oldProcess.Kill();

Process newProcess = Process.Start(new ProcessStartInfo(path)
{
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
});

// wait for vmusrvc to start up and sync the clipboard from the host pc to the guest pc.
newProcess.WaitForInputIdle();

}

// set focus back to the main window using the win32 api.
NativeMethods.SetForegroundWindow(Process.GetCurrentProcess().MainWindowHandle);

// try copying again.
Clipboard.SetDataObject(data, true);

}

This works well enough for me. :)

Colin

Anonymous said...

Hi, i have the same issue here, the hwnd value is always zero, and the Marshal.GetLastWin32Error()
always returns 0x00000057

In the MS website this refers to:
0x00000057
ERROR_INVALID_PARAMETER
The parameter is incorrect.

Thanks,
-srinivas yelamanchili

«/\/\Ø|ö±ò\/»®© said...

Hi Srinivas,

>> Marshal.GetLastWin32Error() <<

Did you use DllImportAttribute, with SetLastError set to true?

Kind regards,

--molotov

Anonymous said...

yeah, i did.
My code looks like this:

Imports System.Runtime.InteropServices
Imports System.Runtime.InteropServices.DllImportAttribute

< DllImport("user32.dll", SetLastError:=True) > _
Private Shared Function GetOpenClipboardWindow() As Integer
End Function

< DllImport("user32.dll", SetLastError:=True) > _
Private Shared Function GetWindowThreadProcessId( _
ByVal hwnd As Long, _
ByRef lpdwProcessId As Long) As Long
End Function

Dim pid As Long
Dim tid As Long
Dim hWnd As Long
Dim pName As String
Dim exePath As String

Private Sub func1()

hWnd = GetOpenClipboardWindow()

If (IntPtr.Zero <> hWnd) Then
pid = 0
Try
tid = GetWindowThreadProcessId(hWnd, pid)
Catch extid As Exception

End Try

If (pid = 0) Then
Dim errorVal As Integer
errorVal = Marshal.GetLastWin32Error()
MsgBox(String.Format("Could not find Process ID error: 0x{0:X8}", errorVal))
End If

pName = Process.GetProcessById(Int(pid)).ProcessName
exePath = Process.GetProcessById(Int(pid)).MainModule.FileName

Else
If (hWnd = 0) Then
Dim errorVal As Integer
errorVal = Marshal.GetLastWin32Error()
MsgBox(String.Format("Could not find Tanks window error: 0x{0:X8}", errorVal))
End If

End If

End Sub

1)
When
Clipboard.SetDataObject
Clipboard. Clipboard.Clear
fails, i am calling the above code

however, the hWnd returned is always ZERO and the Marshal.GetLastWin32Error() returns 0x00000057
On Microsoft website this error code refers to:
ERROR_INVALID_PARAMETER
The parameter is incorrect.

2)
Only on one occassion i got a valid hWnd value, and the corresponding pid and tid,
but Process.GetProcessById(Int(pid)).MainModule.FileName
returns this error: 'Unable to enumerate the process modules'

I spent a lot of time finding solution with no luck yet. I tried declaring hWnd as long and integer with no difference.

3) Is there a simple vb.net code or ready application that i can use to give a list of all process id, names and their handle and hWnd ?
I can check for my hWnd (when i get one) from this list

Thanks,
-srinivas y

«/\/\Ø|ö±ò\/»®© said...

Perhaps it will be helpful to start with some basic verification. Try to emulate an application holding the clipboard open - perhaps, by creating an app that calls Clipboard.SetDataObject() in a tight loop. Then, run the code and see what results you get. If you receive the "Unable to enumerate process modules" message, note the PID, and ensure you're running the code as an Administrator.

Anonymous said...

Hi,
i found the solution finally.
The vb.net application that uses clipboard copy/paste functions need .net3.5sp1 installed. Only .net3.5 was installed on the machine where the clipboard operation was failing in this application.
On adding sp1 to this framework, the problem went away !

I therefore, no longer need code to check for programs that's locking the clipboard.

Thanks,
-srinivas y.