Only one instance: how to pass parameters?

Just starting out? Need help? Post your questions and find answers here.
Trond
Always Here
Always Here
Posts: 7446
Joined: Mon Sep 22, 2003 6:45 pm
Location: Norway

Only one instance: how to pass parameters?

Post 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?
User avatar
tinman
PureBasic Expert
PureBasic Expert
Posts: 1102
Joined: Sat Apr 26, 2003 4:56 pm
Location: Level 5 of Robot Hell
Contact:

Post 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.
If you paint your butt blue and glue the hole shut you just themed your ass but lost the functionality.
(WinXPhSP3 PB5.20b14)
freak
PureBasic Team
PureBasic Team
Posts: 5948
Joined: Fri Apr 25, 2003 5:21 pm
Location: Germany

Post 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.
quidquid Latine dictum sit altum videtur
User avatar
blueznl
PureBasic Expert
PureBasic Expert
Posts: 6172
Joined: Sat May 17, 2003 11:31 am
Contact:

Post by blueznl »

Multiple options... I tried shared memory and mailslots...

http://www.purebasic.fr/english/viewtop ... t=mailslot
( PB6.00 LTS Win11 x64 Asrock AB350 Pro4 Ryzen 5 3600 32GB GTX1060 6GB - upgrade incoming...)
( The path to enlightenment and the PureBasic Survival Guide right here... )
Trond
Always Here
Always Here
Posts: 7446
Joined: Mon Sep 22, 2003 6:45 pm
Location: Norway

Post 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
freak
PureBasic Team
PureBasic Team
Posts: 5948
Joined: Fri Apr 25, 2003 5:21 pm
Location: Germany

Post 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.
quidquid Latine dictum sit altum videtur
User avatar
tinman
PureBasic Expert
PureBasic Expert
Posts: 1102
Joined: Sat Apr 26, 2003 4:56 pm
Location: Level 5 of Robot Hell
Contact:

Post 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.
If you paint your butt blue and glue the hole shut you just themed your ass but lost the functionality.
(WinXPhSP3 PB5.20b14)
Trond
Always Here
Always Here
Posts: 7446
Joined: Mon Sep 22, 2003 6:45 pm
Location: Norway

Post 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.
Trond
Always Here
Always Here
Posts: 7446
Joined: Mon Sep 22, 2003 6:45 pm
Location: Norway

Post 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
freak
PureBasic Team
PureBasic Team
Posts: 5948
Joined: Fri Apr 25, 2003 5:21 pm
Location: Germany

Post 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)
quidquid Latine dictum sit altum videtur
srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Post by srod »

Aye that's a nice solution indeed; thanks Trond.
I may look like a mule, but I'm not a complete ass.
User avatar
Joakim Christiansen
Addict
Addict
Posts: 2452
Joined: Wed Dec 22, 2004 4:12 pm
Location: Norway
Contact:

Post by Joakim Christiansen »

My lousy media player shit has this (open source):
http://jlc-software.com/programs/jmp_source.zip
I like logic, hence I dislike humans but love computers.
Trond
Always Here
Always Here
Posts: 7446
Joined: Mon Sep 22, 2003 6:45 pm
Location: Norway

Post 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
Post Reply