[BMX64] One instance of an app only

BlitzMax Forums/BlitzMax Programming/[BMX64] One instance of an app only

Grisu(Posted 2015) [#1]
Hi guys,

can someone point me to a working code that limits the execution of an exe file to just one instance.

I tried doing this via a simple ini-file setting. But If my app crashes (which should seldom happen), it can't be run again.

Would be great to offer such a feature, so that users can en-/disable the behavior "on the fly".

Grisu


grable(Posted 2015) [#2]
You can do it with a mutex, there is some old windows only code for this in maxide-ce http://sourceforge.net/p/blitzmax-ide/code/HEAD/tree/singleinstance.bmx


xlsior(Posted 2015) [#3]
Another option is to query your app for its name immediate after launch:
Print appargs[0]

Then query the list of running processes, and if you see more than one instance with that name in the list, immediately exit the program.

Unexpected crashes won't leave lock files which would prevent other instances.


Kryzon(Posted 2015) [#4]
You can do that with a named mutex like grable mentioned OR a shared memory object (either of these options work). These are all part of "interprocess communication."
You know that there's already another instance running when you can't map a shared memory object for writing, or lock the named mutex.

You can use bah.Interprocess:
https://github.com/maxmods/bah.mod/tree/master/interprocess.mod/examples


xlsior(Posted 2015) [#5]
Shared memory between processes:

' By Budman
' Simple program to demonstrate sharing memory between process
' compile as console program and run one or more times the first run sets the 
' shared memory
' additonal runs read it

SuperStrict
Extern "Win32"
    Function CloseHandle:Int(Handle:Int)
	Function MapViewOfFile:Byte Ptr(hFileMappingObject: Int,dwDesiredAccess: Int, dwFileOffsetHigh:Int, dwFileOffsetLow:Int, dwNumberOfBytesToMap:Int)
	Function UnmapViewOfFile:Int(lpBaseAddress: Byte Ptr)
	Function CreateFileMappingA:Int(hFile: Int, lpFileMappingAttributes: Byte Ptr,flProtect:Int, dwMaximumSizeHigh:Int, dwMaximumSizeLow:Int, lpName$z)
	Function GetLastError:Int()
End Extern

Const PAGE_READWRITE : Int=4
Const FILE_MAP_WRITE : Int=2

Type TSharedMemory 
	Field _Handle : Int
	Field _Name   : String
	Field _Size   : Int
    Field _Owner  : Int
	Field _Data   : Byte Ptr

   
	Method Delete()
		Close
	End Method
	Method Open:Int(Name: String, Size: Int)
		Close()
   		_Name = Name
    	_Size = Size
       ' CreateFileMapping, when called with $FFFFFFFF For the hanlde value, creates a region of shared memory }
   		_Handle = CreateFileMappingA($FFFFFFFF, Null, PAGE_READWRITE, 0,_Size,_Name)
    	If _Handle = 0 Then Return False
        _Owner = GetLastError() = 0
       ' We still need To map a pointer To the handle of the shared memory region 
        _Data= MapViewOfFile(_Handle, FILE_MAP_WRITE, 0, 0, _Size)
    	If Not _Data
			Close
			Return False
		End If
		Return True
	End Method 
	Method Close()
  		If _Data
	    	UnmapViewOfFile(_Data);
			_Data=Null
		End If
		If _Handle 
			CloseHandle(_Handle)
			_Handle=0
		End If 		
	End Method
End Type

Print "Test Share Mem"
Local mem:TSharedMemory = New TSharedMemory 


mem.Open("TESTMEM",30)

If mem._Owner
	MemClear mem._Data,30
	Local title:String="Start Blank!"
	Local pTitle:Byte Ptr=title.toCString()
	MemCopy mem._Data,pTitle,title.length+1
	MemFree pTitle
End If 
	
Repeat
If mem._Owner
	
	Print "Getting Title (Owner)"
	Local title : String = string.FromCString(mem._Data)		
	Print "The Title is "+title
	GCCollect()
Else
	Print "Setting Title (Guest)"
	MemClear mem._Data,30
	SeedRnd MilliSecs() 

	Local title:String=Rand(0,100000)
	Local pTitle:Byte Ptr=title.toCString()
	MemCopy mem._Data,pTitle,title.length+1
	MemFree pTitle
	GCCollect()
	mem.close
	End
'	Print "Getting Title"
'	Local title : String = string.FromCString(mem._Data)		
'	Print "The Title is "+title
End If
'Input
'Delay 2000

Forever
mem.Close



Grisu(Posted 2015) [#6]
I'd love to keep the footprint of my app light. Using a mutex forces an app to compile in multi-threaded mode, right?

Will try out grable's code example and see how it goes.


Kryzon(Posted 2015) [#7]
No, threaded-mode I think just uses a different code path in BMax modules, but you can for example use the OS API for threads in a non-threaded BMax build.

If you're going cross platform, there seems to be this lightweight library that supports named mutexes for this single instance stuff:
https://github.com/cubiclesoft/cross-platform-cpp

You would just need to interface it with your BMax code by making use of C-style externs.
Since it's an external library, building in the threaded or non-threaded mode would make no difference.


Grisu(Posted 2016) [#8]
Hello again!

I'm currently using the code example form here: http://sourceforge.net/p/blitzmax-ide/code/HEAD/tree/singleinstance.bmx

With Brucey's latest BMX 64 release, it stopped working. :(

Has anyone a code that is compatible with the latest version (and if possible, compiles in non-threaded mode)? As I dropped Linux and Mac support. This only needs to work under Windows.

Grisu


degac(Posted 2016) [#9]
Hi
I just looked at the source, I think you need to update some refers...

in pub.mod/kernel32.bmx Brucey had changed many things

Function GlobalAlloc:Byte Ptr(uFlags,dwBytes) '<... before it was an Int
Function GlobalSize(hMem:Byte Ptr)
Function GlobalFree(hMem:Byte Ptr)
Function GlobalLock:Byte Ptr(hMem:Byte Ptr)
Function GlobalUnlock(hMem:Byte Ptr)


After I changed to
Local hdrop:Byte Ptr = GlobalAlloc( GHND | GMEM_SHARE, sz)

there's no more errore (here!), now there are error in other part!

The source code for IDE was written with WinXP/32 in mind...


degac(Posted 2016) [#10]
Well, I tried to change step by step every single error reported (the error message is very handy! Brucey did a good job, you can find easy what could be the possible error...)

One thing I found is that many 'extern' functions seems already defined (but with other return-type or parameter-type) in some BMX module. I changed all the name in singleinstance.bmx (I added an X at GetClassNameW for example) and changed parameter where required.

The only problem is that I found many errors like the following

Duplicate identifier 'addgadgetitem' found in module 'maxgui.maxgui' and module 'm_locale_language'.


And I think it's a problem with the translator/compiler.
I post this (and other) on the proper forum.


Brucey(Posted 2016) [#11]
Hallo :-)

I've updated pub.win32 with some of those missing APIs.
This is singleinstance.bmx hacked around a bit to work with pointers :

It compiles okay, but I'm not sure about the file drop message stuff at the end. Might need done properly (as an HDROP type?)
I used a size_t type for the enumdata array so that it can scale properly for 32-bit and 64-bit.
I also added "win32" to the EnumProc() callback function so that the compiler produces the correct stdcall code for it.


dw817(Posted 2016) [#12]
Hi Grisu:

I gave a brief thought about you opening a dummy file, and YES, if it crashed, it would be a problem. But with THIS program even if the executable crashes, it will work correctly and cannot hang or incorrectly think another task is open under any circumstances.

So - here is some code I wrote to determine if it's a first run, and it's small too. :)



Please let me know if this covers what you need.


BlitzSupport(Posted 2016) [#13]
I think a 'lockfile' approach like dw817's here would probably be the easiest cross-platform option.

I've used a similar setup on a Linux server, via PHP, and it seemed to work very well: incoming Monkey source code sent from a web-based editor would lock an empty file (waiting for lock if currently in-use), build a HTML5 project in a single folder (which was the resource to be protected from shared usage), send the .js back to the requester, then unlock the build file, all of which took a fraction of a second.


xlsior(Posted 2016) [#14]
I think a 'lockfile' approach like dw817's here would probably be the easiest cross-platform option.


Depends on your point of view -- how do you deal reliably with left-over lockfiles that may be left in place due to unexpected applicaiton crashes, manually ended tasks, power outages, etc. where the lockfile didn't get cleaned up by the program that was in charge of it?

Next time you run the app it would see the existing lock file, think there's already a copy running, and terminate itself. How do you recover from that other than manually removing the lock file?


BlitzSupport(Posted 2016) [#15]
What's your better cross-platform alternative? There are countless options if you disregard non-Windows platforms.

[EDIT: Also, if the holding program has crashed, the locking program shouldn't actually have any problem acquiring a lock/deleting the file as in dw***'s example.]


dw817(Posted 2016) [#16]
Xlsior, I appreciate your attempt at a downplay to my solution but if you look carefully, even if the computer manages to crash as royally as you described, it won't matter. The file is only locked so long as the EXE is running.

The check file can exist or not when you run the program, it makes no difference.

Once the EXE is removed from running, by any means possible, normal shutdown or hot flying bullet through the motherboard, then the hold on the file is always released. This is true with ANY application that has a file open.

If, however, your main program DOES lock and will not shut down, then you should NOT be attempting to run an additional copy in the first place ! That WAS the reason you wrote the code to begin with, was it not ? You need to bring up task manager and shut down the offending main program. This is true with any application that locks.

If you want to bypass your check of a single instance, you can do this via adding code to check a simple command string. I could write this code if you want to cover this instance.

I think the word you are looking for is Checkmate, Xlsior. And yes, I do indeed play a fair game of chess. :)

. . .

Hey, BlitzSupport, glad you like my solution. Sometimes you just gotta make things easy, or as CHRIS tells me, KISS. Keep It Simple Stupid.

I also wrote you a PM, please let me know what is happening on your end, and I love your usage of asterisks !

And really, what is an Asterisk ? A wildcard. It always has been, and I resemble that remark. Thanks !!

Just watch out for the Asterisk Police. They are in strong force this season.


Grisu(Posted 2016) [#17]
Thanks a lot for your support guys!

I'm going to use Brucey's version. Together with his module updates it works fine. Also running 32 Bit vs 64 Bit an app is recognised.

@dw817: Will keep your solution as well for future projects. It's always nice to have different methods for a problem in store.


Derron(Posted 2016) [#18]
@dw817 and post #12

What is it supposed to do?

I run it twice simultaneously:



I assume, your solution only works when running Windows OS - and only as long as it does handle files like it is handling currently.

On Linux you are able to "delete" a file (aka "moving to trash") or just move it to another location - applications having a file handle to it transparently are using the new location then.

So your code won't work on Linux - because the app is able to "delete" the file and the other instance is still having valid access to this file then.


Did not check "Mac OS" now, but I think it handles files similar to Linux.




Regarding crashing apps you might do something in the likes of:
- during execution update your lockfile with a timestamp (eg. every 10 seconds)
- when starting up, check if there exists a lockfile
- - if the lockfile exists and the timestamp is "younger than 10 seconds", another app is running, or it crashed a short time ago (you then either wait the time-difference and check again, or you prompt a "did your app crash"-dialogue to the user which allows for convenient restarts including a lockfile-removal)
- - if the lockfile does not exist, start as usual

if you cannot delete your lockfile, the app might really still be running (but not drawing the gui anymore - like an partial hanging app), in this case you should give the user the hint, where to manually delete the lockfile if he is really sure that there is no stalled instance of the app.


The "10 seconds" is an adjustable time frame - it depends on how time critical it is (eg for single instance scripts run automatically - compared to a user application which is started by a mouse click). Do not forget, that this would kill any power saving functionality for your hard discs).


bye
Ron


dw817(Posted 2016) [#19]
Hi Ron. I would check. I'd be very surprised if ANY operating system allows you to delete a file that is already open and contains data from a separate program. Surprise me. Check it.

If for some reason you can run =2= copies of my program simultaneously, that is a serious handicap of the operating system to permit deletion of an open file that already has data in it.

And that would not be an error on my part - but the operating system, or possibly BlitzMAX for permitting it.


Derron(Posted 2016) [#20]
I wont discuss with you about this topic.

I just said what is happening, and that the given solution might not work on all OS.

My proposal instead is trying to avoid this problem but of course adds other disadvantages (more file accesses).


Regarding potential bugs in BlitzMax:

$ pidof running1
12901

$ lsof -a -p 12901
COMMAND    PID  USER   FD   TYPE             DEVICE SIZE/OFF     NODE NAME
running1 12901 ronny  cwd    DIR                8,7    16384  3686414 /home/ronny/PATH/testcodes
running1 12901 ronny  rtd    DIR                8,6     4096        2 /
running1 12901 ronny  txt    REG                8,7  1182359  3706202 /home/ronny/PATH/testcodes/running1

[...] 
running1 12901 ronny   19w   REG                8,7        0  3706207 /home/ronny/PATH/testcodes/busy


So while running, the file connection is kept alive.


Now if I run the second instance:
$ lsof -a -p 12901
COMMAND    PID  USER   FD   TYPE             DEVICE SIZE/OFF     NODE NAME
[...]
running1 12901 ronny   19w   REG                8,7        0  3706207 /home/ronny/PATH/testcodes/busy (deleted)

important: "(deleted)"

For linux and working with file handles:
http://stackoverflow.com/questions/2028874/what-happens-to-an-open-file-handler-on-linux-if-the-pointed-file-gets-moved-de

Maybe it is done this way, to avoid data loss (one app is working with fileA and another one tries to clean up fileA meanwhile).


Albeit it is not intended, it is to state, that your solution is then no solution for linux - and as this is the expected behaviour of Linux OS, it is not "their" (linux dev) problem, but the problem of the one trying to limit its app to a single instance.

Edit:
I think for Linux you should use some file-locking functionality provided by the OS (you might know it from other languages: "flock" or "fcntl").
But even then, "flock" might have issues with the "unlink()" command used to remove files.


Edit2:
Instead of using a file it might be interesting to use a socket-lock (your process tries to register to a specific "inet socket" and if this is not possible: either an other app is blocking it - or another instance of "our" app).
Nonetheless: better use Brucey's code as this is avoids race conditions and other problems. (renaming the app is then of course still possible)

bye
Ron


dw817(Posted 2016) [#21]
No, I'm not accepting that, Derron. Let's make a few "user-friendly" changes to the code that should fix it.

If the system returns back a flag that says the file does not exist after we 'delete' it and it is already open by another task, then quite simply a few things are taking place.

1. Most likely by running the file, the 'directory' where it is run is different than where the file is stored. We can fix this easily by returning our running directory.

2. More serious, BlitzMAX is returning an invalid result for a deleted file. Let's add code to ensure it even exists after deletion.




Brucey(Posted 2016) [#22]
If Right$(cd$,1)<>"\" Then cd$:+"\"

Those slashes are not *nix friendly...


Derron(Posted 2016) [#23]
This does not help either.

If you have an instance running which holds "busy" open... and another one then is trying to delete the file, the returned value will be "1" (aka successful).
An later added filetype() of course mentions the non-existance of the "busy" file.

If you would now switch back to the first instance, an potential still existing file handle would be valid and working. An filetype() call in that first instance would lead to the same result than the second instance though.

This is one of the reasons why people tend to describe lockfiles as flawed.

Bye
Ron


dw817(Posted 2016) [#24]
Well that's a puzzling thing there, Ron. That's my good bag of tricks on the subject and if that particular OS you are using is going to break the rules of normal file handling, I can't fix that kind of black magic.

It works for Windows though, likely all the way back to Win95. I am now immensely curious to see ANY source code that can solve this little puzzle.

BTW, the simple example that GRISU was referring to earlier would be something like this:


That should work for =ALL= operating systems. The disadvantage, you need to delete the "busy" file if the program crashes or exits prematurely. If this solution is chosen, that previous information about deleting the marker file manually in case of a crash should be included in the instructs.


xlsior(Posted 2016) [#25]
and if that particular OS you are using is going to break the rules of normal file handling, I can't fix that kind of black magic


It's not really "breaking the rules" since the normal behavior under Linux has always been different than under Windows.


Derron(Posted 2016) [#26]
Like xlsior already pointed out, this is no black magic, this is common behaviour for multiple OS. Seems Mac OS also only removes a file if all handles got closed/freed.

That is why Brucey's code box contains "process peeking", which is one of the other solutions (drawback is that a rename of the app will circumvent the identification).

At the end it is best, to use at least 2 of the available options: a lockfile approach, an inet_socket (multiple sockets option, so other apps might block one then...) or process peeking.


Back to your code:
- "\" as path separator is only working for Windows, while "/" works for Linux/Mac/Windows
- using "CurrentDir()" gets circumvented if you have a self-contained file and copy it to somewhere else (similar to process peeking and process renaming), you better use an absolute path (your application data folder on win, on linux its "/home/username/.config/yourapp/datafiles.*")
- DeleteFile(file) returns "1" (true) even if there was no file :
print "filetype = " + FileType("busy")
e=DeleteFile("busy") ' try to delete it
print "deletefile = " + e


[100%] Linking:running1
Executing:running1
filetype = 0
deletefile = 1


This is of course avoidable if you change it to `if FileType("busy") = 1 then e = DeleteFile("busy")` (so the "e" keeps initialized as "0" if it is not existing). One might call it a bug in BlitzMax, but maybe "DeleteFile()" is returning whether it failed (file still there, or did not fail - file is now gone).



You see: it is not as easy as it first seemed to be. Of course the KISS thing would say: use that simple filelock-check and if it seems already to run, present the user a dialogue informing: "app is running. If you are sure it is not, remove the lockfile placed at PATH".


bye
Ron


dw817(Posted 2016) [#27]
Well now, Ron and Xlsior, BlitzMAX does have the ability of retrieving it's own filename if that will help. This was something I needed back in S2 to directly append binary data on the end of the main EXE.

Let's see ... in BlitzMAX, that is:
print appfile$
Which also includes the hard directory it is being run from.

As for the forward and backward slash, I did not know this. I'm pretty Sure Windows 95 and maybe Windows XP will be unhappy with a forward slash. Is there any reason to have a backslash except to separate directories ? Is a backslash a real error on other operating systems ?


Derron(Posted 2016) [#28]
A backslash is a "real error" on other OS, yes.

Dunno if BMax has a builtin function but you could do

?Win32
global dirSeparator:string = "\"
?not Win32
global dirSeparator:string = "/"
?

XP does fine with "/", as I am using this for some cross-platform tools.


@own filename
You will have to use the whole call URI (path + binary name) to distinguish between "copies".

@appending data
What do AntiVirus-Tools say to this behaviour (serious question) ?


bye
Ron


dw817(Posted 2016) [#29]
Hi Ron. That function, appfile$, does return the entire URL (path+binary name).

Appending data is only something I did to start with. Me and Chris had a long talk about coding ethics and finally convinced me that I can keep my data files separate.

Back on Commodore Amiga though, it was quite a thrill to run a tiny single binary program and have it animate the screen wildly and play tinny binary music. :)

In S2, I did binary attach a ton of stuff to the main EXE, no anti-virus raised a flag, nor the 200-some odd people who downloaded it, built world files in it, and sent them back to me.

Nonetheless, it's a pain to do, to keep track of it all, to build your own VTOC. I can still write code like that today, but there really is no need as many MANY games written today do not attach all their media to the main EXE.

It's also not really necessary since BlitzMAX can attach many other files. The main purpose I was attaching binary in BlitzMAX was to create unique and new executables that contained morphing world data from other sources.

Now if BlitzMAX had a unique in-house compiler built for runtime to allow you to create new separate EXEs, that would certainly circumvent any need for me to do binary extension attachments.