branch: externals/llm commit 6054da6b58a16e5e985700ab77f2a80b4df4d842 Author: r0man <ro...@burningswell.com> Commit: GitHub <nore...@github.com>
Plz (#46) --- llm.el | 2 +- plz.el | 918 ----------------------------------------------------------------- 2 files changed, 1 insertion(+), 919 deletions(-) diff --git a/llm.el b/llm.el index e3d79cf725..83bd11af04 100644 --- a/llm.el +++ b/llm.el @@ -4,7 +4,7 @@ ;; Author: Andrew Hyatt <ahy...@gmail.com> ;; Homepage: https://github.com/ahyatt/llm -;; Package-Requires: ((emacs "28.1")) +;; Package-Requires: ((emacs "28.1") (plz "0.8")) ;; Package-Version: 0.12.3 ;; SPDX-License-Identifier: GPL-3.0-or-later ;; diff --git a/plz.el b/plz.el deleted file mode 100644 index 5b5605bb59..0000000000 --- a/plz.el +++ /dev/null @@ -1,918 +0,0 @@ -;;; plz.el --- HTTP library -*- lexical-binding: t; -*- - -;; Copyright (C) 2019-2023 Free Software Foundation, Inc. - -;; Author: Adam Porter <a...@alphapapa.net> -;; Maintainer: Adam Porter <a...@alphapapa.net> -;; URL: https://github.com/alphapapa/plz.el -;; Version: 0.8-pre -;; Package-Requires: ((emacs "26.3")) -;; Keywords: comm, network, http - -;; This file is part of GNU Emacs. - -;;; License: - -;; This program is free software; you can redistribute it and/or modify -;; it under the terms of the GNU General Public License as published by -;; the Free Software Foundation, either version 3 of the License, or -;; (at your option) any later version. - -;; This program is distributed in the hope that it will be useful, -;; but WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -;; GNU General Public License for more details. - -;; You should have received a copy of the GNU General Public License -;; along with this program. If not, see <https://www.gnu.org/licenses/>. - -;;; Commentary: -;; -;; An HTTP library that uses curl as a backend. Inspired by, and some -;; code copied from, Christopher Wellons's library, elfeed-curl.el. -;; -;; Why this package? -;; -;; 1. `url' works well for many things, but it has some issues. -;; 2. `request' works well for many things, but it has some issues. -;; 3. Chris Wellons doesn't have time to factor his excellent -;; elfeed-curl.el library out of Elfeed. This will have to do. -;; -;; Why is it called `plz'? -;; -;; 1. There's already a package called `http'. -;; 2. There's already a package called `request'. -;; 3. Naming things is hard. - -;;;; Usage: - -;; FIXME(v0.8): Remove the following note. - -;; NOTE: In v0.8 of plz, only one error will be signaled: `plz-error'. -;; The existing errors, `plz-curl-error' and `plz-http-error', inherit -;; from `plz-error' to allow applications to update their code while -;; using v0.7 (i.e. any `condition-case' forms should now handle only -;; `plz-error', not the other two). - -;; Call function `plz' to make an HTTP request. Its docstring -;; explains its arguments. `plz' also supports other HTTP methods, -;; uploading and downloading binary files, sending URL parameters and -;; HTTP headers, configurable timeouts, error-handling "else" and -;; always-called "finally" functions, and more. - -;; Basic usage is simple. For example, to make a synchronous request -;; and return the HTTP response body as a string: -;; -;; (plz 'get "https://httpbin.org/get") -;; -;; Which returns the JSON object as a string: -;; -;; "{ -;; \"args\": {}, -;; \"headers\": { -;; \"Accept\": \"*/*\", -;; \"Accept-Encoding\": \"deflate, gzip\", -;; \"Host\": \"httpbin.org\", -;; \"User-Agent\": \"curl/7.35.0\" -;; }, -;; \"origin\": \"xxx.xxx.xxx.xxx\", -;; \"url\": \"https://httpbin.org/get\" -;; }" -;; -;; To make the same request asynchronously, decoding the JSON and -;; printing a message with a value from it: -;; -;; (plz 'get "https://httpbin.org/get" :as #'json-read -;; :then (lambda (alist) (message "URL: %s" (alist-get 'url alist)))) -;; -;; Which, after the request returns, prints: -;; -;; URL: https://httpbin.org/get - -;;;; Credits: - -;; Thanks to Chris Wellons for inspiration, encouragement, and advice. - -;;; Code: - -;;;; Requirements - -(require 'cl-lib) -(require 'rx) -(require 'subr-x) - -;;;; Errors - -(define-error 'plz-error "plz error") -(define-error 'plz-curl-error "plz: Curl error" 'plz-error) -(define-error 'plz-http-error "plz: HTTP error" 'plz-error) - -;;;; Structs - -(cl-defstruct plz-response - version status headers body) - -(cl-defstruct plz-error - curl-error response message) - -;;;; Constants - -(defconst plz-http-response-status-line-regexp - (rx "HTTP/" (group (or "1.0" "1.1" "2")) " " - ;; Status code - (group (1+ digit)) " " - ;; Reason phrase - (optional (group (1+ (not (any "\r\n"))))) - (or - ;; HTTP 1 - "\r\n" - ;; HTTP 2 - "\n")) - "Regular expression matching HTTP response status line.") - -(defconst plz-http-end-of-headers-regexp - (rx (or "\r\n\r\n" "\n\n")) - "Regular expression matching the end of HTTP headers. -This must work with both HTTP/1 (using CRLF) and HTTP/2 (using -only LF).") - -(defconst plz-curl-errors - ;; Copied from elfeed-curl.el. - '((1 . "Unsupported protocol.") - (2 . "Failed to initialize.") - (3 . "URL malformed. The syntax was not correct.") - (4 . "A feature or option that was needed to perform the desired request was not enabled or was explicitly disabled at build-time.") - (5 . "Couldn't resolve proxy. The given proxy host could not be resolved.") - (6 . "Couldn't resolve host. The given remote host was not resolved.") - (7 . "Failed to connect to host.") - (8 . "FTP weird server reply. The server sent data curl couldn't parse.") - (9 . "FTP access denied.") - (11 . "FTP weird PASS reply.") - (13 . "FTP weird PASV reply.") - (14 . "FTP weird 227 format.") - (15 . "FTP can't get host.") - (17 . "FTP couldn't set binary.") - (18 . "Partial file. Only a part of the file was transferred.") - (19 . "FTP couldn't download/access the given file, the RETR (or similar) command failed.") - (21 . "FTP quote error. A quote command returned error from the server.") - (22 . "HTTP page not retrieved.") - (23 . "Write error.") - (25 . "FTP couldn't STOR file.") - (26 . "Read error. Various reading problems.") - (27 . "Out of memory. A memory allocation request failed.") - (28 . "Operation timeout.") - (30 . "FTP PORT failed.") - (31 . "FTP couldn't use REST.") - (33 . "HTTP range error. The range \"command\" didn't work.") - (34 . "HTTP post error. Internal post-request generation error.") - (35 . "SSL connect error. The SSL handshaking failed.") - (36 . "FTP bad download resume.") - (37 . "FILE couldn't read file.") - (38 . "LDAP bind operation failed.") - (39 . "LDAP search failed.") - (41 . "Function not found. A required LDAP function was not found.") - (42 . "Aborted by callback.") - (43 . "Internal error. A function was called with a bad parameter.") - (45 . "Interface error. A specified outgoing interface could not be used.") - (47 . "Too many redirects.") - (48 . "Unknown option specified to libcurl.") - (49 . "Malformed telnet option.") - (51 . "The peer's SSL certificate or SSH MD5 fingerprint was not OK.") - (52 . "The server didn't reply anything, which here is considered an error.") - (53 . "SSL crypto engine not found.") - (54 . "Cannot set SSL crypto engine as default.") - (55 . "Failed sending network data.") - (56 . "Failure in receiving network data.") - (58 . "Problem with the local certificate.") - (59 . "Couldn't use specified SSL cipher.") - (60 . "Peer certificate cannot be authenticated with known CA certificates.") - (61 . "Unrecognized transfer encoding.") - (62 . "Invalid LDAP URL.") - (63 . "Maximum file size exceeded.") - (64 . "Requested FTP SSL level failed.") - (65 . "Sending the data requires a rewind that failed.") - (66 . "Failed to initialise SSL Engine.") - (67 . "The user name, password, or similar was not accepted and curl failed to log in.") - (68 . "File not found on TFTP server.") - (69 . "Permission problem on TFTP server.") - (70 . "Out of disk space on TFTP server.") - (71 . "Illegal TFTP operation.") - (72 . "Unknown TFTP transfer ID.") - (73 . "File already exists (TFTP).") - (74 . "No such user (TFTP).") - (75 . "Character conversion failed.") - (76 . "Character conversion functions required.") - (77 . "Problem with reading the SSL CA cert (path? access rights?).") - (78 . "The resource referenced in the URL does not exist.") - (79 . "An unspecified error occurred during the SSH session.") - (80 . "Failed to shut down the SSL connection.") - (82 . "Could not load CRL file, missing or wrong format (added in 7.19.0).") - (83 . "Issuer check failed (added in 7.19.0).") - (84 . "The FTP PRET command failed") - (85 . "RTSP: mismatch of CSeq numbers") - (86 . "RTSP: mismatch of Session Identifiers") - (87 . "unable to parse FTP file list") - (88 . "FTP chunk callback reported error") - (89 . "No connection available, the session will be queued") - (90 . "SSL public key does not matched pinned public key")) - "Alist mapping curl error code integers to helpful error messages.") - -;;;; Customization - -(defgroup plz nil - "Options for `plz'." - :group 'network - :link '(url-link "https://github.com/alphapapa/plz.el")) - -(defcustom plz-curl-program "curl" - "Name of curl program to call." - :type 'string) - -(defcustom plz-curl-default-args - '("--silent" - "--compressed" - "--location") - "Default arguments to curl. -Note that these arguments are passed on the command line, which -may be visible to other users on the local system." - :type '(repeat string)) - -(defcustom plz-connect-timeout 5 - "Default connection timeout in seconds. -This limits how long the connection phase may last (the -\"--connect-timeout\" argument to curl)." - :type 'number) - -(defcustom plz-timeout 60 - "Default request timeout in seconds. -This limits how long an entire request may take, including the -connection phase and waiting to receive the response (the -\"--max-time\" argument to curl)." - :type 'number) - -;;;; Functions - -;;;;; Public - -(cl-defun plz (method url &rest rest &key headers body else filter finally noquery - (as 'string) (then 'sync) - (body-type 'text) (decode t decode-s) - (connect-timeout plz-connect-timeout) (timeout plz-timeout)) - "Request METHOD from URL with curl. -Return the curl process object or, for a synchronous request, the -selected result. - -HEADERS may be an alist of extra headers to send with the -request. - -BODY may be a string, a buffer, or a list like `(file FILENAME)' -to upload a file from disk. - -BODY-TYPE may be `text' to send BODY as text, or `binary' to send -it as binary. - -AS selects the kind of result to pass to the callback function -THEN, or the kind of result to return for synchronous requests. -It may be: - -- `buffer' to pass the response buffer, which will be narrowed to - the response body and decoded according to DECODE. - -- `binary' to pass the response body as an un-decoded string. - -- `string' to pass the response body as a decoded string. - -- `response' to pass a `plz-response' structure. - -- `file' to pass a temporary filename to which the response body - has been saved without decoding. - -- `(file FILENAME)' to pass FILENAME after having saved the - response body to it without decoding. FILENAME must be a - non-existent file; if it exists, it will not be overwritten, - and an error will be signaled. - -- A function, which is called in the response buffer with it - narrowed to the response body (suitable for, e.g. `json-read'). - -If DECODE is non-nil, the response body is decoded automatically. -For binary content, it should be nil. When AS is `binary', -DECODE is automatically set to nil. - -THEN is a callback function, whose sole argument is selected -above with AS; if the request fails and no ELSE function is -given (see below), the argument will be a `plz-error' structure -describing the error. Or THEN may be `sync' to make a -synchronous request, in which case the result is returned -directly from this function. - -ELSE is an optional callback function called when the request -fails (i.e. if curl fails, or if the HTTP response has a non-2xx -status code). It is called with one argument, a `plz-error' -structure. If ELSE is nil, a `plz-curl-error' or -`plz-http-error' is signaled when the request fails, with a -`plz-error' structure as the error data. For synchronous -requests, this argument is ignored. - -NOTE: In v0.8 of `plz', only one error will be signaled: -`plz-error'. The existing errors, `plz-curl-error' and -`plz-http-error', inherit from `plz-error' to allow applications -to update their code while using v0.7 (i.e. any `condition-case' -forms should now handle only `plz-error', not the other two). - -FINALLY is an optional function called without argument after -THEN or ELSE, as appropriate. For synchronous requests, this -argument is ignored. - -CONNECT-TIMEOUT and TIMEOUT are a number of seconds that limit -how long it takes to connect to a host and to receive a response -from a host, respectively. - -NOQUERY is passed to `make-process', which see. - -FILTER is an optional function to be used as the process filter -for the curl process. It can be used to handle HTTP responses in -a streaming way. The function must accept 2 arguments, the -process object running curl, and a string which is output -received from the process. The default process filter inserts -the output of the process into the process buffer. The provided -FILTER function should at least insert output up to the HTTP body -into the process buffer. - -\(To silence checkdoc, we mention the internal argument REST.)" - ;; FIXME(v0.8): Remove the note about error changes from the docstring. - ;; FIXME(v0.8): Update error signals in docstring. - (declare (indent defun)) - (setf decode (if (and decode-s (not decode)) - nil decode)) - ;; NOTE: By default, for PUT requests and POST requests >1KB, curl sends an - ;; "Expect:" header, which causes servers to send a "100 Continue" response, which - ;; we don't want to have to deal with, so we disable it by setting the header to - ;; the empty string. See <https://gms.tf/when-curl-sends-100-continue.html>. - ;; TODO: Handle "100 Continue" responses and remove this workaround. - (push (cons "Expect" "") headers) - (let* ((data-arg (pcase-exhaustive body-type - ('binary "--data-binary") - ('text "--data"))) - (curl-command-line-args (append plz-curl-default-args - (list "--config" "-"))) - (curl-config-header-args (cl-loop for (key . value) in headers - collect (cons "--header" (format "%s: %s" key value)))) - (curl-config-args (append curl-config-header-args - (list (cons "--url" url)) - (when connect-timeout - (list (cons "--connect-timeout" - (number-to-string connect-timeout)))) - (when timeout - (list (cons "--max-time" (number-to-string timeout)))) - ;; NOTE: To make a HEAD request - ;; requires using the "--head" - ;; option rather than "--request - ;; HEAD", and doing so with - ;; "--dump-header" duplicates the - ;; headers, so we must instead - ;; specify that for each other - ;; method. - (pcase method - ('get - (list (cons "--dump-header" "-"))) - ((or 'put 'post) - (list (cons "--dump-header" "-") - (cons "--request" (upcase (symbol-name method))) - ;; It appears that this must be the last argument - ;; in order to pass data on the rest of STDIN. - (pcase body - (`(file ,filename) - ;; Use `expand-file-name' because curl doesn't - ;; expand, e.g. "~" into "/home/...". - (cons "--upload-file" (expand-file-name filename))) - (_ (cons data-arg "@-"))))) - ('delete - (list (cons "--dump-header" "-") - (cons "--request" (upcase (symbol-name method))))) - ('head - (list (cons "--head" "") - (cons "--request" "HEAD")))))) - (curl-config (cl-loop for (key . value) in curl-config-args - concat (format "%s \"%s\"\n" key value))) - (decode (pcase as - ('binary nil) - (_ decode))) - (default-directory - ;; Avoid making process in a nonexistent directory (in case the current - ;; default-directory has since been removed). It's unclear what the best - ;; directory is, but this seems to make sense, and it should still exist. - temporary-file-directory) - (process-buffer (generate-new-buffer " *plz-request-curl*")) - (stderr-process (make-pipe-process :name "plz-request-curl-stderr" - :buffer (generate-new-buffer " *plz-request-curl-stderr*") - :noquery t - :sentinel #'plz--stderr-sentinel)) - (process (make-process :name "plz-request-curl" - :buffer process-buffer - :coding 'binary - :command (append (list plz-curl-program) curl-command-line-args) - :connection-type 'pipe - :filter filter - :sentinel #'plz--sentinel - :stderr stderr-process - :noquery noquery)) - sync-p) - (when (eq 'sync then) - (setf sync-p t - then (lambda (result) - (process-put process :plz-result result)) - else nil)) - (setf - ;; Set the callbacks, etc. as process properties. - (process-get process :plz-then) - (pcase-exhaustive as - ((or 'binary 'string) - (lambda () - (let ((coding-system (or (plz--coding-system) 'utf-8))) - (pcase as - ('binary (set-buffer-multibyte nil))) - (plz--narrow-to-body) - (when decode - (decode-coding-region (point) (point-max) coding-system)) - (funcall then (or (buffer-string) - (make-plz-error :message (format "buffer-string is nil in buffer:%S" process-buffer))))))) - ('buffer (progn - (setf (process-get process :plz-as) 'buffer) - (lambda () - (let ((coding-system (or (plz--coding-system) 'utf-8))) - (pcase as - ('binary (set-buffer-multibyte nil))) - (plz--narrow-to-body) - (when decode - (decode-coding-region (point) (point-max) coding-system))) - (funcall then (current-buffer))))) - ('response (lambda () - (funcall then (or (plz--response :decode-p decode) - (make-plz-error :message (format "response is nil for buffer:%S buffer-string:%S" - process-buffer (buffer-string))))))) - ('file (lambda () - (set-buffer-multibyte nil) - (plz--narrow-to-body) - (let ((filename (make-temp-file "plz-"))) - (condition-case err - (progn - (write-region (point-min) (point-max) filename) - (funcall then filename)) - (file-already-exists - (funcall then (make-plz-error :message (format "error while writing to file %S: %S" filename err)))) - ;; In case of an error writing to the file, delete the temp file - ;; and signal the error. Ignore any errors encountered while - ;; deleting the file, which would obscure the original error. - (error (ignore-errors - (delete-file filename)) - (funcall then (make-plz-error :message (format "error while writing to file %S: %S" filename err)))))))) - (`(file ,(and (pred stringp) filename)) - (lambda () - (set-buffer-multibyte nil) - (plz--narrow-to-body) - (condition-case err - (progn - (write-region (point-min) (point-max) filename nil nil nil 'excl) - (funcall then filename)) - (file-already-exists - (funcall then (make-plz-error :message (format "error while writing to file %S: %S" filename err)))) - ;; Since we are creating the file, it seems sensible to delete it in case of an - ;; error while writing to it (e.g. a disk-full error). And we ignore any errors - ;; encountered while deleting the file, which would obscure the original error. - (error (ignore-errors - (when (file-exists-p filename) - (delete-file filename))) - (funcall then (make-plz-error :message (format "error while writing to file %S: %S" filename err))))))) - ((pred functionp) (lambda () - (let ((coding-system (or (plz--coding-system) 'utf-8))) - (plz--narrow-to-body) - (when decode - (decode-coding-region (point) (point-max) coding-system)) - (funcall then (funcall as)))))) - (process-get process :plz-else) else - (process-get process :plz-finally) finally - (process-get process :plz-sync) sync-p - ;; Record list of arguments for debugging purposes (e.g. when - ;; using Edebug in a process buffer, this allows determining - ;; which request the buffer is for). - (process-get process :plz-args) (apply #'list method url rest) - ;; HACK: We set the result to a sentinel value so that any other - ;; value, even nil, means that the response was processed, and - ;; the sentinel does not need to be called again (see below). - (process-get process :plz-result) :plz-result) - ;; Send --config arguments. - (process-send-string process curl-config) - (when body - (cl-typecase body - (string (process-send-string process body)) - (buffer (with-current-buffer body - (process-send-region process (point-min) (point-max)))))) - (process-send-eof process) - (if sync-p - (unwind-protect - (with-local-quit - ;; See Info node `(elisp)Accepting Output'. - (unless (and process stderr-process) - (error "Process unexpectedly nil")) - (while (accept-process-output process)) - (while (accept-process-output stderr-process)) - (when (eq :plz-result (process-get process :plz-result)) - ;; HACK: Sentinel seems to not have been called: call it again. (Although - ;; this is a hack, it seems to be a necessary one due to Emacs's process - ;; handling.) See <https://github.com/alphapapa/plz.el/issues/3> and - ;; <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=50166>. - (plz--sentinel process "finished\n") - (when (eq :plz-result (process-get process :plz-result)) - (error "Plz: NO RESULT FROM PROCESS:%S ARGS:%S" - process rest))) - ;; Sentinel seems to have been called: check the result. - (pcase (process-get process :plz-result) - ((and (pred plz-error-p) data) - ;; The AS function signaled an error, which was collected - ;; into a `plz-error' struct: re-signal the error here, - ;; outside of the sentinel. - (if (plz-error-response data) - ;; FIXME(v0.8): Signal only plz-error. - (signal 'plz-http-error (list "HTTP error" data)) - (signal 'plz-curl-error (list "Curl error" data)))) - (else - ;; The AS function returned a value: return it. - else))) - (unless (eq as 'buffer) - (kill-buffer process-buffer)) - (kill-buffer (process-buffer stderr-process))) - ;; Async request: return the process object. - process))) - -;;;;; Queue - -;; A simple queue system. - -(cl-defstruct plz-queued-request - "Structure representing a queued `plz' HTTP request. -For more details on these slots, see arguments to the function -`plz'." - method url headers body else finally noquery - as then body-type decode - connect-timeout timeout - next previous process) - -(cl-defstruct plz-queue - "Structure forming a queue for `plz' requests. -The queue may be appended to (the default) and pre-pended to, and -items may be removed from the front of the queue (i.e. by -default, it's FIFO). Use functions `plz-queue', `plz-run', and -`plz-clear' to queue, run, and clear requests, respectively." - (limit 1 - :documentation "Number of simultaneous requests.") - (active nil - :documentation "Active requests.") - (requests nil - :documentation "Queued requests.") - (canceled-p nil - :documentation "Non-nil when queue has been canceled.") - first-active last-active - first-request last-request - (finally nil - :documentation "Function called with no arguments after queue has been emptied or canceled.")) - -(defun plz-queue (queue &rest args) - "Queue request for ARGS on QUEUE and return QUEUE. -To pre-pend to QUEUE rather than append, it may be a list of the -form (`prepend' QUEUE). QUEUE is a `plz-request' queue. ARGS -are those passed to `plz', which see. Use `plz-run' to start -making QUEUE's requests." - (declare (indent defun)) - (cl-assert (not (equal 'sync (plist-get (cddr args) :then))) nil - "Only async requests may be queued") - (pcase-let* ((`(,method ,url . ,rest) args) - (args `(:method ,method :url ,url ,@rest)) - (request (apply #'make-plz-queued-request args))) - (pcase queue - (`(prepend ,queue) (plz--queue-prepend request queue)) - (_ (plz--queue-append request queue)))) - queue) - -(defun plz--queue-append (request queue) - "Add REQUEST to end of QUEUE and return QUEUE." - (cl-check-type request plz-queued-request - "REQUEST must be a `plz-queued-request' structure.") - (cl-check-type queue plz-queue - "QUEUE must be a `plz-queue' structure.") - (when (plz-queue-last-request queue) - (setf (plz-queued-request-next (plz-queue-last-request queue)) request)) - (setf (plz-queued-request-previous request) (plz-queue-last-request queue) - (plz-queue-last-request queue) request) - (unless (plz-queue-first-request queue) - (setf (plz-queue-first-request queue) request)) - (unless (plz-queue-last-request queue) - (setf (plz-queue-last-request queue) request)) - (push request (plz-queue-requests queue)) - queue) - -(defun plz--queue-prepend (request queue) - "Add REQUEST to front of QUEUE and return QUEUE." - (cl-check-type request plz-queued-request - "REQUEST must be a `plz-queued-request' structure.") - (cl-check-type queue plz-queue - "QUEUE must be a `plz-queue' structure.") - (when (plz-queue-requests queue) - (setf (plz-queued-request-next request) (car (plz-queue-requests queue)) - (plz-queued-request-previous (plz-queued-request-next request)) request)) - (setf (plz-queue-first-request queue) request) - (unless (plz-queue-first-request queue) - (setf (plz-queue-first-request queue) request)) - (unless (plz-queue-last-request queue) - (setf (plz-queue-last-request queue) request)) - (push request (plz-queue-requests queue)) - queue) - -(defun plz--queue-pop (queue) - "Return the first queued request on QUEUE and remove it from QUEUE." - (let* ((request (plz-queue-first-request queue)) - (next (plz-queued-request-next request))) - (when next - (setf (plz-queued-request-previous next) nil)) - (setf (plz-queue-first-request queue) next - (plz-queue-requests queue) (delq request (plz-queue-requests queue))) - (when (eq request (plz-queue-last-request queue)) - (setf (plz-queue-last-request queue) nil)) - request)) - -(defun plz-run (queue) - "Process requests in QUEUE and return QUEUE. -Return when QUEUE is at limit or has no more queued requests. - -QUEUE should be a `plz-queue' structure." - (cl-labels ((readyp (queue) - (and (not (plz-queue-canceled-p queue)) - (plz-queue-requests queue) - ;; With apologies to skeeto... - (< (length (plz-queue-active queue)) (plz-queue-limit queue))))) - (while (readyp queue) - (pcase-let* ((request (plz--queue-pop queue)) - ((cl-struct plz-queued-request method url - headers body finally noquery as body-type decode connect-timeout timeout - (else orig-else) (then orig-then)) - request) - (then (lambda (response) - (unwind-protect - ;; Ensure any errors in the THEN function don't abort the queue. - (funcall orig-then response) - (setf (plz-queue-active queue) (delq request (plz-queue-active queue))) - (plz-run queue)))) - (else (lambda (arg) - ;; FIXME(v0.8): This should be done in `plz-queue' because - ;; `plz-clear' will call the second queued-request's ELSE - ;; before it can be set by `plz-run'. - (unwind-protect - ;; Ensure any errors in the THEN function don't abort the queue. - (when orig-else - (funcall orig-else arg)) - (setf (plz-queue-active queue) (delq request (plz-queue-active queue))) - (plz-run queue)))) - (args (list method url - ;; Omit arguments for which `plz' has defaults so as not to nil them. - :headers headers :body body :finally finally :noquery noquery - :connect-timeout connect-timeout :timeout timeout))) - ;; Add arguments which override defaults. - (when as - (setf args (plist-put args :as as))) - (when else - (setf args (plist-put args :else else))) - (when then - (setf args (plist-put args :then then))) - (when decode - (setf args (plist-put args :decode decode))) - (when body-type - (setf args (plist-put args :body-type body-type))) - (when connect-timeout - (setf args (plist-put args :connect-timeout connect-timeout))) - (when timeout - (setf args (plist-put args :timeout timeout))) - (setf (plz-queued-request-process request) (apply #'plz args)) - (push request (plz-queue-active queue)))) - (when (and (plz-queue-finally queue) - (zerop (length (plz-queue-active queue))) - (zerop (length (plz-queue-requests queue)))) - (funcall (plz-queue-finally queue))) - queue)) - -(defun plz-clear (queue) - "Clear QUEUE and return it. -Cancels any active or pending requests and calls the queue's -FINALLY function. For pending requests, their ELSE functions -will be called with a `plz-error' structure with the message, -\"`plz' queue cleared; request canceled.\"; active requests will -have their curl processes killed and their ELSE functions called -with the corresponding data." - (setf (plz-queue-canceled-p queue) t) - (dolist (request (plz-queue-active queue)) - (when (process-live-p (plz-queued-request-process request)) - (kill-process (plz-queued-request-process request))) - (setf (plz-queue-active queue) (delq request (plz-queue-active queue)))) - (dolist (request (plz-queue-requests queue)) - (funcall (plz-queued-request-else request) - (make-plz-error :message "`plz' queue cleared; request canceled.")) - (setf (plz-queue-requests queue) (delq request (plz-queue-requests queue)))) - (when (plz-queue-finally queue) - (funcall (plz-queue-finally queue))) - (setf (plz-queue-first-active queue) nil - (plz-queue-last-active queue) nil - (plz-queue-first-request queue) nil - (plz-queue-last-request queue) nil - (plz-queue-canceled-p queue) nil) - queue) - -(defun plz-length (queue) - "Return number of of QUEUE's outstanding requests. -Includes active and queued requests." - (+ (length (plz-queue-active queue)) - (length (plz-queue-requests queue)))) - -;;;;; Private - -(defun plz--sentinel (process status) - "Sentinel for curl PROCESS. -STATUS should be the process's event string (see info -node `(elisp) Sentinels'). Calls `plz--respond' to process the -HTTP response (directly for synchronous requests, or from a timer -for asynchronous ones)." - (pcase status - ((or "finished\n" "killed\n" "interrupt\n" - (pred numberp) - (rx "exited abnormally with code " (group (1+ digit)))) - (let ((buffer (process-buffer process))) - (if (process-get process :plz-sync) - (plz--respond process buffer status) - (run-at-time 0 nil #'plz--respond process buffer status)))))) - -(defun plz--respond (process buffer status) - "Respond to HTTP response from PROCESS in BUFFER. -Parses the response and calls the THEN/ELSE callbacks -accordingly. To be called from `plz--sentinel'. STATUS is the -argument passed to `plz--sentinel', which see." - ;; Is it silly to call this function "please respond"? Perhaps, but - ;; naming things is hard. The term "process" has another meaning in - ;; this context, and the old standby, "handle," is much overused. - ;; "Respond" also means "to react to something," which is what this - ;; does--react to receiving the HTTP response--and it's an internal - ;; name, so why not. - (unwind-protect - (with-current-buffer buffer - (pcase-exhaustive status - ((or 0 "finished\n") - ;; Curl exited normally: check HTTP status code. - (goto-char (point-min)) - (plz--skip-proxy-headers) - (while (plz--skip-redirect-headers)) - (pcase (plz--http-status) - ((and status (guard (<= 200 status 299))) - ;; Any 2xx response is considered successful. - (ignore status) ; Byte-compiling in Emacs <28 complains without this. - (funcall (process-get process :plz-then))) - (_ - ;; TODO: If using ":as 'response", the HTTP response - ;; should be passed to the THEN function, regardless - ;; of the status code. Only for curl errors should - ;; the ELSE function be called. (Maybe in v0.8.) - - ;; Any other status code is considered unsuccessful - ;; (for now, anyway). - (let ((err (make-plz-error :response (plz--response)))) - (pcase-exhaustive (process-get process :plz-else) - (`nil (process-put process :plz-result err)) - ((and (pred functionp) fn) (funcall fn err))))))) - - ((or (and (pred numberp) code) - (rx "exited abnormally with code " (let code (group (1+ digit))))) - ;; Curl error. - (let* ((curl-exit-code (cl-typecase code - (string (string-to-number code)) - (number code))) - (curl-error-message (alist-get curl-exit-code plz-curl-errors)) - (err (make-plz-error :curl-error (cons curl-exit-code curl-error-message)))) - (pcase-exhaustive (process-get process :plz-else) - (`nil (process-put process :plz-result err)) - ((and (pred functionp) fn) (funcall fn err))))) - - ((and (or "killed\n" "interrupt\n") status) - ;; Curl process killed or interrupted. - (let* ((message (pcase status - ("killed\n" "curl process killed") - ("interrupt\n" "curl process interrupted"))) - (err (make-plz-error :message message))) - (pcase-exhaustive (process-get process :plz-else) - (`nil (process-put process :plz-result err)) - ((and (pred functionp) fn) (funcall fn err))))))) - (when-let ((finally (process-get process :plz-finally))) - (funcall finally)) - (unless (or (process-get process :plz-sync) - (eq 'buffer (process-get process :plz-as))) - (kill-buffer buffer)))) - -(defun plz--stderr-sentinel (process status) - "Sentinel for STDERR buffer. -Arguments are PROCESS and STATUS (ok, checkdoc?)." - (pcase status - ((or "finished\n" "killed\n" "interrupt\n" - (pred numberp) - (rx "exited abnormally with code " (1+ digit))) - (kill-buffer (process-buffer process))))) - -;;;;;; HTTP Responses - -;; Functions for parsing HTTP responses. - -(defun plz--skip-proxy-headers () - "Skip proxy headers in current buffer." - (when (looking-at plz-http-response-status-line-regexp) - (let* ((status-code (string-to-number (match-string 2))) - (reason-phrase (match-string 3))) - (when (and (equal 200 status-code) - (equal "Connection established" reason-phrase)) - ;; Skip proxy headers (curl apparently offers no way to omit - ;; them). - (unless (re-search-forward "\r\n\r\n" nil t) - (signal 'plz-http-error '("plz--response: End of proxy headers not found"))))))) - -(defun plz--skip-redirect-headers () - "Skip HTTP redirect headers in current buffer." - (when (and (looking-at plz-http-response-status-line-regexp) - (member (string-to-number (match-string 2)) '(301 302 303 307 308))) - ;; Skip redirect headers ("--dump-header" forces redirect headers to be included - ;; even when used with "--location"). - (or (re-search-forward "\r\n\r\n" nil t) - (signal 'plz-http-error '("plz--response: End of redirect headers not found"))))) - -(cl-defun plz--response (&key (decode-p t)) - "Return response structure for HTTP response in current buffer. -When DECODE-P is non-nil, decode the response body automatically -according to the apparent coding system. - -Assumes that point is at beginning of HTTP response." - (save-excursion - ;; Parse HTTP version and status code. - (unless (looking-at plz-http-response-status-line-regexp) - (signal 'plz-http-error - (list "plz--response: Unable to parse HTTP response status line" - (buffer-substring (point) (line-end-position))))) - (let* ((http-version (string-to-number (match-string 1))) - (status-code (string-to-number (match-string 2))) - (headers (plz--headers)) - (coding-system (or (plz--coding-system headers) 'utf-8))) - (plz--narrow-to-body) - (when decode-p - (decode-coding-region (point) (point-max) coding-system)) - (make-plz-response - :version http-version - :status status-code - :headers headers - :body (buffer-string))))) - -(defun plz--coding-system (&optional headers) - "Return coding system for HTTP response in current buffer. -HEADERS may optionally be an alist of parsed HTTP headers to -refer to rather than the current buffer's un-parsed headers." - (let* ((headers (or headers (plz--headers))) - (content-type (alist-get 'content-type headers))) - (when content-type - (coding-system-from-name content-type)))) - -(defun plz--http-status () - "Return HTTP status code for HTTP response in current buffer. -Assumes point is at start of HTTP response." - (when (looking-at plz-http-response-status-line-regexp) - (string-to-number (match-string 2)))) - -(defun plz--headers () - "Return headers alist for HTTP response in current buffer. -Assumes point is at start of HTTP response." - (save-excursion - (forward-line 1) - (let ((limit (save-excursion - (re-search-forward plz-http-end-of-headers-regexp nil) - (point)))) - (cl-loop while (re-search-forward (rx bol (group (1+ (not (in ":")))) ":" (1+ blank) - (group (1+ (not (in "\r\n"))))) - limit t) - ;; NOTE: Some HTTP servers send all-lowercase header keys, which means an alist - ;; lookup with `equal' or `string=' fails when the case differs. We don't want - ;; users to have to worry about this, so for consistency, we downcase the - ;; header name. And while we're at it, we might as well intern it so we can - ;; use `alist-get' without having to add "nil nil #'equal" every time. - collect (cons (intern (downcase (match-string 1))) (match-string 2)))))) - -(defun plz--narrow-to-body () - "Narrow to body of HTTP response in current buffer. -Assumes point is at start of HTTP response." - (unless (re-search-forward plz-http-end-of-headers-regexp nil t) - (signal 'plz-http-error '("plz--narrow-to-body: Unable to find end of headers"))) - (narrow-to-region (point) (point-max))) - -;;;; Footer - -(provide 'plz) - -;;; plz.el ends here