Creating an NT service for a VDF programby Bob Worsley
Creating an NT service for a VDF programBy Bob Worsley mailto:bworsley@optonline.net EnvironmentThis will not work in Windows 95 or 98, there are no "Services" available. Windows NT, 2000 and all later are the only compatible versions. PurposeThere are a number of reasons for using an NT "Service"
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
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.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Descriptive Name
Parameters
Running the serviceYou will start the service by locating "Services" in your OS.
PermissionsOn 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. TimingSo 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:
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. ErrorsOne 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 schedulesThe 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 schedulerThis 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 serviceThere 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 serviceThere 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. DownloadVDF Creating an NT service for a VDF program.doc zipped up word document ~ 54 kb LinksMicrosoft 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 |
|||||||||||||||||
Copyright © 1999 - 2024 VDF-GUIdance on all material published, for details see our Disclaimer. |