v1.3.0.0
Tail Example

Example of monitoring a text file for updates and reading the last several lines of it when it changes. A.K.A "tailing" a file/log.

See the published documentation for a properly formatted version of this README.
Note
Assets for this example, including the code and sample button shown below, can be found in the project's repository at
https://github.com/mpaperno/DSEP4TP/tree/main/resources/examples/Tail/
1// This is a script to "tail" a file -- watching it for changes and reading the last N lines from the end.
2// Typically used to monitor a log file, or some other text document, for changes.
3// This version uses the `FSMonitor` file system monitor utility class to detect changes to the file being tailed.
4//
5// It demonstrates the use of `FSMonitor`, `File` utility static functions,
6// as well as creating and updating Touch Portal states from inside a script.
7//
8// It is meant to be loaded as a module.
9
10// We save data in the global scope, for simplicity.
11// This module is designed to run in a Private engine instance (so the global scope is not shared).
12var fileName = fileName || "";
13var maxLines = maxLines || 0;
14var statusStateId = statusStateId || "";
15var fsWatch = fsWatch || null;
16var fLastMod = fLastMod || null;
17
18// This is the method to invoke from the TP action to start tailing a file.
19// The arguments are the file path/name, and, optionally, the number of lines to read from the end (default is 6 lines).
20export function tail(file, lines = 6)
21{
22 // make sure to stop any existing tail first
23 cancel();
24
25 // validate the function arguments.
26 // DSE.INSTANCE_NAME is a constant representing the current instance which created and executed this module.
27 if (!DSE.INSTANCE_NAME || !file || lines < 0)
28 throw new TypeError(`Invalid arguments for 'tail(${file}, ${lines})' with instance name ${DSE.INSTANCE_NAME}.`);
29
30 // Now validate that the file exists using the File utility.
31 if (!File.exists(file))
32 throw new URIError("File not found: " + file);
33
34 // OK we're good to go... save the arguments for later use and reset the last file modified timestamp.
35 fileName = File.normFilePath(file);
36 maxLines = lines;
37 fLastMod = new Date(0);
38
39 // Log what we're doing (before starting to monitor the file in case we're tailing the plugin's own logs!)
40 console.info(`Tailing ${lines} lines from ${fileName} for state ${DSE.INSTANCE_NAME}`);
41
42 // Configure the FSWatcher instance to monitor the file for changes.
43 fsWatch = new FSWatcher([fileName]);
44 // Connect the watcher's `fileChanged` event to our `checkFile()` function (below).
45 fsWatch.fileChanged.connect(checkFile);
46
47 // We want to notify the user that the tail is running/active. One way we can do this is to create a new TP State
48 // which will reflect the status of this operation. We use the current instance's state ID and name as the basis for the new
49 // state ID and description, and we save it for future use (in `cancel()` or for next time this function is invoked);
50 const newStatusStateId = DSE.instanceStateId() + "_active";
51 // Only do this once if we don't already have a statusStateId from previous runs, or if has changed.
52 if (statusStateId !== newStatusStateId) {
53 statusStateId = newStatusStateId;
54 // The arguments are: the State ID (must be unique), parent category name (within this plugin's categories, or a new one),
55 // a description/name for the new state (shown in TP UI), the default value (0 for inactive),
56 // flag to force TP to evaluate event handlers for this state, and adding a 2ms delay after the state is created.
57 TP.stateCreate(statusStateId, "Dynamic Values", DSE.INSTANCE_NAME + " Active", "0", true, 2);
58 }
59 // Now send the state update indicating the tail operation is active.
60 // This state change can be used to trigger an event in TP, eg. visually indicate the tail is active.
61 TP.stateUpdateById(statusStateId, "1");
62
63 // Run the initial file read operation now.
64 checkFile();
65}
66
67// This function is called by the FSWatcher we created in tail() when the monitored file is modified.
68// This checks the file modification time for changes, reads in any new data and updates
69// the corresponding State in TP.
70function checkFile()
71{
72 // Catch any errors so we can stop the tail and exit "gracefully" if the file couldn't be read.
73 try {
74 // Check if the file still exists. If it has been renamed or removed, try looking for a new file with the same name.
75 if (!fsWatch.files().includes(fileName)) {
76 if (!File.exists(fileName) || !fsWatch.addPath(fileName))
77 throw new URIError("Monitored file has been removed");
78 }
79
80 // Get the watched file modification time, then compare it to the last saved time.
81 const fmod = File.mtime(fileName);
82 if (fmod.getTime() === fLastMod.getTime())
83 return;
84
85 // File has changed, save the new modification date.
86 fLastMod = fmod;
87
88 // Read in `maxLines` of text from the file, starting at the end (-1) and trimming any trailing newlines.
89 // The lines are read asynchronously and then sent to Touch Portal as a state.
90 // The `TP.stateUpdate(string)` version with one argument will automatically
91 // update the state for the current instance which created and executed this module
92 File.readLinesAsync(fileName, maxLines, -1, true)
93 .then(TP.stateUpdate)
94 .catch((ex) => { throw ex; });
95 }
96 catch (e) {
97 cancel();
98 console.exception(e);
99 throw e;
100 }
101}
102
103// Call this function to cancel the tailing operation.
104export function cancel()
105{
106 if (fsWatch) {
107 // Delete the file watcher
108 fsWatch.destroy();
109 fsWatch = null;
110
111 // Update the state we created earlier to indicate the tail operation has stopped.
112 TP.stateUpdateById(statusStateId, "0");
113 // Log what we did
114 console.info(`Stopped tailing ${fileName} for state ${DSE.INSTANCE_NAME}`);
115 // We could also now delete the status state we created... this is optional.
116 // But is would be best to schedule that to run a little later so TP can first properly register the state change we just sent.
117 //setTimeout(function (){ TP.stateRemove(statusStateId) }, 1000);
118 }
119}
120
121// Call this function to clear the contents of the log text state.
122export function clear() {
123 TP.stateUpdate("Log cleared.");
124}
void clear(int mode=Mode.Clipboard)
Clears the system clipboard of all values. The opitonal mode argument is used to control which part...
The DSE object contains constants and functions related to the plugin environment....
Definition: DSE.h:50
string INSTANCE_NAME
The the current script's Instance Name, as specified in the corresponding Touch Portal action which i...
Definition: DSE.h:144
string instanceStateId()
Gets Touch Portal State ID of the current script's instance. This is what Touch Portal actually uses ...
Definition: DSE.h:324
DynamicScript instance(String name)
Returns the script instance with given name, if any, otherwise returns null.
Date prototype extension methods
The FSWatcher class provides an interface for monitoring files and directories for modifications.
Definition: FSWatcher.h:87
The File class provides access to... files!
Definition: File.dox:412
string normFilePath(string &file)
Returns the canonical path including the file name, i.e. an absolute path without symbolic links or r...
Definition: File.h:235
bool exists(string &file)
Returns true if file exists; otherwise returns false.
Definition: File.h:194
Date mtime(string &file)
Returns the date and local time when the file was last modified. If the file is not available,...
Definition: File.h:252
< Promise|void > readLinesAsync(string file, int maxLines,< int|undefined > fromLine=0,< boolean|undefined > trimTrailingNewlines,< Function|undefined > callback)
Asynchronously Reads up to maxLines lines from file starting at fromLine (default = 0,...
The global TP namespace is used as a container for all Touch Portal API methods, which are invoked wi...
Definition: touchportal.dox:6
void stateUpdateById(string id, string value)
Send a State update to Touch Portal for any arbitrary state/value matching the id,...
void stateUpdate(string value)
Send a State update to Touch Portal with given value for the current script instance.
void stateCreate(string id, string parentGroup, string description, string defaultValue="", boolean forceUpdate=false, int delayMs=0)
Create a new Touch Portal dynamic State (if it doesn't already exist).

Example usage on a button