VDF-GUIdance logo



  Visual DataFlex Logo
  

Shared knowledge leads to accumulated knowledge

        Printer Friendly Page


Size: 54 KB Download
Date Created: 08/18/2002
Date Updated: 08/18/2002
Author: 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.

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


  1. 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.
    • Instsrv.exe
    • Srvany.exe
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.
  1. 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.

  1. If successful, start Regedit and go to the following key:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Descriptive Name


  1. Click on this folder and add a new key:
Parameters


  1. 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