Watcher image attribution: Yours truly
Hi readers,
[ Update: A note to those reading this post via Planet Python or other aggregators:
Before first pulbishing it, I reviewed the post in Blogger's preview mode, and it appeared okay, regarding the use of the less-than character, so I did not escape it. I did not know (or did not remember) that Planet Python's behavior may be different. As a result, the code had appeared without the less-than signs in the Planet, thereby garbling it. After noticing this, I fixed the issue in the post. Apologies to those seeing the post twice as a result. ]
I was browsing Linux command man pages (section 1) for some work, and saw the page for an interesting command called watch. I had not come across it before. So I read the watch man page, and after understanding how it works (it's pretty straightforward [1]), thought of creating a Python version of it. I have not tried to implement exactly the same functionality as watch, though, just something similar to it. I called the program watch.py.
[1] The one-line description of the watch command is:
watch - execute a program periodically, showing output fullscreen
How watch.py works:
It is a command-line Python program. It takes an interval argument (in seconds), followed by a command with optional arguments. It runs the command with those arguments, repeatedly, at that interval. (The Linux watch command has a few more options, but I chose not to implement those in this version. I may add some of them [2], and maybe some other features that I thought of, in a future version.)
[2] For example, the -t, -b and -e options should be easy to implement. The -p (--precise) option is interesting. The idea here is that there is always some time "drift" [3] when trying to run a command periodically at some interval, due to unpredictable and variable overhead of other running processes, OS scheduling overhead, and so on. I had experienced this issue earlier when I wrote a program that I called pinger.sh, at a large company where I worked earlier.
[3] You can observe the time drift in the output of the runs of the watch.py program, shown below its code below. Compare the interval with the time shown for successive runs of the same command.
I had written it at the request of some sysadmin friends there, who wanted a tool like that to monitor the uptime of multiple Unix servers on the company network. So I wrote the tool, using a combination of Unix shell, Perl and C. They later told me that it was useful, and they used it to monitor the uptime of multiple servers of the company in different cities. The C part was where the more interesting stuff was, since I used C to write a program (used in the overall shell script) that sort of tried to compensate for the time drift, by doing some calculations about remaining time left, and sleeping for those intervals. It worked somewhat okay, in that it reduced the drift a good amount. I don't remember the exact logic I used for it right now, but do remember finding out later, that the gettimeofday function might have been usable in place of the custom code I wrote to solve the issue. Good fun. I later published the utility and a description of it in the company's Knowledge Management System.
Anyway, back to watch.py: each time, it first prints a header line with the interval, the command string (truncated if needed), and the current date and time, followed by some initial lines of the output of that command (this is what "watching" the command means). It does this by creating a pipe with the command, using subprocess.Popen and then reading the standard output of the command, and printing the first num_lines lines, where num_lines is an argument to the watch() function in the program.
The screen is cleared with "clear" for Linux and "cls" for Windows. Using "echo ^L" instead of "clear" works on some Linux systems, so changing the clear screen command to that may make the program a little faster, on systems where echo is a shell built-in, since there will be no need to load the clear command into memory each time [4]. (As a small aside, on earlier Unix systems I've worked on, on which there was sometimes no clear command (or it was not installed), as a workaround, I used to write a small C program that printed 25 newlines to the screen, and compile and install that as a command called clear or cls :)
[4] Although, on recent Windows and Linux systems, after a program is run once, if you run it multiple times a short while later, I've noticed that the startup time is faster from the second time onwards. I guess this is because the OS loads the program code into a memory cache in some way, and runs it from there for the later times it is called. Not sure if this is the same as the OS buffer cache, which I think is only for data. I don't know if there is a standard name for this technique. I've noticed for sure, that when running Python programs, for example, the first time you run:
python some_script_name.py
it takes a bit of time - maybe a second or three, but after the first time, it starts up faster. Of course this speedup disappears when you run the same program after a bigger gap, say the next day, or after a reboot. Presumably this is because that program cache has been cleared.
Here is the code for watch.py.
""" ------------------------------------------------------------------ File: watch.py Version: 0.1 Purpose: To work somewhat like the Linux watch command. See: http://man7.org/linux/man-pages/man1/watch.1.html Does not try to replicate its functionality exactly. Author: Vasudev Ram Copyright 2018 Vasudev Ram Web site: https://vasudevram.github.io Blog: https://jugad2.blogspot.com Product store: https://gumroad.com/vasudevram Twitter: https://mobile.twitter.com/vasudevram ------------------------------------------------------------------ """ from __future__ import print_function import sys import os from subprocess import Popen, PIPE import time from error_exit import error_exit # Assuming 25-line terminal. Adjust if different. # If on Unix / Linux, can get value of environment variable # COLUMNS (if defined) and use that instead of 80. DEFAULT_NUM_LINES = 20 def usage(args): lines = [ "Usage: python {} interval command [ argument ... ]".format( args[0]), "Run command with the given arguments every interval seconds,", "and show some initial lines from command's standard output.", "Clear screen before each run.", ] for line in lines: sys.stderr.write(line + '\n') def watch(command, interval, num_lines): # Truncate command for display in the header of watch output. if len(command) > 50: command_str = command[:50] + "..." else: command_str = command hdr_part_1 = "Every {}s: {} ".format(interval, command_str) # Assuming 80 columns terminal width. Adjust if different. # If on Unix / Linux, can get value of environment variable # COLUMNS (if defined) and use that instead of 80. columns = 80 # Compute pad_len only once, before the loop, because # neither len(hdr_part_1) nor len(hdr_part_2) change, # even though hdr_part_2 is recomputed each time in the loop. hdr_part_2 = time.asctime() pad_len = columns - len(hdr_part_1) - len(hdr_part_2) - 1 while True: # Clear screen based on OS platform. if "win" in sys.platform: os.system("cls") elif "linux" in sys.platform: os.system("clear") hdr_str = hdr_part_1 + (" " * pad_len) + hdr_part_2 print(hdr_str + "\n") # Run the command, read and print its output up to num_lines lines. # os.popen is the old deprecated way, Python docs recommend to use # subprocess.Popen. #with os.popen(command) as pipe: with Popen(command, shell=True, stdout=PIPE).stdout as pipe: for line_num, line in enumerate(pipe): print(line, end='') if line_num >= num_lines: break time.sleep(interval) hdr_part_2 = time.asctime() def main(): sa, lsa = sys.argv, len(sys.argv) # Check arguments and exit if invalid. if lsa < 3: usage(sa) error_exit( "At least two arguments are needed: interval and command;\n" "optional arguments can be given following command.\n") try: # Get the interval argument as an int. interval = int(sa[1]) if interval < 1: error_exit("{}: Invalid interval value: {}".format(sa[0], interval)) # Build the command to run from the remaining arguments. command = " ".join(sa[2:]) # Run the command repeatedly at the given interval. watch(command, interval, DEFAULT_NUM_LINES) except ValueError as ve: error_exit("{}: Caught ValueError: {}".format(sa[0], str(ve))) except OSError as ose: error_exit("{}: Caught OSError: {}".format(sa[0], str(ose))) except Exception as e: error_exit("{}: Caught Exception: {}".format(sa[0], str(e))) if __name__ == "__main__": main()Here is the code for error_exit.py, which watch imports.
# error_exit.py # Author: Vasudev Ram # Web site: https://vasudevram.github.io # Blog: https://jugad2.blogspot.com # Product store: https://gumroad.com/vasudevram # Purpose: This module, error_exit.py, defines a function with # the same name, error_exit(), which takes a string message # as an argument. It prints the message to sys.stderr, or # to another file object open for writing (if given as the # second argument), and then exits the program. # The function error_exit can be used when a fatal error condition occurs, # and you therefore want to print an error message and exit your program. import sys def error_exit(message, dest=sys.stderr): dest.write(message) sys.exit(1) def main(): error_exit("Testing error_exit with dest sys.stderr (default).\n") error_exit("Testing error_exit with dest sys.stdout.\n", sys.stdout) with open("temp1.txt", "w") as fil: error_exit("Testing error_exit with dest temp1.txt.\n", fil) if __name__ == "__main__": main()Here are some runs of watch.py and their output:
(BTW, the dfs command shown, is from the Quick-and-dirty disk free space checker for Windows post that I had written recently.)
$ python watch.py 15 ping google.com Every 15s: ping google.com Fri May 04 21:15:56 2018 Pinging google.com [2404:6800:4007:80d::200e] with 32 bytes of data: Reply from 2404:6800:4007:80d::200e: time=117ms Reply from 2404:6800:4007:80d::200e: time=109ms Reply from 2404:6800:4007:80d::200e: time=117ms Reply from 2404:6800:4007:80d::200e: time=137ms Ping statistics for 2404:6800:4007:80d::200e: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 109ms, Maximum = 137ms, Average = 120ms Every 15s: ping google.com Fri May 04 21:16:14 2018 Pinging google.com [2404:6800:4007:80d::200e] with 32 bytes of data: Reply from 2404:6800:4007:80d::200e: time=501ms Reply from 2404:6800:4007:80d::200e: time=56ms Reply from 2404:6800:4007:80d::200e: time=105ms Reply from 2404:6800:4007:80d::200e: time=125ms Ping statistics for 2404:6800:4007:80d::200e: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 56ms, Maximum = 501ms, Average = 196ms Every 15s: ping google.com Fri May 04 21:16:33 2018 Pinging google.com [2404:6800:4007:80d::200e] with 32 bytes of data: Reply from 2404:6800:4007:80d::200e: time=189ms Reply from 2404:6800:4007:80d::200e: time=141ms Reply from 2404:6800:4007:80d::200e: time=245ms Reply from 2404:6800:4007:80d::200e: time=268ms Ping statistics for 2404:6800:4007:80d::200e: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 141ms, Maximum = 268ms, Average = 210ms $ python watch.py 15 c:\ch\bin\date Every 15s: c:\ch\bin\date Tue May 01 00:33:00 2018 Tue May 1 00:33:00 India Standard Time 2018 Every 15s: c:\ch\bin\date Tue May 01 00:33:15 2018 Tue May 1 00:33:16 India Standard Time 2018 Every 15s: c:\ch\bin\date Tue May 01 00:33:31 2018 Tue May 1 00:33:31 India Standard Time 2018 Every 15s: c:\ch\bin\date Tue May 01 00:33:46 2018 Tue May 1 00:33:47 India Standard Time 2018 In one CMD window: $ d:\temp\fill-and-free-disk-space In another: $ python watch.py 10 dfs d:\ Every 10s: dfs d:\ Tue May 01 00:43:25 2018 Disk free space on d:\ 37666.6 MiB = 36.78 GiB Every 10s: dfs d:\ Tue May 01 00:43:35 2018 Disk free space on d:\ 37113.7 MiB = 36.24 GiB $ python watch.py 20 dir /b "|" sort Every 20s: dir /b | sort Fri May 04 21:29:41 2018 README.txt runner.py watch-outputs.txt watch-outputs2.txt watch.py watchnew.py $ python watch.py 10 ping com.nosuchsite Every 10s: ping com.nosuchsite Fri May 04 21:30:49 2018 Ping request could not find host com.nosuchsite. Please check the name and try again. $ python watch.py 20 dir z:\ Every 20s: dir z:\ Tue May 01 00:54:37 2018 The system cannot find the path specified. $ python watch.py 2b echo testing watch.py: Caught ValueError: invalid literal for int() with base 10: '2b' $ python watch.py 20 foo Every 20s: foo Fri May 04 21:33:35 2018 'foo' is not recognized as an internal or external command, operable program or batch file. $ python watch.py -1 foo watch.py: Invalid interval value: -1- Enjoy.Interested in a Python programming or Linux commands and shell scripting course? I have good experience built over many years of real-life experience, as well as teaching, in both those subject areas. Contact me for course details via my contact page here.- Vasudev Ram - Online Python training and consultingFast web hosting with A2 HostingGet updates (via Gumroad) on my forthcoming apps and content. Jump to posts: Python * DLang * xtopdf Subscribe to my blog by email My ActiveState Code recipesFollow me on: LinkedIn * Twitter Are you a blogger with some traffic? Get Convertkit:Email marketing for professional bloggers
3 comments:
I forgot to mention in the post that you can even run pipelines under the control of the watch command (this feature only tested on Windows so far), by doing like one of the commands shown in the post:
python watch.py 20 dir /b "|" sort
i.e. surround the pipe sign in double quotes, thereby preventing it from being interpreted by the OS shell (which would make the python command's output be piped to sort - not what we want); by quoting the pipe sign, instead, "dir /b | sort" becomes the command to run, so dir's output is piped to sort, and the output of that pipeline is what is watched.
I also forgot to show the code for the fill-and-free-disk-space.bat file that is called in one of the above program runs. Here it is:
if not exist d:\temp\t md d:\temp\t
xcopy /y/s . d:\temp\t >nul
REM Wait for 10 seconds or until a key is pressed
timeout 10
del /q/s d:\temp\t
timeout 10
REM Recursive call to this batch file.
call d:\temp\fill-and-free-disk-space
It copies all files under your current directory to d:\temp\t, waits 10 seconds, then deletes d:\temp\t, again waits 10 seconds, then recursively calls itself.
If you first change to a directory that has files using a lot of space, and then run this batch file, it provides an assured way of repeatedly increasing and then decreasing disk usage noticeably, so that the dfs command's output can be seen to change.
>Before first pulbishing it
And that should be "Before first publishing it".
Post a Comment