Sometimes when developing we want our program to process items continually. This can be a fairly simple concept: the script sits in a loop and constantly does what we need it to until there are no items to process. Then our program is finished. If we want to run more items, we need to start the script again. As you can see, this could be a tedious, laborious, and potentially inefficient system.
Friday, January 26. 2018
LDC #69: Running in the Background
Well, there’s a better option to be found in background scripts. These scripts start in a separate scripting engine and run independently as a background operation. What this means is your script can sit idle waiting for work without interrupting the user’s workflow. There are some limitations to background scripts but they have many possible uses. In this blog, we will create a background script that keeps track of the time the current user has spent editing files.
The script as presented in this blog can be replicated by using a hook on the “file close” function. However, there are multiple ways to close files in GoFiler, and some may not be captured by the hook. Additionally, the background thread can gather even more information that is not covered in this example. Instead of listing the entire script all at once and walking through it, let’s write the script together. I’ll use colors to indicate changes to code.
First we need to have a method of starting our background thread. We could put this into application startup (covered in LDC #39) but there would be no control for the user. Since we are developing the script, it would be better to make some menu hooks to start and stop the thread. Then later when the script is debugged we can add it to application start up as needed.
Let’s begin by defining our start and stop hooks as well as the setup and main functions.
int start (int f_id, string mode); int stop (int f_id, string mode); int main (); int setup ();
The start and stop functions will be hooked into the ribbon using the setup function. The main function will allow us as developers to run the script to add the menu hooks for testing purposes. These functions are similar to other menu hooks that have been covered in the past, so I will not go into how they work but instead just list the code.
/****************************************/ int setup() { /* Called from Application Startup */ /****************************************/ string fnScript; /* Us */ string item1[10]; /* Menu Item */ string item2[10]; /* Menu Item */ int rc; /* Return Code */ /* */ /* ** Add Menu Item */ /* * Define Functions */ /* o Start */ item1["Code"] = "EXTENSION_START_MONITOR"; /* Function Code */ item1["MenuText"] = "&Start Activity Tracking"; /* Menu Text */ item1["Description"] = "<B>Start Activity Tracking</B>\r\r " + /* Description */ "Starts tracking application activity."; /* of function */ /* o Stop */ item2["Code"] = "EXTENSION_STOP_MONITOR"; /* Function Code */ item2["MenuText"] = "Sto&p Activity Tracking"; /* Menu Text */ item2["Description"] = "<B>Stop Activity Tracking</B>\r\r " + /* Description */ "Stops tracking application activity."; /* of function */ /* * Setup */ fnScript = GetScriptFilename(); /* Get the script filename */ /* * Check for/add menu item */ rc = MenuFindFunctionID(item1["Code"]); /* Look for existing */ if (IsError(rc)) { /* Not already added */ rc = MenuAddFunction(item1); /* Add the item */ if (IsNotError(rc)) { /* Was already be added */ MenuSetHook(item1["Code"], fnScript, "start"); /* Set the Test Hook */ } /* end error */ else { /* Failed? */ return rc; /* Leave with error */ } /* end failed */ } /* end not added */ /* * Check for/add menu item */ rc = MenuFindFunctionID(item2["Code"]); /* Look for existing */ if (IsError(rc)) { /* Not already added */ rc = MenuAddFunction(item2); /* Add the item */ if (IsNotError(rc)) { /* Was already be added */ MenuSetHook(item2["Code"], fnScript, "stop"); /* Set the Test Hook */ } /* end error */ else { /* Failed? */ return rc; /* Leave with error */ } /* end failed */ } /* end not added */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */ /****************************************/ int main() { /* Initialize from Hook Processor */ /****************************************/ /* */ /* ** Initialize Hook Processor */ /* * Check State */ if (GetScriptParent() != "LegatoIDE") { /* Not running in IDE? */ return ERROR_NONE; /* Done */ } /* end not in IDE */ /* * Set up */ setup(); /* Add to the menu */ return ERROR_NONE; /* return */ } /* end function */
So now that we have two new hooks to our start and stop functions, clearly we need those functions to work. We should only start one copy of our thread so the start function needs to have protections for multiple thread initializations. The stop function will need to communicate with our background thread to tell it to stop. In fact, both of these functions need to communicate to our background thread. The start function can do so using the RunBackgroundScript function, which we will discuss in greater detail a little later. As stated in the documentation for the RunBackgroundScript function, the new thread will run in its own scripting engine. This means it will have its own global variables. In future versions of Legato there will be session variables that any script can access but since they are not available yet we can use a file to communicate between the threads.
Now that we’ve discussed how to get the threads to communicate, let’s tackle framing our start and stop functions. First we should make sure we are running in preprocess mode as we do with most hooks so we don’t run them multiple times. We also need a few global variables, mainly a handle the background thread and the name of our file we will use to communicate with it. Let’s add the global variables.
/****************************************/ handle hBGScript; /* Background Script Handle */ string fnTemp; /* File Name for Control File */
Then fill out the start and stop function blocks so we can add the background code in after.
/****************************************/ int start(int f_id, string mode) { /* Start Hook Processor */ /****************************************/ /* */ /* ** Run Start */ /* * Safety */ if (mode != "preprocess") { /* Not preprocess? */ return ERROR_NONE; /* Return w/ no error */ } /* end not preprocess */ if (hBGScript != NULL_HANDLE) { /* Already Running? */ return ERROR_NONE; /* Return w/ no error */ } /* end already running */ /* * Build Name */ // Need to make a name /* * Start it */ // Start Thread return ERROR_NONE; /* return */ } /* end function */ /****************************************/ int stop(int f_id, string mode) { /* Stop Hook Processor */ /****************************************/ int rc; /* Return Code */ /* */ /* ** Run Stop */ /* * Safety */ if (mode != "preprocess") { /* Not preprocess? */ return ERROR_NONE; /* Return w/ no error */ } /* end not preprocess */ if (hBGScript == NULL_HANDLE) { /* Already Running? */ return ERROR_NONE; /* Return w/ no error */ } /* end already running */ /* * Stop it */ // Stop Thread /* * Clean up */ // Delete Files return ERROR_NONE; /* return */ } /* end function */
Once we have the functions framed, we can work on the contents. Let’s dig into the start function. The primary purpose of this function is to use the RunBackgroundScript SDK function. The RunBackgroundScript function takes many of the same parameters as the menu functions that create the hooks, such as the script file and a function name. It also takes optional parameters, which can be any data you wish with the exception of Legato objects like the SGML Object. We will use it to pass the name of file that the thread can use to “talk” to the hook script.
In order to make this happen we will need to create a temp file name for our communication file and then pass that information to the RunBackgroundScript function.
/* * Build Name */ fnTemp = ClipFileExtension(GetTempFile()); /* Get name */ fnTemp += FormatString("%s", GetTickCount()) + ".bgc"; /* Add extra Information */ /* * Start it */ hBGScript = RunBackgroundScript(GetScriptFilename(), /* Run Background Script */ "background", fnTemp); /* name of the function */ if (IsError(hBGScript)) { /* Failed? */ MessageBox("Couldn't start thread 0x%08X", GetLastError()); /* Tell user */ } /* end failed */
The above lines create a file in the Windows temp folder with a semi-unique name and stores it in the fnTemp global variable. Then we run our background script, which is another function in this file with the name of “background”. Also we pass it our fnTemp variable because, even though it is a global variable, the background function will be run in a different script engine, so the global variables of this script won’t be available to it.
This finishes up start so now let’s discuss stop. The stop function needs to write data into the communication file and then see if it actually closes. The latter part is more complicated but depending on your application it might not be needed. It’s a good general rule for programming with multiple threads to make sure they stop when you ask them to. Otherwise, it may cause issues later on.
To accomplish this functionality we are going to use the WaitForObject function. This function has been covered in previous posts but basically it will cause our script to pause while waiting for the handle to trigger an event (in our case, the script ending). Since we don’t want our script to appear unresponsive we should wait many times but for a small amount of time each instance. This means we can do things like update the status bar while still waiting. It also means we need a loop. The first change to the function will be to add a counter variable ix to the function. Then we can add our code.
/* * Stop it */ StringToFile("Stop", fnTemp); /* Create File */ for (ix = 0; ix < 10; ix++) { /* Loop */ rc = WaitForObject(hBGScript, 100); /* Wait for it */ if (IsNotError(rc)) { /* Worked? */ hBGScript = NULL_HANDLE; /* Clear Handle */ break; /* Stop */ } /* end worked */ } /* end loop */ /* * Clean up */ DeleteFile(fnTemp); /* Delete it */
The StringToFile function adds the Stop command to our file. We have now told our thread to stop, so we can wait. We start a loop and use WaitForObject. If the function is successful, we clear the handle to the thread since it has finished and then stop the loop. After the loop, we could detect if the thread handle is still set, meaning the thread hasn’t stopped, and notify the user. However, for this script it’s unimportant. We then cleanup our control file.
We now have the framework for starting and stopping a background thread. What we don’t have is a background thread. So let’s start by thinking about the background function. We want it to track open windows and we want to do something with that data. To accomplish those goals, we are going to need a few global variables.
/****************************************/ string bgWindows[][]; /* Window List */ boolean bgTracking[]; /* Window Tracking */ int bgLastWindow; /* Last Window */ handle bgLogFile; /* Log File Handle */
I preceded the global variables with the letters bg to indicate that they are to be used by the background thread only. If our background thread was in a different script file we wouldn’t need to do that, but here it adds to code clarity. The bgWindows variable stores information about open windows. The bgTracking variable is an array used to track whether that window is still open. The bgLastWindow variable is a counter of how many items are in these arrays. Lastly, the bgLogFile variable is a handle to our output log file.
Now that we have the data structures for our background thread let’s form our background routine.
/****************************************/ int background(string fnName) { /* Background Function */ /****************************************/ int rc; /* Return Code */ /* */ /* ** Run Background Function */ /* * Safety */ if (fnName == "") { /* No Good? */ return ERROR_NONE; /* Done */ } /* end no good */ /* * Set up log */ // Set up Logging /* * Main Loop */ StatusBarMessage("Started activity monitor."); /* Tell User */ while (TRUE) { /* Loop forever */ /* o Reset Open Status */ // Add in list prep /* o Check Open Files */ // Add in open file list /* o Process List */ // Add in list processing /* o Check to Stop */ rc = DoesFileExist(fnName); /* Check for file */ if (rc == TRUE) { /* Exists? */ break; /* Stop Loop */ } /* end exists */ Sleep(500); /* Sleep */ } /* end loop */ /* * Clean up */ // Add in clean up StatusBarMessage("Stopped activity monitor."); /* Tell User */ return ERROR_NONE; /* return */ } /* end function */
The first thing you will notice is that this function contains an infinite loop. This is because, without one, our function would end when the code finishes executing. Depending on what you’re trying to accomplish this may be desirable. A background thread that is spawned to do a specific task doesn’t need to stay open after the task is complete. For our purposes though we want the thread to stay active and processing until we say otherwise. Our loop only stops when the passed file (fnName) exists. We could do fancier logic like reading the file and looking for specific phrases for commands but since all we want to know is whether we should stop just having a file is a good enough indication.
We could run our script as-is. It would start and stop using our hooks and print the fact that it started or stopped in the status bar.
Started:
Stopped:
When the our activity monitor is running the application can be used as normal.
We now have a working background script. Next we should focus on getting it to actually do something productive. To this end let’s figure out how to track what windows are being edited. The EnumerateEditWindows SDK function looks like a good candidate for the job. It returns a string array with named keys about each window. In order to tell if a window has been closed since the last time our loop ran we will use our bgTracking array. Now we can get a list of windows and then compare it to our last list of windows.
/****************************************/ int background(string fnName) { /* Background Function */ /****************************************/ string windows[][]; /* List of All Windows */ int size; /* Size of Edit window List */ int ex, /* Enumeration Index */ wx, /* Window Index */ rc; /* Return Code */ /* */ /* ** Run Background Function */ /* * Safety */ if (fnName == "") { /* No Good? */ return ERROR_NONE; /* Done */ } /* end no good */ /* * Set up log */ // Set up Logging /* * Main Loop */ StatusBarMessage("Started activity monitor."); /* Tell User */ while (TRUE) { /* Loop forever */ /* o Reset Open Status */ for (wx = 0; wx < bgLastWindow; wx++) { /* For each previous window */ bgTracking[wx] = FALSE; /* Clear open status */ } /* end for each previous window */ /* o Check Open Files */ windows = EnumerateEditWindows(); /* Get all edit windows */ size = ArrayGetAxisDepth(windows); /* Get size of array */ for (ex = 0; ex < size; ex++) { /* For each edit window */ /* . In Previous List */ for (wx = 0; wx < bgLastWindow; wx++) { /* For each previous window */ if (windows[ex]["ClientHandle"] == /* Is this Window */ bgWindows[wx]["ClientHandle"]) { /* in our list? */ bgTracking[wx] = TRUE; /* Set open status */ break; /* Stop */ } /* end in list */ } /* end for each previous window */ /* . New Window? */ if (wx == bgLastWindow) { /* Not in list */ bgTracking[wx] = TRUE; /* Set open status */ bgWindows[wx] = windows[ex]; /* Copy Window Stats */ bgLastWindow++; /* Increase List */ } /* end not in list */ } /* end loop through windows */ /* o Process List */ for (wx = 0; wx < bgLastWindow; wx++) { /* For each previous window */ if ((bgTracking[wx] == FALSE) && /* File was closed */ (bgWindows[wx]["ClientHandle"] != "")) { /* since last run? */ // Add Processing of Closed Window } /* end file was closed */ } /* end for each previous window */ /* o Check to Stop */ rc = DoesFileExist(fnName); /* Check for file */ if (rc == TRUE) { /* Exists? */ break; /* Stop Loop */ } /* end exists */ Sleep(500); /* Sleep */ } /* end loop */ /* * Clean up */ // Add in clean up StatusBarMessage("Stopped activity monitor."); /* Tell User */ return ERROR_NONE; /* return */ } /* end function */
We are going to start by assuming that all files in our previous comparison have been closed. If we don’t see them in the new list they actually have been closed. We then get a list of the windows using the EnumerateEditWindows function. We compare the list of windows with our global list of windows using the window handle. Since window handles are unique, this is a good way to track edit windows. Even if a user closed and opened the same file they would have different window handles. If we find the handle, we mark the window as open. If we don’t find it, we add the new window handle to our list of open windows. Please note that this means that as our script runs our array of windows will have every window the user opened during that session. We could add functionality to recover used spaces in the array eventually but it’s not important to this example.
Next we go through our list of windows and for every window that is closed and has a handle, we run our to be created processing function. If we ran our script now it would track windows but not have any way of reporting that information. That’s not overly useful, so let’s add some logging into the script.
We can start by adding a function to write to our log using the global log file handle. All the function does is take in the passed string, add a date and a line return, and write it to the log file. If the log file does not exist (bgLogFile is NULL_HANDLE), it simply does nothing.
/****************************************/ int write_log(string txt) { /* Write to Log */ /****************************************/ string date; /* Date */ string msg; /* Message */ /* */ if (bgLogFile != NULL_HANDLE) { /* Have Log? */ msg = txt + "\r\n"; /* Add Line */ date = FormatDate(GetLocalTime(), "[Y-m-d G:i:s] "); /* Get Date */ WriteBlock(bgLogFile, date, GetStringLength(date)); /* Write Date */ WriteBlock(bgLogFile, msg, GetStringLength(msg)); /* Write it */ } /* end have log */ return ERROR_NONE; /* return */ } /* end function */
Now that we have the write_log function we can set up and use it in our background function.
/****************************************/ int background(string fnName) { /* Background Function */ /****************************************/ string windows[][]; /* List of All Windows */ string fnLog; /* Log File Name */ int size; /* Size of Edit window List */ int ex, /* Enumeration Index */ wx, /* Window Index */ rc; /* Return Code */ /* */ /* ** Run Background Function */ /* * Safety */ if (fnName == "") { /* No Good? */ return ERROR_NONE; /* Done */ } /* end no good */ /* * Set up log */ fnLog = AddPaths(GetScriptFolder(), "Log.txt"); /* Log Name */ bgLogFile = OpenFile(fnLog, FO_WRITE | FO_READ | FO_SHARE_READ); /* Open it */ if (IsError(bgLogFile)) { /* Failed to Open? */ bgLogFile = CreateFile(fnLog, FO_WRITE | FO_READ | FO_SHARE_READ);/* Create it */ } /* end failed to open */ else { /* Worked? */ SetFilePositionAtEnd(bgLogFile); /* Move to end */ } /* end worked */ write_log(""); /* Make Blank Entry */ write_log("Started activity monitor."); /* Add Message */ /* * Main Loop */ StatusBarMessage("Started activity monitor."); /* Tell User */ while (TRUE) { /* Loop forever */ /* o Reset Open Status */ for (wx = 0; wx < bgLastWindow; wx++) { /* For each previous window */ bgTracking[wx] = FALSE; /* Clear open status */ } /* end for each previous window */ /* o Check Open Files */ windows = EnumerateEditWindows(); /* Get all edit windows */ size = ArrayGetAxisDepth(windows); /* Get size of array */ for (ex = 0; ex < size; ex++) { /* For each edit window */ /* . In Previous List */ for (wx = 0; wx < bgLastWindow; wx++) { /* For each previous window */ if (windows[ex]["ClientHandle"] == /* Is this Window */ bgWindows[wx]["ClientHandle"]) { /* in our list? */ bgTracking[wx] = TRUE; /* Set open status */ break; /* Stop */ } /* end in list */ } /* end for each previous window */ /* . New Window? */ if (wx == bgLastWindow) { /* Not in list */ bgTracking[wx] = TRUE; /* Set open status */ bgWindows[wx] = windows[ex]; /* Copy Window Stats */ bgLastWindow++; /* Increase List */ } /* end not in list */ } /* end loop through windows */ /* o Process List */ for (wx = 0; wx < bgLastWindow; wx++) { /* For each previous window */ if ((bgTracking[wx] == FALSE) && /* File was closed */ (bgWindows[wx]["ClientHandle"] != "")) { /* since last run? */ // Add Processing of Closed Window } /* end file was closed */ } /* end for each previous window */ /* o Check to Stop */ rc = DoesFileExist(fnName); /* Check for file */ if (rc == TRUE) { /* Exists? */ break; /* Stop Loop */ } /* end exists */ Sleep(500); /* Sleep */ } /* end loop */ /* * Clean up */ write_log("Stopped activity monitor."); /* Add Message */ if (bgLogFile != NULL_HANDLE) { /* Have Log? */ CloseHandle(bgLogFile); /* Close file */ } /* end have log */ StatusBarMessage("Stopped activity monitor."); /* Tell User */ return ERROR_NONE; /* return */ } /* end function */
We’ll start using the log by adding a string variable that is the name of the file into which we will place our data. For this example, we are going to put the log into the same folder as the script since it is easily obtained; however, we could make this an INI setting or a define based on our environment. Also we will use the simple name of “Log.txt”, but in a environment with multiple users we may want to name the log based on the user and the computer. We will then open the log file. If that fails, we will instead create the log file. Either way our bgLogFile global variable should be a handle to a log file. If we opened the file we move to the end of the file so that when we write more log entries we don’t erase the contents of the log file. We also write a few lines to the log file immediately using our new write_log function. This isn’t required but from a development standpoint it helps identify that the thread started properly and when.
Towards the end of the routine we will add some cleanup which consists of logging the fact the thread is ending and also closing the log file handle. When the thread closes, these handles would be cleaned up anyway but this is a good development practice in case additional operations need to be done after these lines or something causes the thread to stay open.
Now that we have a way to record our data, it’s time to add processing for closed windows. We could do this in our background function but it makes for easier editing if we make another function to handle the reporting. This is where your script can take off in different directions. For our example, we are going to log data using the log file we created above, but the possibilities are almost endless. For instance, we could report file closures to an internal web server to track jobs, or we could log times into an SQL database, or we could build a list of all open files so operators know who to ask if a file is left open.
Let’s take a look at the basic routine.
/****************************************/ int report_stats(int wx) { /* Report File Stats */ /****************************************/ string msg; /* Message */ string name; /* Name */ qword time; /* Open and Current Time */ /* */ /* ** Report File Stats */ /* * Get Stats */ /* o Active Time */ time = StringToDate(bgWindows[wx]["OpenTime"]); /* Get as Time */ time = GetUTCTime() - time; /* Get Time Open as FILETIME */ time /= 10000000; /* Convert from FILETIME to seconds */ /* o Name */ name = bgWindows[wx]["Filename"]; /* Get name */ if (name == "") { /* Blank */ name = "Untitled " + bgWindows[wx]["FileTypeToken"]; /* Set as name */ } /* end blank */ /* * Save to Log */ msg = "Worked on " + name + " for " + SecondsToTime(time); /* Build Message */ write_log(msg); /* Write it */ /* * Clean up */ bgWindows[wx]["ClientHandle"] = ""; /* Clear */ return ERROR_NONE; /* return */ } /* end function */
The function takes a single parameter, which is the index of the file in our window array. We start by getting the amount of time the file has been open. We could also add the amount of time since its last edit and other statistics, but this is a good starting point. In order to get the time the file has been open, we subtract the current time from the time the file at which was opened. This gives us the amount of time in 100-nanosecond intervals. That may be a little more precise than we need, so we can divide it to get to seconds. Next we get the name of the file. This field might be blank for untitled documents so if it is blank we can generate a name based on the type of window. With all this, we add a message stating the user worked on the file for a specific amount of time to our log. This is where you could add additional processing like database calls or calls to a REST interface. Finally, we clear the handle to this window so it doesn’t get added to the log more than once.
Back in our background function we need to call our new report_stats function. This is a single line change.
/****************************************/ int background(string fnName) { /* Background Function */ /****************************************/ string windows[][]; /* List of All Windows */ string fnLog; /* Log File Name */ int size; /* Size of Edit window List */ int ex, /* Enumeration Index */ wx, /* Window Index */ rc; /* Return Code */ /* */ /* ** Run Background Function */ /* * Safety */ if (fnName == "") { /* No Good? */ return ERROR_NONE; /* Done */ } /* end no good */ /* * Set up log */ fnLog = AddPaths(GetScriptFolder(), "Log.txt"); /* Log Name */ bgLogFile = OpenFile(fnLog, FO_WRITE | FO_READ | FO_SHARE_READ); /* Open it */ if (IsError(bgLogFile)) { /* Failed to Open? */ bgLogFile = CreateFile(fnLog, FO_WRITE | FO_READ | FO_SHARE_READ);/* Create it */ } /* end failed to open */ else { /* Worked? */ SetFilePositionAtEnd(bgLogFile); /* Move to end */ } /* end worked */ write_log(""); /* Make Blank Entry */ write_log("Started activity monitor."); /* Tell User */ /* * Main Loop */ StatusBarMessage("Started activity monitor."); /* Tell User */ while (TRUE) { /* Loop forever */ /* o Reset Open Status */ for (wx = 0; wx < bgLastWindow; wx++) { /* For each previous window */ bgTracking[wx] = FALSE; /* Clear open status */ } /* end for each previous window */ /* o Check Open Files */ windows = EnumerateEditWindows(); /* Get all edit windows */ size = ArrayGetAxisDepth(windows); /* Get size of array */ for (ex = 0; ex < size; ex++) { /* For each edit window */ /* . In Previous List */ for (wx = 0; wx < bgLastWindow; wx++) { /* For each previous window */ if (windows[ex]["ClientHandle"] == /* Is this Window */ bgWindows[wx]["ClientHandle"]) { /* in our list? */ bgTracking[wx] = TRUE; /* Set open status */ break; /* Stop */ } /* end in list */ } /* end for each previous window */ /* . New Window? */ if (wx == bgLastWindow) { /* Not in list */ bgTracking[wx] = TRUE; /* Set open status */ bgWindows[wx] = windows[ex]; /* Copy Window Stats */ bgLastWindow++; /* Increase List */ } /* end not in list */ } /* end loop through windows */ /* o Process List */ for (wx = 0; wx < bgLastWindow; wx++) { /* For each previous window */ if ((bgTracking[wx] == FALSE) && /* File was closed */ (bgWindows[wx]["ClientHandle"] != "")) { /* since last run? */ report_stats(wx); /* Report it */ } /* end file was closed */ } /* end for each previous window */ /* o Check to Stop */ rc = DoesFileExist(fnName); /* Check for file */ if (rc == TRUE) { /* Exists? */ break; /* Stop Loop */ } /* end exists */ Sleep(500); /* Sleep */ } /* end loop */ /* * Clean up */ write_log("Stopped activity monitor."); /* Tell User */ if (bgLogFile != NULL_HANDLE) { /* Have Log? */ CloseHandle(bgLogFile); /* Close file */ } /* end have log */ StatusBarMessage("Stopped activity monitor."); /* Tell User */ return ERROR_NONE; /* return */ } /* end function */
Now if we start our monitor it will create a log file that looks a little like this:
[2018-01-22 16:54:46] [2018-01-22 16:54:46] Started activity monitor. [2018-01-22 16:55:22] Worked on Untitled FT_HTML for 2:02:25 [2018-01-22 16:55:36] Stopped activity monitor. [2018-01-22 16:56:12] [2018-01-22 16:56:12] Started activity monitor. [2018-01-22 16:56:21] Worked on Untitled FT_ANSI for 0:04 [2018-01-22 16:56:50] Worked on R:\Website\Blog Posts\Activity Monitor\monitor.ls for 1:40:33 [2018-01-22 16:56:53] Stopped activity monitor.
This isn’t overly useful since it logs data in a non-computer friendly format, but this script could be modified to integrate into a production tracking system or a revenue recovery system to log actual time spent on customer jobs. Using a background thread allows us to easily track changes to the work environment, and we could add editing statistics by tracking the time since last edit and seeing if it has changed.
After reading this blog hopefully you can see the power of background threads in Legato and what they can do for your organization. A copy of the complete script is available below.
// // // GoFiler Legato Script - Activity Monitor // ---------------------------------------- // // Rev 01/22/2018 // // // (c) 2018 Novaworks, LLC -- All rights reserved. // // Monitors GoFiler activity // /********************************************************/ /* Global Defines */ /********************************************************/ int start (int f_id, string mode); int stop (int f_id, string mode); int main (); int setup (); int background (string fnName); int report_stats (int wx); int write_log (string txt); /********************************************************/ /* Hook Support */ /********************************************************/ /* Functions in this section deal with the hooks to */ /* control the background thread. */ /********************************************************/ /************************************************/ /* Hook Globals */ /************************************************/ /****************************************/ handle hBGScript; /* Background Script Handle */ string fnTemp; /* File Name for Control File */ /****************************************/ int setup() { /* Called from Application Startup */ /****************************************/ string fnScript; /* Us */ string item1[10]; /* Menu Item */ string item2[10]; /* Menu Item */ int rc; /* Return Code */ /* */ /* ** Add Menu Item */ /* * Define Functions */ /* o Start */ item1["Code"] = "EXTENSION_START_MONITOR"; /* Function Code */ item1["MenuText"] = "&Start Activity Tracking"; /* Menu Text */ item1["Description"] = "<B>Start Activity Tracking</B>\r\r " + /* Description */ "Starts tracking application activity."; /* of function */ /* o Stop */ item2["Code"] = "EXTENSION_STOP_MONITOR"; /* Function Code */ item2["MenuText"] = "Sto&p Activity Tracking"; /* Menu Text */ item2["Description"] = "<B>Stop Activity Tracking</B>\r\r " + /* Description */ "Stops tracking application activity."; /* of function */ /* * Setup */ fnScript = GetScriptFilename(); /* Get the script filename */ /* * Check for/add menu item */ rc = MenuFindFunctionID(item1["Code"]); /* Look for existing */ if (IsError(rc)) { /* Not already added */ rc = MenuAddFunction(item1); /* Add the item */ if (IsNotError(rc)) { /* Was already be added */ MenuSetHook(item1["Code"], fnScript, "start"); /* Set the Test Hook */ } /* end error */ else { /* Failed? */ return rc; /* Leave with error */ } /* end failed */ } /* end not added */ /* * Check for/add menu item */ rc = MenuFindFunctionID(item2["Code"]); /* Look for existing */ if (IsError(rc)) { /* Not already added */ rc = MenuAddFunction(item2); /* Add the item */ if (IsNotError(rc)) { /* Was already be added */ MenuSetHook(item2["Code"], fnScript, "stop"); /* Set the Test Hook */ } /* end error */ else { /* Failed? */ return rc; /* Leave with error */ } /* end failed */ } /* end not added */ return ERROR_NONE; /* Return value (does not matter) */ } /* end setup */ /****************************************/ int main() { /* Initialize from Hook Processor */ /****************************************/ /* */ /* ** Initialize Hook Processor */ /* * Check State */ if (GetScriptParent() != "LegatoIDE") { /* Not running in IDE? */ return ERROR_NONE; /* Done */ } /* end not in IDE */ /* * Set up */ setup(); /* Add to the menu */ return ERROR_NONE; /* return */ } /* end function */ /****************************************/ int start(int f_id, string mode) { /* Start Hook Processor */ /****************************************/ /* */ /* ** Run Start */ /* * Safety */ if (mode != "preprocess") { /* Not preprocess? */ return ERROR_NONE; /* Return w/ no error */ } /* end not preprocess */ if (hBGScript != NULL_HANDLE) { /* Already Running? */ return ERROR_NONE; /* Return w/ no error */ } /* end already running */ /* * Build Name */ fnTemp = ClipFileExtension(GetTempFile()); /* Get name */ fnTemp += FormatString("%s", GetTickCount()) + ".bgc"; /* Add extra Information */ /* * Start it */ hBGScript = RunBackgroundScript(GetScriptFilename(), /* Run Background Script */ "background", fnTemp); /* name of the function */ if (IsError(hBGScript)) { /* Failed? */ MessageBox("Couldn't start thread 0x%08X", GetLastError()); /* Tell user */ } /* end failed */ return ERROR_NONE; /* return */ } /* end function */ /****************************************/ int stop(int f_id, string mode) { /* Stop Hook Processor */ /****************************************/ int ix, /* Index */ rc; /* Return Code */ /* */ /* ** Run Stop */ /* * Safety */ if (mode != "preprocess") { /* Not preprocess? */ return ERROR_NONE; /* Return w/ no error */ } /* end not preprocess */ if (hBGScript == NULL_HANDLE) { /* Already Running? */ return ERROR_NONE; /* Return w/ no error */ } /* end already running */ /* * Stop it */ StringToFile("Stop", fnTemp); /* Create File */ for (ix = 0; ix < 10; ix++) { /* Loop */ rc = WaitForObject(hBGScript, 100); /* Wait for it */ if (IsNotError(rc)) { /* Worked? */ hBGScript = NULL_HANDLE; /* Clear Handle */ break; /* Stop */ } /* end worked */ } /* end loop */ /* * Clean up */ DeleteFile(fnTemp); /* Delete it */ return ERROR_NONE; /* return */ } /* end function */ /********************************************************/ /* Background Thread */ /********************************************************/ /* Functions below this point run in a seperate script */ /* engine. Therefore global variables will exist but */ /* will not have the same values as above. */ /********************************************************/ /************************************************/ /* Thread Globals */ /************************************************/ /****************************************/ string bgWindows[][]; /* Window List */ boolean bgTracking[]; /* Window Tracking */ int bgLastWindow; /* Last Window */ handle bgLogFile; /* Log File Handle */ /****************************************/ int background(string fnName) { /* Background Function */ /****************************************/ string windows[][]; /* List of All Windows */ string fnLog; /* Log File Name */ int size; /* Size of Edit window List */ int ex, /* Enumeration Index */ wx, /* Window Index */ rc; /* Return Code */ /* */ /* ** Run Background Function */ /* * Safety */ if (fnName == "") { /* No Good? */ return ERROR_NONE; /* Done */ } /* end no good */ /* * Set up log */ fnLog = AddPaths(GetScriptFolder(), "Log.txt"); /* Log Name */ bgLogFile = OpenFile(fnLog, FO_WRITE | FO_READ | FO_SHARE_READ); /* Open it */ if (IsError(bgLogFile)) { /* Failed to Open? */ bgLogFile = CreateFile(fnLog, FO_WRITE | FO_READ | FO_SHARE_READ);/* Create it */ } /* end failed to open */ else { /* Worked? */ SetFilePositionAtEnd(bgLogFile); /* Move to end */ } /* end worked */ write_log(""); /* Make Blank Entry */ write_log("Started activity monitor."); /* Add Message */ /* * Main Loop */ StatusBarMessage("Started activity monitor."); /* Tell User */ while (TRUE) { /* Loop forever */ /* o Reset Open Status */ for (wx = 0; wx < bgLastWindow; wx++) { /* For each previous window */ bgTracking[wx] = FALSE; /* Clear open status */ } /* end for each previous window */ /* o Check Open Files */ windows = EnumerateEditWindows(); /* Get all edit windows */ size = ArrayGetAxisDepth(windows); /* Get size of array */ for (ex = 0; ex < size; ex++) { /* For each edit window */ /* . In Previous List */ for (wx = 0; wx < bgLastWindow; wx++) { /* For each previous window */ if (windows[ex]["ClientHandle"] == /* Is this Window */ bgWindows[wx]["ClientHandle"]) { /* in our list? */ bgTracking[wx] = TRUE; /* Set open status */ break; /* Stop */ } /* end in list */ } /* end for each previous window */ /* . New Window? */ if (wx == bgLastWindow) { /* Not in list */ bgTracking[wx] = TRUE; /* Set open status */ bgWindows[wx] = windows[ex]; /* Copy Window Stats */ bgLastWindow++; /* Increase List */ } /* end not in list */ } /* end loop through windows */ /* o Process List */ for (wx = 0; wx < bgLastWindow; wx++) { /* For each previous window */ if ((bgTracking[wx] == FALSE) && /* File was closed */ (bgWindows[wx]["ClientHandle"] != "")) { /* since last run? */ report_stats(wx); /* Report it */ } /* end file was closed */ } /* end for each previous window */ /* o Check to Stop */ rc = DoesFileExist(fnName); /* Check for file */ if (rc == TRUE) { /* Exists? */ break; /* Stop Loop */ } /* end exists */ Sleep(500); /* Sleep */ } /* end loop */ /* * Clean up */ write_log("Stopped activity monitor."); /* Add Message */ if (bgLogFile != NULL_HANDLE) { /* Have Log? */ CloseHandle(bgLogFile); /* Close file */ } /* end have log */ StatusBarMessage("Stopped activity monitor."); /* Tell User */ return ERROR_NONE; /* return */ } /* end function */ /****************************************/ int report_stats(int wx) { /* Report File Stats */ /****************************************/ string msg; /* Message */ string name; /* Name */ qword time; /* Open and Current Time */ /* */ /* ** Report File Stats */ /* * Get Stats */ /* o Active Time */ time = StringToDate(bgWindows[wx]["OpenTime"]); /* Get as Time */ time = GetUTCTime() - time; /* Get Time Open as FILETIME */ time /= 10000000; /* Convert from FILETIME to seconds */ /* o Name */ name = bgWindows[wx]["Filename"]; /* Get name */ if (name == "") { /* Blank */ name = "Untitled " + bgWindows[wx]["FileTypeToken"]; /* Set as name */ } /* end blank */ /* * Save to Log */ msg = "Worked on " + name + " for " + SecondsToTime(time); /* Build Message */ write_log(msg); /* Write it */ /* * Clean up */ bgWindows[wx]["ClientHandle"] = ""; /* Clear */ return ERROR_NONE; /* return */ } /* end function */ /****************************************/ int write_log(string txt) { /* Write to Log */ /****************************************/ string date; /* Date */ string msg; /* Message */ /* */ if (bgLogFile != NULL_HANDLE) { /* Have Log? */ msg = txt + "\r\n"; /* Add Line */ date = FormatDate(GetLocalTime(), "[Y-m-d G:i:s] "); /* Get Date */ WriteBlock(bgLogFile, date, GetStringLength(date)); /* Write Date */ WriteBlock(bgLogFile, msg, GetStringLength(msg)); /* Write it */ } /* end have log */ return ERROR_NONE; /* return */ } /* end function */
David Theis has been developing software for Windows operating systems for over fifteen years. He has a Bachelor of Sciences in Computer Science from the Rochester Institute of Technology and co-founded Novaworks in 2006. He is the Vice President of Development and is one of the primary developers of GoFiler, a financial reporting software package designed to create and file EDGAR XML, HTML, and XBRL documents to the U.S. Securities and Exchange Commission. |
Additional Resources
Legato Script Developers LinkedIn Group
Primer: An Introduction to Legato