Prepare for additional connection types

This commit is contained in:
David Botton 2024-03-11 23:20:23 -04:00
parent 64b0c663e9
commit 1c9fa342ad
17 changed files with 619 additions and 620 deletions

1
clog.asd vendored
View file

@ -16,6 +16,7 @@
(:module "source"
:components ((:file "asdf-ext")
(:file "clog-connection")
(:file "clog-connection-websockets")
(:file "clog")
(:file "clog-utilities")
(:file "clog-base")

View file

@ -98,6 +98,18 @@ discarded, return CLOG-OBJ. (Internal)"))
(clog-connection:execute (connection-id obj) script))
obj)
;;;;;;;;;;;;;;
;; js-query ;;
;;;;;;;;;;;;;;
(defgeneric js-query (clog-obj script &key default-answer)
(:documentation "Execure SCRIPT on browser and return result. (Internal)"))
(defmethod js-query ((obj clog-obj) script &key (default-answer nil))
(flush-connection-cache obj)
(clog-connection:query (connection-id obj) script
:default-answer default-answer))
;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; with-connection-cache ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -121,18 +133,6 @@ flushed with FLUSH-CONNECTION-CACHE or a query is made."
(clog-connection:execute (connection-id clog-obj) script)))
(setf *connection-cache* (list :cache))))
;;;;;;;;;;;;;;
;; js-query ;;
;;;;;;;;;;;;;;
(defgeneric js-query (clog-obj script &key default-answer)
(:documentation "Execure SCRIPT on browser and return result. (Internal)"))
(defmethod js-query ((obj clog-obj) script &key (default-answer nil))
(flush-connection-cache obj)
(clog-connection:query (connection-id obj) script
:default-answer default-answer))
;;;;;;;;;;;;;
;; execute ;;
;;;;;;;;;;;;;

View file

@ -56,7 +56,7 @@
(defmethod create-context2d ((obj clog-canvas))
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=clog['~A'].getContext('2d')"
web-id
(html-id obj)))
@ -443,7 +443,7 @@ https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/global
(:documentation "Create blank image data"))
(defmethod create-image-data ((obj clog-context2d) width height)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.createImageData(~A,~A)"
web-id (script-id obj)
width height))
@ -459,7 +459,7 @@ https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/global
(:documentation "Create conic gradient"))
(defmethod create-conic-gradient ((obj clog-context2d) start-angle x y)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.createConicGradient(~A,~A,~A)"
web-id (script-id obj)
start-angle x y))
@ -475,7 +475,7 @@ https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/global
(:documentation "Create linear gradient"))
(defmethod create-linear-gradient ((obj clog-context2d) x0 y0 x1 y1)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.createLinearGradient(~A,~A,~A,~A)"
web-id (script-id obj)
x0 y0 x1 y1))
@ -491,7 +491,7 @@ https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/global
(:documentation "Create radial gradient"))
(defmethod create-radial-gradient ((obj clog-context2d) x0 y0 r0 x1 y1 r1)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.createRadialGradient(~A,~A,~A,~A,~A,~A)"
web-id (script-id obj)
x0 y0 r0 x1 y1 r1))
@ -612,7 +612,7 @@ https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/global
(:documentation "Get image data from clog-context2d. Returns a CLOG-IMAGE-DATA"))
(defmethod get-image-data ((obj clog-context2d) sx sy sw sh)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.getImageData(~A,~A,~A,~A)"
web-id (script-id obj)
sx sy sw sh))
@ -639,7 +639,7 @@ https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/global
(:documentation "Get current transform matrix as clog-matrix"))
(defmethod get-transform ((obj clog-context2d))
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.getTransform()"
web-id (script-id obj)))
(make-instance 'clog-matrix
@ -695,7 +695,7 @@ https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/global
(:documentation "Measure text returns a clog-text-metrics object"))
(defmethod measure-text ((obj clog-context2d) text)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.measureText('~A')"
web-id
(script-id obj) text))
@ -1033,7 +1033,7 @@ json array 6 element for 2d or 16 for 3d."))
(defmethod create-matrix ((obj clog-canvas) &key matrix)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=DOMMatrix(~A)"
web-id
(if matrix
@ -1082,7 +1082,7 @@ json array 6 element for 2d or 16 for 3d."))
(:documentation "Return flip-x a clog-matrix"))
(defmethod flip-x ((obj clog-matrix))
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.flipX()"
web-id (script-id obj)))
(make-instance 'clog-matrix
@ -1097,7 +1097,7 @@ json array 6 element for 2d or 16 for 3d."))
(:documentation "Return flip-y a clog-matrix"))
(defmethod flip-y ((obj clog-matrix))
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.flipY()"
web-id (script-id obj)))
(make-instance 'clog-matrix
@ -1112,7 +1112,7 @@ json array 6 element for 2d or 16 for 3d."))
(:documentation "Return inverse a clog-matrix"))
(defmethod inverse ((obj clog-matrix))
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.inverse()"
web-id (script-id obj)))
(make-instance 'clog-matrix
@ -1127,7 +1127,7 @@ json array 6 element for 2d or 16 for 3d."))
(:documentation "Return multiply a clog-matrix"))
(defmethod multiply ((obj clog-matrix) by-matrix)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.multiply(~A)"
web-id (script-id obj) (script-id by-matrix)))
(make-instance 'clog-matrix
@ -1144,7 +1144,7 @@ json array 6 element for 2d or 16 for 3d."))
(:documentation "Return rotate a clog-matrix"))
(defmethod rotate ((obj clog-matrix) angle)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.rotate(~A)"
web-id (script-id obj) angle))
(make-instance 'clog-matrix
@ -1162,7 +1162,7 @@ json array 6 element for 2d or 16 for 3d."))
sy sz ox oy oz"))
(defmethod scale-matrix ((obj clog-matrix) sx &optional sy sz ox oy oz)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.scale(~A~A~A~A~A~A)"
web-id (script-id obj) sx
(if sy (format nil ",~A" sy) "")
@ -1183,7 +1183,7 @@ sy sz ox oy oz"))
sy sz ox oy oz"))
(defmethod scale3d ((obj clog-matrix) sx &optional sy sz ox oy oz)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.scale3d(~A~A~A~A~A~A)"
web-id (script-id obj) sx
(if sy (format nil ",~A" sy) "")
@ -1205,7 +1205,7 @@ sy sz ox oy oz"))
(:documentation "Return translate-matrix a clog-matrix by x y and optionally z"))
(defmethod translate-matrix ((obj clog-matrix) x y &optional z)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.translate(~A,~A~A)"
web-id (script-id obj) x y
(if z (format nil ",~A" z) "")))
@ -1228,7 +1228,7 @@ sy sz ox oy oz"))
(defmethod create-path2d ((obj clog-canvas) &key path2d)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=Path2D(~A)"
web-id
(if path2d

View file

@ -0,0 +1,493 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; CLOG - The Common Lisp Omnificent GUI ;;;;
;;;; (c) 2020-2024 David Botton ;;;;
;;;; License BSD 3 Clause ;;;;
;;;; ;;;;
;;;; clog-connection-websockets.lisp ;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(in-package :clog-connection)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implemetation - clog-connection-websockets
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defvar *app* nil "Clack 'app' middle-ware")
(defvar *client-handler* nil "Clack 'handler' for socket traffic")
(defvar *long-poll-first* nil
"Dynamic variable indicating to use html output instead of
websocket for output at start if connection.")
(defvar *extended-long-poll* nil
"Dynamic variable indicating to extend long polling beyond
extablishing websocket for output.")
(defvar *long-poll-url* nil
"Dynamic variable indicating the url path used.")
(defparameter *compiled-boot-js*
(with-open-file (stream (merge-pathnames #P"static-files/js/boot.js"
(asdf:system-source-directory :clog)))
(let ((content (make-string (file-length stream))))
(read-sequence content stream)
content))
"A compiled version of current version of boot.js (private)")
;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; handle-new-connection ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun handle-new-connection (connection id)
"Handle new incoming websocket CONNECTIONS with ID from boot page. (Private)"
(handler-case
(cond ((and id (gethash id *connection-data*))
(format t "Reconnection id - ~A to ~A~%" id connection)
(handler-case
(websocket-driver:close-connection (gethash id *connection-ids*)
"Aborting this old connection since receiving a reconnection request.")
(t (c)
(when *verbose-output*
(format t "Failed to close the old connection when establishing reconnection. This can be normal: The old connection could probably don't work for the client, so the client is requesting to reconnect.~%Condition - ~A.~&"
c))))
(setf (gethash id *connection-ids*) connection)
(setf (gethash connection *connections*) id))
(id
(format t "Reconnection id ~A not found. Closing the connection.~%" id)
(websocket-driver:close-connection connection)) ; Don't send the reason for better security.
(t
(setf id (random-hex-string))
(setf (gethash connection *connections*) id)
(setf (gethash id *connection-ids*) connection)
(setf (gethash id *connection-data*)
(make-hash-table* :test #'equal))
(setf (gethash "connection-id" (get-connection-data id)) id)
(format t "New connection id - ~A - ~A~%" id connection)
(websocket-driver:send connection
(format nil "clog['connection_id']='~A'" id))
(bordeaux-threads:make-thread
(lambda ()
(if *break-on-error*
(funcall *on-connect-handler* id)
(handler-case
(funcall *on-connect-handler* id)
(t (c)
(format t "Condition caught connection ~A - ~A.~&" id c)
(values 0 c)))))
:name (format nil "CLOG connection ~A"
id))))
(t (c)
(format t "Condition caught in handle-new-connection - ~A.~&" c)
(values 0 c))))
;;;;;;;;;;;;;;;;;;;;
;; handle-message ;;
;;;;;;;;;;;;;;;;;;;;
(defun handle-message (connection message)
"Handle incoming websocket MESSAGE on CONNECTION. (Private)"
(handler-case
(let ((connection-id (gethash connection *connections*))
(ml (ppcre:split ":" message :limit 2)))
(cond ((null connection-id)
;; a zombie connection
(when *verbose-output*
(format t "A zombie connection ~A. CLOG doesn't remember its connection-id. Closing it.~%"
connection))
(websocket-driver:close-connection connection)) ; don't send the reason for better security
((equal (first ml) "0")
;; a ping
(when *verbose-output*
(format t "Connection ~A Ping~%" connection-id)))
((equal (first ml) "E")
;; an event
(let* ((em (ppcre:split " " (second ml) :limit 2))
(event-id (first em))
(data (second em)))
(when *verbose-output*
(format t "Connection ~A Hook = ~A Data = ~A~%"
connection-id event-id data))
(bordeaux-threads:make-thread
(lambda ()
(if *break-on-error*
(let* ((event-hash (get-connection-data connection-id))
(event (when event-hash
(gethash event-id event-hash))))
(when event
(funcall event data)))
(handler-case
(let* ((event-hash (get-connection-data connection-id))
(event (when event-hash
(gethash event-id
event-hash))))
(when event
(funcall event data)))
(t (c)
(format t "Condition caught in handle-message for event - ~A.~&" c)
(values 0 c)))))
:name (format nil "CLOG event handler ~A"
event-id))))
(t
;; a JavaScript execution result
(let ((server-query-id (first ml))
(browser-returned-answer (second ml)))
(when *verbose-output*
(format t "Connection ~A ~A = ~A ~A = ~A~%"
connection-id
'server-query-id
server-query-id
'browser-returned-answer
browser-returned-answer))
(setf (gethash (parse-integer server-query-id) *queries*) browser-returned-answer)
(bordeaux-threads:signal-semaphore
(gethash (parse-integer server-query-id) *queries-sems*))))))
(t (c)
(format t "Condition caught in handle-message - ~A.~&" c)
(values 0 c))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; handle-close-connection ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun handle-close-connection (connection)
"Close websocket CONNECTION. (Private)"
(handler-case
(let ((id (gethash connection *connections*)))
(when id
(when *verbose-output*
(format t "Connection id ~A has closed. ~A~%" id connection))
(remhash id *connection-data*)
(remhash id *connection-ids*)
(remhash connection *connections*)))
(t (c)
(format t "Condition caught in handle-close-connection - ~A.~&" c)
(values 0 c))))
;;;;;;;;;;;;;;;;;
;; clog-server ;;
;;;;;;;;;;;;;;;;;
(defun clog-server (env)
"Setup websocket server on ENV. (Private)"
(handler-case
(let ((ws (websocket-driver:make-server env)))
(websocket-driver:on :open ws
(lambda ()
(handler-case
(let* ((query (getf env :query-string))
(items (when query
(quri:url-decode-params query)))
(id (when items
(cdr (assoc "r" items
:test #'equalp)))))
(handle-new-connection ws id))
(t (c)
(print env)
(format t "Condition caught in clog-server :open - ~A.~&" c)
(values 0 c)))))
(websocket-driver:on :message ws
(lambda (msg)
(handler-case
(handle-message ws msg)
(t (c)
(format t "Condition caught in clog-server :message - ~A.~&" c)
(values 0 c)))))
(websocket-driver:on :error ws
(lambda (msg)
(format t "Websocket error - ~A~&" msg)))
(websocket-driver:on :close ws
(lambda (&key code reason)
(declare (ignore code reason))
(handler-case
(handle-close-connection ws)
(t (c)
(format t "Condition caught in clog-server :close - ~A.~&" c)
(values 0 c)))))
(lambda (responder)
(declare (ignore responder))
(websocket-driver:start-connection ws)))
(t (c)
(format t "Condition caught in clog-server start-up - ~A.~&" c)
(values 0 c))))
;;;;;;;;;;;;;;;;
;; initialize ;;
;;;;;;;;;;;;;;;;
(defun initialize (on-connect-handler
&key
(host "0.0.0.0")
(port 8080)
(server :hunchentoot)
(lack-middleware-list nil)
(extended-routing nil)
(long-poll-first nil)
(boot-file "/boot.html")
(boot-function nil)
(static-boot-html nil)
(static-boot-js nil)
(static-root #P"./static-files/")
(ssl nil)
(ssl-key-file nil)
(ssl-cert-file nil))
"Initialize CLOG on a socket using HOST and PORT to serve BOOT-FILE as the
default route for '/' to establish web-socket connections and static files
located at STATIC-ROOT. The webserver used with CLACK can be chosen with
:SERVER and middlewares prepended with :LACK-MIDDLEWARE-LIST,
NOT supporting LACK.BUILDER DSL.
If LONG-POLLING-FIRST is t, the output is sent as HTML instead of
websocket commands until the end of the on-new-window-handler, if
LONG-POLLING-FIRST is a number will keep long polling till that number of
queries to browser. LONG-POLLING-FIRST is used in webserver applications to
enable crawling of your website. If BOOT-FILE is nil no initial clog-path's will
be setup, use clog-path to add. The on-connect-handler needs to indentify the
path by querying the browser. See PATH-NAME (in CLOG-LOCATION). If
EXTENDED-ROUTING is t routes will match even if extend with additional / and
additional paths. If static-boot-js is nil then boot.js is served from the file
/js/boot.js instead of the compiled version. If static-boot-html is t if
boot.html is not present will use compiled version otherwise if set to nil (default)
if a boot file not found returns returns a blank page, if it is set to :error will
signal an error and if set to a string will display the string. boot-function if set is
called with the url and the contents of boot-file and its return value replaces
the contents sent to the brower."
(set-on-connect on-connect-handler)
(when boot-file
(set-clog-path "/" boot-file))
(setf *app*
(lack:builder
(lambda (app)
(lambda (env)
;; if not static-boot-js use internal compiled boot.js
(if (and (eq static-boot-js nil)
(equalp (getf env :path-info) "/js/boot.js"))
`(200 (:content-type "text/javascript")
(,*compiled-boot-js*))
(funcall app env))))
(lambda (app)
(lambda (env)
;; Special handling of "clog paths"
(let* ((url-path (getf env :path-info))
(clog-path (gethash url-path *url-to-boot-file*)))
(unless clog-path
(when extended-routing
(maphash (lambda (k v)
(unless (equal k "/")
(when (ppcre:scan (format nil "^~A/" k)
url-path)
(setf clog-path v))))
*url-to-boot-file*)))
(cond (clog-path
(let ((file (uiop:subpathname static-root clog-path)))
(with-open-file (stream file :direction :input
:if-does-not-exist nil)
(let ((page-data (if stream
(make-string (file-length stream))
(if static-boot-html
(cond ((eq static-boot-html t)
"")
((eq static-boot-html :error)
(error (format nil "Can not open boot file - ~A"
file)))
(t
static-boot-html))
(compiled-boot-html nil nil))))
(post-data nil))
(when stream
(read-sequence page-data stream))
(when boot-function
(setf page-data (funcall boot-function
url-path
page-data)))
(when (search "multipart/form-data;"
(getf env :content-type))
(let ((id (random-hex-string))
(req (lack.request:make-request env)))
(setf (gethash id *connection-data*)
(lack.request:request-body-parameters req))
(setf post-data id)))
(when (equal (getf env :content-type)
"application/x-www-form-urlencoded")
(setf post-data (cond ((eq (class-name (class-of (getf env :raw-body)))
'circular-streams:circular-input-stream)
(let ((array-buffer (make-array (getf env :content-length)
:adjustable t
:fill-pointer t)))
(read-sequence array-buffer (getf env :raw-body))
(flex:octets-to-string array-buffer)))
(t
(let ((string-buffer (make-string (getf env :content-length))))
(read-sequence string-buffer (getf env :raw-body))
string-buffer)))))
(cond (long-poll-first
(let ((id (random-hex-string)))
(setf (gethash id *connection-data*) (make-hash-table* :test #'equal))
(setf (gethash "connection-id" (get-connection-data id)) id)
(format t "New html connection id - ~A~%" id)
(lambda (responder)
(let* ((writer (funcall responder '(200 (:content-type "text/html"))))
(stream (lack.util.writer-stream:make-writer-stream writer))
(*long-poll-url* url-path)
(*long-poll-first* stream)
(*extended-long-poll* (if (eq long-poll-first t)
:extend
long-poll-first)))
(write-sequence page-data stream)
(write-sequence
(format nil "<script>clog['connection_id']='~A';Open_ws();</script>" id)
stream)
(when post-data
(write-sequence
(format nil "<script>clog['post-data']='~A'</script>"
post-data)
stream))
(if *break-on-error*
(funcall *on-connect-handler* id)
(handler-case
(funcall *on-connect-handler* id)
(t (c)
(format t "Condition caught connection ~A - ~A.~&" id c)
(values 0 c))))
(when *long-poll-first*
(setf *long-poll-first* nil)
(handler-case
(finish-output stream)
(t (c)
(format t "Condition caught finish-output ~A - ~A.~&" id c)
(values 0 c))))
(format t "HTML connection closed - ~A~%" id)))))
(t
(lambda (responder)
(let* ((writer (funcall responder '(200 (:content-type "text/html"))))
(stream (lack.util.writer-stream:make-writer-stream writer)))
(write-sequence page-data stream)
(when post-data
(write-sequence
(format nil "<script>clog['post-data']='~A'</script>"
post-data)
stream))
(finish-output stream)))))))))
;; Pass the handling on to next rule
(t (funcall app env))))))
(:static :path (lambda (path)
;; Request is static path if not the websocket connection.
;; Websocket url is /clog
(cond ((ppcre:scan "^(?:/clog$)" path) nil)
(t path)))
:root static-root)
;; Handle Websocket connection
(lambda (env)
(clog-server env))))
;; Wrap lack middlewares
(setf *app* (reduce #'funcall
lack-middleware-list
:initial-value *app*
:from-end t))
(setf *client-handler* (clack:clackup *app* :server server :address host :port port :ssl ssl :ssl-key-file ssl-key-file :ssl-cert-file ssl-cert-file))
(format t "HTTP listening on : ~A:~A~%" host port)
(format t "HTML root : ~A~%" static-root)
(format t "SSL : ~A~%" (if ssl
"yes"
"no"))
(format t "SSL Key File : ~A~%" ssl-key-file)
(format t "SSL Cert File : ~A~%" ssl-cert-file)
(format t "Long poll first : ~A~%" (if long-poll-first
"yes"
"no"))
(format t "Boot function added : ~A~%" (if boot-function
"yes"
"no"))
(format t "Boot html source use : ~A~%" (if static-boot-html
"static file"
"compiled version, when no file"))
(format t "Boot js source use : ~A~%" (if static-boot-js
"static file"
"compiled version"))
(format t "Boot file for path / : ~A~%" boot-file)
*client-handler*)
;;;;;;;;;;;;;;;;;;;;;;;;;
;; shutdown-connection ;;
;;;;;;;;;;;;;;;;;;;;;;;;;
(defun shutdown-connection ()
(clack:stop *client-handler*)
(setf *app* nil)
(setf *client-handler* nil))
;;;;;;;;;;;;
;; cclose ;;
;;;;;;;;;;;;
(defun cclose (connection-id)
"Close connection to CONNECTION-ID. The boot file may try to reestablish
connectivity."
(execute connection-id "ws.close()"))
;;;;;;;;;;;;;
;; execute ;;
;;;;;;;;;;;;;
(defun execute (connection-id message)
"Execute SCRIPT on CONNECTION-ID, disregard return value."
(if *long-poll-first*
(write-sequence (format nil "<script>~A</script>~%" message)
*long-poll-first*)
(let ((con (get-connection connection-id)))
(when con
(websocket-driver:send con message)))))
;;;;;;;;;;;
;; query ;;
;;;;;;;;;;;
(defun query (connection-id script &key (default-answer nil))
"Execute SCRIPT on CONNECTION-ID, return value. If times out answer
DEFAULT-ANSWER."
;; Provide delay if needed to establish websocket connection for
;; response.
(when *long-poll-first*
(finish-output *long-poll-first*)
(loop
for n from 1 to 10 do
(let ((con (get-connection connection-id)))
(when con
(unless (or (eq *extended-long-poll* :extend)
(> (decf *extended-long-poll*) 0))
(format t "Closing long-poll for ~A~%" connection-id)
(setf *long-poll-first* nil))
(return))
(format t "Awaiting websocket connection for ~A~%" connection-id)
(sleep .1))))
(let ((uid (generate-id)))
(prep-query uid (when default-answer (format nil "~A" default-answer)))
(execute connection-id
(format nil "ws.send (\"~A:\"+eval(\"~A\"));"
uid
(escape-string script)))
(wait-for-answer uid)))
;;;;;;;;;;;;;;;;;;;;;;;;
;; compiled-boot-html ;;
;;;;;;;;;;;;;;;;;;;;;;;;
(defun compiled-boot-html (path content)
(declare (ignore path content))
"Returns a compiled version version of boot.html. The compiled boot.html
uses the jQuery CDN instead of the static js files."
"<!doctype HTML>
<HTML>
<HEAD>
<meta http-equiv='Cache-Control' content='no-cache, no-store, must-revalidate' />
<meta http-equiv='Pragma' content='no-cache' />
<meta http-equiv='Expires' content='0' />
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<script src='https://code.jquery.com/jquery-3.6.0.min.js'
integrity='sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4='
crossorigin='anonymous'></script>
<script src='/js/boot.js' type='text/javascript'></script>
<noscript><%= (@ meta) %></noscript>
</HEAD>
<BODY>
<noscript><%= (@ body) %></noscript>
</BODY>
<noscript>Your browser must support JavaScript and be HTML 5 compilant to see this site.</noscript>
</HTML>")

View file

@ -10,6 +10,7 @@
;;; the low level connectivity between lisp and the browser.
;;;
;;; clog <-> clog-connection <->
;;; [clog-connection-websockets.lisp] - connection type specific layer
;;; clack <-> Hunchentoot (by default) <->
;;; internet/localhost <-> browser
;;;
@ -46,22 +47,25 @@ script."
"CLOG system utilities"
(escape-string function)
(escape-string function)
(generate-id function)
(random-hex-string function)
(make-hash-table* function)
"CLOG connection interface"
(execute function)
(query function)
(validp function)
(cclose function)
(shutdown function)
(compiled-boot-html function)
"CLOG connections"
(execute function)
(query function)
(validp function)
(cclose function)
(shutdown function)
"CLOG low level browser access"
(put function)
(put-line function)
(new-line function)
(alert-box function)
(generate-id function)
(random-hex-string function)
(debug-mode function)
(set-html-on-close function))
@ -80,9 +84,6 @@ script."
(defvar *verbose-output* nil "Verbose server output (default false)")
(defvar *break-on-error* t "Allow invoking debugger (default true)")
(defvar *app* nil "Clack 'app' middle-ware")
(defvar *client-handler* nil "Clack 'handler' for socket traffic")
(defvar *on-connect-handler* nil "New connection event handler.")
(defvar *connections* (make-hash-table*) "Connections to IDs")
@ -105,23 +106,6 @@ generate random hex strings for connection IDs")
(defvar *url-to-boot-file* (make-hash-table* :test 'equalp) "URL to boot-file")
(defvar *long-poll-first* nil
"Dynamic variable indicating to use html output instead of
websocket for output at start if connection.")
(defvar *extended-long-poll* nil
"Dynamic variable indicating to extend long polling beyond
extablishing websocket for output.")
(defvar *long-poll-url* nil
"Dynamic variable indicating the url path used.")
(defparameter *compiled-boot-js*
(with-open-file (stream (merge-pathnames #P"static-files/js/boot.js"
(asdf:system-source-directory :clog)))
(let ((content (make-string (file-length stream))))
(read-sequence content stream)
content))
"A compiled version of current version of boot.js (private)")
;;;;;;;;;;;;;;;;;
;; generate-id ;;
;;;;;;;;;;;;;;;;;
@ -198,182 +182,6 @@ the default answer. (Private)"
(format t "Condition caught in wait-for-answer - ~A.~&" c)
(values 0 c))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; handle-new-connection ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun handle-new-connection (connection id)
"Handle new incoming websocket CONNECTIONS with ID from boot page. (Private)"
(handler-case
(cond ((and id (gethash id *connection-data*))
(format t "Reconnection id - ~A to ~A~%" id connection)
(handler-case
(websocket-driver:close-connection (gethash id *connection-ids*)
"Aborting this old connection since receiving a reconnection request.")
(t (c)
(when *verbose-output*
(format t "Failed to close the old connection when establishing reconnection. This can be normal: The old connection could probably don't work for the client, so the client is requesting to reconnect.~%Condition - ~A.~&"
c))))
(setf (gethash id *connection-ids*) connection)
(setf (gethash connection *connections*) id))
(id
(format t "Reconnection id ~A not found. Closing the connection.~%" id)
(websocket-driver:close-connection connection)) ; Don't send the reason for better security.
(t
(setf id (random-hex-string))
(setf (gethash connection *connections*) id)
(setf (gethash id *connection-ids*) connection)
(setf (gethash id *connection-data*)
(make-hash-table* :test #'equal))
(setf (gethash "connection-id" (get-connection-data id)) id)
(format t "New connection id - ~A - ~A~%" id connection)
(websocket-driver:send connection
(format nil "clog['connection_id']='~A'" id))
(bordeaux-threads:make-thread
(lambda ()
(if *break-on-error*
(funcall *on-connect-handler* id)
(handler-case
(funcall *on-connect-handler* id)
(t (c)
(format t "Condition caught connection ~A - ~A.~&" id c)
(values 0 c)))))
:name (format nil "CLOG connection ~A"
id))))
(t (c)
(format t "Condition caught in handle-new-connection - ~A.~&" c)
(values 0 c))))
;;;;;;;;;;;;;;;;;;;;
;; handle-message ;;
;;;;;;;;;;;;;;;;;;;;
(defun handle-message (connection message)
"Handle incoming websocket MESSAGE on CONNECTION. (Private)"
(handler-case
(let ((connection-id (gethash connection *connections*))
(ml (ppcre:split ":" message :limit 2)))
(cond ((null connection-id)
;; a zombie connection
(when *verbose-output*
(format t "A zombie connection ~A. CLOG doesn't remember its connection-id. Closing it.~%"
connection))
(websocket-driver:close-connection connection)) ; don't send the reason for better security
((equal (first ml) "0")
;; a ping
(when *verbose-output*
(format t "Connection ~A Ping~%" connection-id)))
((equal (first ml) "E")
;; an event
(let* ((em (ppcre:split " " (second ml) :limit 2))
(event-id (first em))
(data (second em)))
(when *verbose-output*
(format t "Connection ~A Hook = ~A Data = ~A~%"
connection-id event-id data))
(bordeaux-threads:make-thread
(lambda ()
(if *break-on-error*
(let* ((event-hash (get-connection-data connection-id))
(event (when event-hash
(gethash event-id event-hash))))
(when event
(funcall event data)))
(handler-case
(let* ((event-hash (get-connection-data connection-id))
(event (when event-hash
(gethash event-id
event-hash))))
(when event
(funcall event data)))
(t (c)
(format t "Condition caught in handle-message for event - ~A.~&" c)
(values 0 c)))))
:name (format nil "CLOG event handler ~A"
event-id))))
(t
;; a JavaScript execution result
(let ((server-query-id (first ml))
(browser-returned-answer (second ml)))
(when *verbose-output*
(format t "Connection ~A ~A = ~A ~A = ~A~%"
connection-id
'server-query-id
server-query-id
'browser-returned-answer
browser-returned-answer))
(setf (gethash (parse-integer server-query-id) *queries*) browser-returned-answer)
(bordeaux-threads:signal-semaphore
(gethash (parse-integer server-query-id) *queries-sems*))))))
(t (c)
(format t "Condition caught in handle-message - ~A.~&" c)
(values 0 c))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; handle-close-connection ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun handle-close-connection (connection)
"Close websocket CONNECTION. (Private)"
(handler-case
(let ((id (gethash connection *connections*)))
(when id
(when *verbose-output*
(format t "Connection id ~A has closed. ~A~%" id connection))
(remhash id *connection-data*)
(remhash id *connection-ids*)
(remhash connection *connections*)))
(t (c)
(format t "Condition caught in handle-close-connection - ~A.~&" c)
(values 0 c))))
;;;;;;;;;;;;;;;;;
;; clog-server ;;
;;;;;;;;;;;;;;;;;
(defun clog-server (env)
"Setup websocket server on ENV. (Private)"
(handler-case
(let ((ws (websocket-driver:make-server env)))
(websocket-driver:on :open ws
(lambda ()
(handler-case
(let* ((query (getf env :query-string))
(items (when query
(quri:url-decode-params query)))
(id (when items
(cdr (assoc "r" items
:test #'equalp)))))
(handle-new-connection ws id))
(t (c)
(print env)
(format t "Condition caught in clog-server :open - ~A.~&" c)
(values 0 c)))))
(websocket-driver:on :message ws
(lambda (msg)
(handler-case
(handle-message ws msg)
(t (c)
(format t "Condition caught in clog-server :message - ~A.~&" c)
(values 0 c)))))
(websocket-driver:on :error ws
(lambda (msg)
(format t "Websocket error - ~A~&" msg)))
(websocket-driver:on :close ws
(lambda (&key code reason)
(declare (ignore code reason))
(handler-case
(handle-close-connection ws)
(t (c)
(format t "Condition caught in clog-server :close - ~A.~&" c)
(values 0 c)))))
(lambda (responder)
(declare (ignore responder))
(websocket-driver:start-connection ws)))
(t (c)
(format t "Condition caught in clog-server start-up - ~A.~&" c)
(values 0 c))))
;;;;;;;;;;;;;;;;;
;; random-port ;;
;;;;;;;;;;;;;;;;;
@ -385,214 +193,17 @@ the default answer. (Private)"
(usocket:socket-close l)
p))
;;;;;;;;;;;;;;;;
;; initialize ;;
;;;;;;;;;;;;;;;;
(defun initialize (on-connect-handler
&key
(host "0.0.0.0")
(port 8080)
(server :hunchentoot)
(lack-middleware-list nil)
(extended-routing nil)
(long-poll-first nil)
(boot-file "/boot.html")
(boot-function nil)
(static-boot-html nil)
(static-boot-js nil)
(static-root #P"./static-files/")
(ssl nil)
(ssl-key-file nil)
(ssl-cert-file nil))
"Initialize CLOG on a socket using HOST and PORT to serve BOOT-FILE as the
default route for '/' to establish web-socket connections and static files
located at STATIC-ROOT. The webserver used with CLACK can be chosen with
:SERVER and middlewares prepended with :LACK-MIDDLEWARE-LIST,
NOT supporting LACK.BUILDER DSL.
If LONG-POLLING-FIRST is t, the output is sent as HTML instead of
websocket commands until the end of the on-new-window-handler, if
LONG-POLLING-FIRST is a number will keep long polling till that number of
queries to browser. LONG-POLLING-FIRST is used in webserver applications to
enable crawling of your website. If BOOT-FILE is nil no initial clog-path's will
be setup, use clog-path to add. The on-connect-handler needs to indentify the
path by querying the browser. See PATH-NAME (in CLOG-LOCATION). If
EXTENDED-ROUTING is t routes will match even if extend with additional / and
additional paths. If static-boot-js is nil then boot.js is served from the file
/js/boot.js instead of the compiled version. If static-boot-html is t if
boot.html is not present will use compiled version otherwise if set to nil (default)
if a boot file not found returns returns a blank page, if it is set to :error will
signal an error and if set to a string will display the string. boot-function if set is
called with the url and the contents of boot-file and its return value replaces
the contents sent to the brower."
(set-on-connect on-connect-handler)
(when boot-file
(set-clog-path "/" boot-file))
(setf *app*
(lack:builder
(lambda (app)
(lambda (env)
;; if not static-boot-js use internal compiled boot.js
(if (and (eq static-boot-js nil)
(equalp (getf env :path-info) "/js/boot.js"))
`(200 (:content-type "text/javascript")
(,*compiled-boot-js*))
(funcall app env))))
(lambda (app)
(lambda (env)
;; Special handling of "clog paths"
(let* ((url-path (getf env :path-info))
(clog-path (gethash url-path *url-to-boot-file*)))
(unless clog-path
(when extended-routing
(maphash (lambda (k v)
(unless (equal k "/")
(when (ppcre:scan (format nil "^~A/" k)
url-path)
(setf clog-path v))))
*url-to-boot-file*)))
(cond (clog-path
(let ((file (uiop:subpathname static-root clog-path)))
(with-open-file (stream file :direction :input
:if-does-not-exist nil)
(let ((page-data (if stream
(make-string (file-length stream))
(if static-boot-html
(cond ((eq static-boot-html t)
"")
((eq static-boot-html :error)
(error (format nil "Can not open boot file - ~A"
file)))
(t
static-boot-html))
(compiled-boot-html nil nil))))
(post-data nil))
(when stream
(read-sequence page-data stream))
(when boot-function
(setf page-data (funcall boot-function
url-path
page-data)))
(when (search "multipart/form-data;"
(getf env :content-type))
(let ((id (random-hex-string))
(req (lack.request:make-request env)))
(setf (gethash id *connection-data*)
(lack.request:request-body-parameters req))
(setf post-data id)))
(when (equal (getf env :content-type)
"application/x-www-form-urlencoded")
(setf post-data (cond ((eq (class-name (class-of (getf env :raw-body)))
'circular-streams:circular-input-stream)
(let ((array-buffer (make-array (getf env :content-length)
:adjustable t
:fill-pointer t)))
(read-sequence array-buffer (getf env :raw-body))
(flex:octets-to-string array-buffer)))
(t
(let ((string-buffer (make-string (getf env :content-length))))
(read-sequence string-buffer (getf env :raw-body))
string-buffer)))))
(cond (long-poll-first
(let ((id (random-hex-string)))
(setf (gethash id *connection-data*) (make-hash-table* :test #'equal))
(setf (gethash "connection-id" (get-connection-data id)) id)
(format t "New html connection id - ~A~%" id)
(lambda (responder)
(let* ((writer (funcall responder '(200 (:content-type "text/html"))))
(stream (lack.util.writer-stream:make-writer-stream writer))
(*long-poll-url* url-path)
(*long-poll-first* stream)
(*extended-long-poll* (if (eq long-poll-first t)
:extend
long-poll-first)))
(write-sequence page-data stream)
(write-sequence
(format nil "<script>clog['connection_id']='~A';Open_ws();</script>" id)
stream)
(when post-data
(write-sequence
(format nil "<script>clog['post-data']='~A'</script>"
post-data)
stream))
(if *break-on-error*
(funcall *on-connect-handler* id)
(handler-case
(funcall *on-connect-handler* id)
(t (c)
(format t "Condition caught connection ~A - ~A.~&" id c)
(values 0 c))))
(when *long-poll-first*
(setf *long-poll-first* nil)
(handler-case
(finish-output stream)
(t (c)
(format t "Condition caught finish-output ~A - ~A.~&" id c)
(values 0 c))))
(format t "HTML connection closed - ~A~%" id)))))
(t
(lambda (responder)
(let* ((writer (funcall responder '(200 (:content-type "text/html"))))
(stream (lack.util.writer-stream:make-writer-stream writer)))
(write-sequence page-data stream)
(when post-data
(write-sequence
(format nil "<script>clog['post-data']='~A'</script>"
post-data)
stream))
(finish-output stream)))))))))
;; Pass the handling on to next rule
(t (funcall app env))))))
(:static :path (lambda (path)
;; Request is static path if not the websocket connection.
;; Websocket url is /clog
(cond ((ppcre:scan "^(?:/clog$)" path) nil)
(t path)))
:root static-root)
;; Handle Websocket connection
(lambda (env)
(clog-server env))))
;; Wrap lack middlewares
(setf *app* (reduce #'funcall
lack-middleware-list
:initial-value *app*
:from-end t))
(setf *client-handler* (clack:clackup *app* :server server :address host :port port :ssl ssl :ssl-key-file ssl-key-file :ssl-cert-file ssl-cert-file))
(format t "HTTP listening on : ~A:~A~%" host port)
(format t "HTML root : ~A~%" static-root)
(format t "SSL : ~A~%" (if ssl
"yes"
"no"))
(format t "SSL Key File : ~A~%" ssl-key-file)
(format t "SSL Cert File : ~A~%" ssl-cert-file)
(format t "Long poll first : ~A~%" (if long-poll-first
"yes"
"no"))
(format t "Boot function added : ~A~%" (if boot-function
"yes"
"no"))
(format t "Boot html source use : ~A~%" (if static-boot-html
"static file"
"compiled version, when no file"))
(format t "Boot js source use : ~A~%" (if static-boot-js
"static file"
"compiled version"))
(format t "Boot file for path / : ~A~%" boot-file)
*client-handler*)
;;;;;;;;;;;;;;;;;;;
;; shutdown-clog ;;
;;;;;;;;;;;;;;;;;;;
(defun shutdown-clog ()
"Shutdown CLOG."
(clack:stop *client-handler*)
(shutdown-connection)
(clrhash *connection-data*)
(clrhash *connections*)
(clrhash *connection-ids*)
(clrhash *url-to-boot-file*)
(setf *app* nil)
(setf *client-handler* nil))
(clrhash *url-to-boot-file*))
;;;;;;;;;;;;;;;;;;;;
;; set-on-connect ;;
@ -621,58 +232,28 @@ the contents sent to the brower."
;; escape-string ;;
;;;;;;;;;;;;;;;;;;;
(defun escape-string (str)
"Escape STR for sending to browser script."
(let ((res))
(setf res (ppcre:regex-replace-all "\\x5C" str "\\x5C")) ; \
(setf res (ppcre:regex-replace-all "\\x22" res "\\x22")) ; "
(setf res (ppcre:regex-replace-all "\\x27" res "\\x27")) ; '
(setf res (ppcre:regex-replace-all "\\x0A" res "\\x0A")) ; \n
(setf res (ppcre:regex-replace-all "\\x0D" res "\\x0D")) ; \r
res))
;;;;;;;;;;;;;
;; execute ;;
;;;;;;;;;;;;;
(defun execute (connection-id message)
"Execute SCRIPT on CONNECTION-ID, disregard return value."
(if *long-poll-first*
(write-sequence (format nil "<script>~A</script>~%" message)
*long-poll-first*)
(let ((con (get-connection connection-id)))
(when con
(websocket-driver:send con message)))))
;;;;;;;;;;;
;; query ;;
;;;;;;;;;;;
(defun query (connection-id script &key (default-answer nil))
"Execute SCRIPT on CONNECTION-ID, return value. If times out answer
DEFAULT-ANSWER."
;; Provide delay if needed to establish websocket connection for
;; response.
(when *long-poll-first*
(finish-output *long-poll-first*)
(loop
for n from 1 to 10 do
(let ((con (get-connection connection-id)))
(when con
(unless (or (eq *extended-long-poll* :extend)
(> (decf *extended-long-poll*) 0))
(format t "Closing long-poll for ~A~%" connection-id)
(setf *long-poll-first* nil))
(return))
(format t "Awaiting websocket connection for ~A~%" connection-id)
(sleep .1))))
(let ((uid (generate-id)))
(prep-query uid (when default-answer (format nil "~A" default-answer)))
(execute connection-id
(format nil "ws.send (\"~A:\"+eval(\"~A\"));"
uid
(escape-string script)))
(wait-for-answer uid)))
(defun escape-string (str &key (no-nil nil) (html nil))
"Escape STR for sending to browser script. If no-nil is t (default is nil)
if str is NIL returns empty string otherwise returns nil. If html is t the
quotes are changed to html entities and \n and \r are eliminated. Escape
string is used for wire readiness i.e. ability to be evaluated client side
and not for security purposes or html escapes."
(if (and (not str) (not no-nil))
nil
(let ((res))
(setf res (format nil "~@[~A~]" str))
(setf res (ppcre:regex-replace-all "\\x5C" res "\\x5C")) ; \
(cond (html
(setf res (ppcre:regex-replace-all "\\x22" res "&#x22;")) ; "
(setf res (ppcre:regex-replace-all "\\x27" res "&#x27;")) ; '
(setf res (ppcre:regex-replace-all "\\x0A" res "&#x0A;")) ; \n
(setf res (ppcre:regex-replace-all "\\x0D" res "&#x0D"))) ; \r
(t
(setf res (ppcre:regex-replace-all "\\x22" res "\\x22")) ; "
(setf res (ppcre:regex-replace-all "\\x27" res "\\x27")) ; '
(setf res (ppcre:regex-replace-all "\\x0A" res "\\x0A")) ; \n
(setf res (ppcre:regex-replace-all "\\x0D" res "\\x0D")))) ; \r
res)))
;;;;;;;;;;;;
;; validp ;;
@ -684,15 +265,6 @@ DEFAULT-ANSWER."
t
nil))
;;;;;;;;;;;;
;; cclose ;;
;;;;;;;;;;;;
(defun cclose (connection-id)
"Close connection to CONNECTION-ID. The boot file may try to reistablish
connectivity."
(execute connection-id "ws.close()"))
;;;;;;;;;;;;;;
;; shutdown ;;
;;;;;;;;;;;;;;
@ -754,31 +326,3 @@ HTML <br />."
the browser contents in case of connection loss."
(execute connection-id (format nil "clog['html_on_close']='~A'"
(escape-string html))))
;;;;;;;;;;;;;;;;;;;;;;;;
;; compiled-boot-html ;;
;;;;;;;;;;;;;;;;;;;;;;;;
(defun compiled-boot-html (path content)
(declare (ignore path content))
"Returns a compiled version version of boot.html. The compiled boot.html
uses the jQuery CDN instead of the static js files."
"<!doctype HTML>
<HTML>
<HEAD>
<meta http-equiv='Cache-Control' content='no-cache, no-store, must-revalidate' />
<meta http-equiv='Pragma' content='no-cache' />
<meta http-equiv='Expires' content='0' />
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<script src='https://code.jquery.com/jquery-3.6.0.min.js'
integrity='sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4='
crossorigin='anonymous'></script>
<script src='/js/boot.js' type='text/javascript'></script>
<noscript><%= (@ meta) %></noscript>
</HEAD>
<BODY>
<noscript><%= (@ body) %></noscript>
</BODY>
<noscript>Your browser must support JavaScript and be HTML 5 compilant to see this site.</noscript>
</HTML>")

View file

@ -123,7 +123,7 @@ clog-document object. (Private)"))
(defmethod (setf title) (value (obj clog-document))
(execute obj
(format nil "title='~A'" (clog-connection:escape-string value)))
(format nil "title='~A'" (escape-string value)))
value)
;;;;;;;;;;;;;;;;;;

View file

@ -48,7 +48,7 @@ clog array but is not in the DOM. If HTML-ID is nil one is generated.
(private)"
(let ((web-id (if html-id
html-id
(format nil "CLOG~A" (clog-connection:generate-id)))))
(format nil "CLOG~A" (generate-id)))))
(clog-connection:execute
connection-id
(format nil
@ -144,14 +144,13 @@ after attachment is changed to one unique to this session."))
&key (clog-type 'clog-element)
(new-id nil))
(if new-id
(let ((id (format nil "CLOG~A" (clog-connection:generate-id))))
(clog-connection:execute (connection-id obj)
(format nil "$('#~A').attr('id','~A');clog['~A']=$('#~A').get(0)"
html-id id id id))
(let ((id (format nil "CLOG~A" (generate-id))))
(js-execute obj
(format nil "$('#~A').attr('id','~A');clog['~A']=$('#~A').get(0)"
html-id id id id))
(setf html-id id))
(clog-connection:execute (connection-id obj)
(format nil "clog['~A']=$('#~A').get(0)"
html-id html-id)))
(js-execute obj (format nil "clog['~A']=$('#~A').get(0)"
html-id html-id)))
(make-clog-element (connection-id obj) html-id :clog-type clog-type))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -16,15 +16,13 @@
(:documentation "Get the form data as an a-list sent by the get method"))
(defmethod form-get-data ((obj clog-obj))
(quri:uri-query-params
(quri:uri (clog-connection:query (connection-id obj) "location.href"))))
(quri:uri-query-params (quri:uri (js-query obj "location.href"))))
(defgeneric form-post-data (clog-obj)
(:documentation "Get the form data as an a-list sent by post method"))
(defmethod form-post-data ((obj clog-obj))
(quri:url-decode-params
(clog-connection:query (connection-id obj) "clog['post-data']")))
(quri:url-decode-params (js-query obj "clog['post-data']")))
(defgeneric form-multipart-data (clog-obj)
(:documentation "Get the form data as an a-list sent with the multipart
@ -438,9 +436,8 @@ have this set true ever. Autofocus on element when form loaded."))
group called NAME."))
(defmethod radio-value ((obj clog-obj) name)
(clog-connection:query (connection-id obj)
(format nil "$('input:radio[name=~A]:checked').val()"
name)))
(js-query obj (format nil "$('input:radio[name=~A]:checked').val()"
name)))
;;;;;;;;;;;;;;;;;;;;
;; checkbox-value ;;
@ -450,9 +447,8 @@ group called NAME."))
(:documentation "Returns t or nil on the selected checkbox button."))
(defmethod checkbox-value ((obj clog-obj) name)
(js-on-p (clog-connection:query (connection-id obj)
(format nil "$('input:checkbox[name=~A]:checked').val()"
name))))
(js-on-p (js-query obj (format nil "$('input:checkbox[name=~A]:checked').val()"
name))))
;;;;;;;;;;;;;;;;;;
;; select-value ;;
@ -463,20 +459,18 @@ group called NAME."))
be unique name on entire document."))
(defmethod select-value ((obj clog-obj) name)
(clog-connection:query (connection-id obj)
(format nil "$('select[name=~A] option:selected').val()" name)))
(js-query obj (format nil "$('select[name=~A] option:selected').val()" name)))
;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;
;; textarea-value ;;
;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;
(defgeneric textarea-value (clog-obj name)
(:documentation "Returns the value of textarea item called NAME and must
be unique name on entire document."))
(defmethod textarea-value ((obj clog-obj) name)
(clog-connection:query (clog::connection-id obj)
(format nil "$('textarea#~A').val()" name)))
(js-query obj (format nil "$('textarea#~A').val()" name)))
;;;;;;;;;;;;;;;;
;; name-value ;;
@ -487,8 +481,7 @@ be unique name on entire document."))
be unique name on entire document."))
(defmethod name-value ((obj clog-obj) name)
(clog-connection:query (connection-id obj)
(format nil "$('input[name=~A]').val()" name)))
(js-query obj (format nil "$('input[name=~A]').val()" name)))
;;;;;;;;;;;;;
;; pattern ;;

View file

@ -846,7 +846,7 @@ window-to-top-by-param or window-by-param."))
(html-id nil))
(let ((app (connection-data-item obj "clog-gui")))
(unless html-id
(setf html-id (clog-connection:generate-id)))
(setf html-id (generate-id)))
(when (eql (hash-table-count (windows app)) 0)
;; If previously no open windows reset default position
(setf (last-x app) 0)
@ -1433,7 +1433,7 @@ interactions. Use window-end-modal to undo."))
"Create an alert toast with option :TIME-OUT. If place-top is t then alert
is placed in DOM at top of html body instead of bottom of html body."
(unless html-id
(setf html-id (clog-connection:generate-id)))
(setf html-id (generate-id)))
(let* ((body (connection-data-item obj "clog-body"))
(win (create-child body
(format nil
@ -1472,7 +1472,7 @@ is placed in DOM at top of html body instead of bottom of html body."
(html-id nil))
"Create an alert dialog box with CONTENT centered."
(unless html-id
(setf html-id (clog-connection:generate-id)))
(setf html-id (generate-id)))
(let* ((body (connection-data-item obj "clog-body"))
(win (create-gui-window obj
:title title
@ -1525,7 +1525,7 @@ is placed in DOM at top of html body instead of bottom of html body."
"Create an input dialog box with CONTENT centered and an input box.
Calls on-input with input box contents or nil if canceled."
(unless html-id
(setf html-id (clog-connection:generate-id)))
(setf html-id (generate-id)))
(let* ((body (connection-data-item obj "clog-body"))
(inp (if (eql rows 1)
(format nil "<input type='text' id='~A-input' size='~A' value='~A'>"
@ -1607,7 +1607,7 @@ Calls on-input with input box contents or nil if canceled."
"Create a confirmation dialog box with CONTENT centered.
Calls on-input with t if confirmed or nil if canceled."
(unless html-id
(setf html-id (clog-connection:generate-id)))
(setf html-id (generate-id)))
(let* ((body (connection-data-item obj "clog-body"))
(win (create-gui-window obj
:title title
@ -1698,7 +1698,7 @@ The size of any texarea field is controled by the size and rows parameters
Calls on-input after OK or Cancel with an a-list of field name to value
if confirmed or nil if canceled."
(unless html-id
(setf html-id (clog-connection:generate-id)))
(setf html-id (generate-id)))
(let* ((body (connection-data-item obj "clog-body"))
(fls (format nil "~{~A~}"
(mapcar (lambda (l)

View file

@ -102,7 +102,7 @@ set (logging to browser console) in the default debug.html boot-file."
(unless *clog-running*
(initialize nil :boot-file boot-file :port port))
(set-on-new-window (lambda (body)
(clog-connection:debug-mode (connection-id body))
(debug-mode body)
(when clog-web-initialize
(clog-web:clog-web-initialize body))
(when clog-gui-initialize

View file

@ -32,12 +32,11 @@ Some sample jquery selectors:
selector1, selectorN, ..."))
(defmethod create-jquery ((obj clog-obj) jquery)
(let ((html-id (format nil "CLOG~A" (clog-connection:generate-id))))
(clog-connection:execute
(connection-id obj)
(format nil
"clog['~A']=$(\"~A\")"
html-id jquery))
(let ((html-id (format nil "CLOG~A" (generate-id))))
(js-execute obj
(format nil
"clog['~A']=$(\"~A\")"
html-id jquery))
(make-clog-element (connection-id obj) html-id :clog-type 'clog-jquery)))
;;;;;;;;;;;;

View file

@ -229,7 +229,7 @@ HTML-ID if set is the base and top,left,right,center, bottom are added e.g.
if :HTML-ID \"myid\" then the HTML-ID for center will be: myid-center"
(let ((panel-box (make-instance 'clog-panel-box-layout)))
(unless html-id
(setf html-id (clog-connection:generate-id)))
(setf html-id (generate-id)))
(setf (top-panel panel-box)
(create-panel clog-obj :left 0 :top 0 :right 0 :height top-height
:units units

View file

@ -8,18 +8,6 @@
(cl:in-package :clog)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation - make-hash-table*
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun make-hash-table* (&rest args)
"Use native concurrent hash tables"
;; This covers sbcl ecl mazzano lw and ccl.
;; (lw and ccl default hash is synchronized)
#+(or sbcl ecl mezzano)
(apply #'make-hash-table :synchronized t args)
#-(or sbcl ecl mezzano) (apply #'make-hash-table args))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Implementation - with-clog-create ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -167,33 +155,6 @@ CLOG-OBJ unless :NAME is set and is used instead."))
(t
(values default value))))
;;;;;;;;;;;;;;;;;;;
;; escape-string ;;
;;;;;;;;;;;;;;;;;;;
(defun escape-string (str &key (no-nil nil) (html nil))
"Escape STR for sending to browser script. If no-nil is t (default is nil)
if str is NIL returns empty string otherwise returns nil. If html is t the
quotes are changed to html entities and \n and \r are eliminated. Escape
string is used for wire readiness i.e. ability to be evaluated client side
and not for security purposes or html escapes."
(if (and (not str) (not no-nil))
nil
(let ((res))
(setf res (format nil "~@[~A~]" str))
(setf res (ppcre:regex-replace-all "\\x5C" res "\\x5C")) ; \
(cond (html
(setf res (ppcre:regex-replace-all "\\x22" res "&#x22;")) ; "
(setf res (ppcre:regex-replace-all "\\x27" res "&#x27;")) ; '
(setf res (ppcre:regex-replace-all "\\x0A" res "&#x0A;")) ; \n
(setf res (ppcre:regex-replace-all "\\x0D" res "&#x0D"))) ; \r
(t
(setf res (ppcre:regex-replace-all "\\x22" res "\\x22")) ; "
(setf res (ppcre:regex-replace-all "\\x27" res "\\x27")) ; '
(setf res (ppcre:regex-replace-all "\\x0A" res "\\x0A")) ; \n
(setf res (ppcre:regex-replace-all "\\x0D" res "\\x0D")))) ; \r
res)))
;;;;;;;;;;;;;;
;; lf-to-br ;;
;;;;;;;;;;;;;;

View file

@ -843,7 +843,7 @@ propetery will be set to nil on creation."))
"Create an alert toast with option :TIME-OUT. If place-top is t then alert
is placed in DOM at top of OBJ instead of bottom of OBJ."
(unless html-id
(setf html-id (clog-connection:generate-id)))
(setf html-id (generate-id)))
(let* ((panel (create-child obj
(format nil
" <div class='w3-panel ~A w3-animate-right w3-display-container'>~
@ -914,7 +914,7 @@ the value if set in the theme settings."
(when (getf (settings website) :color-class)
(setf color-class (getf (settings website) :color-class)))))
(unless html-id
(setf html-id (clog-connection:generate-id)))
(setf html-id (generate-id)))
(let* ((fls (format nil "~{~A~}"
(mapcar (lambda (l)
(cond

View file

@ -1,6 +1,6 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; CLOG - The Common Lisp Omnificent GUI ;;;;
;;;; (c) 2020-2021 David Botton ;;;;
;;;; (c) 2020-2024 David Botton ;;;;
;;;; License BSD 3 Clause ;;;;
;;;; ;;;;
;;;; clog-webgl.lisp ;;;;
@ -176,7 +176,7 @@ can be webgl (version 1) or webgl2 (default)"))
(defmethod create-webgl ((obj clog-canvas) &key (context "webgl2"))
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=clog['~A'].getContext('~A')"
web-id
(html-id obj) context))
@ -869,7 +869,7 @@ See https://github.com/KhronosGroup/WebGL/blob/main/specs/latest/2.0/webgl2.idl
For :GLENUM values"))
(defmethod create-shader ((obj clog-webgl) glenum-type)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.createShader(~A.~A)"
web-id
(script-id obj) (script-id obj) glenum-type))
@ -944,7 +944,7 @@ Returns a GLenum indicating whether the shader is a vertex shader (gl.VERTEX_SHA
(:documentation "Create a clog-webgl-program"))
(defmethod create-program ((obj clog-webgl))
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.createProgram()"
web-id
(script-id obj)))
@ -1010,7 +1010,7 @@ Returns a GLint indicating the number of uniform blocks containing active unifor
(:documentation "Returns the location of an uniform variable in clog-webgl-program"))
(defmethod uniform-location ((obj clog-webgl-program) name)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.getUniformLocation(~A,'~A')"
web-id
(script-id (gl obj)) (script-id obj) name))
@ -1037,7 +1037,7 @@ validation of WebGLProgram objects."))
(:documentation "Query about unknown attributes"))
(defmethod active-attribute ((obj clog-webgl-program) index)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.getActiveAttrib(~A,~A)"
web-id
(script-id (gl obj)) (script-id obj) index))
@ -1049,7 +1049,7 @@ validation of WebGLProgram objects."))
(:documentation "Query about unknown uniforms"))
(defmethod active-uniform ((obj clog-webgl-program) index)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.getActiveUniform(~A,~A)"
web-id
(script-id (gl obj)) (script-id obj) index))
@ -1117,7 +1117,7 @@ in WebGL2 the following added:
:PIXEL_UNPACK_BUFFER : Buffer used for pixel transfer operations."))
(defmethod create-webgl-buffer ((obj clog-webgl) &key bind-type)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.createBuffer()"
web-id
(script-id obj)))
@ -1195,7 +1195,7 @@ DATA-TYPE is the WebGL data type as a string \"Float32Array\""))
(:documentation "Create a clog-webgl-vertex-array"))
(defmethod create-vertex-array ((obj clog-webgl))
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.createVertexArray()"
web-id
(script-id obj)))
@ -1234,7 +1234,7 @@ clear* and blit-frame-buffer.
and blit-frame-buffer."))
(defmethod create-webgl-frame-buffer ((obj clog-webgl) &key bind-type)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.createFramebuffer()"
web-id
(script-id obj)))
@ -1289,7 +1289,7 @@ and blit-frame-buffer"))
is set binds the render-buffer to :RENDERBUFFER"))
(defmethod create-webgl-render-buffer ((obj clog-webgl) &key bind-type)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.createRenderbuffer()"
web-id
(script-id obj)))
@ -1343,7 +1343,7 @@ in WebGL 2 also:
:TEXTURE_2D_ARRAY : A two-dimensional array texture."))
(defmethod create-webgl-texture ((obj clog-webgl) &key bind-type)
(let ((web-id (clog-connection:generate-id)))
(let ((web-id (generate-id)))
(js-execute obj (format nil "clog['~A']=~A.createTexture()"
web-id
(script-id obj)))

View file

@ -33,7 +33,7 @@ them.")
(defun open-clog-popup (obj &key (path *clog-popup-path*)
(add-sync-to-path t)
(sync-key (clog-connection:random-hex-string))
(sync-key (random-hex-string))
(name "_blank")
(specs "")
(wait-timeout 10))
@ -447,7 +447,7 @@ unless is a localhost url."))
(defmethod open-window ((obj clog-window) url &key
(name "_blank")
(specs ""))
(let ((new-id (format nil "CLOG~A" (clog-connection:generate-id))))
(let ((new-id (format nil "CLOG~A" (generate-id))))
(execute obj (format nil "clog['~A']=open('~A','~A','~A')"
new-id url name specs))
(make-clog-window (connection-id obj) :html-id new-id)))

View file

@ -16,6 +16,11 @@
(mgl-pax:define-package :clog
(:documentation "The Common List Omnificent GUI - CLOG")
(:import-from :clog-connection
#:make-hash-table*
#:escape-string
#:generate-id
#:random-hex-string)
(:use #:cl #:parse-float #:mgl-pax))
(cl:in-package :clog)
@ -84,6 +89,10 @@ embedded in a native template application.)"
"Declerative Syntax Support"
(with-clog-create macro)
"CLOG ID utilities"
(generate-id function)
(random-hex-string function)
"CLOG-Group - Utility Class for CLOG-Obj storage"
(clog-group class)
(create-group function)