;;; systemctl.el --- Emacs interface to Systemd -*- lexical-binding: t; -*- ;; Copyright (C) 2016 Free Software Foundation, Inc. ;; Author: Mario Lang ;; Keywords: ;; 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 3 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, see . ;;; Commentary: ;; This library provides a front end to Systemd. ;; ;; Use `M-x systemctl-list-units RET' to see a list of all known ;; Systemd units and their status on localhost. With a prefix ;; argument (`C-u M-x systemctl-list-units RET') you will be prompted ;; for a remote host to connect to. ;; ;; In systemctl-list-units-mode, `RET' will visit all relevant ;; configuration fragments for the unit at point (the equivalent of ;; "systemctl cat some.service"). With a `C-u' prefix argument, it ;; will prompt for a new override.conf file to create (somewhat ;; equivalent to "systemctl edit some.service"). Contrary to the ;; command-line "systemctl" tool, systemctl.el allows viewing and ;; editing of remote unit files thanks to TRAMP. ;; ;; Key bindings `s t a r t' and `s t o p' can be used to start and stop ;; services. Similarily, `e n a b l e' and `d i s a b l e' can be used to ;; permanently enable and disable unit files. ;;; Granting access to non-root users: ;; Some operations are obviously not allowed when executed from within a ;; non-root Emacs session. If you want to explicitly grant access to certain ;; users, you can create a polkit localauthority configuration file. ;; Below is an example. You might want to change the group name, or use ;; "unix-user" instead. ;; ;; /etc/polkit-1/localauthority/50-local.d/10-systemd.pkla: ;; ;; [Normal Staff Permissions] ;; Identity=unix-group:sudo ;; Action=org.freedesktop.systemd1.* ;; ResultAny=no ;; ResultInactive=no ;; ResultActive=yes ;;; Todo: ;; * Have someone with window/frame-fu see if there is a better way to ;; visit N files in a frame, each in a separate window. The current approach ;; feels a bit crude, see `systemctl-edit-unit-files'. ;; * Optionally automatically reload the Systemd daemon when a unit buffer is ;; saved. ;; * Detect if we are not root, and use the sudo method to edit ;; system files on localhost. ;; * Add support for local and remote systemd user sessions. ;; * Figure out what's necessary to support local and remote containers. ;; * Menu entries for `systemctl-list-units-mode'. ;;; Code: (require 'systemd) (require 'tabulated-list) (require 'tramp) (defgroup systemctl nil "Interface to Systemd.") (defcustom systemctl-default-override-file-name "override.conf" "Default file name for new override.conf files." :group 'systemctl :type 'string) (defcustom systemctl-list-units-format (vector (list "Unit" 22 t) (list "Loaded" 9 t) (list "Active" 8 t) (list "State" 8 t) (list "Description" 50 nil)) "Column format specification for the `systemctl-list-units' command." :group 'systemctl :type '(vector (list :tag "Unit" (string :tag "Title") (number :tag "Width") (boolean :tag "Sortable")) (list :tag "Loaded" (string :tag "Title") (number :tag "Width") (boolean :tag "Sortable")) (list :tag "Active" (string :tag "Title") (number :tag "Width") (boolean :tag "Sortable")) (list :tag "State" (string :tag "Title") (number :tag "Width") (boolean :tag "Sortable")) (list :tag "Description" (string :tag "Title") (number :tag "Width") (boolean :tag "Sortable")))) (defcustom systemctl-tramp-method "scpx" "The TRAMP method to use when remotely accessing Systemd Unit files." :group 'systemctl :type (cons 'choice (mapcar (lambda (method) (list 'const (car method))) tramp-methods))) (defvar-local systemctl-bus :system "Default D-Bus bus to use when accessing Systemd. You should use the function `systemctl-bus' to retrieve the value of this variable to make sure the bus is properly initialized in case it is pointing to a remote machine.") (defvar systemctl-list-units-mode-map (let ((map (make-sparse-keymap))) (define-key map "\C-m" #'systemctl-edit-unit-files) (define-key map "f" #'systemctl-find-fragment) (define-key map "start" #'systemctl-start) (define-key map "stop" #'systemctl-stop) (define-key map "enable" #'systemctl-enable) (define-key map "disable" #'systemctl-disable) map) "Keymap for `systemctl-list-units-mode'.") (defun systemctl-bus () (when (stringp systemctl-bus) (dbus-init-bus systemctl-bus)) systemctl-bus) (defun systemctl-list-units-entries () "Retrieve a list of units known to Systemd. See `systemctl-list-units-format' and `tabulated-list-entries'." (mapcar (lambda (desc) (list (nth 6 desc) (vector (nth 0 desc) (nth 2 desc) (nth 3 desc) (nth 4 desc) (nth 1 desc)))) (systemd-ListUnits (systemctl-bus)))) (defun systemctl-unescape-unit-name (string) (while (string-match "\\\\x\\([0-9a-f]\\{2\\}\\)" string) (setq string (replace-match (string (string-to-number (match-string 1 string) 16)) t t string))) string) (defun systemctl-list-units-print-entry (id cols) "Insert a Systemd Units List entry at point. See `tabulated-list-printer'." (let ((beg (point)) (x (max tabulated-list-padding 0)) (inhibit-read-only t)) (when (> x 0) (insert (make-string x ?\s))) (dotimes (n (length tabulated-list-format)) (let ((desc (aref cols n))) (when (= n 0) (setq desc (systemctl-unescape-unit-name desc))) (setq x (tabulated-list-print-col n desc x)))) (insert ?\n) (put-text-property beg (point) 'tabulated-list-id id) (put-text-property beg (point) 'tabulated-list-entry cols))) (define-derived-mode systemctl-list-units-mode tabulated-list-mode "Systemd-Units" "Major mode for displaying a list of Systemd Units." (setq tabulated-list-entries #'systemctl-list-units-entries tabulated-list-format systemctl-list-units-format tabulated-list-printer #'systemctl-list-units-print-entry) (tabulated-list-init-header)) ;;;###autoload (defun systemctl-list-units (&optional host) "Display a list of all Systemd Units." (interactive (list (when (equal current-prefix-arg '(4)) (read-string "Remote host: ")))) (with-current-buffer (let ((buffer-name (if host (format "*Systemd Units (%s)*" host) "*Systemd Units*"))) (get-buffer-create buffer-name)) (systemctl-list-units-mode) (when host (setq systemctl-bus (systemd-remote-bus host) default-directory (systemctl-file-name "/etc/systemd/"))) (tabulated-list-print) (pop-to-buffer (current-buffer)))) (defun systemctl-list-units-get-unit () (when (eq major-mode 'systemctl-list-units-mode) (let ((entry (tabulated-list-get-entry))) (when entry (aref entry 0))))) (defun systemctl-start (unit) "Start Systemd UNIT." (interactive (list (or (systemctl-list-units-get-unit) (read-string "Unit: ")))) (systemd-StartUnit (systemctl-bus) unit "replace") (when (eq major-mode 'systemctl-list-units-mode) (tabulated-list-revert))) (defun systemctl-stop (unit) (interactive (list (or (systemctl-list-units-get-unit) (read-string "Unit: ")))) (systemd-StopUnit (systemctl-bus) unit "replace") (when (eq major-mode 'systemctl-list-units-mode) (tabulated-list-revert))) (defun systemctl-enable (unit) "Enable Systemd UNIT." (interactive (list (or (systemctl-list-units-get-unit) (read-string "Unit: ")))) (pcase (systemd-EnableUnitFiles (systemctl-bus) (list unit) nil nil) (`(,carries-install-info ,changes) (if changes (pcase-dolist (`(,type ,from ,to) changes) (message "%s %s -> %s" type from to)) (message "No changes"))))) (defun systemctl-disable (unit) "Disable Systemd UNIT." (interactive (list (or (systemctl-list-units-get-unit) (read-string "Unit: ")))) (let ((changes (systemd-DisableUnitFiles (systemctl-bus) (list unit) nil))) (if changes (pcase-dolist (`(,type ,from ,to) changes) (message "%s %s -> %s" type from to)) (message "No changes")))) (defun systemctl-reload () "Reload all unit files." (interactive) (systemd-Reload (systemctl-bus))) (defun systemctl-file-name (file-name) (if (and (stringp systemctl-bus) (string-match "unixexec:path=ssh,.*argv2=\\([^,]*\\)," systemctl-bus)) (let ((host (systemd-unescape-dbus-address (match-string 1 systemctl-bus)))) (concat "/" systemctl-tramp-method ":" host ":" file-name)) file-name)) (defun systemctl-find-fragment (unit) (interactive (list (or (and (eq major-mode 'systemctl-list-units-mode) (tabulated-list-get-id)) (systemd-GetUnit (systemctl-bus) (read-string "Unit: "))))) (let ((fragment-path (systemd-unit-FragmentPath (systemctl-bus) unit))) (when fragment-path (find-file (systemctl-file-name fragment-path))))) (defun systemctl-edit-unit-files (unit &optional override-file) "Visit all configuration files related to UNIT simultaneously. If optional OVERRIDE-FILE is specified, or if a prefix argument has been given interactively, open a (new) override file." (interactive (let* ((unit (if (tabulated-list-get-entry) (systemctl-unescape-unit-name (aref (tabulated-list-get-entry) 0)) (read-string "Unit: "))) (unit-path (or (tabulated-list-get-id) (systemd-GetUnit (systemctl-bus) unit))) (override-file (when (equal current-prefix-arg '(4)) (read-file-name "Override file: " (systemctl-file-name (concat "/etc/systemd/system/" unit ".d/")) nil nil systemctl-default-override-file-name)))) (list unit-path override-file))) (let ((files (mapcar #'systemctl-file-name (systemd-unit-DropInPaths (systemctl-bus) unit)))) (when override-file (push override-file files)) (let ((path (systemd-unit-FragmentPath (systemctl-bus) unit))) (when (not (string= path "")) (setq files (nconc files (list (systemctl-file-name path)))))) (let ((path (systemd-unit-SourcePath (systemctl-bus) unit))) (when (not (string= path "")) (setq files (nconc files (list (systemctl-file-name path)))))) (if files (let ((buffers (mapcar #'find-file-noselect files))) (pop-to-buffer (pop buffers)) (when buffers (delete-other-windows) (dolist (buffer buffers) (let ((window (split-window (car (last (window-list)))))) (shrink-window-if-larger-than-buffer) (set-window-buffer window buffer))) (dolist (window (window-list)) (shrink-window-if-larger-than-buffer window)))) (when (called-interactively-p 'interactive) (message "No configuration files associated with `%s'." unit))))) (provide 'systemctl) ;;; systemctl.el ends here