soffice.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. """
  2. Helper for running LibreOffice (soffice) in environments where AF_UNIX
  3. sockets may be blocked (e.g., sandboxed VMs). Detects the restriction
  4. at runtime and applies an LD_PRELOAD shim if needed.
  5. Usage:
  6. from office.soffice import run_soffice, get_soffice_env
  7. # Option 1 – run soffice directly
  8. result = run_soffice(["--headless", "--convert-to", "pdf", "input.docx"])
  9. # Option 2 – get env dict for your own subprocess calls
  10. env = get_soffice_env()
  11. subprocess.run(["soffice", ...], env=env)
  12. """
  13. import os
  14. import socket
  15. import subprocess
  16. import tempfile
  17. from pathlib import Path
  18. def get_soffice_env() -> dict:
  19. env = os.environ.copy()
  20. env["SAL_USE_VCLPLUGIN"] = "svp"
  21. if _needs_shim():
  22. shim = _ensure_shim()
  23. env["LD_PRELOAD"] = str(shim)
  24. return env
  25. def run_soffice(args: list[str], **kwargs) -> subprocess.CompletedProcess:
  26. env = get_soffice_env()
  27. return subprocess.run(["soffice"] + args, env=env, **kwargs)
  28. _SHIM_SO = Path(tempfile.gettempdir()) / "lo_socket_shim.so"
  29. def _needs_shim() -> bool:
  30. try:
  31. s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  32. s.close()
  33. return False
  34. except OSError:
  35. return True
  36. def _ensure_shim() -> Path:
  37. if _SHIM_SO.exists():
  38. return _SHIM_SO
  39. src = Path(tempfile.gettempdir()) / "lo_socket_shim.c"
  40. src.write_text(_SHIM_SOURCE)
  41. subprocess.run(
  42. ["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"],
  43. check=True,
  44. capture_output=True,
  45. )
  46. src.unlink()
  47. return _SHIM_SO
  48. _SHIM_SOURCE = r"""
  49. #define _GNU_SOURCE
  50. #include <dlfcn.h>
  51. #include <errno.h>
  52. #include <signal.h>
  53. #include <stdio.h>
  54. #include <stdlib.h>
  55. #include <sys/socket.h>
  56. #include <unistd.h>
  57. static int (*real_socket)(int, int, int);
  58. static int (*real_socketpair)(int, int, int, int[2]);
  59. static int (*real_listen)(int, int);
  60. static int (*real_accept)(int, struct sockaddr *, socklen_t *);
  61. static int (*real_close)(int);
  62. static int (*real_read)(int, void *, size_t);
  63. /* Per-FD bookkeeping (FDs >= 1024 are passed through unshimmed). */
  64. static int is_shimmed[1024];
  65. static int peer_of[1024];
  66. static int wake_r[1024]; /* accept() blocks reading this */
  67. static int wake_w[1024]; /* close() writes to this */
  68. static int listener_fd = -1; /* FD that received listen() */
  69. __attribute__((constructor))
  70. static void init(void) {
  71. real_socket = dlsym(RTLD_NEXT, "socket");
  72. real_socketpair = dlsym(RTLD_NEXT, "socketpair");
  73. real_listen = dlsym(RTLD_NEXT, "listen");
  74. real_accept = dlsym(RTLD_NEXT, "accept");
  75. real_close = dlsym(RTLD_NEXT, "close");
  76. real_read = dlsym(RTLD_NEXT, "read");
  77. for (int i = 0; i < 1024; i++) {
  78. peer_of[i] = -1;
  79. wake_r[i] = -1;
  80. wake_w[i] = -1;
  81. }
  82. }
  83. /* ---- socket ---------------------------------------------------------- */
  84. int socket(int domain, int type, int protocol) {
  85. if (domain == AF_UNIX) {
  86. int fd = real_socket(domain, type, protocol);
  87. if (fd >= 0) return fd;
  88. /* socket(AF_UNIX) blocked – fall back to socketpair(). */
  89. int sv[2];
  90. if (real_socketpair(domain, type, protocol, sv) == 0) {
  91. if (sv[0] >= 0 && sv[0] < 1024) {
  92. is_shimmed[sv[0]] = 1;
  93. peer_of[sv[0]] = sv[1];
  94. int wp[2];
  95. if (pipe(wp) == 0) {
  96. wake_r[sv[0]] = wp[0];
  97. wake_w[sv[0]] = wp[1];
  98. }
  99. }
  100. return sv[0];
  101. }
  102. errno = EPERM;
  103. return -1;
  104. }
  105. return real_socket(domain, type, protocol);
  106. }
  107. /* ---- listen ---------------------------------------------------------- */
  108. int listen(int sockfd, int backlog) {
  109. if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) {
  110. listener_fd = sockfd;
  111. return 0;
  112. }
  113. return real_listen(sockfd, backlog);
  114. }
  115. /* ---- accept ---------------------------------------------------------- */
  116. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
  117. if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) {
  118. /* Block until close() writes to the wake pipe. */
  119. if (wake_r[sockfd] >= 0) {
  120. char buf;
  121. real_read(wake_r[sockfd], &buf, 1);
  122. }
  123. errno = ECONNABORTED;
  124. return -1;
  125. }
  126. return real_accept(sockfd, addr, addrlen);
  127. }
  128. /* ---- close ----------------------------------------------------------- */
  129. int close(int fd) {
  130. if (fd >= 0 && fd < 1024 && is_shimmed[fd]) {
  131. int was_listener = (fd == listener_fd);
  132. is_shimmed[fd] = 0;
  133. if (wake_w[fd] >= 0) { /* unblock accept() */
  134. char c = 0;
  135. write(wake_w[fd], &c, 1);
  136. real_close(wake_w[fd]);
  137. wake_w[fd] = -1;
  138. }
  139. if (wake_r[fd] >= 0) { real_close(wake_r[fd]); wake_r[fd] = -1; }
  140. if (peer_of[fd] >= 0) { real_close(peer_of[fd]); peer_of[fd] = -1; }
  141. if (was_listener)
  142. _exit(0); /* conversion done – exit */
  143. }
  144. return real_close(fd);
  145. }
  146. """
  147. if __name__ == "__main__":
  148. import sys
  149. result = run_soffice(sys.argv[1:])
  150. sys.exit(result.returncode)