#!/bin/sh exec guile -e main -s "$0" "$@" !# ;;; gnunet-download-manager -- Manage GNUnet downloads. ;;; Copyright (C) 2004 Ludovic Courtès ;;; ;;; 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 2 ;;; 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, write to the Free Software ;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ;;; Remember ongoing GNUnet downloads so as to be able to resume them ;;; later. Typical usage is to define the following alias in your ;;; favorite shell: ;;; ;;; alias gnunet-download='gnunet-download-manager.scm download' ;;; ;;; You may have a ~/.gnunet-download-manager.scm Scheme configuration ;;; file. In particular, if you would like to be notified of ;;; completed downloads, you may want to add the following line to ;;; your configuration file: ;;; ;;; (add-hook! *completed-download-hook* ;;; completed-download-notification-hook) ;;; ;;; This script works fine with GNU Guile 1.6.4, and doesn't run with ;;; Guile 1.4.x. ;;; ;;; Enjoy! ;;; Ludovic Courtès (use-modules (ice-9 format) (ice-9 optargs) (ice-9 regex) (ice-9 and-let-star) (ice-9 pretty-print) (ice-9 documentation)) ;; Overall user settings (define *debug?* #f) (define *rc-file* (string-append (getenv "HOME") "/.gnunet-download-manager.scm")) (define *status-directory* (string-append (getenv "HOME") "/" ".gnunet-download-manager")) (define *gnunet-download* "gnunet-download") ;; Helper macros (define-macro (gnunet-info fmt . args) `(format #t (string-append *program-name* ": " ,fmt "~%") ,@args)) (define-macro (gnunet-debug fmt . args) (if *debug?* (cons 'gnunet-info (cons fmt args)) #t)) (define-macro (gnunet-error fmt . args) `(and ,(cons 'gnunet-info (cons fmt args)) (exit 1))) (define (exception-string key args) "Describe an error, using the format from @var{args}, if available." (if (< (length args) 4) (format #f "Scheme exception: ~S" key) (string-append (if (string? (car args)) (string-append "In " (car args)) "Scheme exception") ": " (apply format `(#f ,(cadr args) ,@(caddr args)))))) ;; Regexps matching GNUnet URIs (define *uri-base* "([[:alnum:]]+).([[:alnum:]]+).([[:alnum:]]+).([0-9]+)") (define *uri-re* (make-regexp (string-append "^gnunet://fs/chk/" *uri-base* "$") regexp/extended)) (define *uri-status-file-re* (make-regexp (string-append "^" *uri-base* "$") regexp/extended)) (define (uri-status-file-name directory uri) "Return the name of the status file for URI @var{uri}." (let ((match (regexp-exec *uri-re* uri))) (if (not match) (and (gnunet-info "~a: Invalid URI" uri) #f) (let ((start (match:start match 1)) (end (match:end match 4))) (string-append directory "/" (substring uri start end)))))) (define (uri-status directory uri) "Load the current status alist for URI @var{uri} from @var{directory}." (gnunet-debug "uri-status") (let ((filename (uri-status-file-name directory uri))) (catch 'system-error (lambda () (let* ((file (open-input-file filename)) (status (read file))) (begin (close-port file) status))) (lambda (key . args) (and (gnunet-debug (exception-string key args)) '()))))) (define (process-exists? pid) (false-if-exception (begin (kill pid 0) #t))) (define (fork-and-exec directory program . args) "Launch @var{program} and return its PID." (gnunet-debug "fork-and-exec: ~a ~a" program args) (let ((pid (primitive-fork))) (if (= 0 pid) (begin (if directory (chdir directory)) (apply execlp (cons program (cons program args)))) pid))) (define* (start-downloader downloader uri options #:key (directory #f)) "Start the GNUnet downloader for URI @var{uri} with options @var{options}. Return an alist describing the download status." (catch 'system-error (lambda () (let* ((pid (apply fork-and-exec `(,(if directory directory (getcwd)) ,downloader ,@options)))) (gnunet-info "Launched process ~a" pid) `((uri . ,uri) (working-directory . ,(if directory directory (getcwd))) (options . ,options) (pid . ,(getpid)) (downloader-pid . ,pid)))) (lambda (key . args) (gnunet-error (exception-string key args))))) (define (download-process-alive? uri-status) "Return true if the download whose status is that described by @var{uri-status} is still alive." (let ((pid (assoc-ref uri-status 'pid)) (downloader-pid (assoc-ref uri-status 'downloader-pid))) (and (process-exists? pid) (process-exists? downloader-pid)))) (define (start-file-download downloader status-dir uri options) "Dowload the file located at @var{uri}, with options @var{options} and return an updated status alist." (gnunet-debug "start-file-download") (let ((uri-status (uri-status status-dir uri))) (if (null? uri-status) (acons 'start-date (current-time) (start-downloader downloader uri options)) (if (download-process-alive? uri-status) (and (gnunet-info "~a already being downloaded by process ~a" uri (assoc-ref uri-status 'pid)) #f) (and (gnunet-info "Resuming download") (let ((start-date (assoc-ref uri-status 'start-date)) (dir (assoc-ref uri-status 'working-directory)) (options (assoc-ref uri-status 'options))) (acons 'start-date start-date (start-downloader downloader uri options #:directory dir)))))))) (define *completed-download-hook* (make-hook 1)) (define (download-file downloader status-dir uri options) "Start downloading file located at URI @var{uri}, with options @var{options}, resuming it if it's already started." (catch 'system-error (lambda () (and-let* ((status (start-file-download downloader status-dir uri options)) (pid (assoc-ref status 'downloader-pid)) (filename (uri-status-file-name status-dir uri)) (file (open-file filename "w"))) ;; Write down the status (pretty-print status file) (close-port file) ;; Wait for `gnunet-download' (gnunet-info "Waiting for process ~a" pid) (let* ((process-status (waitpid pid)) (exit-val (status:exit-val (cdr process-status))) (term-sig (status:term-sig (cdr process-status)))) ;; Terminate (delete-file filename) (gnunet-info "Download completed (PID ~a, exit code ~a)" pid exit-val) (let ((ret `((end-date . ,(current-time)) (exit-code . ,exit-val) (terminating-signal . ,term-sig) ,@status))) (run-hook *completed-download-hook* ret) ret)))) (lambda (key . args) (gnunet-error (exception-string key args))))) (define (uri-status-files directory) "Return the list of URI status files in @var{directory}." (catch 'system-error (lambda () (let ((dir (opendir directory))) (let loop ((filename (readdir dir)) (file-list '())) (if (eof-object? filename) file-list (if (regexp-exec *uri-status-file-re* filename) (loop (readdir dir) (cons filename file-list)) (loop (readdir dir) file-list)))))) (lambda (key . args) (gnunet-error (exception-string key args))))) (define (output-file-option option-list) "Return the output file specified in @var{option-list}, false if anavailable." (if (null? option-list) #f (let ((rest (cdr option-list)) (opt (car option-list))) (if (null? rest) #f (if (or (string=? opt "-o") (string=? opt "--output")) (car rest) (output-file-option rest)))))) (define (download-command . args) "Start downloading a file using the given `gnunet-download' arguments." (gnunet-debug "download-command") (let* ((argc (length args)) ;; FIXME: We're assuming the URI is the last argument (uri (car (list-tail args (- argc 1)))) (options args)) (download-file *gnunet-download* *status-directory* uri options))) (define (status-command . args) "Print status info about files being downloaded." (for-each (lambda (status) (format #t "~a: ~a~% ~a~% ~a~% ~a~%" (assoc-ref status 'uri) (if (download-process-alive? status) (string-append "running (PID " (number->string (assoc-ref status 'pid)) ")") "not running") (string-append "Started on " (strftime "%c" (localtime (assoc-ref status 'start-date)))) (string-append "Directory: " (assoc-ref status 'working-directory)) (string-append "Output file: " (or (output-file-option (assoc-ref status 'options)) "")))) (map (lambda (file) (uri-status *status-directory* (string-append "gnunet://fs/chk/" file))) (uri-status-files *status-directory*)))) (define (resume-command . args) "Resume stopped downloads." (for-each (lambda (status) (if (not (download-process-alive? status)) (if (= 0 (primitive-fork)) (let* ((ret (download-file *gnunet-download* *status-directory* (assoc-ref status 'uri) (assoc-ref status 'options))) (code (assoc-ref ret 'exit-code))) (exit code))))) (map (lambda (file) (uri-status *status-directory* (string-append "gnunet://fs/chk/" file))) (uri-status-files *status-directory*)))) (define (killall-command . args) "Stop all running downloads." (for-each (lambda (status) (if (download-process-alive? status) (let ((pid (assoc-ref status 'pid)) (dl-pid (assoc-ref status 'downloader-pid))) (and (gnunet-info "Stopping processes ~a and ~a" pid dl-pid) (kill pid 15) (kill dl-pid 15))))) (map (lambda (file) (uri-status *status-directory* (string-append "gnunet://fs/chk/" file))) (uri-status-files *status-directory*)))) (define (help-command . args) "Show this help message." (format #t "Usage: ~a [options]~%" *program-name*) (format #t "Where may be one of the following:~%~%") (for-each (lambda (command) (if (not (eq? (cdr command) help-command)) (format #t (string-append " " (car command) ": " (object-documentation (cdr command)) "~%")))) *commands*) (format #t "~%")) (define (settings-command . args) "Dump the current settings." (format #t "Current settings:~%~%") (module-for-each (lambda (symbol variable) (if (string-match "^\\*.*\\*$" (symbol->string symbol)) (format #t " ~a: ~a~%" symbol (variable-ref variable)))) (current-module)) (format #t "~%")) (define (version-command . args) "Show version information." (format #t "~a ~a.~a (~a)~%" *program-name* *version-major* *version-minor* *version-date*)) ;; This hook may be added to *completed-download-hook*. (define (completed-download-notification-hook status) "Notifies of the completion of a file download." (let ((msg (string-append "GNUnet download of " (output-file-option (assoc-ref status 'options)) " in " (assoc-ref status 'working-directory) " complete!"))) (if (getenv "DISPLAY") (waitpid (fork-and-exec #f "xmessage" msg)) (waitpid (fork-and-exec #f "write" (cuserid) msg))))) ;; Available user commands (define *commands* `(("download" . ,download-command) ("status" . ,status-command) ("resume" . ,resume-command) ("killall" . ,killall-command) ("settings" . ,settings-command) ("version" . ,version-command) ("help" . ,help-command) ("--help" . ,help-command) ("-h" . ,help-command))) (define *program-name* "gnunet-download-manager") (define *version-major* 0) (define *version-minor* 1) (define *version-date* "april 2004") (define (main args) (set! *program-name* (basename (car args))) ;; Load the user's configuration file (if (file-exists? *rc-file*) (load *rc-file*)) ;; Check whether the status directory already exists (if (not (file-exists? *status-directory*)) (begin (gnunet-info "Creating status directory ~a..." *status-directory*) (catch 'system-error (lambda () (mkdir *status-directory*)) (lambda (key . args) (and (gnunet-error (exception-string key args)) (exit 1)))))) ;; Go ahead (if (< (length args) 2) (and (format #t "Usage: ~a [options]~%" *program-name*) (exit 1)) (let* ((command-name (cadr args)) (command (assoc-ref *commands* command-name))) (if command (apply command (cddr args)) (and (gnunet-info "~a command not found" command-name) (exit 1))))))