xref: /trunk/test/aoo-test/src/aootest/office.py (revision 2cb6d6f119bbe2f78742f1c5c8538cb0d4e72ede)
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