Flat API: GS5_Intf.pas
Provides low-level api interface to native gsCore.dll.
OOP API: GS5.pas
Based on flat api, these two files provides Object-Oriented programming api.
Extension: GS5_Ext.pas
Based on OOP API, these two files are needed to develop customized license model, easy license event handling, etc.
Debug Helper: unDebugHelper.pas
Provides debug helper utility functions.
Before you can call most SDK apis, you must initialize the SDK first with the project-specific parameters. Depending on how you pass in the license file, there are two methods to initialize the SDK:
uses GS5_Intf, GS5;
//These information are copied from IDE:
const
productId = '8d11ec62-bfa0-4794-9688-41f8aa04630d';
password = 'walml_9282&APNQ&18163';
license_filename = 'Advanced Tutorial of Notepad2.lic';
//Resolve full path to license file
//Depending where you put the license file, its path can be resolved in different way.
function getFullPathToLicenseFile: string;
begin
return IncludeTrailingPathDelimiter(GetEnvironmentVariable('MY_DEV_PATH')) + license_filename;
end;
function initSDK: Boolean;
var
core: TGSCore;
begin
core := TGSCore.getInstance;
if(not core.init(productId, getFullPathToLicenseFile, password))
{
ShowMessageFmt('Error initializing SDK, error code: %d, error message: %s', [core.LastErrorCode, core.LastErrorMessage]);
}
end;
In this demo, we copy our license file to a folder and setup an environment variable MY_DEV_PATH to point to it, so that the app can locate the license file easily.
You can also put the license file side by side with your exe, and resolve the exe path at run-time via Application.ExeName, anyway, you must pass in the full path of the license file, otherwise the SDK will blinkdly assume the license file is in current directory, which will fail if the app is launched from another folder.
Instead of dealing with the license file path, you can also pass the license information as raw data array:
//The license info must be set in other function before calling init()
Const
{ f:\Advanced Tutorial of Notepad2.lic }
BinaryData : array[0..644] of byte = (
$47,$53,$03,$00,$01,$01,$0C,$03,$00,$00,$6B,$02,$00,$00,$95,$88,
$47,$B8,$1E,$57,$E0,$82,$C1,$87,$8D,$6A,$DF,$6F,$A6,$14,$88,$23,
... );
ok := core.init(productId, @BinaryData[0], Length(BinaryData), password);
So given a license file, how do you transfer it to a block of memory that can be accessed by Pascal code? there are several ways:
bin2pas: Convert binary file to pascal source
Usage: bin2pas inputfile [ outfile ]
Parameters:
input: binary data file to convert
outfile: [Optional] the output pascal source file
if not specified, outfile = input + ".pas"
The output looks like:
Const
{ f:\Advanced Tutorial of Notepad2.lic }
BinaryData : array[0..644] of byte = (
$47,$53,$03,$00,$01,$01,$0C,$03,$00,$00,$6B,$02,$00,$00,$95,$88,
$47,$B8,$1E,$57,$E0,$82,$C1,$87,$8D,$6A,$DF,$6F,$A6,$14,$88,$23,
... );
Windows provides win32 api to allow you retrieve resource data in memory.
To query license information, you first need to find the entity objects:
var
i: Integer;
entity: TGSEntity;
for i := 0 to core.EntityCount-1 do
begin
entity := core.Entities[i];
//dump entity information...
Writeln( "entity name: ", entity.Name, " id: ", entity.Id );
entity.Free;
end
You can also get the specific entity by its entityId:
//EntityId is copied from IDE
const entityId = "b15c8ac2-d87c-4483-be77-5a8cbc89a62b";
entity := core.getEntityById( entityId );
If your app has only one entity, let's get it by a single line:
entity := core.Entities[0];
This is because the entity is the only single entity with index 0.
Given an entity object, we can easily retrieve its properties:
var
id, name, description: string;
id = entity.Id;
name = entity.Name;
description = entity.Description;
If your entity is associated with a trial license model (Expire by Access Times, Expire By Hard Date, Expire By Period, Expire by Duration, Expire by Session Time), you can test the entity's license status as following:
if entity.Unlocked then
begin
//this entity is already activated / fully purchased.
//app should run in full featured mode for this entity (game level, app module, etc.)...
run_in_full_featured_mode();
end else if entity.Locked then
begin
//this entity is locked or trial already expired.
//we may pop up info to prompt for purchase.
cannot_run();
end else
begin
//this entity still in trial period or duration
run_in_trial_mode();
end
If your entity is using License Model Always Lock, then the logic can be simplied as:
if entity.Unlocked then
begin
//this entity is already activated / fully purchased.
//app should run in full featured mode for this entity (game level, app module, etc.)...
run_in_full_featured_mode();
end else
begin
//this entity is still locked, we need a license to continue.
//we may pop up info to prompt for purchase.
cannot_run();
end
In the above section, we have detected that an entity is still in trial mode, so in order to display proper license information to end user on app UI, such as tell the game player how many trial time left before the game will stop running, we must be able to get more details of the license model.
var
lic: TGSLicense;
lic := entity.License;
Now that the license object is ready, we can inspect it in various perspectives:
As a developer we have designed the license project in SoftwareShield IDE before, so we should have known which kind of license model being used for an entity. However, if you are developing a universal logic to parse any entity, it is helpful to find out the license model type being bundled with the input entity:
var
model_id, model_name, model_description: string;
model_id := lic.Id;
model_name := lic.Name;
model_description := lic.Description;
For example, for Expire By Period, its LicenseID is "gs.lm.expire.period.1", so by comparing the license id you can figure out the correct license model type to inspect further.
Now we have known which license model to inspect, let's retrieve the license parameters according to its model type:
This license model (Expire by Access Times) has two parameters of int type: "usedTimes" and "maxAccessTimes"
var
usedTimes, maxAccessTimes, how_many_access_left: Integer;
usedTimes := lic.ParamInt['usedTimes'];
maxAccessTimes := lic.ParamInt['maxAccessTimes'];
//How many times left the app can launch in trial mode?
how_many_access_left := maxAccessTimes - usedTimes;
This model (Expire By Hard Date) is a bit complex since it have three different use cases, in this tutorial we only demonstrate the most commonly used one: "Expire After", in this case, the license has a predefined expiry date "timeEnd", the app trial mode expires after this date.
var
expiryDateUTC, expiryDateLocal: TDateTime;
expiryDateUTC := lic.ParamUTCTime('timeEnd');
//Convert UTC to local time
expiryDateLocal := UTCToLocal( expiryDateUTC );
The interest part is that SoftwareShield SDK api always returns TDateTime in UTC time zone, you might have to convert it to local time to display the expiry date in local time zone. SoftwareShield SDK/Delphi has several time convert functions in GS5.pas:
//Helpers
//UTC <=> Local
function UTCToLocal(const dtUTC: TDateTime): TDateTime;
function LocalToUTC(const dtLocal: TDateTime): TDateTime;
//UTC <=> Unix time_t
function UnixTimeToUTC(const unixTime: Int64): TDateTime;
function UTCToUnixTime(const dtUTC: TDateTime): Int64;
//Format seconds to a string in format "XX day(s) YY hour(s) ZZ minute(s) DD second(s)"
function TimeSpanStr(const seconds: Integer): string;
This license model (Expire By Period) has a UTC datetime parameter "timeFirstAccess", specify the timestamp the entity is first accessed, and an integer parameter "periodInSeconds", specify the total trial period allowed. The following code tries to figure out when the license will expire and how many trial time left before expire.
uses DateUtils;
var
periodInSeconds, timeLeftInSeconds: Integer;
timeFirstAccess, expiryDate: TDateTime;
//Total trial period in seconds
periodInSeconds := lic.ParamInt('periodInSeconds');
//Figure out when the entity is first accessed (aka. entity.beginAccess() is first called)
//if never accessed before, the _timeFirstAccess_ parameter won't hold a valid datetime and the api can throw exception.
try
timeFirstAccess := lic.ParamUTCTime('timeFirstAccess');
//accessed before, so we can calculate the expiry date
expiryDate := IncSecond(timeFirstAccess, periodInSeconds);
//How many trial time left?
timeLeftInSeconds := SecondsBetween( Now, expiryDate );
except
//Never accessed, the expiry date is still unknown!
//How many trial time left = full trial period.
int timeLeftInSeconds := periodInSeconds;
end;
The interesting part of the above code is that: if the "timeFirstAccess" does not hold a valid timestamp (the default value is undefined), which occurs when the entity is never accessed before, we cannot deduce the exact expiry date, however we can deduce how many time left before expire.
This license model (Expire by Duration) is simple, it just adds up all elapsed trial time cumulatively (in "usedDurationInSeconds") until the total trial time exceeds the pre-defined maximum value ("maxDurationInSeconds"). We cannot figure out the expiry date.
//Total trial period in seconds
int usedDurationInSeconds = lic->getParamInt("usedDurationInSeconds");
int maxDurationInSeconds = lic->getParamInt("maxDurationInSeconds");
//How many trial time left?
int timeLeftInSeconds = maxDurationInSeconds - usedDurationInSeconds;
This license model (Expire by Session Time) allows an entity can always be accessed (never expire) but in a limited time, we can get the session time left but cannot get the expiry date.
int sessionTimeUsed = lic->getParamInt("sessionTimeUsed");
int maxSessionTime = lic->getParamInt("maxSessionTime");
//How many trial time left?
int timeLeftInSeconds = maxSessionTime - sessionTimeUsed;
There are two methods to activate your app, online and offline.
After user've made purchase of your app, you send a serial number to the customer as a proof of valid customer and the key to activate your app.
var
rc: Integer; //returned error code
success: Boolean;
if core.isServerAlive then
begin
success := core.applySN(serial, rc);
end else
begin
// license server is not available!
//optionally fall back to offline activation...
end;
In order to do offline activation, we need to go through three steps:
var
requeseCode: string;
requestCode := core.DummyRequestCode;
We are using dummy request code because the license server can deduce all valid actions from the customer's serial number.
You display the request code on UI, asking the customer to contact your support team for a valid license code, or if possible, let the customer uses another machine (or a mobile device) to generate the license code by himself/herself via the app's public App Web Activator.
Once the user input a license code, we can apply it to the app:
var
licenseCode: string; //user input the license code.
success: boolean;
success := core.applyLicenseCode(licenseCode, serial);
SoftwareShield SDK allows you to revoke serial numbers to license server so that the revoked serial numbers can be reused in another machine or the same machine (after hardware upgrade, system re-install, etc.)
var
serialToRevoke: string;
success := core.revokeSN(serialToRevoke);
After successful revoke, all those entities previously unlocked by this serial are locked.
success := core.revokeApp;
After successful revoke, all previously unlocked entities are locked.
Revoke serial number is good for most cases, but there are other scenerios that the serial-based revoke won't work. For example, your license model is based on Pay-as-you-go concept, your app is not fully activated, the serial is used to extend the app's life span, so the unlock-lock based serial-number revoke does not make sense at all.
On machine A (sender):
var
receiptSN: string;
receiptSN := core.uploadApp;
On machine B (receiver):
success := core.applySN(receiptSN);
On machine A (sender):
var
lic_backup: string;
lic_backup := core.exportApp;
On machine B (receiver):
var
mv: TMovePackage;
success: Boolean;
moveRequestCode: string;
// the user reads the request code from app UI and gets license code from support team (or via App Web Activator)
licenseCode: string;
try
mv := core.createMovePackage( lic_backup );
if core.isServerAlive then
success := mv.importOnline( serial ) //online import
else
begin
//offline import
moveRequestCode := mv.getImportOfflineRequestCode;
success := mv.importOffline(licenseCode);
end;
finally
mv.Free;
end;
The workflow seems a little messy, but you do not have to fully support all online/offline cases in your app. The SoftwareShield SDK provides complete apis to support all possible use cases.
Sometimes you want to implement a logic to test if a serial number is still valid before app can run at client side. You can disable or delete a serial number at CheckPoint2 license management web portal, as a result you can remote control if the app should run by manipulating the serial number on the server side.
var
serial: string; //the serial issued from app vendor
if not core.isValidSN( serial ) then
begin
core.lockAllEntities;
//quit app...
end;
To handle SoftwareShield license events, you can add event handler to interested event:
In GS5.pas, there are three event handler protocals corresponding to three categories of event type (Application, License and Entity):
TGSAppEventHandler = procedure (eventId: Integer) of object;
TGSLicenseEventHandler = procedure (eventId: Integer) of object;
TGSEntityEventHandler = procedure (eventId: Integer; entity: TGSEntity) of object;
In MyApp.pas:
unit MyApp;
interface
uses GS5, GS5_Intf;
type
TMyApp = class
private
_core : TGSCore;
//Event listeners
procedure OnAppEvent(eventId: Integer);
procedure OnLicenseEvent(eventId: Integer);
procedure OnEntityEvent(eventId: Integer; entity: TGSEntity);
public
procedure initSDK;
end;
Implementation
uses dialogs;
procedure TMyApp.initSDK;
begin
_core := TGSCore.getInstance;
//Hook event listeners
_core.OnAppEvent := OnAppEvent;
_core.OnLicenseEvent := OnLicenseEvent;
_core.OnEntityEvent := OnEntityEvent;
//init SDK
// if not _core.init(...) then raise Exception.Create('Error initializing SDK!');
end;
procedure TMyApp.OnAppEvent(eventId: Integer);
var
entity: TGSEntity;
begin
case eventId of
EVENT_APP_BEGIN:
//App launches, say hello...
exit;
EVENT_APP_END:
try
//Render Exit-UI
entity := _core.getEntityByIndex(0);
if entity.Unlocked then
begin
//activated, do nothing
end
else if entity.Locked then
begin
//trial already expired, need license to run
ShowMessage('your trial is expired, please activate now');
end
else
begin
//trial mode...
ShowMessage('your trial will expire in XXX');
end;
finally
entity.Free;
end;
end;
end;
procedure TMyApp.OnLicenseEvent(eventId: Integer);
begin
end;
procedure TMyApp.OnEntityEvent(eventId: Integer; entity: TGSEntity);
begin
case eventId of
EVENT_ENTITY_TRY_ACCESS:
begin
if entity.Unlocked then
begin
//activated, do nothing
end
else if entity.Locked then
begin
//trial already expired, need license to run
//render_activate_UI(entity);
end
else
begin
//trial mode...
end;
end;
EVENT_ENTITY_ACCESS_INVALID:
//trial expired!
//render_activate_UI(entity);
Exit;
EVENT_ENTITY_ACCESS_HEARTBEAT:
//check when the entity will expire, inspect its license parameter to give user feedback
//when 5 minutes left before expire...
end;
end;
Upon app launching, you should call TMyApp.initSDK() first to initialize SoftwareShield SDK.