processes.scm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2020 John Soo <jsoo1@asu.edu>
  4. ;;;
  5. ;;; This file is part of GNU Guix.
  6. ;;;
  7. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  8. ;;; under the terms of the GNU General Public License as published by
  9. ;;; the Free Software Foundation; either version 3 of the License, or (at
  10. ;;; your option) any later version.
  11. ;;;
  12. ;;; GNU Guix is distributed in the hope that it will be useful, but
  13. ;;; 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 GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  19. (define-module (guix scripts processes)
  20. #:use-module ((guix store) #:select (%store-prefix))
  21. #:use-module (guix scripts)
  22. #:use-module (guix ui)
  23. #:use-module (srfi srfi-1)
  24. #:use-module (srfi srfi-9)
  25. #:use-module (srfi srfi-9 gnu)
  26. #:use-module (srfi srfi-37)
  27. #:use-module (ice-9 ftw)
  28. #:use-module (ice-9 match)
  29. #:use-module (ice-9 rdelim)
  30. #:use-module (ice-9 format)
  31. #:export (process?
  32. process-id
  33. process-parent-id
  34. process-command
  35. processes
  36. daemon-session?
  37. daemon-session-process
  38. daemon-session-client
  39. daemon-session-children
  40. daemon-session-locks-held
  41. daemon-sessions
  42. guix-processes))
  43. ;; Process as can be found in /proc on GNU/Linux.
  44. (define-record-type <process>
  45. (process id parent command)
  46. process?
  47. (id process-id) ;integer
  48. (parent process-parent-id) ;integer | #f
  49. (command process-command)) ;list of strings
  50. (define (write-process process port)
  51. (format port "#<process ~a>" (process-id process)))
  52. (set-record-type-printer! <process> write-process)
  53. (define (read-status-ppid port)
  54. "Read the PPID from PORT, an input port on a /proc/PID/status file. Return
  55. #f for PID 1 and kernel pseudo-processes."
  56. (let loop ()
  57. (match (read-line port)
  58. ((? eof-object?) #f)
  59. (line
  60. (if (string-prefix? "PPid:" line)
  61. (string->number (string-trim-both (string-drop line 5)))
  62. (loop))))))
  63. (define %not-nul
  64. (char-set-complement (char-set #\nul)))
  65. (define (read-command-line port)
  66. "Read the zero-split command line from PORT, a /proc/PID/cmdline file, and
  67. return it as a list."
  68. (string-tokenize (read-string port) %not-nul))
  69. (define (processes)
  70. "Return a list of process records representing the currently alive
  71. processes."
  72. ;; This assumes a Linux-compatible /proc file system. There exists one for
  73. ;; GNU/Hurd.
  74. (filter-map (lambda (pid)
  75. ;; There's a TOCTTOU race here. If we get ENOENT, simply
  76. ;; ignore PID.
  77. (catch 'system-error
  78. (lambda ()
  79. (define ppid
  80. (call-with-input-file (string-append "/proc/" pid "/status")
  81. read-status-ppid))
  82. (define command
  83. (call-with-input-file (string-append "/proc/" pid "/cmdline")
  84. read-command-line))
  85. (process (string->number pid) ppid command))
  86. (lambda args
  87. (if (= ENOENT (system-error-errno args))
  88. #f
  89. (apply throw args)))))
  90. (scandir "/proc" string->number)))
  91. (define (process-open-files process)
  92. "Return the list of files currently open by PROCESS."
  93. (let ((directory (string-append "/proc/"
  94. (number->string (process-id process))
  95. "/fd")))
  96. (filter-map (lambda (fd)
  97. ;; There's a TOCTTOU race here, hence the 'catch'.
  98. (catch 'system-error
  99. (lambda ()
  100. (readlink (string-append directory "/" fd)))
  101. (lambda args
  102. (if (= ENOENT (system-error-errno args))
  103. #f
  104. (apply throw args)))))
  105. (or (scandir directory string->number) '()))))
  106. ;; Daemon session.
  107. (define-record-type <daemon-session>
  108. (daemon-session process client children locks)
  109. daemon-session?
  110. (process daemon-session-process) ;<process>
  111. (client daemon-session-client) ;<process>
  112. (children daemon-session-children) ;list of <process>
  113. (locks daemon-session-locks-held)) ;list of strings
  114. (define (daemon-sessions)
  115. "Return two values: the list of <daemon-session> denoting the currently
  116. active sessions, and the master 'guix-daemon' process."
  117. (define (lock-file? file)
  118. (and (string-prefix? (%store-prefix) file)
  119. (string-suffix? ".lock" file)))
  120. (let* ((processes (processes))
  121. (daemons (filter (lambda (process)
  122. (match (process-command process)
  123. ((argv0 _ ...)
  124. (string=? (basename argv0) "guix-daemon"))
  125. (_ #f)))
  126. processes))
  127. (children (filter (lambda (process)
  128. (match (process-command process)
  129. ((argv0 (= string->number argv1) _ ...)
  130. (integer? argv1))
  131. (_ #f)))
  132. daemons))
  133. (master (remove (lambda (process)
  134. (memq process children))
  135. daemons)))
  136. (define (lookup-process pid)
  137. (find (lambda (process)
  138. (and (process-id process)
  139. (= pid (process-id process))))
  140. processes))
  141. (define (lookup-children pid)
  142. (filter (lambda (process)
  143. (and (process-parent-id process)
  144. (= pid (process-parent-id process))))
  145. processes))
  146. (define (child-process->session process)
  147. (match (process-command process)
  148. ((argv0 (= string->number client) _ ...)
  149. (let ((files (process-open-files process))
  150. (client (lookup-process client)))
  151. ;; After a client has died, there's a window during which its
  152. ;; corresponding 'guix-daemon' process is still alive, in which
  153. ;; case 'lookup-process' returns #f. In that case ignore the
  154. ;; session.
  155. (and client
  156. (daemon-session process client
  157. (lookup-children
  158. (process-id process))
  159. (filter lock-file? files)))))))
  160. (values (filter-map child-process->session children)
  161. master)))
  162. (define (lock->recutils lock port)
  163. (format port "LockHeld: ~a~%" lock))
  164. (define (daemon-session->recutils session port)
  165. "Display SESSION information in recutils format on PORT."
  166. (format port "SessionPID: ~a~%"
  167. (process-id (daemon-session-process session)))
  168. (format port "ClientPID: ~a~%"
  169. (process-id (daemon-session-client session)))
  170. (format port "ClientCommand:~{ ~a~}~%"
  171. (process-command (daemon-session-client session)))
  172. (for-each (lambda (lock) (lock->recutils lock port))
  173. (daemon-session-locks-held session))
  174. (for-each (lambda (process)
  175. (format port "ChildPID: ~a~%"
  176. (process-id process))
  177. (format port "ChildCommand: :~{ ~a~}~%"
  178. (process-command process)))
  179. (daemon-session-children session)))
  180. (define (daemon-sessions->recutils port sessions)
  181. "Display denormalized SESSIONS information to PORT."
  182. (for-each (lambda (session)
  183. (daemon-session->recutils session port)
  184. (newline port))
  185. sessions))
  186. (define session-rec-type
  187. "%rec: Session
  188. %type: PID int
  189. %type: ClientPID int
  190. %key: PID
  191. %mandatory: ClientPID ClientCommand")
  192. (define lock-rec-type
  193. "%rec: Lock
  194. %mandatory: LockHeld
  195. %type: Session rec Session")
  196. (define child-process-rec-type
  197. "%rec: ChildProcess
  198. %type: PID int
  199. %type: Session rec Session
  200. %key: PID
  201. %mandatory: Command")
  202. (define (session-key->recutils session port)
  203. "Display SESSION PID as a recutils field on PORT."
  204. (format
  205. port "Session: ~a"
  206. (process-id (daemon-session-process session))))
  207. (define (session-scalars->normalized-record session port)
  208. "Display SESSION scalar fields to PORT in normalized form."
  209. (format port "PID: ~a~%"
  210. (process-id (daemon-session-process session)))
  211. (format port "ClientPID: ~a~%"
  212. (process-id (daemon-session-client session)))
  213. (format port "ClientCommand:~{ ~a~}~%"
  214. (process-command (daemon-session-client session))))
  215. (define (child-process->normalized-record process port)
  216. "Display PROCESS record on PORT in normalized form"
  217. (format port "PID: ~a" (process-id process))
  218. (newline port)
  219. (format port "Command:~{ ~a~}" (process-command process)))
  220. (define (daemon-sessions->normalized-record port sessions)
  221. "Display SESSIONS recutils on PORT in normalized form"
  222. (display session-rec-type port)
  223. (newline port)
  224. (newline port)
  225. (for-each (lambda (session)
  226. (session-scalars->normalized-record session port)
  227. (newline port))
  228. sessions)
  229. (display lock-rec-type port)
  230. (newline port)
  231. (newline port)
  232. (for-each (lambda (session)
  233. (for-each (lambda (lock)
  234. (lock->recutils "testing testing" port)
  235. (session-key->recutils session port)
  236. (newline port)
  237. (newline port))
  238. (daemon-session-locks-held session)))
  239. sessions)
  240. (display child-process-rec-type port)
  241. (newline port)
  242. (newline port)
  243. (for-each (lambda (session)
  244. (for-each (lambda (process)
  245. (child-process->normalized-record process port)
  246. (newline port)
  247. (session-key->recutils session port)
  248. (newline port)
  249. (newline port))
  250. (daemon-session-children session)))
  251. sessions))
  252. ;;;
  253. ;;; Options.
  254. ;;;
  255. (define %available-formats
  256. '("recutils" "normalized"))
  257. (define (list-formats)
  258. (display (G_ "The available formats are:\n"))
  259. (newline)
  260. (for-each (lambda (f)
  261. (format #t " - ~a~%" f))
  262. %available-formats))
  263. (define (show-help)
  264. (display (G_ "Usage: guix processes
  265. List the current Guix sessions and their processes."))
  266. (newline)
  267. (display (G_ "
  268. -h, --help display this help and exit"))
  269. (display (G_ "
  270. -V, --version display version information and exit"))
  271. (newline)
  272. (display (G_ "
  273. -f, --format=FORMAT display results as normalized record sets"))
  274. (display (G_ "
  275. --list-formats display available formats"))
  276. (newline)
  277. (show-bug-report-information))
  278. (define %options
  279. (list (option '(#\h "help") #f #f
  280. (lambda args
  281. (show-help)
  282. (exit 0)))
  283. (option '(#\V "version") #f #f
  284. (lambda args
  285. (show-version-and-exit "guix processes")))
  286. (option '(#\f "format") #t #f
  287. (lambda (opt name arg result)
  288. (unless (member arg %available-formats)
  289. (leave (G_ "~a: unsupported output format~%") arg))
  290. (alist-cons 'format (string->symbol arg) result)))
  291. (option '("list-formats") #f #f
  292. (lambda (opt name arg result)
  293. (list-formats)
  294. (exit 0)))))
  295. (define %default-options '((format . recutils)))
  296. ;;;
  297. ;;; Entry point.
  298. ;;;
  299. (define-command (guix-processes . args)
  300. (category plumbing)
  301. (synopsis "list currently running sessions")
  302. (define options
  303. (parse-command-line args %options (list %default-options)
  304. #:build-options? #f))
  305. (with-paginated-output-port port
  306. (match (assoc-ref options 'format)
  307. ('normalized
  308. (daemon-sessions->normalized-record port (daemon-sessions)))
  309. (_ (daemon-sessions->recutils port (daemon-sessions))))
  310. ;; Pass 'R' (instead of 'r') so 'less' correctly estimates line length.
  311. #:less-options "FRX"))