A Concurrent Licensing System for Alpha Five Applications

by Tom Cone

Tom shows us 2 ways to limit the number of users of an application to the number the client has actually paid for. The first way is straightforward but fails if the application, the user, or Windows exits abnormally. The second method is devilishly clever in attempting to recover from abnormal program termination.

This article outlines two different approaches to building a concurrent licensing system for applications built with Alpha Five.

So, what the 'heck is a "concurrent licensing system", anyway? By this I mean a license agreement which limits the customer's use of the application to no more than a specified number of workstations at one time. For example, if the customer buys a two seat license the app can be installed on as many network workstations as may be desired, but the license authorizes no more than two simultaneous (concurrent) users at a time. It need not be the same two workstations each time. Any two on the network would be permitted, but no more than two at any one time. The "system" keeps the customer honest by working in the background to prevent more copies of the application being run than have been licensed.

First approach - a check file

The first approach uses a check file that is read when the application's main menu form is initialized. In the example outlined in the code below, a single line is read from the file. The line contains only a single numeric value which represents the number of users already logged on. If additional seats are available the number is incremented and written back to the file. The app continues to load. If there are no more available seats under the customer's license, then the user is informed and the app is terminated.

This approach works, and works quickly. However, it is not perfect. In my design I write a simple text file to the database's folder. A knowledgeable user could navigate there with Windows Explorer and manually edit the number appearing in line one of the file. Worse, the user could simply delete the file, in which case the next user to log on would recreate the file with a user count of "1". At the outset I figured that most of my users wouldn't know how to do this, and others would be buffaloed by a serious admonishment inserted in line two of the check file.

Other complications arise, however. First, you need to decrement the user count in the check file as users exit your application. I accomplished this by adding a short script to my Exit Application button on my MainMenu form. It works quickly and accurately, but only if the user presses the Exit Application button. If the user shuts down Windows, or exits Alpha Five with the system menu "X" in the top right corner of the caption to my MainMenu form, then the script attached to the Exit Application button never runs, and the check file never gets decremented. [Editor's note: A program crash would also leave the check file with an outdated user count. Of course, Tom's app is crash-proof...]

As if this isn't enough, remember also that Alpha Five shuts itself down during the process of doing a network optimize. It then restarts after orienting itself to the shadow table folder. The check file is not decremented when Alpha Five shuts itself down, because the script attached to my Exit Application button never gets fired.

There may be other occasions in which one or more of your scripts will shut down your application on purpose. I have a routine like that in my autoexec script: if the script detects that the files on the server have been updated more recently, I run refresh shadow operation, and then shut down Alpha Five without user intervention. With my check file approach to a license control system I have to remember to decrement the user count on the way out.

If the application is terminated without reducing the user count often enough, the erroneous count eventually causes the customer to be unable to even run one instance of the application. The check file thinks that lots of users are already on board. The solution is to delete the check file. I imagined the tech support phone call, and my quick answer: "Just delete the check file from the database folder, it's named "USERS.TXT", and you can find it with Windows Explorer". As this conversation played itself out in my head, I began to realize that after I tell the user this two or three times even they will eventually realize what's going on, and will thereafter be able to bypass the system whenever they wish, defeating the entire purpose of my licensing system. Not good!

Nevertheless, the scripts are presented below because you may find solutions to these limitations. However, I fear that this approach will require more or less regular intervention from tech support (and that means "me", and possibly "you") unless you convince your users they really, really must push that Exit Application button each time.

Script 1 is intended to be placed in the OnInit event of your MainMenu form. It should be placed at the beginning of your script. This is the script which reads the check file and updates it.

Licensed_users=2 
'this number differs for each customer. Change it here
before delivering the application. This is the only place it appears.
old_directory=dir_get() 'store initial current working directory
old_path=A_DB_Current_path 'get path to database
master_path=A5.Get_master_path() 
'again, this time checking for shadow
'
if len(trim(master_path))=0 then 'user did not open a shadowed table
 targetfolder=old_path 'trailing backslash present already
else
 targetfolder=master_path+chr(92) 'add trailing backslash
end if
'
'when script gets here targetfolder holds path 
'(terminated with backslash) pointing to the actual
'database files. This is where the check file should be written.
filename=targetfolder+"users.txt" 'this is name of the check file 
result=file.exists(filename) 'is it present already?
if result then 'yes, it is there, so read it
 file_pointer=file.open(filename, File_rw_shared)
 text=file_pointer.read_line() 'read first line
 text=alltrim(text)
 currentusers=val(text)
 if currentusers >=Licensed_users then 'lic count exceeded
 file_pointer.close() 'close the lic count file
 ui_msg_box("Sorry",\
		"Your network is using the maximum number of licensed copies\
	 of this application already. You must wait until someone\
		else stops using the application.") 
 :A5.close()
 else 
 	currentusers=currentusers+1 'increment currentusers
 	text=alltrim(str(currentusers))
 	'position file pointer to beginning of file
 	file_pointer.seek(0)
 	file_pointer.write(text) 
		'write first line, contain current number of users
 	'now write the sternly worded admonition
	 file_pointer.write_line("") 'move pointer to next line
 	file_pointer.write("Do not edit or delete this file. \
		Membership 1999	needs it just like it is.")
 	file_pointer.flush() 'flush to disk
 	file_pointer.close() 'close the check file
 end if
else 'the check file doesn't exist. Means this is first user to log on.
 'so now we create the check file the first time.
 file_pointer=file.create(filename, File_rw_shared)
 'write first line
 file_pointer.write(ltrim(str(1)))
 'new line
 file_pointer.write_line("")
 file_pointer.write("Do not edit or delete this file. \
	Membership 1999 needs it just like it is.")
 file_pointer.flush() 'flush to disk file
 file_pointer.close() 'close lic count file
end if

Script 1. The OnInit script for the MainMenu form of the "Membership 1999" application.

The next script is intended to be placed in the OnPush event of your Exit Application button on your MainMenu form. Its sole purpose is to decrement the check file as the user exits your application. The last user to log off will "turn out the lights" : the script will remove (delete) the check file altogether.

old_path=A_DB_Current_path
master_path=A5.Get_master_path()
'master_path contains path to actual tables, if shadowed
'old_path contains path to actual tables if not shadowed
'decrement or erase licensing file in actual tables folder
if len(trim(master_path))=0 then 'not shadowed 
 targetfolder=old_path 'trailing backslash present already
else
 targetfolder=master_path+chr(92) 'add trailing backslash
end if
filename=targetfolder+"users.txt"
result=file.exists(filename)
if result then 'licensing file exists
 file_pointer=file.open(filename, File_rw_shared)
 'read first line
 text=file_pointer.read_line()
 text=alltrim(text)
 currentusers=val(text)
 if currentusers > 1 then 'decrement user count
 currentusers=currentusers - 1
 text=alltrim(str(currentusers))
 'position file pointer to beginning of file
 file_pointer.seek(0)
 'write first line
 file_pointer.write(text)
 'new line
 file_pointer.write_line("")
 file_pointer.write("Do not edit or delete this file. Membership 1999
needs it just like it is.")
 file_pointer.flush()
 file_pointer.close()
 else 'license count=1, so erase the file as this user logs off
 file_pointer.close()
 file.remove(filename) 
 end if
end if

Script 2 decrements the user count as users exit the application.

The better way - a dummy table

The second approach to a concurrent licensing system is the one I currently employ. It does not use a check file and does not require that my user push the Exit Application Button. Instead, it uses the "dummy" table my MainMenu form is based on, and does so in a slightly unusual way.

Here's the idea. Do you know what happens if one user starts changing a record, and another user tries to change it at the same time? Alpha Five throws an error message at you that reads "Record is locked by another session or user". Suppose the first user starts changing a record and then is suddenly overwhelmed with longings for a cheese danish. When that person leaves the office to go get his sugar fix, no one else on the network can edit the record which is locked by the first user's pending change. Whenever someone tries, the error is thrown.

Now suppose your application ran a special script everytime it began, one that tried to lock the first record in the dummy table by putting it in change mode. If it succeeded, suppose the script simply ends, leaving the record locked in change mode, while the rest of your application cranks up. The next user, when they logged on, would run the same script from a different workstation. They would hit the locked record and be unable to lock it themselves, right? However, the resulting error could be "trapped", and the script could navigate to the next record in the dummy table, and try to lock that one by putting it in change mode. If it succeeds, then the script would end, leaving the second record locked in change mode, while the rest of your application cranks up. When the third user logs on, the process is continued. And so on, and so on, until the licensed number of "seats" have all been used up. When this occurs a corresponding number of records have been locked in the dummy table, each by a different workstation.

For good measure suppose you add a short script to your Exit Application button on your Main Menu, to commit the previous "change" before exiting to Windows. This releases the lock on the record that had previously been placed in change mode. Neat. Tidy. No loose ends.

But I don't worry about users exiting my application in other ways. If that happens the network releases the lock on the record in my dummy table, and it's immediately available for the next user. Occasionally, in Windows 98, Alpha Five will report that a record is in change mode, do I want to save or cancel the changes? Either choice is ok. It makes no difference. After all, it is a dummy table, and in point of fact no edits are ever made. Save it, cancel it, it makes no difference.

To implement this second approach I did four things.

First, I made certain I had enough records in the dummy table corresponding to the maximum concurrent license count I will be issuing. (Fifteen in my case). Each workstation will need to lock a record, so there must be enough records so that each workstation can find one to lock.

Second, I built a simple text file, similar to the check file described previously. It is saved in the database folder on the server. This file will hold a coded numeric value corresponding to the number of licenses each customer purchased. When additional licenses are purchased all I have to do is update this number, or send them a new license control file. They can read it with any text editor, but the value appearing there makes no sense. For example, the holder of a four (4) seat license would see this in his license control file:

6487
Do not delete or modify this file. It is needed by Membership 1999 just like
it is. 

Third, I added script 3 to the OnInit event for my MainMenu form. This form is based on the dummy table.

dim global license as n 
'will hold authorized number of authorized seats on network
old_directory=dir_get()
old_path=A_DB_Current_path
master_path=A5.Get_master_path()
'
if len(trim(master_path))=0 then 'not shadowed 
 targetfolder=old_path 'trailing backslash present already
else
 targetfolder=master_path+chr(92) 'add trailing backslash
end if
filename=targetfolder+"lcontrol.txt"
statusbar.set_text("Please standby. Program is loading.")
result=file.exists(filename)
if result then
 file_pointer=file.open(filename, File_rw_shared) 'open it
 text=file_pointer.read_line() 'read first line
 file_pointer.close() 'close it
 text=alltrim(text)
 textnumber=val(text)
 'now convert to licensed seats using arbitrary scheme
 Select
 Case textnumber=9190
 license=1
 Case textnumber=8289
 license=2 
 Case textnumber=7388
 license=3 
 Case textnumber=6487
 license=4
 Case textnumber=5586
 license=5
 Case textnumber=4685
 license=6 
 Case textnumber=3784
 license=7
 Case textnumber=2883
 license=8 
 Case textnumber=1982
 license=9 
 Case textnumber=2181
 license=10 
 Case textnumber=3280
 license=11 
 Case textnumber=4379
 license=12 
 Case textnumber=5478
 license=13 
 Case textnumber=6577
 license=14 
 Case textnumber=7676
 license=15 
 Case Else 
 code=ui_stop_symbol + ui_ok
 ui_msg_box("Error","\
 	Your license control file is corrupt.\
	Contact tech support for assistance.",code)
 	:A5.close()
 end select 'license holds number of authorized seats
 tbl=table.current() 'startup (dummy)
 licensecounter=1
 tbl.fetch_first()
 checklicense:
 ON ERROR GOTO License_error 
 'the error handler will advance to the next record and then loop
 'back to checklicense: to see if the next record can be put in change mode 
 'this process continues until a record in startup is put in change mode
 'or the 15 user limit is hit.
 tbl.change_begin() 
 'will fail if another user has record locked in change mode
 ON ERROR GOTO 0 'release error trap
 GOTO oktoloadapplication 'get here only if error did not occur
 
else 'lcontrol.txt could not be found
 code=ui_stop_symbol + ui_ok
 ui_msg_box("Error","\
 	Your license control file is missing.\
	Contact tech support for assistance.",code)
 	:A5.close()
end if 
oktoloadapplication: 
statusbar.clear() 'clear message from license_error routine
 
' NOTE: the following portion of script#3 should be placed at the end of the
' OnInit script.
' this is the errorhandler portion
license_error: 'error trap for lic control module
 'on entry of this module 
 'license holds authorized number of seats
 'licensecounter holds number of users logged on
On error goto 0 'release the trap
ui_beep(ui_system_beep)
statusbar.clear()
statusbar.set_text("Please standby. Program is loading.\
	"+ltrim(str(licensecounter)))
licensecounter=licensecounter +1 
if licensecounter > 15 then 'recommended number of users is exceeded
 code=ui_stop_symbol + ui_ok
 ui_msg_box("Sorry","\
 	You have reached the recommended limit for Membership \
	(15 simultaneous users). You must wait for someone else on your\
	network to exit Membership before you can proceed.",code)
 :A5.close() 
end if
if licensecounter > license then 
'number of authorized seats are already in use
 code=ui_stop_symbol + ui_ok
 ui_msg_box("Sorry",ltrim(str(license))+"\
  workstations are working with Membership now. \
  No more simultaneous users are allowed under your\
  current license. You must wait for someone else on your network\
  to exitMembership before you can proceed. \
  Additional licenses may be purchased from\
	the publisher.",code)
 :A5.close() 
end if
tbl.fetch_next() 'check next record in startup dummy
 'to see if it can be put in change mode
 'no need to check eof() cause loop ends at 15 licenses and there 
 'are 15 records in the dummy table
RESUME checklicense

Script 3. A better approach to license control.

Fourth, I added the following script to the Exit Application Button on my MainMenu form

''XBasic script for OnPush event of Exit Application Button
tbl=table.current()
if tbl.mode_get() > 0 then 
	'startup dummy table still in change mode.
 parent.commit()
end if
:A5.close()

Script 4. Exit App script for second approach.

This second approach has one drawback that may make it unsuitable for some applications. The process of checking to see if a record is locked, and then handling the resulting error is quite slow. It takes perhaps 5 to 10 seconds per record. As more and more people log on the delay increases incrementally. If you have a six seat license this is about a minute for the last user, during which the user may be wondering what is going on. However, slow and steady wins the race, at least in my particular application, so I've opted for this approach, instead of the faster, but more problem prone approach described initially, above.

Conclusion

These two approaches illustrate different techniques for dealing with a common problem in multi-user settings. In developing the second approach I relied heavily on the works of Dave Navarro, Alan Earnshaw, and others in the PowerBASIC community. Approach number 1 is completely my own handiwork. Neither approach is perfect. Both do the job at hand, but each has its own problems.

Tom Cone
Thomas E. Cone, Jr., P.A.
150-A Whitaker Rd.
Lutz, FL 33549-7611
tcone@ix.netcom.com

11/21/99

Don't forget, we need your feedback to make this site better!

Return to home