$theTitle=wp_title(" - ", false); if($theTitle != "") { ?>
About Virtualization, VDI, SBC, Application Compatibility and anything else I feel like
Yesterday I showed a video demonstrating it’s possible to run multiple instances of the Microsoft Lync 2010 client simultaneously.
A little warning before we go on: the Lync Client was not designed to run with multiple instances. Or better said: it was designed specifically to prevent this, let’s see how it does this:
On startup Lync calls an internal function called COcAppNoUI::InitializeMainInstance. In this function it creates a Mutex named “Office Communicator_” in the Global namespace. It also creates an Event in the Global namespace called “COMMUNICATOR-“.
When a second instance of Lync is launched it checks if the Global Mutex exists and if it does it fires the Global Event. The Main instance has a thread that waits for this event using the WaitForMultipleObjects API.
Let’s check this using Process Explorer, select the Lync process (communicator.exe) and display the process handles in the lower pane view:
If we close the handles to the Mutex and the Event (which is NOT recommended) we are able to launch a second instance of Lync.
But to get a fully working Lync Client we there a few more things we need to take care of:
Shared Memory Section
Lync creates a shared memory section named “MicrosoftOfficeCommunicatorSharedMemoryAccess”. Since we do not know what kind of data is exchanged in this shared section we must ensure that each instance has it’s own sections.
Additional Mutexes
Lync creates several additional Mutexes that need to be unique as well. Unfortunately the Mutex names are composed in a special way;
The first Mutex is named “<guid> – SID” eg: “eed3bd3a-a1ad-4e99-987b-d7cb3fcfa7f0 – S-1-5-21-1792247254-158287795-3068212004-1000”. The guid is obtained by calling the GetUserNameEx API with the NameUniqueId parameter. The SID is of course the SID of the logged-on user.
The second Mutex is named “Communicator.<#>_<SID>” eg: “Communicator.0_S-1-5-21-1792247254-158287795-3068212004-1000”
Registry
Lync stores it’s user settings in HKCU\Software\Microsoft\Communicator. Every instance will need it’s own settings or it will user (and overwrite) the settings of other users. This includes the sign-in address and the (encrypted) password.
Although I was able to run two instances of the Lync Client locally by closing the handles I decided the easiest solution was to use Application Virtualization. In this how-to I will use ThinApp.
ThinApp
To get the Lync Client to work with ThinApp we need to enable external out-of-process COM objects to run by adding the following line to the package.ini:
1 2 | [BuildOptions] VirtualizeExternalOutOfProcessCOM=0 |
Next we need to make sure that we isolate the Mutexes and Events by adding the following line to the package.ini:
1 2 | [BuildOptions] IsolatedSynchronizationObjects=*MicrosoftOfficeCommunicatorSharedMemoryAccess*;*COMMUNICATOR-*;*Office Communicator_*;*Communicator.*;* - S-1-5-21-* |
As you can see I used wildcards to match both Local and Global objects and to match the Guid and Sid.
ThinApp fail
ThinApp will now add the Sandbox Path to the object name, however for some reason it fails to do this for the “COMMUNICATOR-” event:
I tried several variation with wildcard but I didn’t get this thing to work. I presume this happens because the event name is not hardcoded but assembled from a dynamic string.
Delphi to the rescue
To solve this final step I wrote a small commandline tool in Delphi that searches for the Communicator.exe process and closes the handles to the “COMMUNICATOR-” event. I will go into details about the Delphi code below as it’s out of scope for this blog. If you have questions about the code please let me know.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | program CloseEvent; {$APPTYPE CONSOLE} uses JwaWinBase, JwaWinNT, JwaWinType, JwaNtStatus, JwaNative, JwaWinsta, SysUtils, StrUtils; {$IFDEF RELEASE} // Leave out Relocation Table in Release version {$SetPEFlags IMAGE_FILE_RELOCS_STRIPPED} {$ENDIF RELEASE} {$SetPEOptFlags IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE} // No need for RTTI {$WEAKLINKRTTI ON} {$RTTI EXPLICIT METHODS([]) PROPERTIES([]) FIELDS([])} const EventName = 'COMMUNICATOR-'; var dwPid: DWORD; hProcess: THandle; {$ALIGN 8} {$MINENUMSIZE 4} type _SYSTEM_HANDLE = record ProcessId: ULONG; ObjectTypeNumber: Byte; Flags: Byte; Handle: USHORT; _Object: PVOID; GrantedAccess: ACCESS_MASK; end; SYSTEM_HANDLE = _SYSTEM_HANDLE; PSYSTEM_HANDLE = ^SYSTEM_HANDLE; _SYSTEM_HANDLE_INFORMATION = record HandleCount: ULONG; Handles: array[0..0] of SYSTEM_HANDLE; end; SYSTEM_HANDLE_INFORMATION = _SYSTEM_HANDLE_INFORMATION; PSYSTEM_HANDLE_INFORMATION = ^SYSTEM_HANDLE_INFORMATION; _OBJECT_NAME_INFORMATION = record Length: USHORT; MaximumLength: USHORT; Pad: DWORD; Name: array[0..MAX_PATH-1] of Char; end; OBJECT_NAME_INFORMATION = _OBJECT_NAME_INFORMATION; POBJECT_NAME_INFORMATION = ^OBJECT_NAME_INFORMATION; function GetObjectName(const hObject: THandle): String; var oni: OBJECT_NAME_INFORMATION; cbSize: DWORD; nts: NTSTATUS; begin Result := ''; cbSize := SizeOf(oni) - (2 * SizeOf(USHORT)); oni.Length := 0; oni.MaximumLength := cbSize; nts := NtQueryObject(hObject, ObjectNameInformation, @oni, cbSize, @cbSize); if nts = STATUS_SUCCESS then begin Result := oni.Name; end; end; function GetCurrentSessionId: DWORD; asm mov eax,fs:[$00000018]; // Get TEB mov eax,[eax+$30]; // PPEB mov eax,[eax+$1d4]; // PEB.SessionId end; function GetProcessByName(const ProcessName: string): DWORD; var ProcName: PChar; Count: Integer; tsapi: PTS_ALL_PROCESSES_INFO_ARRAY; i: Integer; dwSessionId: DWORD; begin Result := 0; tsapi := nil; if not WinStationGetAllProcesses(SERVERNAME_CURRENT, 0, Count, tsapi) then Exit; ProcName := PChar(ProcessName); dwSessionId := GetCurrentSessionId; WriteLn(Format('Looking for Process %s in Session %d', [ProcessName, dwSessionId])); for i := 0 to Count - 1 do begin with tsapi^[i], tsapi^[i].pTsProcessInfo^ do begin if (dwSessionId = SessionId) and (ImageName.Buffer <> nil) and (StrIComp(ProcName, ImageName.Buffer) = 0) then begin Result := UniqueProcessId; WriteLn(Format('%s has Pid %d', [ProcessName, Result])); Break end; end; end; if tsapi <> nil then WinStationFreeGAPMemory(0, tsapi, Count); end; procedure EnumHandles; var shi: PSYSTEM_HANDLE_INFORMATION; cbSize: DWORD; cbRet: DWORD; nts: NTSTATUS; i: Integer; hDupHandle: THandle; dwErr: DWORD; ObjectName: string; begin WriteLn('Enumerating Handles'); cbSize := $5000; GetMem(shi, cbSize); repeat cbSize := cbSize * 2; ReallocMem(shi, cbSize); nts := NtQuerySystemInformation(SystemHandleInformation, shi, cbSize, @cbRet); until nts <> STATUS_INFO_LENGTH_MISMATCH; if nts = STATUS_SUCCESS then begin for i := 0 to shi^.HandleCount - 1 do begin if shi^.Handles[i].GrantedAccess <> $0012019f then begin if shi^.Handles[i].ProcessId = dwPid then begin nts := NtDuplicateObject(hProcess, shi^.Handles[i].Handle, GetCurrentProcess, @hDupHandle, 0, 0, 0); if nts = STATUS_SUCCESS then begin ObjectName := GetObjectName(hDupHandle); if (ObjectName <> '') and SameText(RightStr(ObjectName, Length(EventName)), EventName) then begin WriteLn(Format('Handle=%d Name=%s', [shi^.Handles[i].Handle, ObjectName])); CloseHandle(hDupHandle); nts := NtDuplicateObject(hProcess, shi^.Handles[i].Handle, GetCurrentProcess, @hDupHandle, 0, 0, DUPLICATE_CLOSE_SOURCE); if nts = STATUS_SUCCESS then begin WriteLn(Format('Duplicated Handle with DUPLICATE_CLOSE_SOURCE, new Handle=%d', [hDupHandle])); end; end; if hDupHandle > 0 then CloseHandle(hDupHandle); end; end; end; end; end else begin dwErr := RtlNtStatusToDosError(nts); WriteLn(Format('Failed to read handles, NtQuerySystemInformation failed with %.8x => %d (%s)', [nts, SysErrorMessage(dwErr)])); end; FreeMem(shi); end; procedure AnyKey(const bExit: Boolean = False); begin WriteLn('Finished'); WriteLn('Press any key to continue'); ReadLn; if bExit then Exit; end; begin try WriteLn('CloseEvent 1.0 (c) Remko Weijnen 2012'); WriteLn('(closes "COMMUNICATOR-" event handle)'); WriteLn(''); dwPid := GetProcessByName('Communicator.exe'); if dwPid = 0 then begin WriteLn('Process was not found, exiting.'); AnyKey(True); Exit; end; WriteLn(Format('Opening Process %d with PROCESS_DUP_HANDLE', [dwPid])); hProcess := OpenProcess(PROCESS_DUP_HANDLE, False, dwPid); if hProcess = 0 then begin WriteLn(Format('OpenProcess failed with %s', [SysErrorMessage(GetLastError)])); AnyKey(True); end else begin WriteLn(Format('Process Handle is %d', [hProcess])); end; EnumHandles; except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end. |
I then added a vb script to the ThinApp that calls the commandline tool:
1 2 3 4 5 6 7 8 9 | Function OnFirstParentStart Set WshShell = CreateObject("Wscript.Shell") strCmdLine = Chr(34) & ExpandPath("%ProgramFilesDir%\Microsoft Lync\CloseEvent.exe") & Chr(34) iPid = ExecuteVirtualProcess(strCmdLine) If iPid > 0 Then WaitForProcess iPid, 0 End If End Function |
It’s not ideal but it works
Registry
ThinApp will virtualize the Registry in the Sandbox by default so we don’t need to do anything there.
We can now run one instance of Lync natively and another instance ThinApped.
From my (limited) testing all functionalities seem to work including Instant Messaging and Lync Call but please do keep in mind that this method IS NOT SUPPORTED IN ANY WAY!
Download
I have added a download below for the package.ini I used, the KillMutex.vbs script and the CloseMutex commandline tool (including source).
DualLync (4169 downloads)
5 Responses for "Running multiple instances of Lync (howto)"
Hi Remko,
Does this method work with Office 2007 too? or can it be tweaked to make it work with office2007 somehow?
Dear Remko
So how do you execute the components in DualLync?
CloseEvent
KillMutex
Package
Regards
Dinos
Hi, have someone test it with Lync 2013?
Is this workaround still needed?
Best regards
Ralf
That is one fantastic solution!
Unfortunately our company migrated to Skype for Business 2016. From what I see only lync.exe process is running, no communicator.exe. I guess it’s too much work to patch the code to support newer clients?
I am planning to have a look when I find some time…
Leave a reply