Creating an NT service for a VDF program
by Bob Worsley
Summary
NT services are something that I've had extensive experience with, on both NT-4 and Windows 2000. This article is a complete guide, including code, to installing and running a VDF program as a service. The "code" is a scheduler that once the service is running, kicks off a program at a required time or periodically throughout the day. |
Size: 54 KB |
Download |
Date Created: |
18/08/2002 |
Date Updated: |
18/08/2002 |
Author: |
Bob Worsley
|
Creating an NT service for a VDF program
By Bob Worsley mailto:bworsley@optonline.net
Environment
This will not work in Windows 95 or 98, there are no "Services" available. Windows NT, 2000 and all later are the only compatible versions.
Purpose
There are a number of reasons for using an NT "Service"
- It runs completely unattended
- It's normally run in an automated mode at specific periods or times
- Does not require a user to be logged in
- Can be set up with permissions different from "normal" users
Examples might be for running an unattended nightly upload or download, an automatic backing up of data or running of reports.
What VDF program will qualify as a "Service"
Only programs that will run without any kind of user interface can be used as a service. A program that is running as a service is invisible to the desktop and if user entry were required, it would not be seen, so the program would "hang" until the service was stopped. A BPO would be ideal, but must be called with parameters that are set ahead of time. Errors are also invisible so all possible situations should be analyzed ahead of time.
Of course for troubleshooting any service can be set to "Allow service to interact with desktop" which is a setting in "Properties" but that is not normal operation.
The general VDF program structure would be something like the following:
Use DFAllent
Object ProgramWorkspace is a Workspace
Set WorkspaceName to CURRENT$WORKSPACE
End_Object
Object MyProcess is a BusinessProcessObject
End_Object
Send DoProcess to MyProcess
Or, an even simpler functionality would be to replace the BPO with:
Procedure MyProcedure
End_Procedure
Send MyProcedure
Installing the Service
- Insure that two executables from the NT Resource Kit are in the WinNT directory on the server or PC where the service is to be run.
If you do not have the resource kit, both executables can be found on the web. Instsrv.exe is the installer for the service and Srvany.exe is the service itself.
- To create the service, execute
Instsrv.exe in a DOS window:
instsrv "Descriptive name" c:\Winnt\srvany.exe
where "Descriptive name" is what you want to call your service.
If you don't get this result: "The service was successfully added!" something is incorrect. Verify the path and retry it.
- If successful, start Regedit and go to the following key:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Descriptive Name
- Click on this folder and add a new key:
Parameters
- Click on this new key, "Parameters" and add three new string values to it:
String Value | Populate it with your path and application | Application | d:\vdf7\bin\dfrun.exe | AppDirectory | d:\MyApplication\Programs | AppParameters | d:\MyApplication\Programs\MyProgram.vd7 |
Where AppDirectory is the working directory for the program and AppParameters is the path and program name for the module you want to use as a service.
Running the service
You will start the service by locating "Services" in your OS.
- In NT-4 it's in Control Panel
- In Windows 2K and later it's in Control Panel/Administrative Tools/Services
Before actually "Starting" the service, edit "Properties" in "Services" for a couple of things. These can be set as necessary; depending on what stage you are at in the development of your application.
- To determine that the service is indeed set up properly, check the box that says, "Allow service to interact with desktop". Any errors or showlns will then show on the screen.
- Be sure that the "Startup type" is set to automatic or the service won't automatically restart if the server is rebooted. The above installation defaults to this, but it should be checked.
Permissions
On the above properties tab where the above "interact with desktop" checkbox is shown there is a set of radio buttons titled "Log on as". The default is "Local System Account" which means the service runs essentially as an administrative user on the PC.
The alternative is "This Account" that requires a valid ID and password be entered. The purpose for this is if you want the service to have any special capabilities. An example of this would be if the database were located on a different computer from the one where the service is running. A service running on the one box would not be permitted access to that database on the second box. The strange part is that the errors you would see are not indicative of the problem, so you will spend some serious time figuring out what the problem is. To solve it an account would have to be created and used that had adequate permissions on both boxes. Generally this means the same ID and Password entered on each of the two servers. As stated above, this is an advanced usage and is not supported as a part of this paper.
Timing
So at this point we've installed a service and maybe even run it. If it's been run then you have realized that the sole purpose of Srvany is to call dfrun MyProgram as soon as it's started, the rest is up to you. Without writing more code, this functionality is pretty much useless. The last piece is to determine how to call the program when you want it. Suppose the reason for the program is to export data at 0200? How do we get the program to just sit there and not do anything until that time and go back to sleep after that time?
Alternatively there could be a need to run a program once a minute around the clock. This is a bit different from the first scenario but can be handled in somewhat the same way. We will create a simple DataFlex loop in the schedule program that once it matches an entry in the setup file, performs the necessary action and then goes back to sleep.
To do this we will need:
- A new data file for scheduling actions
- Schedule program Schedule.src to call any module to be run automatically
- Your program(s) to perform the action(s) required - reports, backup, database processing, etc
The premise is that we use a data file to hold the information on when we kick off the specific processes, and a "schedule" module to spin through the records in this file until the correct time arrives. When it does, we chain out to the process program that is specified for the particular time.
There are two kinds of "times" built into this scheduler, daily at a specific time and periodic, such as once a minute, once an hour, every 5 minutes, etc.
The structure of the data file is next, and after that some pseudo-code that isn't very far from actually functioning. The goal is to provide the framework and understanding, so look at the notes in the code.
Errors
One thing that will need to be done if a VDF program is to be used as a service is to write a custom error object that will redirect errors from the screen to a log file of some sort, either a text file or DataFlex file, whichever is more practical for the application. Just as the user normally cannot see operational screen messages, error messages can't be seen either.
The scheduler that is provided below and any VDF programs that are called by it will need to use such an error object in order to be complete. At this point no such work has been completed, but will be for a future update to this paper.
-----------------------------------------------------------------------------
DATE: 08/05/2002 TIME: 13:12 PAGE: 1
FILE DEFINITION FOR FILE: SYSSCHED (# 59)
-----------------------------------------------------------------------------
DRIVER NAME : DATAFLEX
FILE ROOT NAME : SYSSCHED
USER DISPLAY NAME : Master Schedule
DATAFLEX FILE NAME : SYSSCHED
-----------------------------------------------------------------------------
RECORD LENGTH : 39 ( USED: 37 )
MAX NUMBER OF RECORDS : 10000 ( USED: 8 )
FILE COMPRESSION : NONE
RE-USE DELETED SPACE : YES
LOCKING TYPE : FILE
HEADER INTEGRITY CHECKING : NO
TRANSACTION TYPE : CLIENT ATOMIC
RECORD IDENTITY INDEX : 0 ( 0 , 0 )
FILE LOGIN PARAMETER :
SYSTEM FILE : NO
-----------------------------------------------------------------------------
NUM FIELD NAME TYPE SIZE OFFST IX RELATES TO FILE.FIELD
--- --------------- ---- ----- ----- -- ---------------------------------
1 MODULE_ID ASC 14 1 1
2 STATUS NUM 2.0 15
3 CREATE_DATE DAT 6 16
4 ACTION_TIME ASC 4 19 1
5 ACTION_DATE DAT 6 23 1
6 PERIOD ASC 6 26 3
7 LAST_RUN NUM 10.0 32
8 CALL_MODULE ASC 12 37
INDEX# FIELDS DES U/C LENGTH LEVELS SEGMENTS MODE
------ --------------- --- --- ------ ------ -------- -------
1 ACTION_TIME NO NO 21 3 3 ON-LINE
ACTION_DATE NO NO
MODULE_ID NO NO
2 ACTION_DATE NO NO 21 3 3 ON-LINE
ACTION_TIME NO NO
MODULE_ID NO NO
3 PERIOD NO NO 9 3 2 ON-LINE
RECNUM NO NO
The columns in Syssched are described as follows:
Active If checked, the module will be checked by the scheduler.
Any modules that are not used should not be checked as "active" since this just creates unnecessary overhead.
Module Id of the module being scheduled
Examples would be "MYPROG1" or "MYPROG2"
Run Time If a specific time, the time of the event
If a time is entered for a daily or repetitive event, do not enter a date. If a one-time event, enter a time and a date. Run time should be used for all events that happen once a day, do not use "1D". Enter time as 900 or 1430, without a ":" or "." or any other punctuation.
Run Date If a specific date, the date of the event
If a date is entered, the event will only happen on that date. A time is also required or the event will happen at midnight.
Period Periodic event such as every 10 minutes
A periodic event is timed in minutes. This kind of action is designed for calling events every few minutes or hours - multiple times per day. The following codes are used for simplicity:
M Minute
H Hour
D Day
W Week
Enter any of the above codes prefaced by a multiplier of some kind. If only 1 minute, hour, day or week, preface with a 1. Some examples are: 3M, 20M, 1H, 1D and 2W.
A leading number is necessary! The above samples represent every 3 minutes, every 20 minutes, every hour, every day and every 2 weeks respectively. See the section below for a more detailed description.
Last Run The time of the last successful triggering of the event
See the "DM time" explanation below.
Call Module The actual program module that does the work
This program is chained to from the scheduler and actually does the task we want the service to do.
"DM Time" (Date-Minutes time)
Field 7 in the data file is an integer but represents the last date and time that the particular process was run. Working with dates and times presents tremendous headaches when attempting to determine when one time or date combination is ahead of or behind another, especially across midnight.
DataFlex has given us a great way around the problem with the use of Julian dates. If we convert the date into minutes by multiplying it by 1440, the number of minutes in a day, and convert the hour into minutes and then add those two together with the current minutes, we create a single integer that represents the entire combination. Since this integer never has to go back to 0 like a time does at midnight, we can simply compare two of these integers to see if one is greater than the other. This is extremely useful when attempting to see if the process has already been run.
Some further notes on timing
Simple timing such as once a day or less is easily accomplished as described above. It gets a bit more complicated if "D" for "Daily or "W" for "Weekly" is used.
Using "1D" is kind of useless since the time of day for the once a day event hasn't been specified. The simplest way to schedule a daily event is to just specify the run time. If an event needs to be scheduled every other day then it will be necessary to enter both the run time for the time of day and "2D" for every second day.
If "W" is used, a time must also be specified since we need to know the time of day to run the event, just as above.
An example of some practical schedules
The first record is an example of a 1-minute repetitive call to program DISPIN. The second is a one-time daily event that takes place at 2146 each day.
A practical scheduler
This scheduler is driven by the above data file and is designed to call any program either periodically around the clock or at a specific time each day. It has been cut out of another application so may have a few pieces that don't belong in it. It has been tested as written, so should work nicely.
One note to consider is about the "DoProcess" loop in the timer object. This loop, if left uncontrolled, would continuously run the server at 100% maximum CPU usage, which is not really a good idea. In order to make this all practical we need to pause the loop at some scheduled interval to allow the server to do it's other work. A simple method is to put a 1 second sleep command in the code, which will do nicely. Alternative methodologies would be to add a timer object or utilize an external function call to obtain sampling times of less than 1 second, the goal being to look in the timer file more often than once a second. This is an area that is left to the user to develop if necessary.
define WEEK_LENGTH for |CI7
define DAY_LENGTH for |CI1440
define HOUR_LENGTH for |CI60
Class cScheduler is a Message
//--------------------------------------------------------------------------->
Function Decode_Period String lCode Returns Integer
If (uppercase(lCode)="M") Function_Return 1
If (uppercase(lCode)="H") Function_Return HOUR_LENGTH
If (uppercase(lCode)="D") Function_Return DAY_LENGTH
If (uppercase(lCode)="W") Function_Return (WEEK_LENGTH*DAY_LENGTH)
End_Function
//--------------------------------------------------------------------------->
Function Decode_Day# String lDay Returns Integer
If (lDay = "SU") Function_Return 0
If (lDay = "MO") Function_Return 1
If (lDay = "TU") Function_Return 2
If (lDay = "WD") Function_Return 3
If (lDay = "TH") Function_Return 4
If (lDay = "FR") Function_Return 5
If (lDay = "SA") Function_Return 6
End_Function
//--------------------------------------------------------------------------->
Procedure Write_Dtime integer dtime#
If (SYSSCHED.KILL_RECORD = 1) Begin
lock
Delete SYSSCHED
unlock
End
Else begin
Reread
Move dtime# to SYSSCHED.LAST_RUN
saverecord SYSSCHED
Unlock
End
End_Procedure
//--------------------------------------------------------------------------->
Procedure DoProcess
local date lToday
local integer dtime# lMultiplier lPeriod lPeriodItself lRunIt lTodayDay# lSchedDay#
local string lHH lMM lTime sTransaction lPeriodType lDay lModuleId
DateTime dtVar
Sysdate4 lToday lHH lMM
if (integer(lMM)<10) insert "0" in lMM at 1
if (integer(lHH)<10) insert "0" in lHH at 1
move (string(lHH)+string(lMM)) to lTime
//--------------------------------------------------------------------------->
// We need an integer to represent the time of day to eliminate cross midnight problems.
// We create a single integer from the date, hour and minutes that does not have to go
// back to 0 at the beginning of the day or hour.
//--------------------------------------------------------------------------->
move (integer(lToday)*1440 + (integer(lHH)*60)+integer(lMM)) to dtime#
//--------------------------------------------------------------------------->
// We need the day of the week, 1=Sunday, 2=Monday, etc.
//--------------------------------------------------------------------------->
Move (CurrentDateTime()) To dtVar
Move (DateGetDayOfWeek(dtVar)) to lTodayDay#
//--------------------------------------------------------------------------->
//- Conditions to run this section which will be once a day every day at the specified time
//- 1. Has a time
//- 2. No date
//--------------------------------------------------------------------------->
Clear SYSSCHED
Move lTime to SYSSCHED.ACTION_TIME
Find GE SYSSCHED by Index.1 //- time, date, module id
While (found)
Move (left(SYSSCHED.PERIOD,1)) to lMultiplier
Move (mid(trim(SYSSCHED.PERIOD),1,2)) to lPeriodType
//--------------------------------------------------------------------->
//- Day code, SU, MO, etc.
//- Day # specified in the schedule
//--------------------------------------------------------------------->
If (lDay <> "") Move (Decode_Day#(Current_Object, uppercase(lDay))) to lSchedDay#
//- do some action
If (SYSSCHED.STATUS=1 and integer(SYSSCHED.ACTION_TIME)=integer(lTime);
and integer(SYSSCHED.ACTION_DATE)=0;
and SYSSCHED.LAST_RUN <> dtime#) Begin
If (lDay= "" or (lDay <> "" and lSchedDay#= lTodayDay#)) Begin
//--------------------------------------------------------------->
// Call the process that actually does the job
//--------------------------------------------------------------->
Chain Wait (trim(SYSSCHED.CALL_MODULE))
// Write the current date/time back to the data file
Send Write_Dtime dtime#
End
End
Find Gt SYSSCHED by Index.1 //- time, date, module id
Loop
//--------------------------------------------------------------->
//- Conditions to run this section
//- Periodic without time -- every minute or two, etc.
//--------------------------------------------------------------->
Clear SYSSCHED
Move 1 to SYSSCHED.PERIOD
Find GE SYSSCHED by Index.3 //- Period, recnum
While (found)
Move (left(SYSSCHED.PERIOD,1)) to lMultiplier
//- Only do 2 character periods
If (Length(trim(SYSSCHED.PERIOD)) = 2) Begin
Move (right(trim(SYSSCHED.PERIOD),1)) to lPeriodType
//- # of minutes for whatever period... 2D, 3W, etc.
Move ((Decode_Period(Current_Object, lPeriodType))*lMultiplier) to lPeriod
//--------------------------------------------------------------->
//- Do some action. Use of dtime# here is critical as it prevents the repetitive calling
//- of the process within the same minute.
//--------------------------------------------------------------->
If (SYSSCHED.STATUS=1 and SYSSCHED.PERIOD<>"";
and dtime# >= (SYSSCHED.LAST_RUN+lPeriod)) Begin
//- If daily or weekly, we need to specify a time of day as well
If (SYSSCHED.ACTION_TIME = "") Move 1 to lRunIt
//-
Else If (integer(lTime) >= integer(SYSSCHED.ACTION_TIME)) Move 1 to lRunIt
If (lRunIt) Begin
// Call the process
Chain Wait (trim(SYSSCHED.CALL_MODULE))
Send Write_Dtime dtime#
End
End
End
Find Gt SYSSCHED by Index.3 //- Period, recnum
Loop
end_procedure
Procedure Work_Loop
//--------------------------------------------------------------->
// An endless loop? Yes. A service has no "Cancel" button,
// so there's no graceful way to kill it. When you stop the
// service, you are just killing it, there's no other way.
//--------------------------------------------------------------->
While (1)
Send DoProcess
//--------------------------------------------------------------->
// If we didn't do the sleep, the loop would spin way too fast and consume
// more resources on the server than necessary. Adjust as necessary.
// See the text above for a better explanation.
//--------------------------------------------------------------->
Sleep 1
End
End_Procedure
End_Class
Object oScheduler is a cScheduler
End_Object
//--------------------------------------------------------------->
// Kicks it all off.
//--------------------------------------------------------------->
Send Work_Loop to oScheduler
Troubleshooting a service
There are two ways to troubleshoot the scheduler or any other program being used as a service. Either write it so that it can be run "normally" and put some showln's into it, or run it as a service with the same showln's but turn on "Allow service to interact with desktop".
The above scheduler as written will run "normally" but with that endless while loop, it will be difficult to stop unless a keycheck is added.
Starting and Stopping a service
There are several ways to start and stop a service. The manual method has been discussed above, but may not be workable for some situations. The following are command line methods that can be automated if necessary. Both are run from a DOS window and use the same name as used in the above installation.
Net Start "Descriptive name"
Net Stop "Descriptive name"
Alternatively there's another executable in the resource kit named sc.exe that will do the same thing but with some additional functionality. It is not the purpose of this paper to discuss "sc", but one of the things it's good for is doing a complete remote installation of a service. On the command line you can specify a number of the parameters that have been entered by hand in the above installation. There is some good documentation available for "sc" so if you get the executable from the web, be sure you get the documentation as well.
Download
VDF Creating an NT service for a VDF program.doc zipped up word document ~ 54 kb
Links
Microsoft Support Page: HOWTO set up a user-defined service (Article ID: Q137890)
Create a user-defined service by Patrick P. Young
VDF running as a Windows NT Service by Frank Vandervelpen
|