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. :wink:

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.
Joakim Christiansen wrote:My lousy media player shit has this (open source):
http://jlc-software.com/programs/jmp_source.zip
ReadProcessMemory_(), I didn't think of that.

Edit: by the way, this is a media player too. :o