diff --git a/docs/telega-ellit.org b/docs/telega-ellit.org index c9ef03b5..4e948984 100644 --- a/docs/telega-ellit.org +++ b/docs/telega-ellit.org @@ -293,6 +293,13 @@ Make =telega= know you want to use docker by adding this to your =init.el=: (setq telega-use-docker t) #+end_src +On Windows, =telega= rewrites Docker bind mounts and TDLib file paths +for Docker Desktop automatically. If you need to start the image +manually from PowerShell, use a Linux container path, for example: +#+begin_src powershell +docker run --rm -it -v ${HOME}/.telega:/telega zevlg/telega-server:latest /usr/bin/telega-server +#+end_src + That's it, you are ready to get starting. However, you might anyway need to have local =ffmpeg= installation to utilize some =telega= features, such as playing audio/voice messages, capturing video/voice diff --git a/docs/telega-manual.org b/docs/telega-manual.org index 78d87bb7..8c908d15 100644 --- a/docs/telega-manual.org +++ b/docs/telega-manual.org @@ -423,6 +423,13 @@ Make =telega= know you want to use docker by adding this to your =init.el=: (setq telega-use-docker t) #+end_src +On Windows, =telega= rewrites Docker bind mounts and TDLib file paths +for Docker Desktop automatically. If you need to start the image +manually from PowerShell, use a Linux container path, for example: +#+begin_src powershell + docker run --rm -it -v ${HOME}/.telega:/telega zevlg/telega-server:latest /usr/bin/telega-server +#+end_src + That's it, you are ready to get starting. However, you might anyway need to have local =ffmpeg= installation to utilize some =telega= features, such as playing audio/voice messages, capturing video/voice diff --git a/etc/Dockerfile b/etc/Dockerfile index c7346cef..40a330be 100644 --- a/etc/Dockerfile +++ b/etc/Dockerfile @@ -3,7 +3,8 @@ # docker build -f etc/Dockerfile -t zevlg/telega-server:latest . # docker tag $(docker images -q zevlg/telega-server:latest) zevlg/telega-server:latest # docker push zevlg/telega-server:latest -# docker run -v $HOME/.telega:$HOME/.telega zevlg/telega-server:latest /usr/bin/telega-server +# docker run -v $HOME/.telega:/telega zevlg/telega-server:latest /usr/bin/telega-server +# PowerShell: docker run -v ${HOME}/.telega:/telega zevlg/telega-server:latest /usr/bin/telega-server ### Release image # docker build --build-arg tdlib_branch=v1.8.0 --build-arg telega_branch=release-0.8.0 -f etc/Dockerfile -t zevlg/telega-server:1.8.0 . diff --git a/telega-core.el b/telega-core.el index 11178bce..93e46296 100644 --- a/telega-core.el +++ b/telega-core.el @@ -1247,17 +1247,150 @@ If NO-PROPERTIES is specified, then do not keep text properties." #'telega--desurrogate-apply-part-keep-properties) (telega--split-by-text-prop str 'telega-display) "")) +(defconst telega-docker--windows-container-root "/telega" + "Container root used for Windows Docker bind mounts.") + +(defsubst telega-docker--windows-p () + "Return non-nil if telega runs on Windows." + (eq system-type 'windows-nt)) + +(defun telega-docker--normalize-path (path) + "Return PATH expanded with forward slashes and no trailing slash." + (directory-file-name + (replace-regexp-in-string "\\\\" "/" (expand-file-name path)))) + +(defun telega-docker--normalize-container-path (path) + "Return container PATH normalized for comparisons." + (directory-file-name + (replace-regexp-in-string "\\\\" "/" path))) + +(defun telega-docker--path-prefix-p (path prefix) + "Return non-nil if PATH is equal to PREFIX or is inside it." + (let ((norm-path (downcase (telega-docker--normalize-path path))) + (norm-prefix (downcase (telega-docker--normalize-path prefix)))) + (or (string= norm-path norm-prefix) + (string-prefix-p (concat norm-prefix "/") norm-path)))) + +(defun telega-docker--container-path-prefix-p (path prefix) + "Return non-nil if container PATH is equal to PREFIX or is inside it." + (let ((norm-path (telega-docker--normalize-container-path path)) + (norm-prefix (telega-docker--normalize-container-path prefix))) + (or (string= norm-path norm-prefix) + (string-prefix-p (concat norm-prefix "/") norm-path)))) + +(defun telega-docker--path-mappings () + "Return Docker bind mount mappings for current telega runtime." + (when (and telega-use-docker (telega-docker--windows-p)) + (let ((mappings + (list (cons (telega-docker--normalize-path telega-directory) + telega-docker--windows-container-root)))) + (dolist (path-spec + `((,telega-database-dir . "db") + (,telega-cache-dir . "cache") + (,telega-temp-dir . "temp") + (,(when telega-server-logfile + (file-name-directory telega-server-logfile)) + . "logs"))) + (when-let ((host-path (car path-spec))) + (setq host-path (telega-docker--normalize-path host-path)) + (unless (seq-some + (lambda (mapping) + (telega-docker--path-prefix-p host-path (car mapping))) + mappings) + (push (cons host-path + (concat telega-docker--windows-container-root + "/" (cdr path-spec))) + mappings)))) + (sort mappings (lambda (lhs rhs) + (> (length (car lhs)) (length (car rhs)))))))) + +(defun telega-docker-path-to-container (path) + "Translate host PATH into its container path." + (if (not (and path telega-use-docker (telega-docker--windows-p))) + path + (let ((norm-path (telega-docker--normalize-path path))) + (or (seq-some + (lambda (mapping) + (when (telega-docker--path-prefix-p norm-path (car mapping)) + (concat (cdr mapping) + (substring norm-path (length (car mapping)))))) + (telega-docker--path-mappings)) + path)))) + +(defun telega-docker-path-to-host (path) + "Translate container PATH back to its host path." + (if (not (and path telega-use-docker (telega-docker--windows-p))) + path + (let ((norm-path (telega-docker--normalize-container-path path))) + (or (seq-some + (lambda (mapping) + (when (telega-docker--container-path-prefix-p norm-path (cdr mapping)) + (concat (car mapping) + (substring norm-path (length (cdr mapping)))))) + (telega-docker--path-mappings)) + path)))) + +(defun telega-docker-cmd-to-container (cmd) + "Translate known host paths inside shell CMD into container paths." + (if (not (and cmd telega-use-docker (telega-docker--windows-p))) + cmd + (let ((mapped-cmd cmd)) + (dolist (mapping (telega-docker--path-mappings) mapped-cmd) + (setq mapped-cmd + (replace-regexp-in-string + (regexp-quote (car mapping)) (cdr mapping) + mapped-cmd 'fixedcase 'literal)))))) + +(defun telega-docker--tl-path-transform (obj direction) + "Translate Docker file paths inside TL object OBJ. +DIRECTION is either the symbol `to-container' or `to-host'." + (if (not (and telega-use-docker (telega-docker--windows-p))) + obj + (cond ((vectorp obj) + (cl-map 'vector + (lambda (elem) + (telega-docker--tl-path-transform elem direction)) + obj)) + ((consp obj) + (let ((obj-type (plist-get obj :@type)) + (mapped-obj + (mapcar (lambda (elem) + (telega-docker--tl-path-transform elem direction)) + obj))) + (cond ((and (equal obj-type "inputFileLocal") + (eq direction 'to-container)) + (plist-put mapped-obj :path + (telega-docker-path-to-container + (plist-get mapped-obj :path)))) + ((and (equal obj-type "localFile") + (eq direction 'to-host)) + (plist-put mapped-obj :path + (telega-docker-path-to-host + (plist-get mapped-obj :path)))) + ((and (equal obj-type "setTdlibParameters") + (eq direction 'to-container)) + (plist-put mapped-obj :database_directory + (telega-docker-path-to-container + (plist-get mapped-obj :database_directory))) + (plist-put mapped-obj :files_directory + (telega-docker-path-to-container + (plist-get mapped-obj :files_directory))))) + mapped-obj)) + (t obj)))) + (defsubst telega--tl-unpack (obj) "Unpack TL object OBJ." - obj) + (telega-docker--tl-path-transform obj 'to-host)) (defsubst telega--tl-pack (obj) "Pack object OBJ." ;; Remove text props from strings, etc - (cond ((stringp obj) (substring-no-properties obj)) - ((vectorp obj) (cl-map 'vector #'telega--tl-pack obj)) - ((listp obj) (mapcar #'telega--tl-pack obj)) - (t obj))) + (telega-docker--tl-path-transform + (cond ((stringp obj) (substring-no-properties obj)) + ((vectorp obj) (cl-map 'vector #'telega--tl-pack obj)) + ((listp obj) (mapcar #'telega--tl-pack obj)) + (t obj)) + 'to-container)) (defun telega-tl-str (obj &optional prop no-properties) "Get property PROP from OBJ, desurrogating resulting string. diff --git a/telega-server.el b/telega-server.el index 2ba81712..e600d2d1 100644 --- a/telega-server.el +++ b/telega-server.el @@ -175,16 +175,28 @@ Set `telega-server-libs-prefix' to the TDLib installion path" "Create command to start `telega-server' progress. FLAGS - additional. Raise error if not found." - (mapconcat #'identity - (cons - (if telega-use-docker - (telega-docker-run-cmd telega-server-command) - (let ((exec-path (cons telega-directory exec-path))) - (or (executable-find telega-server-command) - (error "`%s' not found in exec-path" - telega-server-command)))) - flags) - " ")) + (let ((flags (delq nil flags))) + (if (and telega-use-docker + (telega-docker--windows-p)) + (mapconcat + #'identity + (cons (telega-docker-run-cmd telega-server-command) + (mapcar (lambda (flag) + (shell-quote-argument + (telega-docker-cmd-to-container flag))) + flags)) + " ") + (mapconcat + #'identity + (cons + (if telega-use-docker + (telega-docker-run-cmd telega-server-command) + (let ((exec-path (cons telega-directory exec-path))) + (or (executable-find telega-server-command) + (error "`%s' not found in exec-path" + telega-server-command)))) + flags) + " ")))) (defun telega-server-version () "Return telega-server version." diff --git a/telega-tdlib-events.el b/telega-tdlib-events.el index 8fad0ecb..3bac6e0c 100644 --- a/telega-tdlib-events.el +++ b/telega-tdlib-events.el @@ -1311,9 +1311,10 @@ Please downgrade TDLib and recompile `telega-server'" (authorizationStateWaitTdlibParameters ;; Tune permissions for docker's /dev/snd, /dev/video* (when-let ((devices-chown-cmd - (telega-docker-exec-cmd - "chmod -R o+rw /dev/snd /dev/video0" nil - "-u 0" 'no-error))) + (unless (telega-docker--windows-p) + (telega-docker-exec-cmd + "chmod -R o+rw /dev/snd /dev/video0" nil + "-u 0" 'no-error)))) (telega-debug "docker RUN: %s" devices-chown-cmd) (shell-command-to-string devices-chown-cmd)) diff --git a/telega-util.el b/telega-util.el index 3308fd01..77789e13 100644 --- a/telega-util.el +++ b/telega-util.el @@ -2904,11 +2904,13 @@ Binds current symbol to SYM-BIND." (defvar telega-docker--user-id nil) (defun telega-docker--user-id () "Return UID:GID suitable for docker's -u." - (unless telega-docker--user-id + (unless (or telega-docker--user-id + (telega-docker--windows-p)) (setq telega-docker--user-id (format "%s:%s" (user-uid) (group-gid)))) - (unless (string-match-p "[0-9]+:[0-9]+" telega-docker--user-id) + (when (and telega-docker--user-id + (not (string-match-p "[0-9]+:[0-9]+" telega-docker--user-id))) (user-error "telega: Can't get UID/GID, set `telega-docker--user-id' explicitly to \":\"")) telega-docker--user-id) @@ -2942,6 +2944,19 @@ Binds current symbol to SYM-BIND." (defvar telega-docker--cidfile nil "Filename to write container id into using --cidfile docker flag.") + +(defun telega-docker--volume-arg (host-path container-path &optional selinux-p) + "Return docker volume argument for HOST-PATH mounted at CONTAINER-PATH." + (if (telega-docker--windows-p) + (concat " -v " + (shell-quote-argument + (concat (expand-file-name host-path) + ":" container-path + (if selinux-p ":z" "")))) + (format " -v %s:%s%s" + host-path container-path + (if selinux-p ":z" "")))) + (defun telega-docker-run-cmd (cmd &rest volumes) "Dockerize command CMD." (declare (indent 1)) @@ -2956,53 +2971,71 @@ Binds current symbol to SYM-BIND." (if (stringp telega-use-docker) telega-use-docker "docker") - (format " run %s --rm --privileged -i -v %s:%s%s" - (or telega-docker-run-arguments "") - telega-directory telega-directory - (if selinux-p ":z" "")) + (format " run %s --rm --privileged -i" + (or telega-docker-run-arguments "")) + (if (telega-docker--windows-p) + (mapconcat + (lambda (mapping) + (telega-docker--volume-arg (car mapping) (cdr mapping))) + (telega-docker--path-mappings) "") + (format " -v %s:%s%s" + telega-directory telega-directory + (if selinux-p ":z" ""))) (when telega-docker--cidfile - (concat " --cidfile " telega-docker--cidfile)) - " -u " (telega-docker--user-id) - ;; Connect container to host networking - " --net=host" - ;; Add host devices to container to allow voice/video - ;; recording - (when (file-exists-p "/dev/snd") - " --device /dev/snd:/dev/snd") - (when (file-exists-p "/dev/video0") - " --device /dev/video0:/dev/video0") - (when (file-exists-p "/dev/video1") - " --device /dev/video1:/dev/video1") - - ;; Export resources for pulseaudio to work - ;; ref: https://stackoverflow.com/questions/28985714/run-apps-using-audio-in-a-docker-container - (concat " -v /dev/shm:/dev/shm" - " -v /etc/machine-id:/etc/machine-id" - (when-let ((xdg-runtime-dir (getenv "XDG_RUNTIME_DIR"))) - (concat (format " -v %s:%s" xdg-runtime-dir xdg-runtime-dir) - " -e XDG_RUNTIME_DIR")) - " -v /var/lib/dbus" - " -e XDG_RUNTIME_DIR" - ;; TODO - ) - ;; Export volumes and env vars need to run appindicator - (when telega-appindicator-mode - (concat " --security-opt apparmor=unconfined" - (format " -v /tmp/.X11-unix:/tmp/.X11-unix%s" - (if selinux-p ":z" "")) - (when-let ((xauthority (getenv "XAUTHORITY"))) - (format " -v %s:%s%s" xauthority xauthority - (if selinux-p ":z" ""))) - (when-let ((bus-addr (getenv "DBUS_SESSION_BUS_ADDRESS")) - (bus-path (nth 1 (split-string bus-addr "=")))) - (format " -v %s:%s%s" bus-path bus-path - (if selinux-p ":z" ""))) - " -e DISPLAY -e XAUTHORITY -e DBUS_SESSION_BUS_ADDRESS")) + (if (telega-docker--windows-p) + (concat " --cidfile " + (shell-quote-argument telega-docker--cidfile)) + (concat " --cidfile " telega-docker--cidfile))) + (when-let ((docker-user-id (telega-docker--user-id))) + (concat " -u " docker-user-id)) + (unless (telega-docker--windows-p) + (concat + ;; Connect container to host networking + " --net=host" + ;; Add host devices to container to allow voice/video + ;; recording + (when (file-exists-p "/dev/snd") + " --device /dev/snd:/dev/snd") + (when (file-exists-p "/dev/video0") + " --device /dev/video0:/dev/video0") + (when (file-exists-p "/dev/video1") + " --device /dev/video1:/dev/video1") + + ;; Export resources for pulseaudio to work + ;; ref: https://stackoverflow.com/questions/28985714/run-apps-using-audio-in-a-docker-container + (concat " -v /dev/shm:/dev/shm" + " -v /etc/machine-id:/etc/machine-id" + (when-let ((xdg-runtime-dir (getenv "XDG_RUNTIME_DIR"))) + (concat (format " -v %s:%s" xdg-runtime-dir xdg-runtime-dir) + " -e XDG_RUNTIME_DIR")) + " -v /var/lib/dbus" + " -e XDG_RUNTIME_DIR") + ;; Export volumes and env vars need to run appindicator + (when telega-appindicator-mode + (concat " --security-opt apparmor=unconfined" + (format " -v /tmp/.X11-unix:/tmp/.X11-unix%s" + (if selinux-p ":z" "")) + (when-let ((xauthority (getenv "XAUTHORITY"))) + (format " -v %s:%s%s" xauthority xauthority + (if selinux-p ":z" ""))) + (when-let ((bus-addr (getenv "DBUS_SESSION_BUS_ADDRESS")) + (bus-path (nth 1 (split-string bus-addr "=")))) + (format " -v %s:%s%s" bus-path bus-path + (if selinux-p ":z" ""))) + " -e DISPLAY -e XAUTHORITY -e DBUS_SESSION_BUS_ADDRESS")))) ;; Additional volumes - (mapconcat (lambda (volume) - (format " -v %s:%s%s" volume volume - (if selinux-p ":z" ""))) - (or volumes telega-docker-volumes) "") + (mapconcat + (lambda (volume) + (if (telega-docker--windows-p) + (telega-docker--volume-arg + volume (telega-docker-path-to-container volume)) + (format " -v %s:%s%s" volume volume + (if selinux-p ":z" "")))) + (if (telega-docker--windows-p) + (cl-remove-if-not #'file-exists-p + (or volumes telega-docker-volumes)) + (or volumes telega-docker-volumes)) + "") " " (telega-docker--image-name)))) " " cmd)) @@ -3035,13 +3068,16 @@ not signal an error and just return nil." "docker") " exec" " " exec-flags + (when (telega-docker--windows-p) + " -w /") ;; NOTE: `exec-flags' might specify its own "-u", ;; for example to run commands under root with ;; "-u 0" `exec-flags' - (unless (string-prefix-p "-u " (or exec-flags "")) + (unless (or (not (telega-docker--user-id)) + (string-prefix-p "-u " (or exec-flags ""))) (concat " -u " (telega-docker--user-id))) " " (telega-docker--container-id) - " " cmd)) + " " (telega-docker-cmd-to-container cmd))) (no-error nil) (t (error "telega: Install `%s' or set `telega-use-docker' to non-nil"