Mark van Renswoude
b08e7df883
VistaMan units; like XPMan but with various requested privilege levels
682 lines
17 KiB
ObjectPascal
682 lines
17 KiB
ObjectPascal
{
|
|
:: X2UtSingleInstance provides functions to detect previous instances of an
|
|
:: application and pass it the new command-line parameters.
|
|
::
|
|
:: Last changed: $Date$
|
|
:: Revision: $Rev$
|
|
:: Author: $Author$
|
|
}
|
|
unit X2UtSingleInstance;
|
|
|
|
interface
|
|
uses
|
|
Classes,
|
|
SysUtils,
|
|
Messages;
|
|
|
|
const
|
|
IM_COMMANDLINE = $00000001;
|
|
IM_APP = $00000100;
|
|
|
|
type
|
|
EInstanceNotActive = class(Exception);
|
|
EInstanceNoAppID = class(Exception);
|
|
|
|
|
|
{
|
|
:$ Notifier observer interface.
|
|
|
|
:: Applications that want to receive notifications on new instances must
|
|
:: implement this interface and call Attach(Instance).
|
|
}
|
|
IX2InstanceObserver = interface
|
|
['{4C435D46-6A7F-4CD7-9400-338E3E8FB5C6}']
|
|
procedure OnInstance(const ACmdLine: String);
|
|
end;
|
|
|
|
{
|
|
:$ Extended notifier observer interface.
|
|
|
|
:: Applications that want to receive custom notifications as well must
|
|
:: implement this interface and call Attach(Instance).
|
|
}
|
|
IX2InstanceObserverEx = interface(IX2InstanceObserver)
|
|
['{755A6548-3EA8-46C2-9FF5-FDE4BD67B699}']
|
|
procedure OnNotify(AMessage: Integer; const AData: String);
|
|
end;
|
|
|
|
|
|
{
|
|
:$ Internal file mapping layout.
|
|
}
|
|
PX2InstanceMapData = ^TX2InstanceMapData;
|
|
TX2InstanceMapData = record
|
|
RefCount: Integer;
|
|
Window: THandle;
|
|
end;
|
|
|
|
{
|
|
:$ Instance object.
|
|
|
|
:: Manages an instance. Instances are identified by an ApplicationID, which
|
|
:: must be unique. For simple single instance checking you can use the
|
|
:: SingleInstance wrapper function.
|
|
}
|
|
TX2Instance = class(TObject)
|
|
private
|
|
FActive: Boolean;
|
|
FApplicationID: String;
|
|
FFirstInstance: Boolean;
|
|
FGlobal: Boolean;
|
|
|
|
FFileMapData: PX2InstanceMapData;
|
|
FFileMapping: THandle;
|
|
FObservers: TInterfaceList;
|
|
FLocalIfGlobalFails: Boolean;
|
|
protected
|
|
function GetCount(): Integer; virtual;
|
|
procedure SetActive(const Value: Boolean); virtual;
|
|
procedure SetApplicationID(const Value: String); virtual;
|
|
procedure SetGlobal(const Value: Boolean); virtual;
|
|
procedure SetLocalIfGlobalFails(const Value: Boolean); virtual;
|
|
|
|
procedure WindowProc(var Message: TMessage); virtual;
|
|
|
|
property FileMapping: THandle read FFileMapping;
|
|
property FileMapData: PX2InstanceMapData read FFileMapData;
|
|
property Observers: TInterfaceList read FObservers;
|
|
public
|
|
constructor Create();
|
|
destructor Destroy(); override;
|
|
|
|
procedure Open(); virtual;
|
|
procedure Close(); virtual;
|
|
|
|
//:$ Sends a notification to the first instance.
|
|
//:! For custom messages you are recommended to start message IDs counting
|
|
//:! from IM_APP. Anything below is reserved for internal use.
|
|
procedure Notify(AMessage: Integer; const AData: String); virtual;
|
|
|
|
//:$ Registers the instance for notifications.
|
|
//:: If an application wants to be notified of new instances it must
|
|
//:: implement the IInstanceNotifier and register the interface using
|
|
//:: this function.
|
|
procedure Attach(const ANotifier: IX2InstanceObserver);
|
|
|
|
//:$ Unregisters a previously registered instance.
|
|
procedure Detach(const ANotifier: IX2InstanceObserver);
|
|
|
|
property Active: Boolean read FActive write SetActive;
|
|
property ApplicationID: String read FApplicationID write SetApplicationID;
|
|
property FirstInstance: Boolean read FFirstInstance;
|
|
property Global: Boolean read FGlobal write SetGlobal;
|
|
property LocalIfGlobalFails: Boolean read FLocalIfGlobalFails write SetLocalIfGlobalFails;
|
|
property Count: Integer read GetCount;
|
|
end;
|
|
|
|
{
|
|
:$ Checks for a previous instance of the application.
|
|
|
|
:: Returns False if a previous instance was found, True if this is the
|
|
:: first registered instance. ApplicationID must be unique to prevent
|
|
:: application conflicts, usage of a generated GUID is recommended.
|
|
::
|
|
:: If AGlobal is True, the check is performed system-wide. This only
|
|
:: affects Terminal Services and XP Fast User Switching sessions.
|
|
::
|
|
:: This function is a wrapper for the TX2Instance object. You can access
|
|
:: the created object through the Instance function.
|
|
|
|
:! Set ANotify to False if you're using SingleInstance in a console
|
|
:! application without a message loop.
|
|
:!
|
|
:! If AGlobal is True, ANotify only works if the previous instance was
|
|
:! started by the same user.
|
|
}
|
|
function SingleInstance(const AApplicationID: String;
|
|
ANotify: Boolean = True;
|
|
AGlobal: Boolean = True): Boolean;
|
|
|
|
{
|
|
:$ Returns a singleton TX2Instance object.
|
|
|
|
:: The object is automatically configured when using the SingleInstance
|
|
:: function.
|
|
}
|
|
function Instance(): TX2Instance;
|
|
|
|
|
|
{
|
|
:$ Registers the instance for notifications
|
|
|
|
:: Calls Attach on the singleton Instance.
|
|
}
|
|
procedure AttachInstance(const ANotifier: IX2InstanceObserver);
|
|
|
|
{
|
|
:$ Unregisters a previously registered instance.
|
|
|
|
:: Calls Detach on the singleton Instance.
|
|
}
|
|
procedure DetachInstance(const ANotifier: IX2InstanceObserver);
|
|
|
|
|
|
|
|
{
|
|
:$ Works like System.ParamCount, but uses the specified string instead
|
|
:$ of the actual command line.
|
|
}
|
|
function ParamCountEx(const ACmdLine: String): Integer;
|
|
|
|
{
|
|
:$ Works like System.ParamStr, but uses the specified string instead
|
|
:$ of the actual command line
|
|
}
|
|
function ParamStrEx(const ACmdLine: String; AIndex: Integer): String;
|
|
|
|
{
|
|
:$ Works like SysUtils.FindCmdLineSwitch, but uses the specified string
|
|
:$ instead of the actual command line
|
|
}
|
|
function FindCmdLineSwitchEx(const ACmdLine, ASwitch: String;
|
|
const AChars: TSysCharSet;
|
|
const AIgnoreCase: Boolean): Boolean; overload;
|
|
|
|
{
|
|
:$ Works like SysUtils.FindCmdLineSwitch, but uses the specified string
|
|
:$ instead of the actual command line
|
|
}
|
|
function FindCmdLineSwitchEx(const ACmdLine, ASwitch: String): Boolean; overload;
|
|
|
|
{
|
|
:$ Works like SysUtils.FindCmdLineSwitch, but uses the specified string
|
|
:$ instead of the actual command line
|
|
}
|
|
function FindCmdLineSwitchEx(const ACmdLine, ASwitch: String;
|
|
const AIgnoreCase: Boolean): Boolean; overload;
|
|
|
|
|
|
implementation
|
|
uses
|
|
Windows;
|
|
|
|
|
|
const
|
|
WindowClass = 'X2UtSingleInstance.Window';
|
|
SE_CREATE_GLOBAL_NAME = 'SeCreateGlobalPrivilege';
|
|
|
|
|
|
var
|
|
GlobalInstance: TX2Instance;
|
|
|
|
|
|
{$WARN SYMBOL_PLATFORM OFF}
|
|
|
|
|
|
|
|
{ TX2Instance }
|
|
constructor TX2Instance.Create();
|
|
begin
|
|
inherited;
|
|
|
|
FObservers := TInterfaceList.Create;
|
|
FLocalIfGlobalFails := True;
|
|
end;
|
|
|
|
destructor TX2Instance.Destroy();
|
|
begin
|
|
Active := False;
|
|
FreeAndNil(FObservers);
|
|
|
|
inherited;
|
|
end;
|
|
|
|
|
|
procedure TX2Instance.Notify(AMessage: Integer; const AData: String);
|
|
var
|
|
copyStruct: TCopyDataStruct;
|
|
|
|
begin
|
|
if not Active then
|
|
raise EInstanceNotActive.Create('Instance not Active');
|
|
|
|
if FileMapData^.Window = 0 then
|
|
Exit;
|
|
|
|
copyStruct.dwData := AMessage;
|
|
copyStruct.cbData := Length(AData);
|
|
copyStruct.lpData := PChar(AData);
|
|
|
|
SendMessage(FileMapData^.Window, WM_COPYDATA, 0, Integer(@copyStruct));
|
|
end;
|
|
|
|
|
|
procedure TX2Instance.Open();
|
|
|
|
procedure AdjustPrivileges();
|
|
var
|
|
tokenHandle: THandle;
|
|
luid: Int64;
|
|
newPrivileges: TTokenPrivileges;
|
|
returnLength: Cardinal;
|
|
|
|
begin
|
|
if OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES or
|
|
TOKEN_QUERY, tokenHandle) then
|
|
begin
|
|
if LookupPrivilegeValue(nil, SE_CREATE_GLOBAL_NAME, luid) then
|
|
begin
|
|
FillChar(newPrivileges, SizeOf(TTokenPrivileges), #0);
|
|
newPrivileges.PrivilegeCount := 1;
|
|
newPrivileges.Privileges[0].Luid := luid;
|
|
newPrivileges.Privileges[0].Attributes := SE_PRIVILEGE_ENABLED;
|
|
|
|
returnLength := 0;
|
|
AdjustTokenPrivileges(tokenHandle, False, newPrivileges,
|
|
SizeOf(TTokenPrivileges), nil, returnLength);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
const
|
|
ScopePrefix: array[Boolean] of String = ('Local\', 'Global\');
|
|
|
|
begin
|
|
if Active then
|
|
Exit;
|
|
|
|
if Length(ApplicationID) = 0 then
|
|
raise EInstanceNoAppID.Create('ApplicationID not specified');
|
|
|
|
FFirstInstance := True;
|
|
|
|
{ Attempt to get global privileges (required for Terminal Services and
|
|
Vista) }
|
|
if Global then
|
|
AdjustPrivileges;
|
|
|
|
{ Attempt to create shared memory }
|
|
SetLastError(0);
|
|
FFileMapping := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0,
|
|
SizeOf(TX2InstanceMapData),
|
|
PChar(ScopePrefix[Global] +
|
|
'SingleInstance.' + ApplicationID));
|
|
if FFileMapping = 0 then
|
|
begin
|
|
if Global and LocalIfGlobalFails then
|
|
begin
|
|
FGlobal := False;
|
|
Open();
|
|
Exit;
|
|
end else
|
|
RaiseLastOSError();
|
|
end;
|
|
|
|
FActive := True;
|
|
try
|
|
FFirstInstance := (GetLastError() <> ERROR_ALREADY_EXISTS);
|
|
FFileMapData := MapViewOfFile(FFileMapping, FILE_MAP_WRITE, 0, 0, 0);
|
|
|
|
if not Assigned(FFileMapData) then
|
|
RaiseLastOSError();
|
|
|
|
if FFirstInstance then
|
|
begin
|
|
FileMapData^.Window := CreateWindow(WindowClass, '', 0, 0, 0, 0, 0, 0,
|
|
0, SysInit.HInstance, nil);
|
|
if FileMapData^.Window = 0 then
|
|
RaiseLastOSError();
|
|
|
|
SetWindowLong(FileMapData^.Window, GWL_WNDPROC,
|
|
Integer(MakeObjectInstance(WindowProc)));
|
|
end;
|
|
|
|
Inc(FFileMapData^.RefCount);
|
|
except
|
|
Close();
|
|
end;
|
|
end;
|
|
|
|
procedure TX2Instance.Close();
|
|
begin
|
|
if not Active then
|
|
Exit;
|
|
|
|
if Assigned(FileMapData) then
|
|
begin
|
|
Dec(FileMapData^.RefCount);
|
|
if FirstInstance then
|
|
DestroyWindow(FileMapData^.Window);
|
|
|
|
UnmapViewOfFile(FileMapData);
|
|
end;
|
|
|
|
if FileMapping <> 0 then
|
|
CloseHandle(FileMapping);
|
|
|
|
FActive := False;
|
|
end;
|
|
|
|
|
|
procedure TX2Instance.Attach(const ANotifier: IX2InstanceObserver);
|
|
begin
|
|
if Observers.IndexOf(ANotifier) = -1 then
|
|
Observers.Add(ANotifier as IX2InstanceObserver);
|
|
end;
|
|
|
|
procedure TX2Instance.Detach(const ANotifier: IX2InstanceObserver);
|
|
begin
|
|
Observers.Remove(ANotifier as IX2InstanceObserver);
|
|
end;
|
|
|
|
|
|
procedure TX2Instance.WindowProc(var Message: TMessage);
|
|
var
|
|
copyData: PCopyDataStruct;
|
|
data: String;
|
|
observerIndex: Integer;
|
|
observerExIntf: IX2InstanceObserverEx;
|
|
|
|
begin
|
|
if Assigned(FileMapData) then
|
|
case Message.Msg of
|
|
WM_COPYDATA:
|
|
begin
|
|
copyData := PCopyDataStruct(Message.LParam);
|
|
data := '';
|
|
|
|
if copyData^.cbData > 0 then
|
|
SetString(data, PChar(copyData^.lpData), copyData^.cbData);
|
|
|
|
case copyData^.dwData of
|
|
IM_COMMANDLINE:
|
|
for observerIndex := 0 to Pred(Observers.Count) do
|
|
IX2InstanceObserver(Observers[observerIndex]).OnInstance(data);
|
|
else
|
|
for observerIndex := 0 to Pred(Observers.Count) do
|
|
if Supports(Observers[observerIndex], IX2InstanceObserverEx, observerExIntf) then
|
|
observerExIntf.OnNotify(copyData^.dwData, data);
|
|
end;
|
|
end;
|
|
else
|
|
Message.Result := DefWindowProc(FileMapData^.Window, Message.Msg,
|
|
Message.WParam, Message.LParam);
|
|
end;
|
|
end;
|
|
|
|
|
|
function TX2Instance.GetCount(): Integer;
|
|
begin
|
|
Result := 0;
|
|
if Active then
|
|
Result := FileMapData^.RefCount;
|
|
end;
|
|
|
|
procedure TX2Instance.SetActive(const Value: Boolean);
|
|
begin
|
|
if Value then
|
|
Open
|
|
else
|
|
Close;
|
|
end;
|
|
|
|
procedure TX2Instance.SetApplicationID(const Value: String);
|
|
var
|
|
wasActive: Boolean;
|
|
|
|
begin
|
|
if Value <> FApplicationID then
|
|
begin
|
|
wasActive := Active;
|
|
Active := False;
|
|
|
|
FApplicationID := Value;
|
|
|
|
Active := wasActive;
|
|
end;
|
|
end;
|
|
|
|
procedure TX2Instance.SetGlobal(const Value: Boolean);
|
|
var
|
|
wasActive: Boolean;
|
|
|
|
begin
|
|
if Value <> FGlobal then
|
|
begin
|
|
wasActive := Active;
|
|
Active := False;
|
|
|
|
FGlobal := Value;
|
|
|
|
Active := wasActive;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TX2Instance.SetLocalIfGlobalFails(const Value: Boolean);
|
|
begin
|
|
FLocalIfGlobalFails := Value;
|
|
end;
|
|
|
|
|
|
// Copied from System unit because Borland didn't make it public
|
|
function GetParamStr(P: PChar; var Param: String): PChar;
|
|
var
|
|
i, Len: Integer;
|
|
Start, S, Q: PChar;
|
|
begin
|
|
while True do
|
|
begin
|
|
while (P[0] <> #0) and (P[0] <= ' ') do
|
|
P := CharNext(P);
|
|
if (P[0] = '"') and (P[1] = '"') then Inc(P, 2) else Break;
|
|
end;
|
|
Len := 0;
|
|
Start := P;
|
|
while P[0] > ' ' do
|
|
begin
|
|
if P[0] = '"' then
|
|
begin
|
|
P := CharNext(P);
|
|
while (P[0] <> #0) and (P[0] <> '"') do
|
|
begin
|
|
Q := CharNext(P);
|
|
Inc(Len, Q - P);
|
|
P := Q;
|
|
end;
|
|
if P[0] <> #0 then
|
|
P := CharNext(P);
|
|
end
|
|
else
|
|
begin
|
|
Q := CharNext(P);
|
|
Inc(Len, Q - P);
|
|
P := Q;
|
|
end;
|
|
end;
|
|
|
|
SetLength(Param, Len);
|
|
|
|
P := Start;
|
|
S := Pointer(Param);
|
|
i := 0;
|
|
while P[0] > ' ' do
|
|
begin
|
|
if P[0] = '"' then
|
|
begin
|
|
P := CharNext(P);
|
|
while (P[0] <> #0) and (P[0] <> '"') do
|
|
begin
|
|
Q := CharNext(P);
|
|
while P < Q do
|
|
begin
|
|
S[i] := P^;
|
|
Inc(P);
|
|
Inc(i);
|
|
end;
|
|
end;
|
|
if P[0] <> #0 then P := CharNext(P);
|
|
end
|
|
else
|
|
begin
|
|
Q := CharNext(P);
|
|
while P < Q do
|
|
begin
|
|
S[i] := P^;
|
|
Inc(P);
|
|
Inc(i);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
Result := P;
|
|
end;
|
|
|
|
|
|
{ Single instance wrappers }
|
|
function SingleInstance(const AApplicationID: String;
|
|
ANotify, AGlobal: Boolean): Boolean;
|
|
var
|
|
newCmdLine: String;
|
|
dummy: String;
|
|
|
|
begin
|
|
with Instance do
|
|
begin
|
|
ApplicationID := AApplicationID;
|
|
Global := AGlobal;
|
|
Active := True;
|
|
|
|
Result := FirstInstance;
|
|
|
|
if (not Result) and ANotify then
|
|
begin
|
|
{ For compatibility with ParamStr(0), we'll modify the command-line to
|
|
include the full executable path. }
|
|
newCmdLine := '"' + ParamStr(0) + '" ' + GetParamStr(CmdLine, dummy);
|
|
Notify(IM_COMMANDLINE, newCmdLine);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function Instance(): TX2Instance;
|
|
begin
|
|
if not Assigned(GlobalInstance) then
|
|
GlobalInstance := TX2Instance.Create;
|
|
|
|
Result := GlobalInstance;
|
|
end;
|
|
|
|
procedure AttachInstance(const ANotifier: IX2InstanceObserver);
|
|
begin
|
|
Instance.Attach(ANotifier);
|
|
end;
|
|
|
|
procedure DetachInstance(const ANotifier: IX2InstanceObserver);
|
|
begin
|
|
Instance.Detach(ANotifier);
|
|
end;
|
|
|
|
|
|
{ Parameter helpers }
|
|
function ParamCountEx(const ACmdLine: String): Integer;
|
|
var
|
|
pCmdLine: PChar;
|
|
sParam: String;
|
|
|
|
begin
|
|
Result := 0;
|
|
pCmdLine := GetParamStr(PChar(ACmdLine), sParam);
|
|
|
|
while True do begin
|
|
pCmdLine := GetParamStr(pCmdLine, sParam);
|
|
|
|
if Length(sParam) = 0 then
|
|
break;
|
|
|
|
Inc(Result);
|
|
end;
|
|
end;
|
|
|
|
function ParamStrEx(const ACmdLine: String; AIndex: Integer): String;
|
|
var
|
|
pCmdLine: PChar;
|
|
|
|
begin
|
|
Result := '';
|
|
pCmdLine := PChar(ACmdLine);
|
|
while True do begin
|
|
pCmdLine := GetParamStr(pCmdLine, Result);
|
|
|
|
if (AIndex = 0) or (Length(Result) = 0) then
|
|
break;
|
|
|
|
Dec(AIndex);
|
|
end;
|
|
end;
|
|
|
|
|
|
function FindCmdLineSwitchEx(const ACmdLine, ASwitch: String;
|
|
const AChars: TSysCharSet;
|
|
const AIgnoreCase: Boolean): Boolean;
|
|
var
|
|
iParam: Integer;
|
|
sParam: String;
|
|
|
|
begin
|
|
for iParam := 1 to ParamCountEx(ACmdLine) do begin
|
|
sParam := ParamStrEx(ACmdLine, iParam);
|
|
|
|
if (AChars = []) or (sParam[1] in AChars) then
|
|
if AIgnoreCase then begin
|
|
if (AnsiCompareText(Copy(sParam, 2, Maxint), ASwitch) = 0) then begin
|
|
Result := True;
|
|
exit;
|
|
end;
|
|
end else begin
|
|
if (AnsiCompareStr(Copy(sParam, 2, Maxint), ASwitch) = 0) then begin
|
|
Result := True;
|
|
exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
Result := False;
|
|
end;
|
|
|
|
function FindCmdLineSwitchEx(const ACmdLine, ASwitch: String): Boolean;
|
|
begin
|
|
Result := FindCmdLineSwitchEx(ACmdLine, ASwitch, SwitchChars, True);
|
|
end;
|
|
|
|
function FindCmdLineSwitchEx(const ACmdLine, ASwitch: String;
|
|
const AIgnoreCase: Boolean): Boolean;
|
|
begin
|
|
Result := FindCmdLineSwitchEx(ACmdLine, ASwitch, SwitchChars, AIgnoreCase);
|
|
end;
|
|
|
|
|
|
|
|
var
|
|
wndClass: TWndClass;
|
|
|
|
initialization
|
|
{ Register window class }
|
|
FillChar(wndClass, SizeOf(wndClass), #0);
|
|
with wndClass do
|
|
begin
|
|
lpfnWndProc := @DefWindowProc;
|
|
hInstance := SysInit.HInstance;
|
|
lpszClassName := WindowClass;
|
|
end;
|
|
|
|
Windows.RegisterClass(wndClass);
|
|
|
|
finalization
|
|
Windows.UnregisterClass(WindowClass, SysInit.HInstance);
|
|
|
|
FreeAndNil(GlobalInstance);
|
|
|
|
end.
|