From 0e59623c3727954b9b2245ff5df632334a95ee61 Mon Sep 17 00:00:00 2001 From: Michael Olson Date: Wed, 5 Aug 2009 08:19:28 -0700 Subject: [PATCH] Check in very rough alpha version of Muse IPC server and ikiwiki test. It won't publish pages or do anything useful yet, other than print diagnostics messages to stdout. --- contrib/ikiwiki/IkiWiki/Plugin/test.pl | 206 +++++++++++++++++++++++++++++++++ lisp/muse-ikiwiki.el | 41 ++++--- lisp/muse-ipc.el | 194 +++++++++++++++++++++++++++++++ lisp/muse-publish.el | 4 +- 4 files changed, 428 insertions(+), 17 deletions(-) create mode 100644 contrib/ikiwiki/IkiWiki/Plugin/test.pl create mode 100644 lisp/muse-ipc.el diff --git a/contrib/ikiwiki/IkiWiki/Plugin/test.pl b/contrib/ikiwiki/IkiWiki/Plugin/test.pl new file mode 100644 index 0000000..8b015a8 --- /dev/null +++ b/contrib/ikiwiki/IkiWiki/Plugin/test.pl @@ -0,0 +1,206 @@ + +use warnings; +use strict; + +use IO::Select qw(); +use IO::Socket::INET qw(); + +my %config = ( + muse_emacs => '/usr/local/bin/emacs', + muse_init => '/stuff/proj/personal-site/ikiwiki/muse-init.el', + muse_shared_secret => 'foo', +); + +my %MUSE_SERVER = ( host => 'localhost' ); + +main(); +exit 0; + +# Determine the emacs binary to use +sub locate_emacs { + my $err = sub { + die "Unable to find your emacs binary.\n", + " Set muse_emacs config to the right value.\n"; + }; + if ( $config{muse_emacs} ) { + ( -x $config{muse_emacs} ) ? return $config{muse_emacs} : $err->(); + } + else { + my $emacs = `which emacs`; + chomp $emacs; + ( $emacs ) ? return $emacs : $err->(); + } +} + +# Initialize connection to the Muse IPC server +sub start_muse_server { + my $secret = $config{muse_shared_secret}; + my $init_port = $config{muse_init_port} || 0; + my $ipc_port = $config{muse_ipc_port}; + + # Perform sanity checks + $config{muse_init} or die "Error: muse_init config option not defined.\n"; + + # Start initialization server + my $pserver = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => 'localhost', + LocalPort => $init_port, + Listen => IO::Socket::INET::SOMAXCONN, + ) or die "Error: Cannot begin initialization for the Muse IPC server.\n"; + $pserver->autoflush(1); + $init_port = $pserver->sockport(); + my $select = IO::Select->new($pserver); + + # Start Emacs + defined(my $pid = fork()) or die "Error: Unable to fork.\n"; + if ( $pid ) { + $MUSE_SERVER{pid} = $pid; + } + else { + exec locate_emacs(), + qw( -q --no-site-file -batch -l ), $config{muse_init}, + qw( --eval ), "(muse-ikiwiki-start-server \"$init_port\"" . + ( $ipc_port ? " \"$ipc_port\"" : '' ) . ")"; + die "Error: Unable to exec emacs.\n"; + } + + my $emacs_port = undef; + + SERVER: + # Respond to clients + while ( my @ready = $select->can_read() ) { + for my $client (@ready) { + if ($client == $pserver) { + my $new = $pserver->accept(); + $select->add($new); + next; + } + my $line = <$client>; + chomp $line if defined $line; + if ( defined $line && $line =~ m/^begin (.+)$/s && + $1 eq $secret ) { + print $client "ok\n"; + $line = <$client>; + chomp $line if defined $line; + if ( defined $line && $line =~ m/^port (.+)$/s ) { + $emacs_port = $1; + } + else { + print STDERR <remove($client); + $client->close(); + } + } + $pserver->close(); + + if ( $emacs_port ) { + $MUSE_SERVER{port} = $emacs_port; + } + else { + kill_muse_server(); + } +} + +sub stop_muse_server { + my ( $sock ) = @_; + + if ( $MUSE_SERVER{pid} ) { + # Give Muse 3 seconds to stop, presuming that it has already + # been sent the "done" command via stop_muse_server. + local $SIG{ALRM} = sub { + kill 9, $MUSE_SERVER{pid}; + die "Timeout"; + }; + eval { + alarm 3; + print $sock "done\n"; + $sock->close(); + waitpid($MUSE_SERVER{pid}, 0); + alarm 0; + }; + delete $MUSE_SERVER{pid}; + } + else { + print $sock "done\n"; + $sock->close(); + } +} + +sub kill_muse_server { + my @msgs = @_; + + kill 9, $MUSE_SERVER{pid} if $MUSE_SERVER{pid}; + die @msgs if @msgs; +} + +sub ipc_expect_ok { + my ( $sock, $err_msg ) = @_; + $err_msg = "Error: Command did not succeed on Muse IPC server.\n"; + + my $line = <$sock>; + chomp $line; + if ( $line ne 'ok' ) { + $sock->close(); + kill_muse_server $err_msg; + } +} + +sub ipc_connect { + my $secret = $config{muse_shared_secret}; + my $host = $MUSE_SERVER{host}; + my $port = $MUSE_SERVER{port}; + $host && $port + or kill_muse_server "Error: No Muse IPC server is active.\n"; + + # Start client connection + my $sock = IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => $host, + PeerPort => $port, + ) or kill_muse_server "Error: Cannot connect to the Muse IPC server.\n"; + $sock->autoflush(1); + + # Authenticate + print $sock "begin $secret\n"; + ipc_expect_ok $sock, + "Error: Could not authenticate to the Muse IPC server.\n"; + + return $sock; +} + +sub test_name { + my ( $sock ) = @_; + + print $sock "name foobar\n"; + ipc_expect_ok $sock, + "Error: Could not set name of page on Muse IPC server.\n"; +} + +sub test_title { + my ( $sock ) = @_; + + print $sock "title quux\n"; + ipc_expect_ok $sock, + "Error: Could not set title of page on Muse IPC server.\n"; +} + +sub main { + print "Starting Muse server ...\n"; + start_muse_server(); + + print "Got port $MUSE_SERVER{port}.\n"; + my $sock = ipc_connect(); + test_name($sock); + test_title($sock); + + print "Shutting down ...\n"; + stop_muse_server($sock); + print "Done shutting down.\n"; +} diff --git a/lisp/muse-ikiwiki.el b/lisp/muse-ikiwiki.el index 09b7cd5..20d4030 100644 --- a/lisp/muse-ikiwiki.el +++ b/lisp/muse-ikiwiki.el @@ -83,38 +83,51 @@ For more on the structure of this list, see `muse-publish-markup-regexps'." '(muse-no-paragraph t)) (muse-publish-mark-read-only (match-beginning 0) (match-end 0)))) +(defun muse-ikiwiki-publish-buffer (name title &optional style) + "Publish a buffer for Ikiwki. +The name of the corresponding file is NAME. +The name of the style is given by STYLE. It defaults to \"ikiwiki\"." + (unless style (setq style "ikiwiki")) + (unless title (setq title (muse-page-name name))) + (let ((muse-batch-publishing-p t) + (muse-publishing-current-file name) + (muse-publishing-current-output-path file) + (muse-publishing-current-style style) + (font-lock-verbose nil) + (vc-handled-backends nil)) ; don't activate VC when publishing files + (run-hooks 'muse-before-publish-hook) + (let ((muse-inhibit-before-publish-hook t)) + (muse-publish-markup-buffer title style)))) + (defun muse-ikiwiki-publish-file (file name &optional style) "Publish a single file for Ikiwiki. -The name of the style is given by STYLE. It defaults to \"ikiwiki\". The name of the real file is NAME, and the name of the temporary -file containing the content is FILE." +file containing the content is FILE. +The name of the style is given by STYLE. It defaults to \"ikiwiki\"." (if (not (stringp file)) (message "Error: No file given to publish") (unless style (setq style "ikiwiki")) - (let ((muse-batch-publishing-p t) - (title (muse-page-name name)) - (output-path file) + (let ((output-path file) (target file) - (muse-publishing-current-file file) - (muse-publishing-current-output-path file) - (font-lock-verbose nil) + (vc-handled-backends nil) ; don't activate VC when publishing files + auto-mode-alist muse-current-output-style) - ;; don't activate VC when publishing files - (setq vc-handled-backends nil) - (setq muse-current-output-style (list :base style :path file)) (setq auto-mode-alist (delete (cons (concat "\\." muse-file-extension "\\'") 'muse-mode-choose-mode) auto-mode-alist)) + (setq muse-current-output-style (list :base style :path file)) (muse-with-temp-buffer (muse-insert-file-contents file) - (run-hooks 'muse-before-publish-hook) - (let ((muse-inhibit-before-publish-hook t)) - (muse-publish-markup-buffer title style)) + (muse-ikiwiki-publish-buffer name nil nil style) (when (muse-write-file output-path t) (muse-style-run-hooks :final style file output-path target)))))) +(defun muse-ikiwiki-start-server (port) + "Start Muse IPC server, initializing with the client on PORT." + (muse-ipc-start "foo" #'muse-ikiwiki-publish-buffer port)) + ;;; Colors (defface muse-ikiwiki-directive diff --git a/lisp/muse-ipc.el b/lisp/muse-ipc.el new file mode 100644 index 0000000..417ec15 --- /dev/null +++ b/lisp/muse-ipc.el @@ -0,0 +1,194 @@ +;;; muse-ipc.el --- publish Muse documents from other processes + +;; Copyright (C) 2009 Free Software Foundation, Inc. + +;; This file is part of Emacs Muse. It is not part of GNU Emacs. + +;; Emacs Muse 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, or (at your +;; option) any later version. + +;; Emacs Muse 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 Emacs Muse; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Commentary: + +;; This file is still in alpha state. Not for production use! + +;;; Contributors: + +;;; Code: + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Muse Inter-Process Communication +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(eval-when-compile (require 'cl)) + +(require 'muse) +(require 'muse-publish) + +(defgroup muse-ipc nil + "Options controlling the behavior of Muse's IPC module." + :group 'muse-publish) + +(defcustom muse-ipc-timeout 60 + "Maximum time to wait for a client to respond." + :group 'muse-ipc + :type 'number) + +(defcustom muse-ipc-ignore-done nil + "If non-nil, ignore any 'done' messages that we get from clients." + :group 'muse-ipc + :type 'boolean) + +(defvar muse-ipc-server-port nil + "Port of the Emacs server.") + +(defvar muse-ipc-server-process nil + "Process of the Emacs server.") + +(defvar muse-ipc-server-registered nil + "Whether we have successfully registered our port with the client.") + +(defun muse-ipc-init-filter (proc string) + "Handle data from client while initiating a connection." + (unless muse-ipc-server-registered + (when (string-match "\\`ok$" string) + (setq muse-ipc-server-registered t)))) + +(defun muse-ipc-delete-client (proc) + "Delete a client." + (let ((buffer (process-get proc :buffer))) + (when (and buffer (buffer-live-p buffer)) + (with-current-buffer buffer + (set-buffer-modified-p nil)) + (kill-buffer buffer))) + (when (eq (process-status proc) 'open) + (delete-process proc))) + +(defun* muse-ipc-server-filter (proc string) + "Handle data from a client after it connects." + ;; Authenticate + (unless (process-get proc :authenticated) + (if (and (string-match "\\`begin \\(.+\\)$" string) + (equal (match-string 1 string) + (process-get proc :shared-secret))) + (progn + (setq string (substring string (match-end 0))) + (process-put proc :authenticated t) + (process-send-string proc "ok\n")) + (process-send-string proc "nok\n") + (delete-process proc)) + (return-from muse-ipc-server-filter)) + + ;; Handle case where the client is sending data to be published + (when (process-get proc :sending-data) + (with-current-buffer (process-get proc :buffer) + (insert string) + (let ((buf-len (1- (point))) + (expected-len (process-get proc :data-bytes))) + (cond ((= buf-len expected-len) + (process-put proc :sending-data nil)) + ((> buf-len expected-len) + (process-send-string proc "nok\n") + (muse-ipc-delete-client proc))))) + (return-from muse-ipc-server-filter)) + + ;; Dispatch commands + (cond + ((string-match "\\`done$" string) + ;; done, close the server + (unless muse-ipc-ignore-done + (muse-ipc-stop-server))) + + ((string-match "\\`name \\(.+\\)$" string) + ;; set name + (process-put proc :file-name (match-string 1 string)) + (process-send-string proc "ok\n")) + + ((string-match "\\`title \\(.+\\)$" string) + ;; set title + (process-put proc :title (match-string 1 string)) + (process-send-string proc "ok\n")) + + (t + ;; unrecognized command + (process-send-string proc "nok\n")))) + +(defun muse-ipc-stop-server () + "Stop Muse IPC server and reset connection data." + (stop-process muse-ipc-server-process) + (delete-process muse-ipc-server-process) + (setq muse-ipc-server-port nil) + (setq muse-ipc-server-process nil)) + +(defun muse-ipc-start (shared-secret publish-fn client-port &optional server-port) + "Start an IPC connection and send a response to CLIENT-PORT. +If SERVER-PORT is provided, start the IPC server on that port, otherwise +choose a random port. + +SHARED-SECRET is used as a very minimal security measure to +authenticate the Muse IPC server during initialization, and also +any incoming clients once the server is started. + +PUBLISH-FN is the function which should be called in buffer of +the received contents. It should transform the buffer into a +published state. It must take at least two arguments. The first +argument is the full path of the file that the contents +correspond with. The second argument is the title to use when +publishing the file." + (when (stringp client-port) + (setq client-port (string-to-number client-port))) + (when (stringp server-port) + (setq server-port (string-to-number server-port))) + (setq muse-ipc-server-process + (make-network-process + :name "muse-ipc" + :buffer nil + :host 'local :service (or server-port t) + :server t :noquery t :nowait t + :plist (list :authenticated nil :shared-secret shared-secret + :publish-fn publish-fn) + :filter 'muse-ipc-server-filter)) + (unless muse-ipc-server-process + (error "Error: Could not start Muse IPC Server process")) + (set-process-coding-system muse-ipc-server-process + 'raw-text-unix 'raw-text-unix) + (setq muse-ipc-server-port + (number-to-string + (cadr (process-contact muse-ipc-server-process)))) + (let ((client-proc + (make-network-process + :name "muse-ipc-client" + :buffer nil + :host 'local :service client-port + :noquery t + :filter 'muse-ipc-init-filter))) + (setq muse-ipc-server-registered nil) + (process-send-string client-proc + (concat "begin " shared-secret "\n")) + (accept-process-output client-proc muse-ipc-timeout nil t) + (unless muse-ipc-server-registered + (error "Error: Did not register listener")) + (process-send-string client-proc + (concat "port " muse-ipc-server-port "\n")) + (stop-process client-proc) + (delete-process client-proc)) + + ;; Accept process output until the server dies + (while muse-ipc-server-process (accept-process-output nil 1))) + +(provide 'muse-ipc) + +;;; muse-ipc.el ends here diff --git a/lisp/muse-publish.el b/lisp/muse-publish.el index 8f17ca6..9e8f6ed 100644 --- a/lisp/muse-publish.el +++ b/lisp/muse-publish.el @@ -795,9 +795,7 @@ The result is placed in a new buffer that includes TITLE in its name." (when (interactive-p) (unless title (setq title (read-string "Title: "))) (unless style (setq style (muse-publish-get-style)))) - (let ((muse-publishing-current-style style) - (muse-publishing-p t) - (text (buffer-substring beg end)) + (let ((text (buffer-substring beg end)) (buf (generate-new-buffer (concat "*Muse: " title "*")))) (with-current-buffer buf (insert text) -- 2.11.4.GIT