Page 1 of 1
Only one instance: how to pass parameters?
Posted: Tue Nov 25, 2008 7:47 pm
by Trond
I only want to run one instance of my program, when the second instance is opened with a parameter (any parameter). I can do that with an api mutex.
Then I want to pass the parameters of the new instance to the old instance. How can I do that?
Posted: Tue Nov 25, 2008 8:31 pm
by tinman
You could use the mutex to discover whether the current process was the first instance. If it is, create a named pipe and check the read end of that in your main loop. If not, use CallNamedPipe_() and pass e.g. each ProgramParameter() concatenated in one block of memory.
Posted: Tue Nov 25, 2008 10:48 pm
by freak
If your program has a window, sending a WM_COPYDATA message to it is probably the simplest way.
Now you just have to find the target program's window. The PB IDE does this by registering a message with RegisterWindowMessage_() which is broadcast to all open windows (including the own window handle as parameter). The target IDE then responds back the sender window with its own handle. After that, the WM_COPYDATA message is sent, and the sender quits.
Posted: Tue Nov 25, 2008 11:39 pm
by blueznl
Multiple options... I tried shared memory and mailslots...
http://www.purebasic.fr/english/viewtop ... t=mailslot
Posted: Wed Nov 26, 2008 6:10 pm
by Trond
I'm trying to do this without too much ping-pong between the processes, and I thought this should do the trick. But when I run it, ReadFile_() hangs. Why?
Code: Select all
Global WM_ACTIVATEOLDINST
Procedure OnlyOneInstance()
Protected MutexName.s = #APPNAME + "_single_instance"
Protected Param.s
Protected hMutex
Protected hReadPipe, hWritePipe, Written
Protected BufferLen = 32*1024
WM_ACTIVATEOLDINST = RegisterWindowMessage_("WM_ACTIVATEOLDINST_" + #APPNAME)
hMutex = CreateMutex_(0, 0, @MutexName)
If GetLastError_() = #ERROR_ALREADY_EXISTS
Param = ProgramParameter()
Param = Left(Param, BufferLen-4)
CreatePipe_(@hReadPipe, @hWritePipe, 0, BufferLen)
WriteFile_(hWritePipe, @Param, Len(Param)+2, @Written, 0)
CloseHandle_(hWritePipe)
S.s + "hReadPipe: " + Str(hReadPipe) + #CRLF$
S + "Written: " + Str(Written) + #CRLF$
MessageRequester("", S)
SendMessage_(#HWND_BROADCAST, WM_ACTIVATEOLDINST, hReadPipe, Written)
CloseHandle_(hReadPipe)
End
EndIf
EndProcedure
Procedure ReceiveNewInstanceNotification(wParam, lParam)
; MessageRequester(Str(wParam), Str(lParam))
Protected File.s = Space(lParam/2)
Protected BytesRead
If wParam
If ReadFile_(wParam, @File, lParam, @BytesRead, 0) = 0
MessageRequester("", lasterrorstring())
EndIf
MessageRequester(#APPNAME, File)
; ; If FileSize(File) > 0
; ; PlayFile(PeekS(wParam))
; ; Else
; ; MessageRequester(#APPNAME, File)
; ; EndIf
EndIf
EndProcedure
Posted: Wed Nov 26, 2008 6:47 pm
by freak
The pipe handle is not valid inside the target process. Also the source process probably ends before the target one gets a chance to do the read in which case the pipe is destroyed as all valid handles get closed.
You could do it this way:
- broadcast a message with pipe handle + current process id
- write to pipe
- target process uses OpenProcess_() to get a process handle
- target process uses DuplicateHandle_() to get a valid local copy of the pipe handle
- target process reads pipe, closes handles and signals source process that it is done
- source process ends
Since the source process has to stay active at least until the DuplicateHandle_() succeeded to keep the pipe alive, you won't get around to have some way to tell the source process when it can actually end.
You can get around the duplication problem by using named pipes (not implemented on Win9x!), but you still have the problem
that you have to wait until the target process has an open handle before closing the source, as else the pipe is destroyed again.
All in all, i think a little back and forth of window messages is the simpler solution.
Posted: Thu Nov 27, 2008 11:31 pm
by tinman
You've maybe already got a solution, but I hacked up an example that uses pipes for completion. I didn't realise that they were blocking and you always had to use overlapped IO, so it's probably more awkward than freak's method.
Anyway, here it is. It seems to work, but you'd want to test it fully. Buffers and strings are fixed. You could also use the pipe instead of the single instance mutex apparently.
Code: Select all
EnableExplicit
Define event
Define quit : quit = 0
Define hMutex
Define.s InstanceMutexName = "PB_Instance_Mutex"
Define hPipe
Define.s InstancePipeName = "\\.\pipe\PB_Instance_Pipe"
Define hClientConnected
Define.OVERLAPPED ConnectionOverlap
Define hParametersRead
Define.OVERLAPPED ReadOverlap
Define wait_result
Dim wait_handles(2)
Dim buffer.c(1024)
Procedure SendProgramParameters()
Shared InstancePipeName
Protected hWritePipe
Protected got_pipe
Protected pipe_mode.l
Protected written.l
Protected message.s = "Message from second instance"
Repeat
hWritePipe = CreateFile_(@InstancePipeName, #GENERIC_WRITE, 0, #Null, #OPEN_EXISTING, 0, #Null)
If hWritePipe = #INVALID_HANDLE_VALUE
If WaitNamedPipe_(@InstancePipeName, #NMPWAIT_WAIT_FOREVER) = 0
Debug "Could not connect to pipe"
got_pipe = #True ; Not got a pipe, but we'll use the invalid handle
EndIf
Else
got_pipe = #True
EndIf
Until got_pipe
If hWritePipe <> #INVALID_HANDLE_VALUE
pipe_mode = #PIPE_READMODE_MESSAGE
If SetNamedPipeHandleState_(hWritePipe, @pipe_mode, #Null, #Null)
If WriteFile_(hWritePipe, @message, (Len(message) + 1) * SizeOf(Character), @written, #Null)
Debug "Message sent OK"
Else
Debug "Failed to write message"
EndIf
Else
Debug "Failed to set pipe handle state"
EndIf
CloseHandle_(hWritePipe)
EndIf
EndProcedure
Procedure AcceptConnections()
Shared hPipe
Shared hClientConnected
Shared ConnectionOverlap
Protected connected
Protected transferred.l
If GetOverlappedResult_(hPipe, @ConnectionOverlap, @transferred, #False) <> 0
ResetEvent_(hClientConnected)
ConnectionOverlap\Internal = 0
ConnectionOverlap\InternalHigh = 0
ConnectionOverlap\Offset = 0
ConnectionOverlap\OffsetHigh = 0
ConnectionOverlap\hEvent = hClientConnected
connected = ConnectNamedPipe_(hPipe, @ConnectionOverlap)
EndIf
ProcedureReturn connected
EndProcedure
Procedure OnlyOneInstance()
Shared hMutex
Shared InstanceMutexName
Shared hClientConnected
Shared hPipe
Shared InstancePipeName
Shared hParametersRead
Protected last_error
Protected exit_now
hMutex = CreateMutex_(#Null, #False, @InstanceMutexName)
last_error = GetLastError_()
If last_error = #ERROR_ALREADY_EXISTS
SendProgramParameters()
exit_now = #True
ElseIf last_error = #ERROR_SUCCESS
hClientConnected = CreateEvent_(#Null, #True, #False, #Null)
hParametersRead = CreateEvent_(#Null, #True, #False, #Null)
hPipe = CreateNamedPipe_(@InstancePipeName, #PIPE_ACCESS_INBOUND|#FILE_FLAG_OVERLAPPED, #PIPE_TYPE_MESSAGE|#PIPE_READMODE_MESSAGE|#PIPE_WAIT, 1, 1024, 1024, 1000, #Null)
If hPipe = #INVALID_HANDLE_VALUE
Debug "Error creating pipe: 0x" + RSet(Hex(GetLastError_()), 8, "0")
Else
AcceptConnections()
EndIf
EndIf
ProcedureReturn exit_now
EndProcedure
Procedure BeginReadSecondInstanceParameters()
Shared hPipe
Shared ReadOverlap
Shared hParametersRead
Shared buffer()
Protected bytes_read.l
ReadOverlap\Internal = 0
ReadOverlap\InternalHigh = 0
ReadOverlap\Offset = 0
ReadOverlap\OffsetHigh = 0
ReadOverlap\hEvent = hParametersRead
If ReadFile_(hPipe, @buffer(0), 1024, @bytes_read, @ReadOverlap) =0
Debug "Failed to read from pipe"
EndIf
EndProcedure
Procedure EndReadSecondInstanceParameters()
Shared hParametersRead
Shared buffer()
Debug "Read from second instance: "
Debug PeekS(@buffer(0))
ResetEvent_(hParametersRead)
EndProcedure
Procedure DisconnectPipeClient(hPipe)
FlushFileBuffers_(hPipe)
DisconnectNamedPipe_(hPipe)
CloseHandle_(hPipe)
EndProcedure
If Not OnlyOneInstance()
If OpenWindow(0, 0, 0, 800, 600, "foo", #PB_Window_SystemMenu|#PB_Window_MinimizeGadget|#PB_Window_MaximizeGadget|#PB_Window_SizeGadget|#PB_Window_TitleBar|#PB_Window_ScreenCentered)
While Not(quit)
; This is a bit poor. We could maintain a state and re-use one event for each pipe instance
; as has been done in the Windows SDK example for a multithreaded named pipe server
wait_handles(0) = hClientConnected
wait_handles(1) = hParametersRead
wait_result = MsgWaitForMultipleObjects_(2, @wait_handles(0), #False, #INFINITE, #QS_ALLINPUT)
If wait_result = #WAIT_OBJECT_0
BeginReadSecondInstanceParameters()
ResetEvent_(hClientConnected)
ElseIf wait_result = (#WAIT_OBJECT_0+1)
EndReadSecondInstanceParameters()
DisconnectPipeClient(hPipe)
AcceptConnections()
ResetEvent_(hParametersRead)
ElseIf wait_result = (#WAIT_OBJECT_0+2)
event = WindowEvent()
While event <> 0
Select event
Case #PB_Event_CloseWindow
quit = 1
EndSelect
event = WindowEvent()
Wend
EndIf
Wend
CloseWindow(0)
EndIf
EndIf
End
It only outputs to the debug just now so you'd need to run two copies from two PB IDEs to see all the strings.
Posted: Fri Nov 28, 2008 10:52 am
by Trond
freak wrote:The pipe handle is not valid inside the target process. Also the source process probably ends before the target one gets a chance to do the read in which case the pipe is destroyed as all valid handles get closed.
You could do it this way:
- broadcast a message with pipe handle + current process id
- write to pipe
- target process uses OpenProcess_() to get a process handle
- target process uses DuplicateHandle_() to get a valid local copy of the pipe handle
- target process reads pipe, closes handles and signals source process that it is done
- source process ends
Thanks, although I still can't make it work properly.
Since the source process has to stay active at least until the DuplicateHandle_() succeeded to keep the pipe alive, you won't get around to have some way to tell the source process when it can actually end.
The source process is blocked in SendMessage() until the target has processed the message completely.
Posted: Fri Nov 28, 2008 11:29 am
by Trond
I caved in and went for a simpler solution.
OnlyOneInstance() is called at startup. ReceiveNewInstanceNotification() is called in my window callback as a response to WM_ACTIVATEOLDINST.
Code: Select all
Global WM_ACTIVATEOLDINST
#MutexName = #APPNAME + "_single_instance"
Procedure OnlyOneInstance()
Protected MutexName.s = #MutexName
Protected hMutex
Protected File
WM_ACTIVATEOLDINST = RegisterWindowMessage_("WM_ACTIVATEOLDINST_" + #APPNAME)
hMutex = CreateMutex_(0, 0, @MutexName)
If GetLastError_() = #ERROR_ALREADY_EXISTS
hMutex = OpenMutex_(0, 0, @MutexName)
If WaitForSingleObject_(hMutex, 1000) = #WAIT_FAILED
End
EndIf
File = CreateFile(#PB_Any, GetTemporaryDirectory() + MutexName)
For I = 1 To CountProgramParameters()
WriteStringN(File, ProgramParameter())
Next
CloseFile(File)
SendMessage_(#HWND_BROADCAST, WM_ACTIVATEOLDINST, 0, 0)
ReleaseMutex_(hMutex)
End
EndIf
EndProcedure
Procedure ReceiveNewInstanceNotification(wParam, lParam)
Protected File
File = ReadFile(#PB_Any, GetTemporaryDirectory() + #MutexName)
A.s = ReadString(File)
CloseFile(File)
MessageRequester(#APPNAME, A)
EndProcedure
Posted: Fri Nov 28, 2008 1:57 pm
by freak
Not a bad solution imho.
One thing: If you get #ERROR_ALREADY_EXISTS from CreateMutex_() then you still have a valid handle to the mutex, so the second OpenMutex_() is not needed. (you now opened it twice)
Posted: Fri Nov 28, 2008 3:19 pm
by srod
Aye that's a nice solution indeed; thanks Trond.
Posted: Fri Nov 28, 2008 3:47 pm
by Joakim Christiansen
My lousy media player shit has this (open source):
http://jlc-software.com/programs/jmp_source.zip
Posted: Fri Nov 28, 2008 9:50 pm
by Trond
freak wrote:Not a bad solution imho.
One thing: If you get #ERROR_ALREADY_EXISTS from CreateMutex_() then you still have a valid handle to the mutex, so the second OpenMutex_() is not needed. (you now opened it twice)
Thanks for the information.
ReadProcessMemory_(), I didn't think of that.
Edit: by the way, this is a media player too.
