Java is definitely the most convenient language to use for sensor development, since it enables direct access to the SensorShell and SensorProperties classes. However, Hackystat sensors have been written in other languages as well, including Python, C#, and Lisp. This section briefly overviews the Lisp-based sensor for Emacs in order to illustrate sensor implementation in a non-Java language.
The following sections discuss some of the more important files in the Emacs sensor implementation. For more details, consult the source code directly in hackySensor_Emacs.
Example 17.27, “sensorshell.el (excerpt)” shows the Lisp code responsible for providing access to the SensorShell. For space reasons, a few comments and a GNU/XEmacs compatibility hack have been removed.
Example 17.27. sensorshell.el (excerpt)
(require 'cl)
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;;; Variables
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(defvar hackystat*sensorshell!buffer-name "*hackystat-shell*"
"The name of the buffer in which the SensorShell process is running.")
(defvar hackystat*sensorshell*start-hooks nil
"A list of functions to be run after the sensorshell is started up.
Users can use this to send initialization information to the SensorShell such
as FileMetric class path information.")
(defvar hackystat*sensorshell*max-buffer-size 500000
"The maximum number of characters to allow in the hackystat sensorshell before
erasing the buffer. Can be changed by the user from the default. A nil value means
never erase the buffer.")
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;;; Functions
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(defun hackystat*sensorshell*start ()
"Sets up the SensorShell subprocess.
Locally binds process-connection-type to nil during process buffer creation to ensure
that pipes rather than PTYs are used for the Hackystat shell process. This appears to
cure a bug in XEmacs that prevents graceful exit."
(interactive)
(let ((shell-jar (concat hackystat*user-home ".hackystat/emacs/sensorshell.jar"))
(process-connection-type nil))
(setq comint-scroll-to-bottom-on-output t)
(hackystat*make-comint-in-buffer "hackystat-shell" hackystat*sensorshell!buffer-name
"java" nil "-jar" shell-jar "emacs"))
(process-kill-without-query (get-buffer-process hackystat*sensorshell!buffer-name))
(run-hooks 'hackystat*sensorshell*start-hooks))
(defun hackystat*sensorshell*shutdown ()
"Shutdown the SensorShell subprocess."
(interactive)
(when (hackystat*sensorshell*alive-p)
(save-excursion
(set-buffer hackystat*sensorshell!buffer-name)
(goto-char (point-max))
(insert "quit")
(comint-send-input)
(let ((dots "."))
(while (and (hackystat*sensorshell*alive-p)
(< (length dots) 20))
(sit-for 1)
(setq dots (concat dots "."))
(message (concat "Shutting down hackystat shell" dots)))))
(message "Shutting down hackystat shell.. done.")))
(defun hackystat*sensorshell*send-command (command)
"Sends COMMAND to the sensor shell."
(when (hackystat*sensorshell*alive-p)
(save-excursion
(set-buffer hackystat*sensorshell!buffer-name)
(goto-char (point-max))
(insert command)
(comint-send-input)
(hackystat*sensorshell!clear-buffer-if-needed))))
(defun hackystat*sensorshell!clear-buffer-if-needed()
"Clears the sensorshell buffer if hackystat*sensorshell*max-buffer-size
is non-nil and the buffer has exceeded that value. Writes a message to the minibuffer."
(when hackystat*sensorshell*max-buffer-size
(save-excursion
(set-buffer hackystat*sensorshell!buffer-name)
(when (> (buffer-size) hackystat*sensorshell*max-buffer-size)
(erase-buffer)
(message (format "%s %s %s %s" (current-time-string)
" Hackystat shell buffer erased (exceeded "
hackystat*sensorshell*max-buffer-size
" characters)."))))))
(defun hackystat*sensorshell*alive-p ()
"Returns non-nil if the sensor shell process is still alive."
(comint-check-proc hackystat*sensorshell!buffer-name))
(provide 'sensorshell)
The purpose of this code is to implement a "wrapper" around the SensorShell. In Emacs, it is quite easy to create subprocesses that you can send data to and receive data from. Therefore, this code includes a "startup" function called hackystat*sensorshell*start that creates a subprocess running Java on the sensorshell.jar file.
Once the subprocess is running, the hackystat*sensorshell*send-command function allows other Emacs code to send data to the subprocess. The hackystat*sensorshell*shutdown function must be called when Emacs exits, and it invokes the "send" command on the sensorshell subprocess to make sure that any buffered data is sent to the server before Emacs exits and the subprocess terminates.
Example 17.28, “sensor-properties.el (excerpt)” shows the Lisp code responsible for providing access to the sensor.properties file. In Java, the SensorProperties class encapsulates all access to this functionality. For the Emacs sensor, code must be implemented to mirror that functionality. Some utility functions have been omitted for space purposes.
Example 17.28. sensor-properties.el (excerpt)
(require 'cl)
;; This file will not load correctly if hackystat*user-home is not set.
;; Functions to read in and parse the sensor.properties file.
;;
;; (hackystat*props*make <home directory>) ;; initializes the system.
;; (hackystat*props*enabled-p) ;; if the Emacs sensor is turned on
;; (hackystat*props*file-available-p) ;; if sensor.properties is found
;; (hackystat*props*host) ;; hackystat host
;; (hackystat*props*user-email) ;; user email
;; (hackystat*props*state-change-interval) ;; interval between wakeups to check state.
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;;; Variables
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(defvar hackystat*props!file ""
"The file in which the sensor properties should be found.")
(defvar hackystat*props!table (make-hash-table :test #'equal)
"A table storing the key value pairs read from the sensor properties file.")
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;;; Constructor
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(defun hackystat*props*make (user-home-directory)
"Enables access to sensor properties values."
(setq hackystat*props!table (make-hash-table :test #'equal))
(setq hackystat*props!file (concat user-home-directory ".hackystat/sensor.properties"))
(cond ((file-readable-p hackystat*props!file)
(save-excursion
(find-file hackystat*props!file)
(goto-char (point-min))
(while (> (- (point-max) (point)) 1)
;; only process lines that aren't commented.
(cond ((hackystat*props!noncommentline)
(let ((key (hackystat*props!get-key))
(value (hackystat*props!get-value)))
(if (and key value)
(setf (gethash key hackystat*props!table) value))
;;(message (concat "Got: '" key "' and '" value "'"))
;;(sit-for 1)
)))
(forward-line 1))
(kill-buffer (current-buffer))))
(t
(message "Hackystat sensor properties file not found."))))
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;;; Property Retrieval Functions
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(defun hackystat*props*enabled-p ()
"Returns true if the Emacs sensor is enabled.
For this to be true, the ENABLE_EMACS_SENSOR property must be present and equal to true."
(string-equal (gethash "ENABLE_EMACS_SENSOR" hackystat*props!table) "true"))
(defun hackystat*props*bufftrans-enabled-p ()
"Returns true if the Emacs bufftrans sensor is enabled.
For this to be true, the ENABLE_EMACS_BUFFTRANS_SENSOR property must be present and equal to true,
and ENABLE_EMACS_SENSOR must be present and equal to true."
(and (hackystat*props*enabled-p)
(string-equal (gethash "ENABLE_EMACS_BUFFTRANS_SENSOR" hackystat*props!table) "true")))
(defun hackystat*props*devevent-enabled-p ()
"Returns true if the Emacs devevent sensor is enabled.
For this to be true, the ENABLE_EMACS_DEVEVENT_SENSOR property must be present and equal to true,
and ENABLE_EMACS_SENSOR must be present and equal to true."
(and (hackystat*props*enabled-p)
(string-equal (gethash "ENABLE_EMACS_DEVEVENT_SENSOR" hackystat*props!table) "true")))
(defun hackystat*props*get(key)
"Returns the value associated with key, or nil if not found."
(gethash key hackystat*props!table))
(defun hackystat*props*file-available-p ()
"Returns non-nil if the properties file could be found."
(file-readable-p hackystat*props!file))
(defun hackystat*props*host()
"Returns the hackystat host or http://www.hackystat.org:8080/ if none found."
(or (hackystat*props!guarantee-suffix (gethash "HACKYSTAT_HOST" hackystat*props!table) "/")
"http://www.hackystat.org:8080/"))
(defun hackystat*props*user-key()
"Returns the user email or 'unknownkey' if none found."
(or (gethash "HACKYSTAT_KEY" hackystat*props!table)
"unknownkey"))
(defun hackystat*props*state-change-interval ()
"Returns the state change interval number or 60 (1 minute) if not found."
;; string-to-number returns 0
(let ((interval (gethash "HACKYSTAT_STATE_CHANGE_INTERVAL" hackystat*props!table)))
(cond (interval
(let ((num (string-to-number interval)))
(if (= num 0)
60
num)))
(t
60))))
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;;; Utility functions to parse the line and extract keys and values
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(defun hackystat*props!guarantee-suffix (str suffix)
"Returns STR with SUFFIX appended if it is not already on STR."
(if (equal (substring str (- (length str) (length suffix))) suffix)
str
(concat str suffix)))
(defun hackystat*props!noncommentline ()
"Returns non-nil if the point is currently on a line that
contains a key-value pair. In other words, it is not a comment or empty."
(let ((line-string (buffer-substring (progn (beginning-of-line)
(point))
(progn (end-of-line)
(point)))))
(and (not (string-equal line-string ""))
(not (string-equal line-string ""))
(not (string-equal (substring line-string 0 1) "#")))))
(defun hackystat*props!get-key ()
"Returns the key string on the current non-comment line or nil if none found."
(let ((start (progn </home>
(beginning-of-line)
(point)))
(equal-sign (search-forward "=" (point-at-eol) t)))
(if equal-sign
(hackystat*props!trim-string
(buffer-substring start (1- equal-sign))))))
(defun hackystat*props!get-value ()
"Returns the value string on the current non-comment line or nil if none found."
(let ((start (progn
(beginning-of-line)
(search-forward "=" (point-at-eol) t))))
(if start
(hackystat*props!trim-string
(buffer-substring start (point-at-eol))))))
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;;; Set up the sensor properties
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(setq hackystat*user-home (hackystat*props!guarantee-suffix hackystat*user-home "/"))
(hackystat*props*make hackystat*user-home)
(provide 'sensor-properties)
As you can see, this file simply reads in the sensor.properties file, places its contents into a hash table, and provides access to the data of interest to the Hackystat Emacs sensor.
Example 17.29, “devevent-sensor.el (excerpt)” shows the Lisp code responsible for generating DevEvents based upon the user behavior in Emacs.
Example 17.29. devevent-sensor.el (excerpt)
(require 'cl)
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;;; Variables
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(defvar hackystat*devevent*display-p nil
"Global variable controlling whether or not to display devevents in the minibuffer.")
(defvar hackystat*devevent!edit-timer nil
"The timer that runs once every state-change-interval to check
the active buffer and record a state change activity type (if necessary) and send off the
data (if necessary).")
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;;; Public API
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(defun hackystat*devevent*init ()
"Initializes the devevent sensor code.
Sends some initialization stuff to the SensorShell which should already be running."
(when (hackystat*props*devevent-enabled-p)
(hackystat*devevent*msg "Initializing")
(hackystat*sensorshell*send-command "DevEvent#set#tool=Emacs")
(unless hackystat*devevent!edit-timer
(setq hackystat*activity!edit-timer
(run-with-timer (hackystat*props*state-change-interval)
(hackystat*props*state-change-interval)
#'(lambda ()
(hackystat*devevent*check-edit)))))))
(defun hackystat*devevent*msg (msg)
"If hackystat*devevent*display-p, then print MSG to the minibuffer."
(when (and (hackystat*props*devevent-enabled-p) hackystat*bufftrans*display-p)
(message (concat (substring (current-time-string) 11 19) " DevEvent: " msg))))
(defun hackystat*devevent*add-event (type path &optional pMap)
"Adds the passed DevEvent data. pMap should be key-value pairs delimited by the # character."
(when (hackystat*props*devevent-enabled-p)
(when hackystat*devevent*display-p
(message (concat (substring (current-time-string) 11 19) " DevEvent: " type " " path " " pMap)))
(hackystat*sensorshell*send-command
(concat "DevEvent#add#"
"type=" type "#"
"path=" path
(if (null pMap) "" (concat "#" pMap))))))
(defun hackystat*devevent*check-edit ()
"Runs once every hackystat*devevent*edit-timer-interval seconds and if
the current buffer is associated with a file, sends the file name and the buffer
size to the SensorShell to determine whether a DevEvent edit event should be posted."
(when (buffer-file-name)
(hackystat*sensorshell*send-command
(concat "DevEvent#edit#" "path=" (buffer-file-name) "#" "size=" (format "%d" (buffer-size))))))
(provide 'devevent-sensor)
This code is quite simple, but does not show how these functions are invoked. That is the purpose of the "hook" functions, illustrated in the next file.
Example 17.30, “sensor-hooks.el (excerpt)” shows the Lisp code responsible for ensuring that the preceding Lisp functions are invoked at appropriate times during execution of Emacs.
Example 17.30. sensor-hooks.el (excerpt)
(require 'cl)
(require 'comint)
;; Load remainder of system.
(require 'sensor-properties)
(require 'sensor-utils)
(require 'sensorshell)
(require 'activity-sensor)
(require 'devevent-sensor)
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;;; Hook definition functions
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(defun hackystat*hook*init-sensors ()
"Initializes all of the currently installed sensors."
(when (hackystat*props*enabled-p)
;; SensorShell must be started before anything else can be init'd.
(hackystat*sensorshell*start)
(hackystat*activity*init)
(hackystat*devevent*init)))
(defun hackystat*hook!add-info (event-type)
"Records the passed EVENT-TYPE regarding the current buffer if it is bound to a file."
(let ((file (buffer-file-name)))
(when file
(when (hackystat*props*enabled-p)
(hackystat*activity*add-activity event-type file)
(hackystat*activity*check-state-change))
(when (hackystat*props*devevent-enabled-p)
(hackystat*devevent*add-event "Edit" file (concat "subtype=" event-type))))))
(defun hackystat*hook*open-file ()
"Records that the current file was just visited if not recorded already."
(hackystat*hook!add-info "OpenFile"))
(defun hackystat*hook*save-file ()
"Records that the current file was saved."
(hackystat*hook!add-info "SaveFile"))
(defun hackystat*hook*close-file ()
"Records when the current file is closed."
(hackystat*hook!add-info "CloseFile"))
(defun hackystat*hook*shell-command (command)
"Records that a shell program has been invoked. Does not record hackystat shell stuff, of course."
(unless (equal (buffer-name) hackystat*sensorshell!buffer-name)
(when (hackystat*props*enabled-p)
(hackystat*activity*add-activity "RunProgram" command))))
(defun hackystat*hook*kill-emacs ()
"Send activity and session data whenever Emacs exits."
(when (hackystat*props*enabled-p)
(hackystat*activity*add-activity "Edit" "subtype=ToolShutdown")
(hackystat*sensorshell*shutdown)))
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;;; Install hooks
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(add-hook 'after-init-hook 'hackystat*hook*init-sensors)
(add-hook 'find-file-hooks 'hackystat*hook*open-file)
(add-hook 'after-save-hook 'hackystat*hook*save-file)
(add-hook 'kill-buffer-hook 'hackystat*hook*close-file)
(add-hook 'comint-input-filter-functions 'hackystat*hook*shell-command)
(add-hook 'kill-emacs-hook 'hackystat*hook*kill-emacs)
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
;;; Stop hackystat processing.
;;; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(defun hackystat*uninstall ()
"Disable hackystat hook functions in case problems occur in installation."
(interactive)
(remove-hook 'after-init-hook 'hackystat*hook*init-sensors)
(remove-hook 'find-file-hooks 'hackystat*hook*open-file)
(remove-hook 'after-save-hook 'hackystat*hook*save-file)
(remove-hook 'kill-buffer-hook 'hackystat*hook*close-file)
(remove-hook 'comint-input-filter-functions 'hackystat*hook*shell-command)
(remove-hook 'kill-emacs-hook 'hackystat*hook*kill-emacs))
(provide 'sensor-hooks)
(provide 'sensor-package)
As you can see, this file implements a set of "hook" functions (which is Emacs jargon for "callback"), and invokes the "add-hook" and "remove-hook" functions to ensure that they are called at appropriate times during execution.