Sunday, September 05, 2021

psCron | psTaskScheduler

Not into board games? Need a challenge? Re-write a utility. 

So I was considering the inner workings of a task scheduler and figured I'd re-write, though with less polish, the classic cron command from Linux. Of course my needs are to run Powershell and other tasks and while Windows Task Scheduler works fine in most cases this was simply a personal challenge. This time in Powershell, but perhaps next time it'll happen in Python or Rust so I can turn it into a Windows Service.

Though, you can turn a Powershell Script into a Windows Service: link

So, to start I used the following accepted rules of Cron:

Again, I'm not perfect but I enjoy the challenges of coding, figuring out how to make a computer do what I'd like it to. You may write better code than me and any comments I hope are what we like to call constructive criticism rather than hurling insults but here we go explaining my process...


First-off, the format is well established, as noted above, and I've aimed to follow this as accurately as possible, though at this time the */x tasks are only on the Hour and Minute attributes. I created a sample psTaskScheduler.jobs file and worked from there. Because I wanted this to run indefinitely the exit is triggered by adding a file called psTaskScheduler.quit and killing the "in-use" indicator of a psTaskScheduler.running file. Cheesy but effective methods. If you crash or abort the script you need to use the quit option to reset it. I might change this if I moved this up to being a Windows Service.

I define the parameters (command-line options) for the functions I want in the script including quit, debug, audio, and log (logging is not done yet). This is as much for planning as it is the practical side, but I follow this up with a help page of sorts. I may add a help parameter later (-h | -help).

Lastly I define a few variables. Global variables are best to use sparingly but for this it seemed logical and a couple of these may vanish as I head into cleaning up the code.

I try to use function main{} and position it at the top of the file and end the script with:

All of the supporting functions are between the closing } of the main function and that # for main. In this case the main function calls testForEcho() and loadSchedules() within the primary loop that persists through the life of the script.

The main() [function] is comprised of the header/title, handling a stop request, setting the running state, the primary loop, and the post-op that wraps things up. 

The Header:

The header is mostly to establish what the script is going to be for the user. They (you?) would probably like to know what you're running and who wrote it I had some fun and improvised an asterisk-based Canadian flag into this. 

The Stop Request (brakes):

The parameter for -q/quit is in the variable $stopTS which is boolean (true/false) these are simple to use with an if as they are absolute. This code will only run when a -q or -quit (or technically -stopTS) are used in the command line. We move on to Test-Path to check of the file psTaskScheduler.quit already exists because why do again what you have already done? Much like when you're riding a modern bus you can't ring the bell more than once.

We move on to creating the .quit file and clearing the .running file if present.

Finally we let the user know the stop has been requested and exit.

Set The Running State:

So, this is the real startup of the script where we establish the baselines. We create a psTaskScheduler.running file if it doesn't already exist and clear the .quit file if we're set to go. This is to prevent the script being run twice* and causing issues. We let the user know why we can't run if the script is found to be running.

If all is well we continue to syncronise the script's execution with the top of the minute (00 seconds, line 92). We repeat this after each task loop to ensure we stay in synch' with the minute's "00 seconds."

The Primary Loop:

I'm using a Do Loop here because I'd prefer to check if we need to continue at the end of the sequence. The test, line 112, is whether the .quit file now exists.

The first task is to load the Schedules from the psTaskScheduler.jobs file, we call the loadSchedules() function for this. We will get to that soon.

If there are no tasks, we let the user know and keep going. It's okay to let this happen. Once we have a list of scheduled jobs we cycle through the jobs (line 103~105) calling testForEcho, (If you're a fan of RUSH you'll get this) which does the heavy-lifting of carrying out the tasks.

Because we have the ability to run tasks on start (or reboot) we follow this loop up by turning off the $Global:onReboot flag, a boolean value that we're done with because we've completed our first pass after startup.

The script is not commanded to sleep until the top of the next second (00, line 111).

The Post Op:   

If the stop request bell has been ring the Post Op process cleans up the .quit and .running files and the script exits politely.

On to the functions we glazed over earlier...

loadSchedules():

We start by setting the newFile flag to false but to prevent errors, we use Test-Path to ensure the expected file is present before trying to load it. The type of file, or -PathType parameter indicates whether this is a file (leaf) or folder (container), in this case we're looking for the leaf (file) named psTaskScheduler.jobs in the current folder (".\...") 

We now check the last write time on the .jobs file to determine if it's been changed recently (since the first loop of the script). We set the file's Last Write Time to $fileDate then follow this up with a check to see if the currently stored date is less than this newly acquired date but we will also accept move forward if there was no date set yet (line 134). 

    If the file or the stored date unset ($null) we set the stored value, $Global:jobsFileDate, and read the file into the $arrNewSchedule array (line 135). We've elected to exclude and commented lines, those beginning with the pound or hash symbol ("#") and empty lines. Please ignore the excessive tabs.

We extract any generic notes in the .jobs file into scheduleNotes and move forward.

If this is the first time through the script's loop the $Global:jobsFileDate will be null (not set), on line 137 we are looking to display information on file updates and skip this on first execution. On subsequent iterations if there's an updated .jobs file loaded this will be indicated with a green note about a New Job File and the date of the file.

Now that we're through that we update $Global:jobsFileDate with the latest file's LastWriteTime, print the corresponding notes, and update the $Global:arrSchedule (array) with the task information to performing the checks every minute.

Note: Line 157 makes line 144 irrelevant, remove it or remark it out line 144 with a "#" character. Oops.

testForEcho():

This is where the real work is done. In this function we break down the task entry and separate it out to match the time we need to do the work of running the required task.

(updated: corrected switch options for @reboot, @daily, and yearly)*

We start with a required parameter sourced from line 104 and a line of data from $arrSchedule as $task. The data arrives here as $thisTask and we'll be working elements of that data through this function.

While there should be no null or zero-character strings, thisTask is checked anyway. If it is not empty, it is trimmed (spaces and tabs removed from the beginning and end of the string) followed by a scrub of tabs, double-spaces, and double-asterisks so we have a good starting point that's easy to break out into the components.

In preparation for a @reboot task we set the local flag for $onRebootTask to $false. We also set $thisCmd to a default $null string before stepping into the next phase.

We will now check for the special time tags noted in the cron specification including: @yearly or @annually, @monthly, @weekly, @daily or @midnight, @hourly, or @reboot 

We use a switch function in Powershell to achieve this after checking to see if the first character in the string is an @ symbol and breaking the command at the first space where the first portion is expected to be the special tag and the command to be the remainder of thisTask (the value for $thisCmd).

We use the switch function to set the value of $thisTask to a preset mask based on the standard definitions. For the @reboot the flag for $onRebootTask is set to $true (on or 1) in boolean.

We now begin to break down $thisTask into the required sections and values. Determining the values set in the schedule for Minute, Hour, Day of the Month, the Month, and the Day of the Week for comparison. We again use the Powershell Switch function based on the element count (segment) and the $scheduleData which is an array based on the split of $thisTask.

And extra functionality, notes! This mostly useless function came out of debugging and stayed. If $thisCmd begins with an exclamation point (!) a note is displayed and the placeholders %T, %D, and %d are fulfilled. 
 
We're nearly there, we can now break out the current time functions to match against the scheduled job. We'll need a mirror of the settings with the current minute, hour, day of the month, the month, and day of the week are assigned to variables (lines 242~246).

Determining the perfect match is based on asterisks and matching numbers is completed in the lines 248~277, with an override for @reboot on 279~281. The premise is that where there is and asterisk in $thisTask (the scheduled job) we get a free point and where the numbers match we get a point. The total based on the elements is 5. If the matchCount equals 5 then the task is executed (or the note displayed). The override for @reboot ensures the matchCount is 5... once. Lines 282~290 ensure the task is carried out.

The Result:

Though I have cut the noise a little with both the -a and -d parameters the result is pretty-much as shown. I've also been tweaking this all day as I see little refinements. This is called scope-creep. It's not ideal in big projects but in hobbyist activities like this do what you'd like. It's your project, have fun!

I hope this inspires you a little to explore coding. I have a week off and may get a few more of these done. I'm not sure I'll blog about each one though.




Brag Rights - Notes from my Performance Review



 

Saturday, May 08, 2021

Scripting: How Do I Learn Something New?

 Learning is different from person-to-person. We all have our own methods and while often we can adopt new methods, there's no guarantee that what works for you will work for me, or what works for me will work for someone else.

What I'm going to try to explain is how I learn scripting languages and systems, including what I used to do, but need to get back to. I believe that age and stress can impact your ability to absorb new information and concepts so if you're having trouble soaking up or adapting, try calming exercises and practices before digging in. 

So, last year I took on a new role that gave me new opportunities to learn Powershell, a Microsoft/Windows-centric, scripting language that's particularly useful in DevOps roles. Now I have a saying that's mostly true, "Anything is possible in software," but this has constraints depending on the language you're trying to work with to get the job done. Often in your career you will need to adapt, for me this was reaching my limits and blowing past my comfort-zone with the batch language in the Windows Command Prompt and needing to build more advanced scripts to automate processes in my role as a Cloud Technical Analyst.

I have had, many years ago, experience with PHP, JavaScript (DHTML), Visual Basic, VBScript and ASP, but I had not kept up. It just wasn't part of my life so I let it slide.

Coming back to last year, I accepted that Powershell, among other languages, would be necessary so I started playing. Challenging myself to re-write my precious batch files (.bat/.cmd) into Powershell scripts.

What a journey!

I'm not a classically trained programmer. I took programming in high-school, then taught myself from there on in. Of course I started with BASIC/BASICA on an IBM PCjr but I quickly moved on to the BASIC Compiler (7.1) and Visual Basic for Windows as this was my realm. I learned what I needed by reading the manual, buying books like Peter Norton's pink shirt book, and many others, then applying the ideas and concepts to personal or work projects. This was before the World Wide Web existed so books, bulletin boards, browsing in libraries was my source of knowledge. I learned what I needed and adapted as necessary.

Adapting is key.

Today, books have become eBooks, the bulletin boards have become the Internet and the sites information is endless... most of the time. 

First, to learn how to code you need a drive to take an idea and pushing yourself to make it real. Whether it's building a website or writing an application for yourself on your PC, come up with a goal and stick to it. It doesn't even matter if it's great, because with software, while anything is possible, you are also, rarely, done. Often you will add features and functions, sometimes you'll rewrite the code better because you've learned how to do something better. Channel Bob Ross and enjoy the happy little accidents because every mistake or rabbit hole leads somewhere.

Chasing rabbit holes is fine, it's knowing when to come back to it later or leave it be forever.

Before I go too far, coding is logical, but it's primarily creative. If you're a creative person you will enjoy the reward of designing a software solution. You don't even need to wait for the paint to dry or finish the work to enjoy a success so relax and just think things through.

So, if you have trouble with logical thinking, coding may seem like a daunting task. Writing an application (app/script/utility) is all about the destination, so just like driving from Toronto to Vancouver, you need to plan out what you need to succeed in your journey. You'll need one or more vehicles, supplies (fuel for you and the transportation options), and you need to know what constraints you have on the journey to completing the trip (or project). The vehicle is your platform on which you want to build the project, the properties of the vehicle (seats, windows, doors, etc.) may be the features and functions of the vehicle, you'll need to know what mode of transport (planes, trains, automobiles), and you'll need to design the route to the destination, this might be considered your programming language choice.

What if there's a road closure? These are part of the journey and you will adapt because, it's only software.

What has this got to do with learning something new?

So, you have this journey, what do you do? You gather information and tools together to help you get where you want to go. Learning to code, or learning a new language even, is about finding information, growing your comprehension of it, then applying it. Rinse and repeat on that.

You can start with small projects, experiments and challenges, And sometimes these are hard to come up with or get behind because you look at them like Grade 11 math and can't see where they might apply in a real world. I want to help you with that by laying out a list of challenges and letting you come up with ideas on how to solve them. There may be more that one answer, but if the destination is reached, safely, you're okay. If there's a better route that's fine, sometimes the fastest route is not the best route.

Challenges

These are only examples, you may find your own cause to develop a scripted solution, a boring or repetitive task that a bit of code can effectively erase. It starts from a need. 

Challenge 1:

    This may seem an archaic task in this world but create a logging journal. This could be used to track your time or comments through the day. We all know that keeping track of how long we've done x or y tasks through our day is tricky, let alone by the end of the week or month. Create a script that writes out a timestamped log of activities to a file in the current directory that it is being executed from. 

Example:

2021-04-01 12:01:00 # 01:15 - researching how to explain how to write a script in Python
2021-04-01 13:43:00 # 00:20 - assisting user on how to restart windows, again.
2021-04-01 14:45:00 # 01:00 - lunch
2021-04-01 17:30:00 # --:-- - end of day

You can write this in any language. Pick a language you want to understand better. This is a simple script but you're going to find out how to write this by breaking down the the steps to deliver the output. It can be something entered from the command line, or through prompts within the script. You could build a GUI for it. You have many options. If you like a pretty interface consider using Electron and JavaScript. 

... I'll add more, soon.


There is no individual ownership when you are part of a team, it's the sum of the parts that makes you the RESILIENT team you need to be...