Check in very rough alpha version of Muse IPC server and ikiwiki test.
authorMichael Olson <mwolson@gnu.org>
Wed, 5 Aug 2009 15:19:28 +0000 (5 08:19 -0700)
committerMichael Olson <mwolson@gnu.org>
Wed, 5 Aug 2009 15:19:28 +0000 (5 08:19 -0700)
It won't publish pages or do anything useful yet, other than print
diagnostics messages to stdout.

contrib/ikiwiki/IkiWiki/Plugin/test.pl [new file with mode: 0644]
lisp/muse-ikiwiki.el
lisp/muse-ipc.el [new file with mode: 0644]
lisp/muse-publish.el

diff --git a/contrib/ikiwiki/IkiWiki/Plugin/test.pl b/contrib/ikiwiki/IkiWiki/Plugin/test.pl
new file mode 100644 (file)
index 0000000..8b015a8
--- /dev/null
@@ -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 <<EOF;
+Error: Invalid response while initializing Muse IPC server.
+EOF
+                }
+                last SERVER;
+            }
+            print $client "nok\n" if $line;
+            $select->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";
+}
index 09b7cd5..20d4030 100644 (file)
@@ -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 (file)
index 0000000..417ec15
--- /dev/null
@@ -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
index 8f17ca6..9e8f6ed 100644 (file)
@@ -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)