Modify

Opened 8 years ago

Closed 8 years ago

Last modified 2 months ago

#2319 closed Bug (Works For Me)

_Singleton is not working at all

Reported by: Eugenii Owned by:
Milestone: Component: AutoIt
Version: 3.3.8.1 Severity: None
Keywords: singleton Cc:

Description

_Singleton doesn't return 0, if there are more than one instance of script. There is no difference for compiled or .au3 version. Example from help also don't work.
Win 7 x64 Home Pro SP1

Attachments (0)

Change History (5)

comment:1 follow-up: Changed 8 years ago by guinness

  • Resolution set to Works For Me
  • Status changed from new to closed

The example in the help file worked under Windows 7 x64 SP1 and XP SP3. Please use the Forum if you're unclear of how to use the function.

Last edited 8 years ago by guinness (previous) (diff)

comment:2 Changed 8 years ago by ifbhaamw <sample@…>

1

comment:3 Changed 8 years ago by ifbhaamw <sample@…>

1

comment:4 in reply to: ↑ 1 Changed 8 years ago by ifbhaamw <sample@…>

1

comment:5 Changed 2 months ago by mbit90

Please re-open this ticket as the function clearly has a bug that makes it unreliable.

In certain situations, _Singleton returns the Mutex handle instead of 0 when an instance is already running.

I can reproduce the issue reliably, but only in compiled scripts.
I'm using the current stable Version of AutoIt 3.3.14.5 on Win7 64-bit.

The example from the help file is sufficient to reproduce the issue, so I will simply cite it here as a minimal example:

#include <Misc.au3>
#include <MsgBoxConstants.au3>

If _Singleton("test", 1) = 0 Then
    MsgBox($MB_SYSTEMMODAL, "Warning", "An occurrence of test is already running")
    Exit
EndIf
MsgBox($MB_SYSTEMMODAL, "OK", "the first occurrence of test is running")

I think I have found the root cause of the issue.
From the source code of _Singleton you can see that the function creates a system-wide named Mutex with a DllCall to CreateMutexW (kernel32.dll).
Then it tests whether GetLastError (another DllCall to kernel32.dll) returns $ERROR_ALREADY_EXISTS (183). If so, the Mutex already existed and we know we're not the first instance. This bit of logic fails for me in compiled scripts, because GetLastError returns $ERROR_MOD_NOT_FOUND (126) (The specified module could not be found), regardless of which instance we're at. So the function always returns 0 and programs using it must assume they are the single instance.

My guess it that, in compiled scipts, DllCalls with the DLL given as string name ("kernel32.dll"), as done in _Singleton, all fail internally at first. Then the runtime (silently) re-opens the DLL and performs the call successfully, but the Last Error variable has been set to $ERROR_MOD_NOT_FOUND. That means calling GetLastError will always return $ERROR_MOD_NOT_FOUND because this call itself will pollute the Last Error variable.
I changed the DllCalls in _Singleton to use a handle to kernel32.dll which I opened beforehand, and the bug went away (i.e. GetLastError returned $ERROR_ALREADY_EXISTS correctly and _Singleton performed as expected in compiled as well as non-compiled scripts).

However, since I don't really know how AutoIt handles DllCalls internally, and whether the runtime may do its own DLL calls between script statements and as such pollute the Last Error variable, I propose a modified solution that does not rely on GetLastError. It could serve as a direct replacement for the current function in the Misc UDF:

; #FUNCTION# ====================================================================================================================
; Author ........: Valik
; Modified.......: MBit90
; ===============================================================================================================================
Func _Singleton($sOccurrenceName, $iFlag = 0)
	Local Const $WAIT_ABANDONED = 0x00000080 ; Mutex' original owner closed and did not release it. Now we own the mutex.
	Local Const $WAIT_OBJECT_0  = 0x00000000 ; Mutex is signaled (in this case, we own it already)
	Local Const $WAIT_TIMEOUT   = 0x00000102 ; Wait timed out (in our case, we're not the owner, i.e. not the first instance)
	Local Const $SECURITY_DESCRIPTOR_REVISION = 1
	Local $tSecurityAttributes = 0

	If BitAND($iFlag, 2) Then
		; The size of SECURITY_DESCRIPTOR is 20 bytes.  We just
		; need a block of memory the right size, we aren't going to
		; access any members directly so it's not important what
		; the members are, just that the total size is correct.
		Local $tSecurityDescriptor = DllStructCreate("byte;byte;word;ptr[4]")
		; Initialize the security descriptor.
		Local $aRet = DllCall("advapi32.dll", "bool", "InitializeSecurityDescriptor", _
				"struct*", $tSecurityDescriptor, "dword", $SECURITY_DESCRIPTOR_REVISION)
		If @error Then Return SetError(@error, @extended, 0)
		If $aRet[0] Then
			; Add the NULL DACL specifying access to everybody.
			$aRet = DllCall("advapi32.dll", "bool", "SetSecurityDescriptorDacl", _
					"struct*", $tSecurityDescriptor, "bool", 1, "ptr", 0, "bool", 0)
			If @error Then Return SetError(@error, @extended, 0)
			If $aRet[0] Then
				; Create a SECURITY_ATTRIBUTES structure.
				$tSecurityAttributes = DllStructCreate($tagSECURITY_ATTRIBUTES)
				; Assign the members.
				DllStructSetData($tSecurityAttributes, 1, DllStructGetSize($tSecurityAttributes))
				DllStructSetData($tSecurityAttributes, 2, DllStructGetPtr($tSecurityDescriptor))
				DllStructSetData($tSecurityAttributes, 3, 0)
			EndIf
		EndIf
	EndIf

	; Create a Mutex with the specified name. If it already exists, open the mutex. Returns a handle to the mutex object.
	Local $aHandle = DllCall("kernel32.dll", "handle", "CreateMutexW", "struct*", $tSecurityAttributes, "bool", 1, "wstr", $sOccurrenceName)
	If @error Then Return SetError(@error, @extended, 0)
	If $aHandle[0] = 0 Then Return SetError(10, 0, 0) ; CreateMutexW failed for unknown reasons.

	; Wait on the Mutex (with timeout=0) that we either created (1st instance) or opened (nth instance).
	; If we're the 1st instance, we own the Mutex since we specified bInitialOwner=True. WaitForSingleObject returns $WAIT_OBJECT_0.
	; If the 1st instance has subsequently closed and we're the single instance now, WaitForSingleObject returns $WAIT_ABANDONED.
	; If another instance owns the Mutex, WaitForSingleObject returns $WAIT_TIMEOUT.
	Local $aWait = DllCall("kernel32.dll", "dword", "WaitForSingleObject", "handle", $aHandle[0], "dword", 0)
	If @error Then Return SetError(@error, @extended, 0)
	Switch $aWait[0]
		Case $WAIT_TIMEOUT
			If BitAND($iFlag, 1) Then
				DllCall("kernel32.dll", "bool", "CloseHandle", "handle", $aHandle[0])
				If @error Then Return SetError(@error, @extended, 0)
				Return 0
			Else
				Exit -1
			EndIf
		Case $WAIT_OBJECT_0, $WAIT_ABANDONED
			Return $aHandle[0]
		Case Else
			Return SetError(11, 0, 0) ; WaitForSingleObject failed for unknown reasons.
	EndSwitch
EndFunc   ;==>_Singleton

N.B: If my above assumption about DllCalls in compiled scripts is true, _WinAPI_GetLastError would also have to be rewritten, as well as any other UDFs doing their own error checking with DllCall("kernel32.dll", "dword", "GetLastError").

Alternatively, DllCall could be modified to save the Last Error variable internally, and restore it after performing the call.

Guidelines for posting comments:

  • You cannot re-open a ticket but you may still leave a comment if you have additional information to add.
  • In-depth discussions should take place on the forum.

For more information see the full version of the ticket guidelines here.

Add Comment

Modify Ticket

Action
as closed The ticket will remain with no owner.
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.