From 7d384f92539b97cdf12fe9d0a76cd09d9e3e04a2 Mon Sep 17 00:00:00 2001
From: Waket Zheng <waketzheng@gmail.com>
Date: Fri, 30 Aug 2024 00:54:43 +0800
Subject: [PATCH] Support `directory` expansions

---
 CHANGES.rst                      |  2 ++
 docs/configuration.rst           |  4 +++-
 supervisor/options.py            |  3 +++
 supervisor/tests/test_options.py | 23 ++++++++++++++++-------
 4 files changed, 24 insertions(+), 8 deletions(-)

diff --git a/CHANGES.rst b/CHANGES.rst
index c09f64747..e26ea7fd1 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,6 +1,8 @@
 4.3.0.dev0 (Next Release)
 -------------------------
 
+- Support ``directory`` expansions. Patch by Waket Zheng.
+
 - Fixed a bug where the poller would not unregister a closed
   file descriptor under some circumstances, which caused excessive
   polling, resulting in higher CPU usage.  Patch by aftersnow.
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 9029c2b4d..66098f4af 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -642,6 +642,7 @@ where specified.
   expressions are evaluated against a dictionary containing the keys
   ``group_name``, ``host_node_name``, ``program_name``, ``process_num``,
   ``numprocs``, ``here`` (the directory of the supervisord config file),
+  ``directory`` (if set in this section), ``user`` (if set in section),
   and all supervisord's environment variables prefixed with ``ENV_``.
   Controlled programs should themselves not be daemons, as supervisord
   assumes it is responsible for daemonizing its subprocesses (see
@@ -916,7 +917,8 @@ where specified.
   can contain Python string expressions that will evaluated against a
   dictionary that contains the keys ``group_name``, ``host_node_name``,
   ``process_num``, ``program_name``, and ``here`` (the directory of the
-  supervisord config file).
+  supervisord config file). If ``directory`` section is set, the value
+  ``%(directory)s`` can be used.
 
   .. note::
 
diff --git a/supervisor/options.py b/supervisor/options.py
index 271735200..ff8a6fa34 100644
--- a/supervisor/options.py
+++ b/supervisor/options.py
@@ -943,6 +943,7 @@ def get(section, opt, *args, **kwargs):
             uid = None
         else:
             uid = name_to_uid(user)
+            common_expansions['user'] = user
 
         umask = get(section, 'umask', None)
         if umask is not None:
@@ -977,6 +978,8 @@ def get(section, opt, *args, **kwargs):
                 expansions['ENV_%s' % k] = v
 
             directory = get(section, 'directory', None)
+            if directory is not None:
+                expansions['directory'] = directory
 
             logfiles = {}
 
diff --git a/supervisor/tests/test_options.py b/supervisor/tests/test_options.py
index 4f3ff71de..1db998348 100644
--- a/supervisor/tests/test_options.py
+++ b/supervisor/tests/test_options.py
@@ -1692,7 +1692,7 @@ def test_processes_from_section(self):
         instance = self._makeOne()
         text = lstrip("""\
         [program:foo]
-        command = /bin/cat
+        command = /bin/cat /%(user)s/.vimrc
         priority = 1
         autostart = false
         autorestart = false
@@ -1719,7 +1719,7 @@ def test_processes_from_section(self):
         self.assertEqual(len(pconfigs), 2)
         pconfig = pconfigs[0]
         self.assertEqual(pconfig.name, 'bar_foo_00')
-        self.assertEqual(pconfig.command, '/bin/cat')
+        self.assertEqual(pconfig.command, '/bin/cat /root/.vimrc')
         self.assertEqual(pconfig.autostart, False)
         self.assertEqual(pconfig.autorestart, False)
         self.assertEqual(pconfig.startsecs, 100)
@@ -1753,13 +1753,21 @@ def test_processes_from_section_host_node_name_expansion(self):
 
     def test_processes_from_section_process_num_expansion(self):
         instance = self._makeOne()
+        nums = (0, 1)
+        for num in nums:
+            log_dir = '/tmp/foo_{0}/foo_{0}_stdout'.format(num)
+            if not os.path.exists(log_dir):
+                parent = os.path.dirname(log_dir)
+                if not os.path.exists(parent):
+                    os.mkdir(parent)
+                os.mkdir(log_dir)
         text = lstrip("""\
         [program:foo]
         process_name = foo_%(process_num)d
-        command = /bin/foo --num=%(process_num)d
+        command = /bin/foo --num=%(process_num)d --dir=%(directory)s
         directory = /tmp/foo_%(process_num)d
         stderr_logfile = /tmp/foo_%(process_num)d_stderr
-        stdout_logfile = /tmp/foo_%(process_num)d_stdout
+        stdout_logfile = %(directory)s/foo_%(process_num)d_stdout
         environment = NUM=%(process_num)d
         numprocs = 2
         """)
@@ -1768,14 +1776,15 @@ def test_processes_from_section_process_num_expansion(self):
         config.read_string(text)
         pconfigs = instance.processes_from_section(config, 'program:foo', 'bar')
         self.assertEqual(len(pconfigs), 2)
-        for num in (0, 1):
+        for num in nums:
             self.assertEqual(pconfigs[num].name, 'foo_%d' % num)
-            self.assertEqual(pconfigs[num].command, "/bin/foo --num=%d" % num)
+            self.assertEqual(pconfigs[num].command,
+                "/bin/foo --num=%d --dir=/tmp/foo_%d" % (num, num))
             self.assertEqual(pconfigs[num].directory, '/tmp/foo_%d' % num)
             self.assertEqual(pconfigs[num].stderr_logfile,
                 '/tmp/foo_%d_stderr' % num)
             self.assertEqual(pconfigs[num].stdout_logfile,
-                '/tmp/foo_%d_stdout' % num)
+                '/tmp/foo_%d/foo_%d_stdout' % (num, num))
             self.assertEqual(pconfigs[num].environment, {'NUM': '%d' % num})
 
     def test_processes_from_section_numprocs_expansion(self):