1*2cb6d6f1SJim Jagielski################################################################ 2*2cb6d6f1SJim Jagielski# 3*2cb6d6f1SJim Jagielski# Licensed to the Apache Software Foundation (ASF) under one 4*2cb6d6f1SJim Jagielski# or more contributor license agreements. See the NOTICE file 5*2cb6d6f1SJim Jagielski# distributed with this work for additional information 6*2cb6d6f1SJim Jagielski# regarding copyright ownership. The ASF licenses this file 7*2cb6d6f1SJim Jagielski# to you under the Apache License, Version 2.0 (the 8*2cb6d6f1SJim Jagielski# "License"); you may not use this file except in compliance 9*2cb6d6f1SJim Jagielski# with the License. You may obtain a copy of the License at 10*2cb6d6f1SJim Jagielski# 11*2cb6d6f1SJim Jagielski# http://www.apache.org/licenses/LICENSE-2.0 12*2cb6d6f1SJim Jagielski# 13*2cb6d6f1SJim Jagielski# Unless required by applicable law or agreed to in writing, 14*2cb6d6f1SJim Jagielski# software distributed under the License is distributed on an 15*2cb6d6f1SJim Jagielski# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16*2cb6d6f1SJim Jagielski# KIND, either express or implied. See the License for the 17*2cb6d6f1SJim Jagielski# specific language governing permissions and limitations 18*2cb6d6f1SJim Jagielski# under the License. 19*2cb6d6f1SJim Jagielski# 20*2cb6d6f1SJim Jagielski################################################################ 21*2cb6d6f1SJim Jagielski 22*2cb6d6f1SJim Jagielski"""Starting the office, its profile, and the session that restarts it after a hang or crash.""" 23*2cb6d6f1SJim Jagielskiimport socket 24*2cb6d6f1SJim Jagielskiimport uuid 25*2cb6d6f1SJim Jagielskifrom pathlib import Path 26*2cb6d6f1SJim Jagielski 27*2cb6d6f1SJim Jagielskifrom aootest import proc 28*2cb6d6f1SJim Jagielskifrom aootest.client import ProtocolError, WorkerCrash, WorkerHang 29*2cb6d6f1SJim Jagielski 30*2cb6d6f1SJim JagielskiXCU_HEAD = ('<?xml version="1.0" encoding="UTF-8"?>\n<oor:items xmlns:oor="http://openoffice.org/2001/registry" ' 31*2cb6d6f1SJim Jagielski 'xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n') 32*2cb6d6f1SJim Jagielski 33*2cb6d6f1SJim Jagielski 34*2cb6d6f1SJim Jagielskidef registry_xcu(default_version=None, extra=None): 35*2cb6d6f1SJim Jagielski """registrymodifications.xcu content. `default_version` is Save/ODF/DefaultVersion (3 = ODF 1.2 extended, 4 = 1.2, 5 = 1.3, 36*2cb6d6f1SJim Jagielski 6 = 1.3 extended; 0 leaves the product default). `extra` maps other Save/ODF boolean properties (UseSHA1InODF12, ...) to values.""" 37*2cb6d6f1SJim Jagielski values = {} 38*2cb6d6f1SJim Jagielski if default_version is not None: 39*2cb6d6f1SJim Jagielski values["DefaultVersion"] = "%d" % default_version 40*2cb6d6f1SJim Jagielski for name, v in (extra or {}).items(): 41*2cb6d6f1SJim Jagielski values[name] = ("true" if v else "false") if isinstance(v, bool) else str(v) 42*2cb6d6f1SJim Jagielski items = "".join('<item oor:path="/org.openoffice.Office.Common/Save/ODF"><prop oor:name="%s" oor:op="fuse">' 43*2cb6d6f1SJim Jagielski '<value>%s</value></prop></item>\n' % kv for kv in values.items()) 44*2cb6d6f1SJim Jagielski return XCU_HEAD + items + "</oor:items>\n" 45*2cb6d6f1SJim Jagielski 46*2cb6d6f1SJim Jagielski 47*2cb6d6f1SJim Jagielskidef make_profile(directory, default_version=None, extra=None): 48*2cb6d6f1SJim Jagielski """Creates a user profile in `directory` (empty or absent; nothing is deleted) and returns its file URL.""" 49*2cb6d6f1SJim Jagielski d = Path(directory).resolve() 50*2cb6d6f1SJim Jagielski if d.is_dir() and any(d.iterdir()): 51*2cb6d6f1SJim Jagielski raise ValueError("profile directory is not empty: %s" % d) 52*2cb6d6f1SJim Jagielski (d / "user").mkdir(parents=True, exist_ok=True) 53*2cb6d6f1SJim Jagielski (d / "user" / "registrymodifications.xcu").write_text(registry_xcu(default_version, extra), encoding="utf-8") 54*2cb6d6f1SJim Jagielski return d.as_uri() 55*2cb6d6f1SJim Jagielski 56*2cb6d6f1SJim Jagielski 57*2cb6d6f1SJim Jagielskidef free_port(): 58*2cb6d6f1SJim Jagielski with socket.socket() as s: 59*2cb6d6f1SJim Jagielski s.bind(("127.0.0.1", 0)) 60*2cb6d6f1SJim Jagielski return s.getsockname()[1] 61*2cb6d6f1SJim Jagielski 62*2cb6d6f1SJim Jagielski 63*2cb6d6f1SJim Jagielskidef accept_string(connect="pipe", name=None, port=None): 64*2cb6d6f1SJim Jagielski """The -accept= value: a uniquely named pipe, or a socket on `port` (a free one by default).""" 65*2cb6d6f1SJim Jagielski if connect == "pipe": 66*2cb6d6f1SJim Jagielski return "pipe,name=%s;urp;" % (name or "aootest_" + uuid.uuid4().hex[:12]) 67*2cb6d6f1SJim Jagielski if connect == "socket": 68*2cb6d6f1SJim Jagielski return "socket,host=localhost,port=%d;urp;" % (port or free_port()) 69*2cb6d6f1SJim Jagielski raise ValueError("connect must be 'pipe' or 'socket'") 70*2cb6d6f1SJim Jagielski 71*2cb6d6f1SJim Jagielski 72*2cb6d6f1SJim Jagielskidef command(layout, profile_url, accept, headless=True): 73*2cb6d6f1SJim Jagielski """soffice command line. AOO takes single-dash options only and ignores `--headless`.""" 74*2cb6d6f1SJim Jagielski return ([str(layout.soffice)] + (["-headless"] if headless else []) + 75*2cb6d6f1SJim Jagielski ["-invisible", "-nofirststartwizard", "-norestore", "-nologo", "-nodefault", "-nolockcheck", 76*2cb6d6f1SJim Jagielski "-env:UserInstallation=" + profile_url, "-accept=" + accept]) 77*2cb6d6f1SJim Jagielski 78*2cb6d6f1SJim Jagielski 79*2cb6d6f1SJim Jagielskiclass Office: 80*2cb6d6f1SJim Jagielski """A running soffice. Readiness is the worker connecting to it, so there is nothing to poll here.""" 81*2cb6d6f1SJim Jagielski 82*2cb6d6f1SJim Jagielski def __init__(self, cmd, env=None, log=None): 83*2cb6d6f1SJim Jagielski self.cmd, self.env, self.log, self._proc = list(cmd), env, log, None 84*2cb6d6f1SJim Jagielski 85*2cb6d6f1SJim Jagielski def start(self): 86*2cb6d6f1SJim Jagielski out = open(self.log, "ab") if self.log else None 87*2cb6d6f1SJim Jagielski try: 88*2cb6d6f1SJim Jagielski self._proc = proc.spawn(self.cmd, env=self.env, stdin=None, stdout=out, stderr=out) 89*2cb6d6f1SJim Jagielski finally: 90*2cb6d6f1SJim Jagielski if out: 91*2cb6d6f1SJim Jagielski out.close() 92*2cb6d6f1SJim Jagielski return self 93*2cb6d6f1SJim Jagielski 94*2cb6d6f1SJim Jagielski @property 95*2cb6d6f1SJim Jagielski def pid(self): 96*2cb6d6f1SJim Jagielski return self._proc.pid if self._proc else None 97*2cb6d6f1SJim Jagielski 98*2cb6d6f1SJim Jagielski def alive(self): 99*2cb6d6f1SJim Jagielski return self._proc is not None and self._proc.poll() is None 100*2cb6d6f1SJim Jagielski 101*2cb6d6f1SJim Jagielski def kill(self): 102*2cb6d6f1SJim Jagielski if self._proc is not None: 103*2cb6d6f1SJim Jagielski proc.kill_tree(self._proc) 104*2cb6d6f1SJim Jagielski 105*2cb6d6f1SJim Jagielski 106*2cb6d6f1SJim Jagielskiclass SessionAborted(Exception): 107*2cb6d6f1SJim Jagielski pass 108*2cb6d6f1SJim Jagielski 109*2cb6d6f1SJim Jagielski 110*2cb6d6f1SJim Jagielskiclass OfficeSession: 111*2cb6d6f1SJim Jagielski """One office and one worker, started lazily. A hang, crash or protocol error tears both down, is re-raised to the 112*2cb6d6f1SJim Jagielski caller, and the next call starts fresh; after more than `max_restarts` such failures the session is aborted. 113*2cb6d6f1SJim Jagielski 114*2cb6d6f1SJim Jagielski start_office() returns an object with kill(); start_worker(office) returns a started client with call() and close().""" 115*2cb6d6f1SJim Jagielski 116*2cb6d6f1SJim Jagielski def __init__(self, start_office, start_worker, max_restarts=5): 117*2cb6d6f1SJim Jagielski self._start_office, self._start_worker, self.max_restarts = start_office, start_worker, max_restarts 118*2cb6d6f1SJim Jagielski self.office = self.worker = None 119*2cb6d6f1SJim Jagielski self.failures = 0 120*2cb6d6f1SJim Jagielski self.aborted = False 121*2cb6d6f1SJim Jagielski 122*2cb6d6f1SJim Jagielski def _ensure(self): 123*2cb6d6f1SJim Jagielski if self.aborted: 124*2cb6d6f1SJim Jagielski raise SessionAborted("aborted after %d failures (limit %d)" % (self.failures, self.max_restarts)) 125*2cb6d6f1SJim Jagielski if self.worker is None: 126*2cb6d6f1SJim Jagielski self.office = self._start_office() 127*2cb6d6f1SJim Jagielski try: 128*2cb6d6f1SJim Jagielski self.worker = self._start_worker(self.office) 129*2cb6d6f1SJim Jagielski except BaseException as e: 130*2cb6d6f1SJim Jagielski self._teardown() 131*2cb6d6f1SJim Jagielski if isinstance(e, (WorkerHang, WorkerCrash, ProtocolError)): 132*2cb6d6f1SJim Jagielski self._failed() 133*2cb6d6f1SJim Jagielski raise 134*2cb6d6f1SJim Jagielski 135*2cb6d6f1SJim Jagielski def _failed(self): 136*2cb6d6f1SJim Jagielski self.failures += 1 137*2cb6d6f1SJim Jagielski if self.failures > self.max_restarts: 138*2cb6d6f1SJim Jagielski self.aborted = True 139*2cb6d6f1SJim Jagielski 140*2cb6d6f1SJim Jagielski def _teardown(self): 141*2cb6d6f1SJim Jagielski w, o, self.worker, self.office = self.worker, self.office, None, None 142*2cb6d6f1SJim Jagielski if w is not None: 143*2cb6d6f1SJim Jagielski w.close() 144*2cb6d6f1SJim Jagielski if o is not None: 145*2cb6d6f1SJim Jagielski o.kill() 146*2cb6d6f1SJim Jagielski 147*2cb6d6f1SJim Jagielski def call(self, op, args=None, timeout=60.0): 148*2cb6d6f1SJim Jagielski self._ensure() 149*2cb6d6f1SJim Jagielski try: 150*2cb6d6f1SJim Jagielski return self.worker.call(op, args, timeout) 151*2cb6d6f1SJim Jagielski except (WorkerHang, WorkerCrash, ProtocolError): 152*2cb6d6f1SJim Jagielski self._teardown() 153*2cb6d6f1SJim Jagielski self._failed() 154*2cb6d6f1SJim Jagielski raise 155*2cb6d6f1SJim Jagielski 156*2cb6d6f1SJim Jagielski def close(self): 157*2cb6d6f1SJim Jagielski self._teardown() 158