Reading OpenVPN Status Data with Python (2 of 3)

This is the 2nd part of a 3-part series on using wxPython + PyWin32 to grab the output from an OpenVPN session on Windows. In this article, I will show how how to start OpenVPN with Python and how to watch a file that OpenVPN writes its data logs to.

If you weren’t here last time, you need to go to Tim Golden’s website and download the watch_directory.py file. Once you’ve got it, open the file in your favorite text editor and copy the following out of it:

# watch_directory code
ACTIONS = {
  1 : "Created",
  2 : "Deleted",
  3 : "Updated",
  4 : "Renamed to something",
  5 : "Renamed from something"
}

def watch_path (path_to_watch, include_subdirectories=False):
    FILE_LIST_DIRECTORY = 0x0001
    hDir = win32file.CreateFile (
        path_to_watch,
        FILE_LIST_DIRECTORY,
        win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE,
        None,
        win32con.OPEN_EXISTING,
        win32con.FILE_FLAG_BACKUP_SEMANTICS,
        None
        )
    while 1:
        results = win32file.ReadDirectoryChangesW (
            hDir,
            1024,
            include_subdirectories,
            win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
            win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
            win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
            win32con.FILE_NOTIFY_CHANGE_SIZE |
            win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
            win32con.FILE_NOTIFY_CHANGE_SECURITY,
            None,
            None
            )
        for action, file in results:
            full_filename = os.path.join (path_to_watch, file)
            if not os.path.exists (full_filename):
                file_type = ""
            elif os.path.isdir (full_filename):
                file_type = 'folder'
            else:
                file_type = 'file'
            yield (file_type, full_filename, ACTIONS.get (action, "Unknown"))

class Watcher (threading.Thread):

    def __init__ (self, path_to_watch, results_queue, **kwds):
        threading.Thread.__init__ (self, **kwds)
        self.setDaemon (1)
        self.path_to_watch = path_to_watch
        self.results_queue = results_queue
        self.start ()

    def run (self):
        for result in watch_path (self.path_to_watch):
            self.results_queue.put (result)

Golden’s website explains the code in detail so I am not going to go over it, but the basic gist of the code is that it watches for specific changes to the directory and then returns the name of the file that was changed and the type of change. OpenVPN writes its log file to the following location by default: “C:\Program Files\OpenVPN\log\mcisvpn.log”. Whenever my program is alerted to any change in the log directory, it opens the “mcisvpn.log” file, reads it and then appends the new data to the text widget.

Since I only want to read the new data from the file, I have to keep track of where I’m at in the file. The following is a function which shows how I keep track of changes to the file and how I keep track of the last position in the file that the script read. Also, this “watcher” script is run in a separate thread to keep the wxPython GUI responsive.

def _resultProducer(self, jobID, abortEvent):
    """
    GUI will freeze if this method is not called in separate thread.
    """
    PATH_TO_WATCH = [r'C:\Program Files\OpenVPN\log']
    try: path_to_watch = sys.argv[1].split (",") or PATH_TO_WATCH
    except: path_to_watch = PATH_TO_WATCH
    path_to_watch = [os.path.abspath (p) for p in path_to_watch]

    print "Watching %s at %s" % (", ".join (path_to_watch), time.asctime ())
    # create a Queue object that is updated with the file(s) that is/are changed               
    files_changed = Queue.Queue ()
    for p in path_to_watch:
        Watcher (p, files_changed)

    filepath = os.path.join(PATH_TO_WATCH[0], 'mcisvpn.log')    
    f = open(filepath)
    for line in f.readlines():
        print line
    
    # get the last position before closing the file
    last_pos = f.tell()
    f.close()
    
    while not abortEvent():
        try:
            file_type, filename, action = files_changed.get_nowait ()
            # if the change was an update, seek to the last position read
            # and read the update
            if action == 'Updated':
                f = open(filepath)
                f.seek(last_pos)
                for line in f.readlines():
                    if line != '\n':
                        print line
                        f.close()
                # get the last position before closing the file
                last_pos = f.tell()
                f.close()
        except Queue.Empty:
            pass
        time.sleep (1)
    
    return jobID

The code above also include bits and pieces from the wxPython Demo entitled “DelayedResult”. It takes care of starting and stopping the thread for me, although I can’t be completely certain that it kills Golden’s Watcher class cleanly. I just don’t know how to tell for sure; but it seems to work.

Next time I’ll show my wxPython code so you can see how to integrate these disparate parts together.

You can download my code below: