1"""Test suite for zdrun.py."""
2from __future__ import print_function
3
4import os
5import sys
6import time
7import shutil
8import signal
9import tempfile
10import unittest
11import socket
12
13try:
14    from StringIO import StringIO
15except:
16    # Python 3 support.
17    from io import StringIO
18
19import ZConfig
20
21from zdaemon import zdrun, zdctl
22
23
24class ConfiguredOptions:
25    """Options class that loads configuration from a specified string.
26
27    This always loads from the string, regardless of any -C option
28    that may be given.
29    """
30
31    def set_configuration(self, configuration):
32        self.__configuration = configuration
33        self.configfile = "<preloaded string>"
34
35    def load_configfile(self):
36        sio = StringIO(self.__configuration)
37        cfg = ZConfig.loadConfigFile(self.schema, sio, self.zconfig_options)
38        self.configroot, self.confighandlers = cfg
39
40
41class ConfiguredZDRunOptions(ConfiguredOptions, zdrun.ZDRunOptions):
42
43    def __init__(self, configuration):
44        zdrun.ZDRunOptions.__init__(self)
45        self.set_configuration(configuration)
46
47
48class ZDaemonTests(unittest.TestCase):
49
50    python = os.path.abspath(sys.executable)
51    assert os.path.exists(python)
52    here = os.path.abspath(os.path.dirname(__file__))
53    assert os.path.isdir(here)
54    nokill = os.path.join(here, "nokill.py")
55    assert os.path.exists(nokill)
56    parent = os.path.dirname(here)
57    zdrun = os.path.join(parent, "zdrun.py")
58    assert os.path.exists(zdrun)
59
60    ppath = os.pathsep.join(sys.path)
61
62    def setUp(self):
63        self.zdsock = tempfile.mktemp()
64        self.new_stdout = StringIO()
65        self.save_stdout = sys.stdout
66        sys.stdout = self.new_stdout
67        self.expect = ""
68
69    def tearDown(self):
70        sys.stdout = self.save_stdout
71        for sig in (signal.SIGTERM,
72                    signal.SIGHUP,
73                    signal.SIGINT,
74                    signal.SIGCHLD):
75            signal.signal(sig, signal.SIG_DFL)
76        try:
77            os.unlink(self.zdsock)
78        except os.error:
79            pass
80        output = self.new_stdout.getvalue()
81        self.assertEqual(self.expect, output)
82
83    def quoteargs(self, args):
84        for i in range(len(args)):
85            if " " in args[i]:
86                args[i] = '"%s"' % args[i]
87        return " ".join(args)
88
89    def rundaemon(self, args):
90        # Add quotes, in case some pathname contains spaces (e.g. Mac OS X)
91        args = self.quoteargs(args)
92        cmd = ('PYTHONPATH="%s" "%s" "%s" -d -s "%s" %s' %
93               (self.ppath, self.python, self.zdrun, self.zdsock, args))
94        os.system(cmd)
95        # When the daemon crashes, the following may help debug it:
96        #   os.system("PYTHONPATH=%s %s %s -s %s %s &" %
97        #       (self.ppath, self.python, self.zdrun, self.zdsock, args))
98
99    def _run(self, args, cmdclass=None, module=zdctl):
100        if isinstance(args, str):
101            args = args.split()
102        kw = {}
103        if cmdclass:
104            kw['cmdclass'] = cmdclass
105        try:
106            module.main(["-s", self.zdsock] + args, **kw)
107        except SystemExit:
108            pass
109
110    def testCmdclassOverride(self):
111        class MyCmd(zdctl.ZDCmd):
112            def do_sproing(self, rest):
113                print(rest)
114        self._run("-p echo sproing expected", cmdclass=MyCmd)
115        self.expect = "expected\n"
116
117    def testSystem(self):
118        self.rundaemon(["echo", "-n"])
119        self.expect = ""
120
121    def test_help_zdrun(self):
122        self._run("-h", module=zdrun)
123        self.expect = zdrun.__doc__
124
125    def test_help_zdctl(self):
126        self._run("-h")
127        self.expect = zdctl.__doc__
128
129    def testOptionsSysArgv(self):
130        # Check that options are parsed from sys.argv by default
131        options = zdrun.ZDRunOptions()
132        save_sys_argv = sys.argv
133        try:
134            sys.argv = ["A", "B", "C"]
135            options.realize()
136        finally:
137            sys.argv = save_sys_argv
138        self.assertEqual(options.options, [])
139        self.assertEqual(options.args, ["B", "C"])
140
141    def testOptionsBasic(self):
142        # Check basic option parsing
143        options = zdrun.ZDRunOptions()
144        options.realize(["B", "C"], "foo")
145        self.assertEqual(options.options, [])
146        self.assertEqual(options.args, ["B", "C"])
147        self.assertEqual(options.progname, "foo")
148
149    def testOptionsHelp(self):
150        # Check that -h behaves properly
151        options = zdrun.ZDRunOptions()
152        try:
153            options.realize(["-h"], doc=zdrun.__doc__)
154        except SystemExit as err:
155            self.assertEqual(err.code, 0)
156        else:
157            self.fail("SystemExit expected")
158        self.expect = zdrun.__doc__
159
160    def testSubprocessBasic(self):
161        # Check basic subprocess management: spawn, kill, wait
162        options = zdrun.ZDRunOptions()
163        options.realize(["sleep", "100"])
164        proc = zdrun.Subprocess(options)
165        self.assertEqual(proc.pid, 0)
166        pid = proc.spawn()
167        self.assertEqual(proc.pid, pid)
168        msg = proc.kill(signal.SIGTERM)
169        self.assertEqual(msg, None)
170        wpid, wsts = os.waitpid(pid, 0)
171        self.assertEqual(wpid, pid)
172        self.assertEqual(os.WIFSIGNALED(wsts), 1)
173        self.assertEqual(os.WTERMSIG(wsts), signal.SIGTERM)
174        proc.setstatus(wsts)
175        self.assertEqual(proc.pid, 0)
176
177    def testEventlogOverride(self):
178        # Make sure runner.eventlog is used if it exists
179        options = ConfiguredZDRunOptions("""\
180            <runner>
181              program /bin/true
182              <eventlog>
183                level 42
184              </eventlog>
185            </runner>
186
187            <eventlog>
188              level 35
189            </eventlog>
190            """)
191        options.realize(["/bin/true"])
192        self.assertEqual(options.config_logger.level, 42)
193
194    def testEventlogWithoutOverride(self):
195        # Make sure eventlog is used if runner.eventlog doesn't exist
196        options = ConfiguredZDRunOptions("""\
197            <runner>
198              program /bin/true
199            </runner>
200
201            <eventlog>
202              level 35
203            </eventlog>
204            """)
205        options.realize(["/bin/true"])
206        self.assertEqual(options.config_logger.level, 35)
207
208    def testRunIgnoresParentSignals(self):
209        # Spawn a process which will in turn spawn a zdrun process.
210        # We make sure that the zdrun process is still running even if
211        # its parent process receives an interrupt signal (it should
212        # not be passed to zdrun).
213        tmp = tempfile.mkdtemp()
214        zdrun_socket = os.path.join(tmp, 'testsock')
215        try:
216            zdctlpid = os.spawnvpe(
217                os.P_NOWAIT,
218                sys.executable,
219                [sys.executable, os.path.join(self.here, 'parent.py'), tmp],
220                dict(os.environ,
221                     PYTHONPATH=":".join(sys.path),
222                     )
223            )
224            # Wait for it to start, but no longer than a minute.
225            deadline = time.time() + 60
226            is_started = False
227            while time.time() < deadline:
228                response = send_action('status\n', zdrun_socket)
229                if response is None:
230                    time.sleep(0.05)
231                else:
232                    is_started = True
233                    break
234            self.assertTrue(is_started,
235                            "spawned process failed to start in a minute")
236            # Kill it, and wait a little to ensure it's dead.
237            os.kill(zdctlpid, signal.SIGINT)
238            time.sleep(0.25)
239            # Make sure the child is still responsive.
240            response = send_action('status\n', zdrun_socket,
241                                   raise_on_error=True)
242            self.assertTrue(b'\n' in response,
243                            'no newline in response: ' + repr(response))
244            # Kill the process.
245            send_action('stop\n', zdrun_socket)
246        finally:
247            # Remove the tmp directory.
248            # Caution:  this is delicate.  The code here used to do
249            # shutil.rmtree(tmp), but that suffers a sometimes-fatal
250            # race with zdrun.py.  The 'testsock' socket is created
251            # by zdrun in the tmp directory, and zdrun tries to
252            # unlink it.  If shutil.rmtree sees 'testsock' too, it
253            # will also try to unlink it, but zdrun may complete
254            # unlinking it before shutil gets to it (there's more
255            # than one process here).  So, in effect, we code a
256            # 1-level rmtree inline here, suppressing errors.
257            for fname in os.listdir(tmp):
258                try:
259                    os.unlink(os.path.join(tmp, fname))
260                except os.error:
261                    pass
262            os.rmdir(tmp)
263
264    def testUmask(self):
265        # people have a strange tendency to run the tests as root
266        if os.getuid() == 0:
267            self.fail("""
268I am root!
269Do not run the tests as root.
270Testing proper umask handling cannot be done as root.
271Furthermore, it is not a good idea and strongly discouraged to run zope, the
272build system (configure, make) or the tests as root.
273In general do not run anything as root unless absolutely necessary.
274""")
275
276        path = tempfile.mktemp()
277        # With umask 666, we should create a file that we aren't able
278        # to write.  If access says no, assume that umask works.
279        try:
280            touch_cmd = "/bin/touch"
281            if not os.path.exists(touch_cmd):
282                touch_cmd = "/usr/bin/touch"  # Mac OS X
283            self.rundaemon(["-m", "666", touch_cmd, path])
284            for i in range(5):
285                if not os.path.exists(path):
286                    time.sleep(0.1)
287            self.assertTrue(os.path.exists(path))
288            self.assertTrue(not os.access(path, os.W_OK))
289        finally:
290            if os.path.exists(path):
291                os.remove(path)
292
293
294class TestRunnerDirectory(unittest.TestCase):
295
296    def setUp(self):
297        super(TestRunnerDirectory, self).setUp()
298        self.root = tempfile.mkdtemp()
299        self.save_stdout = sys.stdout
300        self.save_stderr = sys.stdout
301        sys.stdout = StringIO()
302        sys.stderr = StringIO()
303        self.expect = ''
304        self.cmd = "/bin/true"
305        if not os.path.exists(self.cmd):
306            self.cmd = "/usr/bin/true"  # Mac OS X
307
308    def tearDown(self):
309        shutil.rmtree(self.root)
310        got = sys.stdout.getvalue()
311        err = sys.stderr.getvalue()
312        sys.stdout = self.save_stdout
313        sys.stderr = self.save_stderr
314        if err:
315            print(err, end='', file=sys.stderr)
316        self.assertEqual(self.expect, got)
317        super(TestRunnerDirectory, self).tearDown()
318
319    def run_ctl(self, opts):
320        options = zdctl.ZDCtlOptions()
321        options.realize(opts + ['fg'])
322        self.expect = self.cmd + '\n'
323        proc = zdctl.ZDCmd(options)
324        proc.onecmd(" ".join(options.args))
325
326    def testCtlRunDirectoryCreation(self):
327        path = os.path.join(self.root, 'rundir')
328        self.run_ctl(['-z', path, '-p', self.cmd])
329        self.assertTrue(os.path.exists(path))
330
331    def testCtlRunDirectoryCreationFromConfigFile(self):
332        path = os.path.join(self.root, 'rundir')
333        options = ['directory ' + path,
334                   'program ' + self.cmd]
335        config = self.writeConfig(
336            '<runner>\n%s\n</runner>' % '\n'.join(options))
337        self.run_ctl(['-C', config])
338        self.assertTrue(os.path.exists(path))
339
340    def testCtlRunDirectoryCreationOnlyOne(self):
341        path = os.path.join(self.root, 'rundir', 'not-created')
342        self.assertRaises(SystemExit,
343                          self.run_ctl, ['-z', path, '-p', self.cmd])
344        self.assertFalse(os.path.exists(path))
345        got = sys.stderr.getvalue().strip()
346        sys.stderr = StringIO()
347        self.assertTrue(got.startswith('Error: invalid value for -z'))
348
349    def testCtlSocketDirectoryCreation(self):
350        path = os.path.join(self.root, 'rundir', 'sock')
351        self.run_ctl(['-s', path, '-p', self.cmd])
352        self.assertTrue(os.path.exists(os.path.dirname(path)))
353
354    def testCtlSocketDirectoryCreationRelativePath(self):
355        path = os.path.join('rundir', 'sock')
356        self.run_ctl(['-s', path, '-p', self.cmd])
357        self.assertTrue(
358            os.path.exists(os.path.dirname(os.path.join(os.getcwd(), path))))
359
360    def testCtlSocketDirectoryCreationOnlyOne(self):
361        path = os.path.join(self.root, 'rundir', 'not-created', 'sock')
362        self.assertRaises(SystemExit,
363                          self.run_ctl, ['-s', path, '-p', self.cmd])
364        self.assertFalse(os.path.exists(path))
365        got = sys.stderr.getvalue().strip()
366        sys.stderr = StringIO()
367        self.assertTrue(got.startswith('Error: invalid value for -s'))
368
369    def testCtlSocketDirectoryCreationFromConfigFile(self):
370        path = os.path.join(self.root, 'rundir')
371        options = ['socket-name %s/sock' % path,
372                   'program ' + self.cmd]
373        config = self.writeConfig(
374            '<runner>\n%s\n</runner>' % '\n'.join(options))
375        self.run_ctl(['-C', config])
376        self.assertTrue(os.path.exists(path))
377
378    def testCtlSocketDirectoryCreationFromConfigFileRelativePath(self):
379        path = 'rel-rundir'
380        options = ['socket-name %s/sock' % path,
381                   'program ' + self.cmd]
382        config = self.writeConfig(
383            '<runner>\n%s\n</runner>' % '\n'.join(options))
384        self.run_ctl(['-C', config])
385        self.assertTrue(os.path.exists(os.path.join(os.getcwd(), path)))
386
387    def writeConfig(self, config):
388        config_file = os.path.join(self.root, 'config')
389        with open(config_file, 'w') as f:
390            f.write(config)
391        return config_file
392
393    def testDirectoryChown(self):
394        path = os.path.join(self.root, 'foodir')
395        options = zdctl.ZDCtlOptions()
396        options.realize(['-p', self.cmd, 'status'])
397        cmd = zdctl.ZDCmd(options)
398        options.uid = 27
399        options.gid = 28
400        # Patch chown and geteuid, because we're not root
401        chown = os.chown
402        geteuid = os.geteuid
403        calls = []
404
405        def my_chown(*args):
406            calls.append(('chown',) + args)
407
408        def my_geteuid():
409            return 0
410
411        try:
412            os.chown = my_chown
413            os.geteuid = my_geteuid
414            cmd.create_directory(path)
415        finally:
416            os.chown = chown
417            os.geteuid = geteuid
418        self.assertEqual([('chown', path, 27, 28)], calls)
419
420
421def send_action(action, sockname, raise_on_error=False):
422    """Send an action to the zdrun server and return the response.
423
424    Return None if the server is not up or any other error happened.
425    """
426    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
427    try:
428        sock.connect(sockname)
429        sock.send(action.encode() + b"\n")
430        sock.shutdown(1)  # We're not writing any more
431        response = b""
432        while 1:
433            data = sock.recv(1000)
434            if not data:
435                break
436            response += data
437        sock.close()
438        return response
439    except socket.error as msg:
440        if str(msg) == 'AF_UNIX path too long':
441            # MacOS has apparent small limits on the length of a UNIX
442            # domain socket filename, we want to make MacOS users aware
443            # of the actual problem
444            raise
445        if raise_on_error:
446            raise
447        return None
448    finally:
449        sock.close()
450
451
452def test_suite():
453    suite = unittest.TestSuite()
454    if os.name == "posix":
455        suite.addTest(unittest.makeSuite(ZDaemonTests))
456        suite.addTest(unittest.makeSuite(TestRunnerDirectory))
457    return suite
458
459if __name__ == '__main__':
460    __file__ = sys.argv[0]
461    unittest.main(defaultTest='test_suite')
462