Chapters

  1. Introduction
  2. Structure
  3. Packaging
  4. Logging
  5. Configuration
  6. Debugging EXEs
  7. Handling errors
  8. Testing
  9. Documentation
  10. Make
  11. Providing help
  12. Scheduled Tasks
  13. Windows Services
  14. Windows Event Log
  15. Windows Registry
  16. Creating SetUp.exe
  17. Regular Expressions
  18. Acre
  19. GUI
  20. Git

Appendices

  1. Windows environment vars
  2. User commands
  3. aplcores & WS integrity
  4. Development environment
  5. Special characters

Misc

While the Windows Task Manager just starts any ordinary application, any application that runs as a Windows Service must be specifically designed to meet certain requirements.

In particular, services communicate by exchanging messages with the Windows Service Control Manager (SCM).

Commands can be issued by the SC.exe (Service Controller) application or interactively via the Services application. This allows the user to start, pause, continue (resume) and stop a Windows Service.

Our application is already prepared to write log files and save information in the event of a crash, but that's not sufficient: while applications started by the Windows Task Scheduler might write to the Windows Event Log, applications running as a Windows Service are expected to do that, and for good reasons: when running on a server one cannot expect anybody to be keeping an eye on log or crash files.

In large organisations running server farms it is common to have software in place that scans the Windows Event Logs of all the servers, and raises an alarm (TCP messages, text messages, emails, whatever) if it finds problems.

We won't add the ability to write to the Windows Event Log in this chapter, but rather discuss how to do this in the next chapter.

With Dyalog version 16.0 we cannot install a stand-alone EXE as a Windows Service. All we can do is to install a given interpreter and then ask it to load a workspace, which implies running ⎕LX. (This restriction may be lifted in a future version of Dyalog.)

That means that unless you're happy to expose your code, you have a problem. There are solutions:

All three solutions add a level of complexity just to hide the code, but at least there are several escape routes available.

We resume, as usual, by saving a copy of Z:\code\v12 as Z:\code\v13.

To simplify matters we shall use the ServiceState namespace, also from the APLTree project. It requires you to do just two things:

  1. Call ServiceState.Init as early as possible. This function will ensure the Service can communicate with the SCM.

    Do it as early as possible to enure any request from the SCM will be answered promptly. Windows is not exactly patient when it waits for a Service to respond to a Pause, Resume or Stop request: after only 5 seconds you may see an error message complaining that the Service refused to cooperate.

    However, note that the interpreter confirms the Start request for us; no further action is required.

    Create a parameter space by calling CreateParmSpace, and set at least the name of the log function and possibly the namespace (or class instance) the log function is in. This log function is used to log any incoming requests from the SCM. The parameter space is then passed to Init as the right argument.

  2. In its main loop the Service is expected to call ServiceState.CheckServiceMessages.

    This is an operator, so it needs a function as operand: that is, a function that is doing the logging, allowing CheckServiceMessages to log its actions to the log file. (If you don't need a log file then simply passing {⍵} will do.

    When CheckServiceMessages is called if no request of the SCM is pending it will quit straight away and return a 0. If a Pause is pending then it goes into a loop, and continues to loop (with a ⎕DL in between) until either Continue (a.k.a. Resume) or Stop is requested by the SCM.

    If a Stop is requested, the operator will quit and return a 1.

Note: for installing or uninstalling a Service you need admin rights.

Suppose you have loaded a WS MyService which you want to install as a Windows Service run by the same version of Dyalog you are currently in:

aplexe←'"',(2 ⎕NQ # 'GetEnvironment' 'dyalog'),'\dyalogrt.exe"'
wsid←'"whereEverTheWsLives\MyAppService.DWS"'
cmd←aplexe,' ',wsid,' APL_ServiceInstall=MyAppService DYALOG_NOPOPUPS=1'

cmd could now be executed with the Execute class introduced in the chapter “Handling Errors”. That would do the trick.

DYALOG_NOPOPUPS

Setting this to 1 prevents any dialogs popping up (aplcore, WS FULL etc.). You don't want them when Dyalog is running in the background because there's nobody around to click the OK button.

This also prevents the Service MyAppService successfully installed message from popping up. You don't want to see that when executing tests that install, start, pause, resume, stop and uninstall a Service.

To uninstall the Service, simply open a console window with Run as administrator and enter:

sc delete MyAppService

and you are done.

Pitfalls when installing or uninstalling Windows Services

When you have opened the Services GUI while installing or uninstalling a Windows Service you must press F5 on the GUI to refresh the display.

The problem is not just that the GUI does not update itself – annoying as that may be. You might end up with a Service marked in the GUI as disabled, aftrer which you need to reboot the machine.

This can happen if you try to perform an action from the GUI when it is out of sync with the Service's current state.

SC: Service Control

SC is a command line program that allows a user with admin rights to issue particular commands regarding Windows Services. The general format is:

SC.exe {command} {serviceName}

Commonly used commands are:

  • create
  • start
  • pause
  • continue
  • stop
  • query
  • delete

From experience we know there are pitfalls. In general, there are kinds of problem you might encounter:

  1. The Service appears to start (shows running in the Services GUI) but nothing happens at all.
  2. The Service starts, the log file confirms it, but once you request the Service pauses or stops you get nothing but a Windows error message.
  3. The Service runs, but the application does not do anything, or it does something unexpected.

If a Service does not seem to do anything when started:

CONTINUE workspaces

The Service might have created a CONTINUE workspace, for various reasons.

Note that, starting with version 16.0, Dyalog no longer drops a CONTINUE workspace by default. You must configure Dyalog to do so.

A CONTINUE cannot be saved if there is more than one thread running – and Services are by definition multi-threaded. However, if it fails very early there might still be a CONTINUE.

Once a second thread is started, Dyalog is no longer able to save a CONTINUE workspace. Establishing error trapping before a second thread is started avoids this problem.

aplcores

Writing to the directory the service is installed in might be prohibited by Windows. That might well prevent a CONTINUE or aplcore from being saved.

While you cannot choose the folder in which a CONTINUE would be saved you can define a folder for aplcores:

`APLCORENAME='/pathToFolder/my_aplcore*`.

This saves any aplcore as “my_aplcore” followed by a running number. For more information regarding aplcores see “Appendix 3 — aplcores and WS integrity”.

Here the log file contains everything we expect: calling parameters etc. In such a case we know the Service started and is running.

If these two conditions are met then it's hard to imagine what could prevent the application from reacting to any requests of the SCM, except when you have an endless loop somewhere in your application.

The very thought of you And I forget to do Those ordinary things That everyone ought to do

Cole Porter

First and foremost it is worth repeating that any application supposed to run as a Service should be developed as an ordinary application, including test cases. When it passes such test cases you have reasons to be confident the application will run as a Service too.

Having said this, there can be surprising differences between running as an ordinary application and a Service. For example, when a Service runs, not with a user's account, but with the system account (which is quite normal to do) any call to #.FilesAndDirs.GetTempPath results in

"C:\Windows\System32\config\systemprofile\AppData\Local\Apps"

while for a user's account it would be something like

'C:\Users\{username}\AppData\Local\Temp'.

When the application behaves in an unexpected way you need to debug it, and for that Ride is invaluable.

First of all we have to make sure that the application will give us a Ride if we need one. Since passing any arguments for a Ride via the command line requires the Service to be uninstalled and installed at least twice we recommend preparing the Service from within the application instead.

If you have trouble Riding into any kind of application, check there is not an old instance of the Service running and occupying the port you need for the Ride.

There are two very different scenarios in which you might want to use Ride:

  1. The Service does not seem to start or does not respond to Pause or Stop requests.
  2. The Service starts fine and responds properly to Pause or Stop requests, but the application is otherwise behaving unexpectedly.

In the former case ensure as soon as possible after startup you allow the programmer to Ride into the Service. The first line of the function in ⎕LX should provide a Ride.

At such an early stage we don't have an INI file instantiated, so we cannot switch Ride on and off via the INI file, we have to modify the code for that.

You might feel tempted to overcome this by doing it a bit later (e.g. after processing the INI file) but beware: if a Service is not cooperating then “a bit later” might be too late to get investigate the problem. Resist temptation.

In the second case, add the call to CheckForRide once the INI file has been instantiated.

Information

Make sure that you have never more than one of the two calls to CheckForRide active. If both are active you would be able to use the first one, but the second one would throw you out!

We want to log as soon as possible any command-line parameters, as well as any message exchange between the Service and the SCM.

Again we advise you not to wait until the folder holding the log files is defined by instantiating the INI file. Instead we suggest making the assumption that a certain folder (“Logs”) will (or might) exist in the current directory which will become where the workspace was loaded from.

If that's not convenient, consider passing the directory that will contain the Logs folder as a command line parameter.

In the next chapter (“The Windows Event Log”) we will discuss why and how to use the Windows Event Log, in particular when it comes to Services.

First of all we need to point out that MyApp as it stands is hardly a candidate for a Service. We need to make something up. Let's specify one or more folders to be watched by the MyApp Service.

If any files are found then they are processed. Finally the app will store hashes for all the files it has processed. That allows it to recognize any added, changed or removed files efficiently.

For the Service we need to create a workspace that can be loaded by that Service. Therefore we need to set ⎕LX, and for that we create a new function:

 ∇ {r}←SetLXForService(earlyRide ridePort)
   ⍝ Set Latent Expression (needed in order to export workspace as EXE)
   ⍝ `earlyRide` is a flag. 1 allows a Ride.
   ⍝ `ridePort`  is the port number to be used for a Ride.
      #.⎕IO←1 ⋄ #.⎕ML←1 ⋄ #.⎕WX←3 ⋄ #.⎕PP←15 ⋄ #.⎕DIV←1
      r←⍬
      ⎕LX←'#.MyApp.RunAsService ',(⍕earlyRide),' ',(⍕ridePort)
 ∇

The function takes a flag earlyRide and an integer ridePort as arguments. How and when this function is called will be discussed in a moment.

Because we have now two functions that set ⎕LX we shall rename the original one (SetLX) to SetLXForApplication to tell them apart.

Next we need the main function for the service:

 ∇ {r}←RunAsService(earlyRide ridePort);⎕TRAP;MyLogger;Config;∆FileHashes
    ⍝ Main function when app is running as a Windows Service.
    ⍝ `earlyRide`: flag that allows a very early Ride.
    ⍝ `ridePort`: Port number used by Ride.
      r←⍬
      CheckForRide earlyRide ridePort
      #.FilesAndDirs.PolishCurrentDir
      ⎕TRAP←#.HandleError.SetTrap ⍬
      (Config MyLogger)←Initial #.ServiceState.IsRunningAsService
      ⎕TRAP←(Config.Debug=0)SetTrap Config
      Config.ControlFileTieNo←CheckForOtherInstances ⍬
      ∆FileHashes←0 2⍴''
      :If #.ServiceState.IsRunningAsService
          {MainLoop ⍵}&⍬
          ⎕DQ'.'
      :Else
          MainLoop ⍬
      :EndIf
      Cleanup ⍬
      Off EXIT.OK
    ∇

Notes:

Time to change the MyApp.Initial function:

leanpub-start-insert
 ∇ (Config MyLogger)←Initial isService;parms
    ⍝ Prepares the application.
      Config←CreateConfig isService
      Config.ControlFileTieNo←CheckForOtherInstances ⍬
      CheckForRide (0≠Config.Ride) Config.Ride
      MyLogger←OpenLogFile Config.LogFolder
      MyLogger.Log'Started MyApp in ',F.PWD
      MyLogger.Log 2 ⎕NQ'#' 'GetCommandLine'
      MyLogger.Log↓⎕FMT Config.∆List
      :If isService
          parms←#.ServiceState.CreateParmSpace
          parms.logFunction←'Log'
          parms.logFunctionParent←MyLogger
          #.ServiceState.Init parms
      :EndIf

Note that we pass isService as right argument to CreateConfig, so we must amend CreateConfig accordingly:

leanpub-start-insert
 ∇ Config←CreateConfig isService;myIni;iniFilename
   Config←⎕NS''
   ...
   Config.IsService←isService
   ...
       Config.Accents←⊃Config.Accents myIni.Get'Config:Accents'
        :If isService
            Config.WatchFolders←⊃myIni.Get'Folders:Watch'
        :Else
            Config.LogFolder←'expand'F.NormalizePath⊃Config.LogFolder myIni.Get'Folders:Logs'
        :EndIf
        Config.DumpFolder←'expand'F.NormalizePath⊃Config.DumpFolder myIni.Get'Folders:Errors'
   ...
 ∇

Note that WatchFolder is introduced only when the application is running as a Service.

Time to introduce the function MainLoop:

∇ {r}←MainLoop port;S
  r←⍬
  MyLogger.Log'"MyApp" server started'
  S←#.ServiceState
  :Repeat
      LoopOverFolder ⍬
      :If (MyLogger.Log S.CheckServiceMessages)S.IsRunningAsService
          MyLogger.Log'"MyApp" is about to shut down...'
          :Leave
      :EndIf
      ⎕DL 2
  :Until 0
 ⍝Done
∇

Notes:

The function LoopOverFolder:

∇ {r}←LoopOverFolder dummy;folder;files;hashes;noOf;rc
  r←⍬
  :For folder :In Config.WatchFolders
      files←#.FilesAndDirs.ListFiles folder,'\*.txt'
      hashes←GetHash¨files
      (files hashes)←(~hashes∊∆FileHashes[;2])∘/¨files hashes
      :If 0<noOf←LoopOverFiles files hashes
          :If EXIT.OK=rc←TxtToCsv folder
              MyLogger.Log'Totals.csv updated'
          :Else
              LogError rc('Could not update Totals.csv, RC=',EXIT.GetName rc)
          :EndIf
      :EndIf
  :EndFor
∇

This function calls GetHash so we had better introduce it:

 GetHash←{
 ⍝ Get hash for file ⍵
     ⊣2 ⎕NQ'#' 'GetBuildID'⍵
 }

The function LoopOverFiles:

 ∇ noOf←LoopOverFiles(files hashes);file;hash;rc
   noOf←0
   :For file hash :InEach files hashes
       :If EXIT.OK=rc←TxtToCsv file
           ∆FileHashes⍪←file hash
           noOf+←1
       :EndIf
   :EndFor
 ∇

This function finally calls TxtToCsv.

Because of the change we've made to the right argument of Initial we need to change StartFromCmdLine. Here the function Initial needs to be told it is not running as a Service:

∇ {r}←StartFromCmdLine arg;MyLogger;Config;rc;⎕TRAP
...
   (Config MyLogger)←Initial #.ServiceState.IsRunningAsService
...

Two more changes:

 ∇ {r}←Cleanup dummy
   r←⍬
   ⎕FUNTIE Config.ControlFileTieNo
   Config.ControlFileTieNo←⍬
   '#'⎕WS'Event' 'ServiceNotification' 0

This disconnects the handler from the ServiceNotification event.

Finally we redefine which functions are public:

 ∇ r←PublicFns
   r←'StartFromCmdLine' 'TxtToCsv' 'SetLXForApplication' 'SetLXForService' 'RunAsService'

Now it's time to see if we broke anything. Double-click MyApp.dyapp and type y to answer the question whether you would like to run all test cases. If anything fails, execute #.Tests.RunDebug 0 and fix the problem.

In order to install or uninstall the Service we need two BATs: InstallService.bat and Uninstall_Service.bat. We will write these BATs from Dyalog. For that we write a class ServiceHelpers:

:Class ServiceHelpers

    ∇ {r}←CreateBatFiles dummy;path;cmd;aplexe;wsid
      :Access Public Shared
    ⍝ Write two BAT files to the current directory:
    ⍝ Install_Service.bat and Uninstall_Service.bat
      r←⍬
      path←#.FilesAndDirs.PWD

      aplexe←'"',(2 ⎕NQ'#' 'GetEnvironment' 'dyalog'),'\dyalogrt.exe"'
      wsid←'"%~dp0\MyAppService.DWS"'
      cmd←aplexe,' ',wsid,' APL_ServiceInstall=MyAppService'
     ⍝cmd,←' APLCORENAME={foldername}'
     ⍝cmd,←' DYALOG_EVENTLOGNAME={foo}'
      cmd,←' DYALOG_NOPOPUPS=1'
      cmd,←' MAXWS=64MB'
      #.APLTreeUtils.WriteUtf8File(path,'\Install_Service.bat')cmd

      cmd←⊂'sc delete MyAppService'
      cmd,←⊂'@echo off'
      cmd,←⊂'    echo Error %errorlevel%'
      cmd,←⊂'    if NOT ["%errorlevel%"]==["0"] ('
      cmd,←⊂'    pause'
      cmd,←⊂'exit /b %errorlevel%'
      cmd,←⊂')'
      #.APLTreeUtils.WriteUtf8File(path,'\Uninstall_Service.bat')cmd
     ⍝Done
    ∇

:EndClass

Notes:

Now it's time to create a DYAPP for the service. For that copy Make.dyapp as MakeService.dyapp and then edit it:

Target #
Load ..\AplTree\APLTreeUtils
Load ..\AplTree\FilesAndDirs
Load ..\AplTree\HandleError
Load ..\AplTree\IniFiles
Load ..\AplTree\OS
Load ..\AplTree\Logger
Load ..\AplTree\EventCodes
Load Constants
Load Utilities
Load MyApp

Load ..\AplTree\ServiceState
Load ..\AplTree\Tester
Load ..\AplTree\Execute
Load ..\AplTree\WinSys
Load TestsForServices
Load ServiceHelpers

Run #.ServiceHelpers.CreateBatFiles ⍬
Run '#.⎕EX''ServiceHelpers'''
Run #.MyApp.SetLXForService 0 4599   ⍝ [1|0]: Ride/no Ride, [n] Ride port number

Load MakeService
Run #.MakeService.Run 0

Notes:

That obviously requires the class MakeService:

:Class MakeService
⍝ Creates a workspace "MyAppService" which can then run as a service.
⍝ 1. Re-create folder DESTINATION in the current directory
⍝ 2. Copy the INI file template over to DESTINATION\ as MyApp.ini
⍝ 3. Save the workspace within DESTINATION
    ⎕IO←1 ⋄ ⎕ML←1
    DESTINATION←'MyAppService'

    ∇ {r}←Run offFlag;en;F;U
      :Access Public Shared
      r←⍬
      (F U)←##.(FilesAndDirs Utilities)
      (rc en more)←F.RmDir DESTINATION
      U.Assert 0=rc
      U.Assert 'Create!'##.FilesAndDirs.CheckPath DESTINATION
      'MyApp.ini.template' CopyTo DESTINATION,'\MyApp.ini'
      'Install_Service.bat' CopyTo DESTINATION,'\'
      'Uninstall_Service.bat' CopyTo DESTINATION,'\'
      ⎕WSID←DESTINATION,'\',DESTINATION
      #.⎕EX⍕⎕THIS
      0 ⎕SAVE ⎕WSID
      {⎕OFF}⍣(⊃offFlag)⊣⍬
    ∇

    ∇ {r}←from CopyTo to;rc;more;msg
      r←⍬
      (rc more)←from F.CopyTo to
      msg←'Copy failed RC=' ,(⍕rc),'; ',more
      msg ⎕signal 11/⍨0≠rc
    ∇
:EndClass

Notes:

Self-deleting code

How it is possible for the function MakeService.Run to delete itself and keep running anyway?

APL code (functions, operators and scripts) is copied onto the stack for execution. You can investigate the stack at any given moment with )SI and )SINL; for details type the command in question into the session and then press F1.

Even if the code of a class executes ⎕EX ⍕⎕THIS or a function or operator ⎕EX ⊃⎕SI the code keeps running because the copy on the stack will exist until the function or operator quits. Scripts might even live longer: only when the last reference pointing to a script is deleted does the script cease to exist.

We have test cases that tell us the “business logig” of MyApp works just fine. What we also need are tests that it runs fine as a Service as well.

Since the two test scenarios are only loosely related we want to keep those tests separate. It is easy to see a way: testing the Service means assembling all the needed stuff, installing the Service, carrying out the tests and finally un-installing the tests and cleaning up.

We don't want to execute all this unless we really have to.

We start by creating a new script TestsForServices which we save alongside the other scrips in v13/:

:Namespace TestsForServices
⍝ Installs a service "MyAppService" in a folder within the Windows Temp directory with
⍝ a randomly chosen name. The tests then start, pause, continue and stop the service.\\
⍝ They also check whether the application produces the expected results.

    ⎕IO←1 ⋄ ⎕ML←1

:EndNamespace

We now discuss the functions we are going to add one after the other. Note that the Initial function is particularly important in this scenario: we need to copy over all the stuff we need, code as well as input files, make adjustments, and install the Service.

This could all be done in a single function but it would be lengthy and difficult to read. To avoid this we split the function into obvious units. By naming those functions carefully we should get away without adding any comments because the code explains itself. Here we go:

∇ r←Initial;rc;ini;row;bat;more
   ∆Path←##.FilesAndDirs.GetTempFilename''
   #.FilesAndDirs.DeleteFile ∆Path
   ∆Path←¯4↓∆Path
   ∆ServiceName←'MyAppService'
   r←0
   :If 0=#.WinSys.IsRunningAsAdmin
       ⎕←'Sorry, but you need admin rights to run this test suite!'
       :Return
   :EndIf
   ∆CreateFolderStructure ⍬
   ∆CopyFiles ⍬
   ∆CreateBATs ⍬
   ∆CreateIniFile ⍬
   ∆InstallService ⍬
   ⎕←'*** Service ',∆ServiceName,' successfully installed'
   r←1

Note that all the sub-function and global variables start their names with . An example is the function ∆Execute_SC_Cmd:

∇ {(rc msg)}←∆Execute_SC_Cmd command;cmd;buff
 ⍝ Executes a SC (Service Control) command
   rc←1 ⋄ msg←'Could not execute the command'
   cmd←'SC ',command,' ',∆ServiceName
   buff←#.Execute.Process cmd
   →FailsIf 0≠1⊃buff
   msg←⊃,/2⊃buff
   rc←3⊃buff
∇

It executes SC commands like start, pause, continue, stop and query by preparing a string and then passing it to Execute.Process.

It analyzes the result and returns the text part of it as well as a return code. While the first four commands aim to change the current status of a Service, query is designed to establish what the current status of a Service actually is.

After having executed the test suite we want to clean up, so we create a function Cleanup.

Remember: if the test framework finds a function Initial, it executes it before executing the actual test cases, while any function Cleanup will be executed after the test cases have been executed.

∇ {r}←Cleanup
   r←⍬
   :If 0<⎕NC'∆ServiceName'
       ∆Execute_SC_Cmd'stop'
       ∆Execute_SC_Cmd'delete'
       ##.FilesAndDirs.RmDir ∆Path
       ⎕EX¨'∆Path' '∆ServiceName'
   :EndIf
∇

We also need ∆Pause:

∇ {r}←∆Pause seconds
   r←⍬
   ⎕←'   Pausing for ',(⍕seconds),' seconds...'
   ⎕DL seconds
∇

We could discuss all the sub functions called by these two functions but it would tell us little. Therefore we suggest you copy the code from the web site. We discuss here just the two test functions:

∇ R←Test_01(stopFlag batchFlag);⎕TRAP;rc;more
  ⍝ Start, pause, continue and stop the service.
  ⎕TRAP←(999 'C' '. ⍝ Deliberate error')(0 'N')
  R←∆Failed

  (rc more)←∆Execute_SC_Cmd'start'
  →FailsIf 0≠rc
  ∆Pause 2
  (rc more)←∆Execute_SC_Cmd'query'
  →FailsIf 0≠rc
  →FailsIf 0=∨/'STATE : 4 RUNNING'⍷#.APLTreeUtils.dmb more

  (rc more)←∆Execute_SC_Cmd'pause'
  →FailsIf 0≠rc
  ∆Pause 2
  →FailsIf 1≠⍴#.FilesAndDirs.ListFiles ∆Path,'\service\Logs\'
  (rc more)←∆Execute_SC_Cmd'query'
  →FailsIf 0=∨/'STATE : 7 PAUSED'⍷#.APLTreeUtils.dmb more

  (rc more)←∆Execute_SC_Cmd'continue'
  →FailsIf 0≠rc
  ∆Pause 2
  (rc more)←∆Execute_SC_Cmd'query'
  →FailsIf 0=∨/'STATE : 4 RUNNING'⍷#.APLTreeUtils.dmb more

  (rc more)←∆Execute_SC_Cmd'stop'
  →FailsIf 0≠rc
  ∆Pause 2
  (rc more)←∆Execute_SC_Cmd'query'○
  →FailsIf 0=∨/'STATE : 1 STOPPED'⍷#.APLTreeUtils.dmb more

  R←∆OK
∇

In order to understand the →FailsIf statements it is essential to have a look at a typical result returned by the ∆Execute_SC_Cmd function, in this case a query:

      ⍴more
328
      ≡more
1
      #.APLTreeUtils.dmb more
SERVICE_NAME: MyAppService TYPE : 10 WIN32_OWN_PROCESS STATE : 4 RUNNING (STOPPABLE, PAUSABLE, ACCEPTS_SHUTDOWN) WIN32_EXIT_CODE : 0 (0x0) SERVICE_EXIT_CODE
       : 0 (0x0) CHECKPOINT

We have removed white space here to increase readability. (The result is richly endowed with them.)

This test starts, pauses, continues and finally stops the Service after having processed some files:

∇ R←Test_02(stopFlag batchFlag);⎕TRAP;rc;more;noOfCSVs;success;oldTotal;newTotal;A;F
  ⍝ Start service, check results, give it some more work to do, check and stop it.
   ⎕TRAP←(999 'C' '. ⍝ Deliberate error')(0 'N')
   R←∆Failed
   (A F)←#.(APLTreeUtils FilesAndDirs)

   (rc more)←∆Execute_SC_Cmd'start'
   →FailsIf 0≠rc
   ∆Pause 1
   (rc more)←∆Execute_SC_Cmd'query'
   →FailsIf 0=∨/'STATE : 4 RUNNING'⍷A.dmb more

   ⍝ At this point the service will have processed all the text files, so there
   ⍝ must now be some CSV files, including the Total.csv file.
   ⍝ We then copy 6 more text files, so we should see 6 more CSVs & a changed Total.
   oldTotal←↑{','A.Split ⍵}¨A.ReadUtf8File ∆Path,'\input\en\total.csv'
   noOfCSVs←⍴F.ListFiles ∆Path,'\input\en\*.csv'
   (success more list)←(∆Path,'\texts')F.CopyTree ∆Path,'\input\'  ⍝ All of them
   {1≠⍵:.}success
   ∆Pause 2
   newTotal←↑{','A.Split ⍵}¨A.ReadUtf8File ∆Path,'\input\en\total.csv'
   →PassesIf(noOfCSVs+6)=⍴F.ListFiles ∆Path,'\input\en\*.csv'
   →PassesIf oldTotal≢newTotal
   oldTotal[;2]←⍎¨oldTotal[;2]
   newTotal[;2]←⍎¨newTotal[;2]
   →PassesIf oldTotal[;2]∧.≤newTotal[;2]

   (rc more)←∆Execute_SC_Cmd'stop'
   →FailsIf 0≠rc
   ∆Pause 2
   (rc more)←∆Execute_SC_Cmd'query'
   →FailsIf 0=∨/'STATE : 1 STOPPED'⍷A.dmb more

   R←∆OK
∇

Though this test starts and stops the Service, its real purpose is to make sure that the Service processes input files as expected.

First we need to ensure everything is assembled freshly, and with admin rights.

The best way to do that is to run the script MakeService.dyapp from a console that was started with admin rights. (Unfortunately you cannot right-click on a DYAPP and select “Run as administrator” from the context menu.)

You must change the current directory in the console window to where the DYAPP lives before actually calling it.

Console with admin rights.

The best way to start a console window with admin rights:

  1. Press the Windows key.
  2. Type cmd. If you wonder, “where shall I type this into” don't worry - just type.
  3. Right-click on Command prompt and select Run as administrator.

A Dyalog instance is started. In the session you should see something similar to this:

Booting C:\...\v13\MakeService.dyapp
Loaded: #.APLTreeUtils
Loaded: #.FilesAndDirs
Loaded: #.HandleError
Loaded: #.IniFiles
Loaded: #.OS
Loaded: #.Logger
Loaded: #.EventCodes
Loaded: #.Constants
Loaded: #.Utilities
Loaded: #.MyApp
Loaded: #.ServiceState
Loaded: #.Tester
Loaded: #.Execute
Loaded: #.WinSys
Loaded: #.TestsForServices
Loaded: #.ServiceHelpers
#.⎕EX'ServiceHelpers'
Loaded: #.MakeService

In the next step establish the test helpers by calling #.TestsForServices.GetHelpers.

Finally run #.TestsForServices.RunDebug 0. You should see something like this:

#.TestsForServices.RunDebug 0
--- Test framework "Tester" version 3.3.0 from YYYY-MM-DD -----------------------------
Searching for INI file testcases_APLTEAM2.ini
  ...not found
Searching for INI file Testcases.ini
  ...not found
Looking for a function "Initial"...
*** Service MyAppService successfully installed
  "Initial" found and sucessfully executed
--- Tests started at YYYY-MM-DD hh:mm:dd on #.TestsForServices ------------------------
   Pausing for 2 seconds...
   Pausing for 2 seconds...
   Pausing for 2 seconds...
   Pausing for 2 seconds...
  Test_01 (1 of 2) : Start, pause and continue the service.
   Pausing for 2 seconds...
   Pausing for 2 seconds...
   Pausing for 2 seconds...
  Test_02 (2 of 2) : Start service, check results, give it some more work to do, check and stop it.
 --------------------------------------------------------------------------------------------------
   2 test cases executed
   0 test cases failed
   0 test cases broken
Time of execution recorded on variable #.TestsForServices.TestCasesExecutedAt in: YYYY-MM-DD hh:mm:ss
Looking for a function "Cleanup"...
  Function "Cleanup" found and sucessfully executed.
*** Tests done