gnunet-download-manager.scm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. #!/bin/sh
  2. exec guile -e main -s "$0" "$@"
  3. !#
  4. ;;; gnunet-download-manager -- Manage GNUnet downloads.
  5. ;;; Copyright (C) 2004 Ludovic Courtès
  6. ;;;
  7. ;;; This program is free software; you can redistribute it and/or
  8. ;;; modify it under the terms of the GNU General Public License
  9. ;;; as published by the Free Software Foundation; either version 2
  10. ;;; of the License, or (at your option) any later version.
  11. ;;;
  12. ;;; This program is distributed in the hope that it will be useful,
  13. ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;;; GNU General Public License for more details.
  16. ;;;
  17. ;;; You should have received a copy of the GNU General Public License
  18. ;;; along with this program; if not, write to the Free Software
  19. ;;; Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  20. ;;; Remember ongoing GNUnet downloads so as to be able to resume them
  21. ;;; later. Typical usage is to define the following alias in your
  22. ;;; favorite shell:
  23. ;;;
  24. ;;; alias gnunet-download='gnunet-download-manager.scm download'
  25. ;;;
  26. ;;; You may have a ~/.gnunet-download-manager.scm Scheme configuration
  27. ;;; file. In particular, if you would like to be notified of
  28. ;;; completed downloads, you may want to add the following line to
  29. ;;; your configuration file:
  30. ;;;
  31. ;;; (add-hook! *completed-download-hook*
  32. ;;; completed-download-notification-hook)
  33. ;;;
  34. ;;; This script works fine with GNU Guile 1.6.4, and doesn't run with
  35. ;;; Guile 1.4.x.
  36. ;;;
  37. ;;; Enjoy!
  38. ;;; Ludovic Courtès <ludo@chbouib.org>
  39. (use-modules (ice-9 format)
  40. (ice-9 optargs)
  41. (ice-9 regex)
  42. (ice-9 and-let-star)
  43. (ice-9 pretty-print)
  44. (ice-9 documentation))
  45. ;; Overall user settings
  46. (define *debug?* #f)
  47. (define *rc-file* (string-append (getenv "HOME")
  48. "/.gnunet-download-manager.scm"))
  49. (define *status-directory* (string-append (getenv "HOME") "/"
  50. ".gnunet-download-manager"))
  51. (define *gnunet-download* "gnunet-download")
  52. ;; Helper macros
  53. (define-macro (gnunet-info fmt . args)
  54. `(format #t (string-append *program-name* ": " ,fmt "~%")
  55. ,@args))
  56. (define-macro (gnunet-debug fmt . args)
  57. (if *debug?*
  58. (cons 'gnunet-info (cons fmt args))
  59. #t))
  60. (define-macro (gnunet-error fmt . args)
  61. `(and ,(cons 'gnunet-info (cons fmt args))
  62. (exit 1)))
  63. (define (exception-string key args)
  64. "Describe an error, using the format from @var{args}, if available."
  65. (if (< (length args) 4)
  66. (format #f "Scheme exception: ~S" key)
  67. (string-append
  68. (if (string? (car args))
  69. (string-append "In " (car args))
  70. "Scheme exception")
  71. ": "
  72. (apply format `(#f ,(cadr args) ,@(caddr args))))))
  73. ;; Regexps matching GNUnet URIs
  74. (define *uri-base*
  75. "([[:alnum:]]+)\.([[:alnum:]]+)\.([[:alnum:]]+)\.([0-9]+)")
  76. (define *uri-re*
  77. (make-regexp (string-append "^gnunet://afs/" *uri-base* "$")
  78. regexp/extended))
  79. (define *uri-status-file-re*
  80. (make-regexp (string-append "^" *uri-base* "$")
  81. regexp/extended))
  82. (define (uri-status-file-name directory uri)
  83. "Return the name of the status file for URI @var{uri}."
  84. (let ((match (regexp-exec *uri-re* uri)))
  85. (if (not match)
  86. (and (gnunet-info "~a: Invalid URI" uri) #f)
  87. (let ((start (match:start match 1))
  88. (end (match:end match 4)))
  89. (string-append directory "/"
  90. (substring uri start end))))))
  91. (define (uri-status directory uri)
  92. "Load the current status alist for URI @var{uri} from @var{directory}."
  93. (gnunet-debug "uri-status")
  94. (let ((filename (uri-status-file-name directory uri)))
  95. (catch 'system-error
  96. (lambda ()
  97. (let* ((file (open-input-file filename))
  98. (status (read file)))
  99. (begin
  100. (close-port file)
  101. status)))
  102. (lambda (key . args)
  103. (and (gnunet-debug (exception-string key args))
  104. '())))))
  105. (define (process-exists? pid)
  106. (false-if-exception (begin (kill pid 0) #t)))
  107. (define (fork-and-exec directory program . args)
  108. "Launch @var{program} and return its PID."
  109. (gnunet-debug "fork-and-exec: ~a ~a" program args)
  110. (let ((pid (primitive-fork)))
  111. (if (= 0 pid)
  112. (begin
  113. (if directory (chdir directory))
  114. (apply execlp (cons program (cons program args))))
  115. pid)))
  116. (define* (start-downloader downloader uri options
  117. #:key (directory #f))
  118. "Start the GNUnet downloader for URI @var{uri} with options
  119. @var{options}. Return an alist describing the download status."
  120. (catch 'system-error
  121. (lambda ()
  122. (let* ((pid (apply fork-and-exec
  123. `(,(if directory directory (getcwd))
  124. ,downloader
  125. ,@options))))
  126. (gnunet-info "Launched process ~a" pid)
  127. `((uri . ,uri)
  128. (working-directory . ,(if directory directory (getcwd)))
  129. (options . ,options)
  130. (pid . ,(getpid))
  131. (downloader-pid . ,pid))))
  132. (lambda (key . args)
  133. (gnunet-error (exception-string key args)))))
  134. (define (download-process-alive? uri-status)
  135. "Return true if the download whose status is that described by
  136. @var{uri-status} is still alive."
  137. (let ((pid (assoc-ref uri-status 'pid))
  138. (downloader-pid (assoc-ref uri-status 'downloader-pid)))
  139. (and (process-exists? pid)
  140. (process-exists? downloader-pid))))
  141. (define (start-file-download downloader status-dir uri options)
  142. "Dowload the file located at @var{uri}, with options @var{options}
  143. and return an updated status alist."
  144. (gnunet-debug "start-file-download")
  145. (let ((uri-status (uri-status status-dir uri)))
  146. (if (null? uri-status)
  147. (acons 'start-date (current-time)
  148. (start-downloader downloader uri options))
  149. (if (download-process-alive? uri-status)
  150. (and (gnunet-info "~a already being downloaded by process ~a"
  151. uri (assoc-ref uri-status 'pid))
  152. #f)
  153. (and (gnunet-info "Resuming download")
  154. (let ((start-date (assoc-ref uri-status 'start-date))
  155. (dir (assoc-ref uri-status 'working-directory))
  156. (options (assoc-ref uri-status 'options)))
  157. (acons 'start-date start-date
  158. (start-downloader downloader uri options
  159. #:directory dir))))))))
  160. (define *completed-download-hook* (make-hook 1))
  161. (define (download-file downloader status-dir uri options)
  162. "Start downloading file located at URI @var{uri}, with options
  163. @var{options}, resuming it if it's already started."
  164. (catch 'system-error
  165. (lambda ()
  166. (and-let* ((status (start-file-download downloader
  167. status-dir
  168. uri options))
  169. (pid (assoc-ref status 'downloader-pid))
  170. (filename (uri-status-file-name status-dir
  171. uri))
  172. (file (open-file filename "w")))
  173. ;; Write down the status
  174. (pretty-print status file)
  175. (close-port file)
  176. ;; Wait for `gnunet-download'
  177. (gnunet-info "Waiting for process ~a" pid)
  178. (let* ((process-status (waitpid pid))
  179. (exit-val (status:exit-val (cdr process-status)))
  180. (term-sig (status:term-sig (cdr process-status))))
  181. ;; Terminate
  182. (delete-file filename)
  183. (gnunet-info
  184. "Download completed (PID ~a, exit code ~a)"
  185. pid exit-val)
  186. (let ((ret `((end-date . ,(current-time))
  187. (exit-code . ,exit-val)
  188. (terminating-signal . ,term-sig)
  189. ,@status)))
  190. (run-hook *completed-download-hook* ret)
  191. ret))))
  192. (lambda (key . args)
  193. (gnunet-error (exception-string key args)))))
  194. (define (uri-status-files directory)
  195. "Return the list of URI status files in @var{directory}."
  196. (catch 'system-error
  197. (lambda ()
  198. (let ((dir (opendir directory)))
  199. (let loop ((filename (readdir dir))
  200. (file-list '()))
  201. (if (eof-object? filename)
  202. file-list
  203. (if (regexp-exec *uri-status-file-re* filename)
  204. (loop (readdir dir)
  205. (cons filename file-list))
  206. (loop (readdir dir) file-list))))))
  207. (lambda (key . args)
  208. (gnunet-error (exception-string key args)))))
  209. (define (output-file-option option-list)
  210. "Return the output file specified in @var{option-list}, false if
  211. anavailable."
  212. (if (null? option-list)
  213. #f
  214. (let ((rest (cdr option-list))
  215. (opt (car option-list)))
  216. (if (null? rest)
  217. #f
  218. (if (or (string=? opt "-o")
  219. (string=? opt "--output"))
  220. (car rest)
  221. (output-file-option rest))))))
  222. (define (download-command . args)
  223. "Start downloading a file using the given `gnunet-download'
  224. arguments."
  225. (gnunet-debug "download-command")
  226. (let* ((argc (length args))
  227. ;; FIXME: We're assuming the URI is the last argument
  228. (uri (car (list-tail args (- argc 1))))
  229. (options args))
  230. (download-file *gnunet-download* *status-directory* uri options)))
  231. (define (status-command . args)
  232. "Print status info about files being downloaded."
  233. (for-each (lambda (status)
  234. (format #t "~a: ~a~% ~a~% ~a~% ~a~%"
  235. (assoc-ref status 'uri)
  236. (if (download-process-alive? status)
  237. (string-append "running (PID "
  238. (number->string (assoc-ref status
  239. 'pid))
  240. ")")
  241. "not running")
  242. (string-append "Started on "
  243. (strftime "%c"
  244. (localtime (assoc-ref
  245. status
  246. 'start-date))))
  247. (string-append "Directory: "
  248. (assoc-ref status
  249. 'working-directory))
  250. (string-append "Output file: "
  251. (or (output-file-option (assoc-ref
  252. status
  253. 'options))
  254. "<unknown>"))))
  255. (map (lambda (file)
  256. (uri-status *status-directory*
  257. (string-append "gnunet://afs/" file)))
  258. (uri-status-files *status-directory*))))
  259. (define (resume-command . args)
  260. "Resume stopped downloads."
  261. (for-each (lambda (status)
  262. (if (not (download-process-alive? status))
  263. (if (= 0 (primitive-fork))
  264. (let* ((ret (download-file *gnunet-download*
  265. *status-directory*
  266. (assoc-ref status 'uri)
  267. (assoc-ref status 'options)))
  268. (code (assoc-ref ret 'exit-code)))
  269. (exit code)))))
  270. (map (lambda (file)
  271. (uri-status *status-directory*
  272. (string-append "gnunet://afs/" file)))
  273. (uri-status-files *status-directory*))))
  274. (define (killall-command . args)
  275. "Stop all running downloads."
  276. (for-each (lambda (status)
  277. (if (download-process-alive? status)
  278. (let ((pid (assoc-ref status 'pid))
  279. (dl-pid (assoc-ref status 'downloader-pid)))
  280. (and (gnunet-info "Stopping processes ~a and ~a"
  281. pid dl-pid)
  282. (kill pid 15)
  283. (kill dl-pid 15)))))
  284. (map (lambda (file)
  285. (uri-status *status-directory*
  286. (string-append "gnunet://afs/" file)))
  287. (uri-status-files *status-directory*))))
  288. (define (help-command . args)
  289. "Show this help message."
  290. (format #t "Usage: ~a <command> [options]~%" *program-name*)
  291. (format #t "Where <command> may be one of the following:~%~%")
  292. (for-each (lambda (command)
  293. (if (not (eq? (cdr command) help-command))
  294. (format #t (string-append " " (car command) ": "
  295. (object-documentation
  296. (cdr command))
  297. "~%"))))
  298. *commands*)
  299. (format #t "~%"))
  300. (define (settings-command . args)
  301. "Dump the current settings."
  302. (format #t "Current settings:~%~%")
  303. (module-for-each (lambda (symbol variable)
  304. (if (string-match "^\\*.*\\*$" (symbol->string symbol))
  305. (format #t " ~a: ~a~%"
  306. symbol (variable-ref variable))))
  307. (current-module))
  308. (format #t "~%"))
  309. (define (version-command . args)
  310. "Show version information."
  311. (format #t "~a ~a.~a (~a)~%"
  312. *program-name* *version-major* *version-minor* *version-date*))
  313. ;; This hook may be added to *completed-download-hook*.
  314. (define (completed-download-notification-hook status)
  315. "Notifies of the completion of a file download."
  316. (let ((msg (string-append "GNUnet download of "
  317. (output-file-option
  318. (assoc-ref status 'options))
  319. " in "
  320. (assoc-ref status
  321. 'working-directory)
  322. " complete!")))
  323. (if (getenv "DISPLAY")
  324. (waitpid (fork-and-exec #f "xmessage" msg))
  325. (waitpid (fork-and-exec #f "write"
  326. (cuserid) msg)))))
  327. ;; Available user commands
  328. (define *commands*
  329. `(("download" . ,download-command)
  330. ("status" . ,status-command)
  331. ("resume" . ,resume-command)
  332. ("killall" . ,killall-command)
  333. ("settings" . ,settings-command)
  334. ("version" . ,version-command)
  335. ("help" . ,help-command)
  336. ("--help" . ,help-command)
  337. ("-h" . ,help-command)))
  338. (define *program-name* "gnunet-download-manager")
  339. (define *version-major* 0)
  340. (define *version-minor* 1)
  341. (define *version-date* "april 2004")
  342. (define (main args)
  343. (set! *program-name* (basename (car args)))
  344. ;; Load the user's configuration file
  345. (if (file-exists? *rc-file*)
  346. (load *rc-file*))
  347. ;; Check whether the status directory already exists
  348. (if (not (file-exists? *status-directory*))
  349. (begin
  350. (gnunet-info "Creating status directory ~a..." *status-directory*)
  351. (catch 'system-error
  352. (lambda ()
  353. (mkdir *status-directory*))
  354. (lambda (key . args)
  355. (and (gnunet-error (exception-string key args))
  356. (exit 1))))))
  357. ;; Go ahead
  358. (if (< (length args) 2)
  359. (and (format #t "Usage: ~a <command> [options]~%"
  360. *program-name*)
  361. (exit 1))
  362. (let* ((command-name (cadr args))
  363. (command (assoc-ref *commands* command-name)))
  364. (if command
  365. (apply command (cddr args))
  366. (and (gnunet-info "~a command not found" command-name)
  367. (exit 1))))))