oclaw/runtime/skills/xlsx-pro/scripts/office/soffice.py
oliver 4d9232f3b3 统一技能安装与 Skills 市场接入链路。
新增公开安装工具与多来源 provider 支持,补齐管理端与目录加载逻辑,并同步更新相关测试与文档以保证可见性和路径安全。

Made-with: Cursor
2026-04-30 22:17:50 +08:00

211 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Helper pour exécuter LibreOffice (soffice) dans des environnements
où les sockets AF_UNIX peuvent être bloqués (VMs sandboxées).
Usage:
from office.soffice import run_soffice, get_soffice_env
# Option 1 – exécuter soffice directement
result = run_soffice(["--headless", "--convert-to", "pdf", "input.docx"])
# Option 2 – obtenir env dict pour vos propres appels subprocess
env = get_soffice_env()
subprocess.run(["soffice", ...], env=env)
"""
import os
import socket
import subprocess
import tempfile
from pathlib import Path
def get_soffice_env() -> dict:
"""Retourne un env dict adapté pour exécuter soffice en headless.
Définit toujours SAL_USE_VCLPLUGIN=svp pour le rendu headless (pas de X11).
Dans les environnements sandboxés où AF_UNIX est bloqué, ajoute aussi
LD_PRELOAD (socket shim).
"""
env = os.environ.copy()
env["SAL_USE_VCLPLUGIN"] = "svp"
if _needs_shim():
shim = _ensure_shim()
env["LD_PRELOAD"] = str(shim)
return env
def run_soffice(args: list, **kwargs) -> subprocess.CompletedProcess:
"""Exécute soffice avec les arguments donnés, appliquant le socket shim
si nécessaire. Accepte les mêmes arguments que subprocess.run.
Dans les environnements sandboxés, le shim gère l'arrêt propre en appelant
_exit(0) quand le socket listener de soffice.bin se ferme (après conversion).
"""
env = get_soffice_env()
return subprocess.run(["soffice"] + args, env=env, **kwargs)
# ---------------------------------------------------------------------------
# Internals
# ---------------------------------------------------------------------------
_SHIM_SO = Path(tempfile.gettempdir()) / "lo_socket_shim.so"
def _needs_shim() -> bool:
"""Vérifie si les sockets AF_UNIX sont bloqués."""
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.close()
return False
except OSError:
return True
def _ensure_shim() -> Path:
"""Compile le shim .so s'il n'est pas déjà en cache."""
if _SHIM_SO.exists():
return _SHIM_SO
src = Path(tempfile.gettempdir()) / "lo_socket_shim.c"
src.write_text(_SHIM_SOURCE)
try:
subprocess.run(
["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"],
check=True,
capture_output=True,
)
except (subprocess.CalledProcessError, FileNotFoundError):
# Si gcc n'est pas disponible ou échoue, on continue sans shim
pass
finally:
if src.exists():
src.unlink()
return _SHIM_SO
# ---------------------------------------------------------------------------
# LD_PRELOAD shim – source C
#
# Problème
# --------
# LibreOffice utilise des sockets AF_UNIX pour la gestion single-instance
# (OSL_PIPE). Dans les environnements sandboxés, le filtre seccomp bloque
# socket(AF_UNIX) tout en permettant socketpair(AF_UNIX). Sans ce shim,
# soffice crash ou reste bloqué après conversion.
#
# Solution
# --------
# Intercepte les appels concernés et fournit des substituts fonctionnels.
# ---------------------------------------------------------------------------
_SHIM_SOURCE = r"""
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
static int (*real_socket)(int, int, int);
static int (*real_socketpair)(int, int, int, int[2]);
static int (*real_listen)(int, int);
static int (*real_accept)(int, struct sockaddr *, socklen_t *);
static int (*real_close)(int);
static int (*real_read)(int, void *, size_t);
static int is_shimmed[1024];
static int peer_of[1024];
static int wake_r[1024];
static int wake_w[1024];
static int listener_fd = -1;
__attribute__((constructor))
static void init(void) {
real_socket = dlsym(RTLD_NEXT, "socket");
real_socketpair = dlsym(RTLD_NEXT, "socketpair");
real_listen = dlsym(RTLD_NEXT, "listen");
real_accept = dlsym(RTLD_NEXT, "accept");
real_close = dlsym(RTLD_NEXT, "close");
real_read = dlsym(RTLD_NEXT, "read");
for (int i = 0; i < 1024; i++) {
peer_of[i] = -1;
wake_r[i] = -1;
wake_w[i] = -1;
}
}
int socket(int domain, int type, int protocol) {
if (domain == AF_UNIX) {
int fd = real_socket(domain, type, protocol);
if (fd >= 0) return fd;
int sv[2];
if (real_socketpair(domain, type, protocol, sv) == 0) {
if (sv[0] >= 0 && sv[0] < 1024) {
is_shimmed[sv[0]] = 1;
peer_of[sv[0]] = sv[1];
int wp[2];
if (pipe(wp) == 0) {
wake_r[sv[0]] = wp[0];
wake_w[sv[0]] = wp[1];
}
}
return sv[0];
}
errno = EPERM;
return -1;
}
return real_socket(domain, type, protocol);
}
int listen(int sockfd, int backlog) {
if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) {
listener_fd = sockfd;
return 0;
}
return real_listen(sockfd, backlog);
}
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) {
if (wake_r[sockfd] >= 0) {
char buf;
real_read(wake_r[sockfd], &buf, 1);
}
errno = ECONNABORTED;
return -1;
}
return real_accept(sockfd, addr, addrlen);
}
int close(int fd) {
if (fd >= 0 && fd < 1024 && is_shimmed[fd]) {
int was_listener = (fd == listener_fd);
is_shimmed[fd] = 0;
if (wake_w[fd] >= 0) {
char c = 0;
write(wake_w[fd], &c, 1);
real_close(wake_w[fd]);
wake_w[fd] = -1;
}
if (wake_r[fd] >= 0) { real_close(wake_r[fd]); wake_r[fd] = -1; }
if (peer_of[fd] >= 0) { real_close(peer_of[fd]); peer_of[fd] = -1; }
if (was_listener)
_exit(0);
}
return real_close(fd);
}
"""
if __name__ == "__main__":
import sys
result = run_soffice(sys.argv[1:])
sys.exit(result.returncode)