diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Office2PdfPageConverter.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Office2PdfPageConverter.java index 1e56943d8fef..96233285cb8c 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Office2PdfPageConverter.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/Office2PdfPageConverter.java @@ -20,21 +20,33 @@ package org.bigbluebutton.presentation.imp; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; import java.util.HashMap; import java.util.Map; +import com.sun.org.apache.xerces.internal.impl.xs.opti.DefaultDocument; +import org.apache.commons.io.FilenameUtils; import org.bigbluebutton.presentation.UploadedPresentation; +import org.jodconverter.core.document.DefaultDocumentFormatRegistry; +import org.jodconverter.core.document.DocumentFormat; +import org.jodconverter.core.job.AbstractConverter; import org.jodconverter.local.LocalConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; -public class Office2PdfPageConverter { +public abstract class Office2PdfPageConverter { private static Logger log = LoggerFactory.getLogger(Office2PdfPageConverter.class); - public boolean convert(File presentationFile, File output, int page, UploadedPresentation pres, - final LocalConverter converter){ + public static boolean convert(File presentationFile, File output, int page, UploadedPresentation pres, + LocalConverter converter){ + + FileInputStream inputStream = null; + FileOutputStream outputStream = null; + try { Map logData = new HashMap<>(); logData.put("meetingId", pres.getMeetingId()); @@ -46,7 +58,15 @@ public boolean convert(File presentationFile, File output, int page, UploadedPre String logStr = gson.toJson(logData); log.info(" --analytics-- data={}", logStr); - converter.convert(presentationFile).to(output).execute(); + final DocumentFormat sourceFormat = DefaultDocumentFormatRegistry.getFormatByExtension( + FilenameUtils.getExtension(presentationFile.getName())); + + inputStream = new FileInputStream(presentationFile); + outputStream = new FileOutputStream(output); + + converter.convert(inputStream).as(sourceFormat).to(outputStream).as(DefaultDocumentFormatRegistry.PDF).execute(); + outputStream.flush(); + if (output.exists()) { return true; } else { @@ -74,6 +94,22 @@ public boolean convert(File presentationFile, File output, int page, UploadedPre String logStr = gson.toJson(logData); log.error(" --analytics-- data={}", logStr, e); return false; + } finally { + if(inputStream!=null) { + try { + inputStream.close(); + } catch(Exception e) { + + } + } + + if(outputStream!=null) { + try { + outputStream.close(); + } catch(Exception e) { + + } + } } } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/OfficeToPdfConversionService.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/OfficeToPdfConversionService.java index 264b0430ade0..fb24ce2b2b1b 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/OfficeToPdfConversionService.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/imp/OfficeToPdfConversionService.java @@ -20,6 +20,8 @@ package org.bigbluebutton.presentation.imp; import java.io.File; +import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -27,9 +29,9 @@ import org.bigbluebutton.presentation.SupportedFileTypes; import org.bigbluebutton.presentation.UploadedPresentation; import org.jodconverter.core.office.OfficeException; -import org.jodconverter.core.office.OfficeManager; +import org.jodconverter.core.office.OfficeUtils; import org.jodconverter.local.LocalConverter; -import org.jodconverter.local.office.LocalOfficeManager; +import org.jodconverter.local.office.ExternalOfficeManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,20 +41,12 @@ public class OfficeToPdfConversionService { private static Logger log = LoggerFactory.getLogger(OfficeToPdfConversionService.class); private OfficeDocumentValidator2 officeDocumentValidator; - private final OfficeManager officeManager; - private final LocalConverter documentConverter; + private final ArrayList officeManagers; + private ExternalOfficeManager currentManager = null; private boolean skipOfficePrecheck = false; public OfficeToPdfConversionService() throws OfficeException { - officeManager = LocalOfficeManager - .builder() - .portNumbers(8100, 8101, 8102, 8103, 8104) - .build(); - documentConverter = LocalConverter - .builder() - .officeManager(officeManager) - .filterChain(new OfficeDocumentConversionFilter()) - .build(); + officeManagers = new ArrayList<>(); } /* @@ -121,8 +115,39 @@ private File setupOutputPdfFile(UploadedPresentation pres) { private boolean convertOfficeDocToPdf(UploadedPresentation pres, File pdfOutput) { - Office2PdfPageConverter converter = new Office2PdfPageConverter(); - return converter.convert(pres.getUploadedFile(), pdfOutput, 0, pres, documentConverter); + boolean success = false; + int attempts = 0; + + while(!success) { + LocalConverter documentConverter = LocalConverter + .builder() + .officeManager(currentManager) + .filterChain(new OfficeDocumentConversionFilter()) + .build(); + + success = Office2PdfPageConverter.convert(pres.getUploadedFile(), pdfOutput, 0, pres, documentConverter); + + if(!success) { + // In case of failure, try with other open Office Manager + + if(++attempts != officeManagers.size()) { + // Go to next Office Manager ( if the last retry with the first one ) + int currentManagerIndex = officeManagers.indexOf(currentManager); + + boolean isLastManager = ( currentManagerIndex == officeManagers.size()-1 ); + if(isLastManager) { + currentManager = officeManagers.get(0); + } else { + currentManager = officeManagers.get(currentManagerIndex+1); + } + } else { + // We tried to use all our office managers and it's still failing + break; + } + } + } + + return success; } private void makePdfTheUploadedFileAndSetStepAsSuccess(UploadedPresentation pres, File pdf) { @@ -139,19 +164,41 @@ public void setSkipOfficePrecheck(boolean skipOfficePrecheck) { } public void start() { - try { - officeManager.start(); - } catch (OfficeException e) { - log.error("Could not start Office Manager", e); + for(int managerIndex = 0; managerIndex < 4; managerIndex ++) { + Integer instanceNumber = managerIndex + 1; // starts at 1 + + try { + final File workingDir = new File("/var/tmp/soffice_0" +instanceNumber); + ExternalOfficeManager officeManager = ExternalOfficeManager + .builder() + .connectTimeout(2000L) + .retryInterval(500L) + .portNumber(8200 + instanceNumber) + .connectOnStart(false) // If it's true and soffice is not available, exception is thrown here ( we don't want exception here - we want the manager alive trying to reconnect ) + .workingDir(workingDir) + .build(); + + // Workaround for jodconverter not calling makeTempDir when connectOnStart=false (issue 211) + Method method = officeManager.getClass().getSuperclass().getDeclaredMethod("makeTempDir"); + method.setAccessible(true); + method.invoke(officeManager); + // End of workaround for jodconverter not calling makeTempDir + + officeManager.start(); + officeManagers.add(officeManager); + } catch (Exception e) { + log.error("Could not start Office Manager " + instanceNumber + ". Details: " + e.getMessage()); + } + + currentManager = officeManagers.get(0); } } public void stop() { try { - officeManager.stop(); - } catch (OfficeException e) { + officeManagers.forEach(officeManager -> officeManager.stop() ); + } catch (Exception e) { log.error("Could not stop Office Manager", e); } - } } diff --git a/bbb-libreoffice/assets/bbb-libreoffice.service b/bbb-libreoffice/assets/bbb-libreoffice.service new file mode 100644 index 000000000000..c4eb3232570b --- /dev/null +++ b/bbb-libreoffice/assets/bbb-libreoffice.service @@ -0,0 +1,19 @@ +[Unit] +Description=BigBlueButton Libre Office container +Requires=network.target + +[Service] +Type=simple +WorkingDirectory=/tmp +ExecStart=/usr/share/bbb-libreoffice/libreoffice_container.sh INSTANCE_NUMBER +ExecStop=/usr/bin/docker kill bbb-libreoffice-INSTANCE_NUMBER +Restart=always +RestartSec=60 +SuccessExitStatus= +TimeoutStopSec=30 +PermissionsStartOnly=true +LimitNOFILE=1024 + +[Install] +WantedBy=multi-user.target + diff --git a/bbb-libreoffice/assets/libreoffice_container.sh b/bbb-libreoffice/assets/libreoffice_container.sh new file mode 100755 index 000000000000..d82cd6aa6c11 --- /dev/null +++ b/bbb-libreoffice/assets/libreoffice_container.sh @@ -0,0 +1,33 @@ +#!/bin/bash +INSTANCE_NUMBER=$1 + +if [ -z "$INSTANCE_NUMBER" ]; then + INSTANCE_NUMBER=0 +fi; + +_kill() { + CHECK_CONTAINER=`docker inspect bbb-libreoffice-${INSTANCE_NUMBER} &> /dev/null && echo 1 || echo 0` + if [ "$CHECK_CONTAINER" = "1" ]; then + echo "Killing container" + docker kill bbb-libreoffice-${INSTANCE_NUMBER}; + sleep 1 + fi; +} + +trap _kill SIGINT + + +if (($INSTANCE_NUMBER >= 1 && $INSTANCE_NUMBER <= 10)); then + PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + + _kill + + docker run --name bbb-libreoffice-${INSTANCE_NUMBER} -p 82${INSTANCE_NUMBER}:8000 -v/var/tmp/soffice${INSTANCE_NUMBER}:/var/tmp/soffice${INSTANCE_NUMBER} --rm bbb-libreoffice & + + wait $! +else + echo ; + echo "Invalid or missing parameter INSTANCE_NUMBER" + echo " Usage: $0 INSTANCE_NUMBER" + exit 1 +fi; diff --git a/bbb-libreoffice/docker/Dockerfile b/bbb-libreoffice/docker/Dockerfile new file mode 100644 index 000000000000..a34bc249dfa3 --- /dev/null +++ b/bbb-libreoffice/docker/Dockerfile @@ -0,0 +1,26 @@ +FROM openjdk:8-jre + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt update + +RUN addgroup --system --gid 996 libreoffice +RUN adduser --disabled-password --system --disabled-login --shell /sbin/nologin --gid 996 --uid 996 libreoffice + +RUN apt -y install locales-all fontconfig libxt6 libxrender1 +RUN apt -y install libreoffice --no-install-recommends + +RUN dpkg-reconfigure fontconfig && fc-cache -f -s -v + +RUN for i in `seq -w 1 10` ; do mkdir -m 777 /var/tmp/soffice${i}; done + +VOLUME ["/usr/share/fonts/"] +RUN chown libreoffice /home/libreoffice/ + +ADD ./bbb-libreoffice-entrypoint.sh /home/libreoffice/ +RUN chown -R libreoffice /home/libreoffice/ +RUN chmod 700 /home/libreoffice/bbb-libreoffice-entrypoint.sh + +USER libreoffice + +ENTRYPOINT ["/home/libreoffice/bbb-libreoffice-entrypoint.sh" ] diff --git a/bbb-libreoffice/docker/bbb-libreoffice-entrypoint.sh b/bbb-libreoffice/docker/bbb-libreoffice-entrypoint.sh new file mode 100644 index 000000000000..e94594109498 --- /dev/null +++ b/bbb-libreoffice/docker/bbb-libreoffice-entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +## Initialize environment +/usr/lib/libreoffice/program/soffice.bin -env:UserInstallation="file:///tmp/" + +## Run daemon +/usr/lib/libreoffice/program/soffice.bin --accept="socket,host=0.0.0.0,port=8000,tcpNoDelay=1;urp;StarOffice.ServiceManager" --headless --invisible --nocrashreport --nodefault --nofirststartwizard --nolockcheck --nologo --norestore -env:UserInstallation="file:///tmp/" diff --git a/bbb-libreoffice/install.sh b/bbb-libreoffice/install.sh new file mode 100755 index 000000000000..1dc146b7fe34 --- /dev/null +++ b/bbb-libreoffice/install.sh @@ -0,0 +1,51 @@ +#!/bin/bash +if [ "$EUID" -ne 0 ]; then + echo "Please run this script as root ( or with sudo )" ; + exit 1; +fi; + +DOCKER_CHECK=`docker --version &> /dev/null && echo 1 || echo 0` + +if [ "$DOCKER_CHECK" = "0" ]; then + echo "Docker not found"; + apt update; + apt install apt-transport-https ca-certificates curl software-properties-common + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - + add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable" + apt update + apt install docker-ce -y + systemctl enable docker + systemctl start docker + systemctl status docker +else + echo "Docker already installed"; +fi + + +IMAGE_CHECK=`docker image inspect bbb-libreoffice &> /dev/null && echo 1 || echo 0` +if [ "$IMAGE_CHECK" = "0" ]; then + echo "Docker image doesn't exists, building" + docker build -t bbb-libreoffice docker/ +else + echo "Docker image already exists"; +fi + +FOLDER_CHECK=`[ -d /usr/share/bbb-libreoffice/ ] && echo 1 || echo 0` +if [ "$FOLDER_CHECK" = "0" ]; then + echo "Install folder doesn't exists, installing" + mkdir -m 755 /usr/share/bbb-libreoffice/ + cp assets/libreoffice_container.sh /usr/share/bbb-libreoffice/ + chmod 700 /usr/share/bbb-libreoffice/libreoffice_container.sh + chown -R root /usr/share/bbb-libreoffice/ + + for i in `seq 1 4` ; do + cat assets/bbb-libreoffice.service | sed 's/INSTANCE_NUMBER/0'${i}'/g' > /lib/systemd/system/bbb-libreoffice-0${i}.service + systemctl daemon-reload + systemctl enable bbb-libreoffice-0${i} + systemctl start bbb-libreoffice-0${i} + done + +else + echo "Install folder already exists" +fi; + diff --git a/bbb-libreoffice/uninstall.sh b/bbb-libreoffice/uninstall.sh new file mode 100755 index 000000000000..270824660129 --- /dev/null +++ b/bbb-libreoffice/uninstall.sh @@ -0,0 +1,29 @@ +#!/bin/bash +if [ "$EUID" -ne 0 ]; then + echo "Please run this script as root ( or with sudo )" ; + exit 1; +fi; + +IMAGE_CHECK=`docker image inspect bbb-libreoffice 2>&1 > /dev/null && echo 1 || echo 0` +if [ "$IMAGE_CHECK" = "1" ]; then + echo "Stopping services" + systemctl --no-pager --no-legend --value --state=running | grep bbb-libreoffice | awk -F '.service' '{print $1}' | xargs -n 1 systemctl stop + + echo "Removing image" + docker image rm bbb-libreoffice +fi + + +FOLDER_CHECK=`[ -d /usr/share/bbb-libreoffice/ ] && echo 1 || echo 0` +if [ "$FOLDER_CHECK" = "1" ]; then + echo "Stopping services" + systemctl --no-pager --no-legend --value --state=running | grep bbb-libreoffice | awk -F '.service' '{print $1}' | xargs -n 1 systemctl stop + + echo "Removing install folder" + rm -rf /usr/share/bbb-libreoffice/ + + echo "Removing service definitions" + rm /lib/systemd/system/bbb-libreoffice-0* + find /etc/systemd/ | grep bbb-libreoffice | xargs --no-run-if-empty -n 1 -I __ rm __ + systemctl daemon-reload +fi;