lib.state_helpers
1import shlex 2from ipaddress import IPv4Address, IPv4Interface 3 4from SRE.lib_sre import NetScheme0 5 6 7def set_unbound_server(net_scheme: NetScheme0, machine: str): 8 """Write a permissive Unbound DNS config and start the service on *machine*.""" 9 set_basic_unbound_server(net_scheme=net_scheme, machine=machine) 10 11 12def set_basic_unbound_server(net_scheme: NetScheme0, machine: str): 13 """Write /etc/unbound/unbound.conf (listen on 0.0.0.0, allow all) and start unbound on *machine*.""" 14 net_scheme.file(machine=machine, filename='/etc/unbound/unbound.conf', content=""" 15# Unbound configuration file for Debian. 16# 17# See the unbound.conf(5) man page. 18# 19# See /usr/share/doc/unbound/examples/unbound.conf for a commented 20# reference config file. 21# 22# The following line includes additional configuration files from the 23# /etc/unbound/unbound.conf.d directory. 24include-toplevel: "/etc/unbound/unbound.conf.d/*.conf" 25server: 26 interface: 0.0.0.0 27 access-control: 0.0.0.0/0 allow 28""") 29 net_scheme.cmd(machine, "systemctl start unbound") 30 31 32def set_nat_gateway(net_scheme: NetScheme0, machine: str): 33 """Add an iptables MASQUERADE rule on *machine*'s bridged interface. 34 35 The machine must have been declared with ``bridged=True``; Kathara appends 36 the bridged interface as the next ``eth{N}`` after all topology-defined 37 adapters (i.e. its index equals the highest assigned interface number + 1). 38 """ 39 m = net_scheme.get_machine(machine) 40 iface_numbers = [a.interface for a in m.net_adapters.values()] 41 bridged_iface = (max(iface_numbers) + 1) if iface_numbers else 0 42 net_scheme.cmd(machine, 43 f"sh -c 'iptables -t nat -C POSTROUTING -o eth{bridged_iface} -j MASQUERADE 2>/dev/null" 44 f" || iptables -t nat -A POSTROUTING -o eth{bridged_iface} -j MASQUERADE'") 45 46 47def hosts_file_content(net_scheme: NetScheme0, domain_extension: str, included=None, ips=None, 48 separator: str = "\t\t") -> str: 49 """Return /etc/hosts lines for the given machines. 50 51 Args: 52 net_scheme: the NetScheme0 instance 53 domain_extension: domain suffix (e.g. 'example.com') 54 included: list of machine names; defaults to get_visibles_machines() 55 ips: dict {machine_name: [IPv4Interface|IPv4Address, ...]} one ip per network, 56 in the same order as host_interfaces_from_topology(). 57 If None, addresses are read from net_scheme.data.ips.* 58 separator: string placed between fields (default: two tabs) 59 """ 60 if included is None: 61 included = [m.name for m in net_scheme.get_visibles_machines()] 62 63 machine_nets = net_scheme.host_interfaces_from_topology() 64 lines = [] 65 66 for machine_name in included: 67 nets = machine_nets.get(machine_name, []) 68 single = len(nets) == 1 69 70 if ips is not None: 71 addrs = ips.get(machine_name, []) 72 for i, net_name in enumerate(nets): 73 ip = str(addrs[i]).split('/')[0] if i < len(addrs) else None 74 if ip is None: 75 continue 76 if single: 77 lines.append(f"{ip}{separator}{machine_name}{separator}{machine_name}.{domain_extension}") 78 else: 79 lines.append( 80 f"{ip}{separator}{machine_name}_{net_name}{separator}{machine_name}_{net_name}.{domain_extension}") 81 else: 82 for net_name in nets: 83 attr = machine_name if single else f"{machine_name}_{net_name}" 84 ip_obj = getattr(net_scheme.data.ips, attr, None) 85 if ip_obj is None: 86 continue 87 ip = str(ip_obj).split('/')[0] 88 if single: 89 lines.append(f"{ip}{separator}{machine_name}{separator}{machine_name}.{domain_extension}") 90 else: 91 lines.append( 92 f"{ip}{separator}{machine_name}_{net_name}{separator}{machine_name}_{net_name}.{domain_extension}") 93 94 return '\n'.join(lines) + '\n' if lines else '' 95 96 97def create_hosts_file(net_scheme: NetScheme0, domain_extension: str, machine_list=None, included=None, ips=None, 98 separator: str = "\t\t"): 99 """Write /etc/hosts to each machine in machine_list. 100 101 Each file starts with the standard loopback entries (127.0.0.1 localhost and 102 127.0.1.1 for the machine itself), followed by the lines produced by 103 hosts_file_content() for the machines in included. 104 105 Args: 106 net_scheme: the NetScheme0 instance 107 domain_extension: domain suffix appended to every hostname (e.g. 'example.com') 108 machine_list: machines that receive the /etc/hosts file; defaults to get_visibles_machines() 109 included: machines whose entries appear in the hosts table; passed through to 110 hosts_file_content() — defaults to get_visibles_machines() when None 111 ips: dict {machine_name: [IPv4Interface|IPv4Address, ...]} — see hosts_file_content() 112 separator: string placed between fields (default: two tabs) 113 """ 114 if machine_list is None: 115 machine_list = included if included is not None else [m.name for m in net_scheme.get_visibles_machines()] 116 117 hosts = hosts_file_content(net_scheme=net_scheme, domain_extension=domain_extension, included=included, ips=ips, 118 separator=separator) 119 120 for m in machine_list: 121 hosts_start = f"127.0.0.1\t\tlocalhost\n127.0.1.1\t\t{m}\t\t{m}.{domain_extension}\n" 122 net_scheme.file(machine=m, filename='/etc/hosts', content=hosts_start + hosts, permissions=0o0644, 123 owner="root:root") 124 125 126def change_password(net_scheme: NetScheme0, machine: str, username: str, password: str): 127 """Set *username*'s password on *machine* via chpasswd. 128 129 The password is written to a temporary file (never passed on the command line). 130 """ 131 # Write "username:password" to a file so the password never appears in a shell command. 132 net_scheme.file(machine=machine, filename='/tmp/.sre_chpasswd', 133 content=f'{username}:{password}\n', permissions=0o600) 134 net_scheme.cmd(machine, 'sh -c "chpasswd < /tmp/.sre_chpasswd; rm -f /tmp/.sre_chpasswd"') 135 136 137def create_user(net_scheme: NetScheme0, machine: str, username: str, password: str, uid: int = None, gid: int = None, shell: str = "/bin/bash"): 138 """Create *username* on *machine* (if not already present) and set its password. 139 140 Uses ``useradd -m`` with optional *uid*/*gid* and login *shell*. The password is written to a 141 temporary file; the username is passed via an environment variable to prevent shell injection. 142 """ 143 # Write "username:password" to a file so the password never appears in a shell command. 144 net_scheme.file(machine=machine, filename='/tmp/.sre_chpasswd', 145 content=f'{username}:{password}\n', permissions=0o600) 146 useradd_opts = f" -s {shlex.quote(shell)}" 147 if uid is not None: 148 useradd_opts += f" -u {int(uid)}" 149 if gid is not None: 150 useradd_opts += f" -g {int(gid)}" 151 # Pass the username via an env var so no user-controlled text appears inside the sh -c string. 152 # "$SRE_USER" is double-quoted in the shell command to prevent word-splitting and glob expansion. 153 net_scheme.cmd(machine, 154 f"env SRE_USER={shlex.quote(username)} " 155 f"sh -c 'id \"$SRE_USER\" >/dev/null 2>&1" 156 f" || useradd{useradd_opts} -m -k /etc/skel \"$SRE_USER\";" 157 f" chpasswd < /tmp/.sre_chpasswd; rm -f /tmp/.sre_chpasswd'") 158 159 160def setup_simple_tcp_server(net_scheme: NetScheme0, machine: str, port: int, answer: str, 161 ip: "str | IPv4Interface | IPv4Address" = None): 162 """Setup and (re)launch an idempotent TCP server on *machine*. 163 164 The server listens on *port* — bound to *ip* if provided (the network prefix of an 165 ``IPv4Interface`` is stripped), or to ``0.0.0.0`` otherwise. On each client connection 166 it sends *answer* (UTF-8) and closes the socket. Calling this function again for the 167 same *port* kills the previous instance before relaunching. 168 """ 169 if ip is None: 170 bind_addr = "0.0.0.0" 171 elif isinstance(ip, IPv4Interface): 172 bind_addr = str(ip.ip) 173 else: 174 bind_addr = str(ip).split('/')[0] 175 176 script_path = f"/usr/local/sbin/sre_tcp_server_{port}.py" 177 answer_file = f"/var/lib/sre_tcp_server_{port}.answer" 178 log_file = f"/var/log/sre_tcp_server_{port}.log" 179 pid_file = f"/run/sre_tcp_server_{port}.pid" 180 181 net_scheme.file(machine=machine, filename=answer_file, content=answer, permissions=0o644) 182 183 # The script double-forks AND closes the stdio fds inherited from docker exec — 184 # otherwise exec_run keeps streaming and the state op hangs forever. After the 185 # second fork, fds 0/1/2 are reopened on /dev/null (stdin) and the per-port log 186 # file (stdout/stderr), so binding/startup failures land in the log. The daemon 187 # also writes its PID to a per-port file so the launcher can kill the previous 188 # instance without using `pkill -f` (which would match the launcher's own sh -c 189 # argument and kill the shell before the python3 command runs). 190 script_content = ( 191 "#!/usr/bin/env python3\n" 192 "import os, socket, traceback\n" 193 "if os.fork() != 0: os._exit(0)\n" 194 "os.setsid()\n" 195 "if os.fork() != 0: os._exit(0)\n" 196 "# detach from docker exec's stdio so exec_run can return\n" 197 "for fd in (0, 1, 2):\n" 198 " try: os.close(fd)\n" 199 " except OSError: pass\n" 200 "os.open(os.devnull, os.O_RDONLY) # fd 0\n" 201 f"_log = os.open({log_file!r}, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644) # fd 1\n" 202 "os.dup2(_log, 2) # fd 2 = log\n" 203 f"with open({pid_file!r}, 'w') as _pf: _pf.write(str(os.getpid()))\n" 204 "try:\n" 205 f" with open({answer_file!r}, 'rb') as f:\n" 206 " answer = f.read()\n" 207 " s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n" 208 " s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n" 209 f" s.bind(({bind_addr!r}, {int(port)}))\n" 210 " s.listen(16)\n" 211 " while True:\n" 212 " conn, _ = s.accept()\n" 213 " try:\n" 214 " conn.sendall(answer)\n" 215 " finally:\n" 216 " conn.close()\n" 217 "except Exception:\n" 218 " traceback.print_exc()\n" 219 " os._exit(1)\n" 220 ) 221 net_scheme.file(machine=machine, filename=script_path, content=script_content, permissions=0o755) 222 223 quoted_script = shlex.quote(script_path) 224 quoted_pidfile = shlex.quote(pid_file) 225 # If a previous instance left a PID file, kill it and give the kernel a moment 226 # to release the port; ignore stale PIDs. Then launch the new daemon. The script 227 # daemonizes itself, so this command returns immediately. 228 net_scheme.cmd(machine, 229 f"sh -c '[ -f {quoted_pidfile} ] && kill $(cat {quoted_pidfile}) 2>/dev/null; " 230 f"sleep 0.2; " 231 f"python3 {quoted_script}'")
8def set_unbound_server(net_scheme: NetScheme0, machine: str): 9 """Write a permissive Unbound DNS config and start the service on *machine*.""" 10 set_basic_unbound_server(net_scheme=net_scheme, machine=machine)
Write a permissive Unbound DNS config and start the service on machine.
13def set_basic_unbound_server(net_scheme: NetScheme0, machine: str): 14 """Write /etc/unbound/unbound.conf (listen on 0.0.0.0, allow all) and start unbound on *machine*.""" 15 net_scheme.file(machine=machine, filename='/etc/unbound/unbound.conf', content=""" 16# Unbound configuration file for Debian. 17# 18# See the unbound.conf(5) man page. 19# 20# See /usr/share/doc/unbound/examples/unbound.conf for a commented 21# reference config file. 22# 23# The following line includes additional configuration files from the 24# /etc/unbound/unbound.conf.d directory. 25include-toplevel: "/etc/unbound/unbound.conf.d/*.conf" 26server: 27 interface: 0.0.0.0 28 access-control: 0.0.0.0/0 allow 29""") 30 net_scheme.cmd(machine, "systemctl start unbound")
Write /etc/unbound/unbound.conf (listen on 0.0.0.0, allow all) and start unbound on machine.
33def set_nat_gateway(net_scheme: NetScheme0, machine: str): 34 """Add an iptables MASQUERADE rule on *machine*'s bridged interface. 35 36 The machine must have been declared with ``bridged=True``; Kathara appends 37 the bridged interface as the next ``eth{N}`` after all topology-defined 38 adapters (i.e. its index equals the highest assigned interface number + 1). 39 """ 40 m = net_scheme.get_machine(machine) 41 iface_numbers = [a.interface for a in m.net_adapters.values()] 42 bridged_iface = (max(iface_numbers) + 1) if iface_numbers else 0 43 net_scheme.cmd(machine, 44 f"sh -c 'iptables -t nat -C POSTROUTING -o eth{bridged_iface} -j MASQUERADE 2>/dev/null" 45 f" || iptables -t nat -A POSTROUTING -o eth{bridged_iface} -j MASQUERADE'")
Add an iptables MASQUERADE rule on machine's bridged interface.
The machine must have been declared with bridged=True; Kathara appends
the bridged interface as the next eth{N} after all topology-defined
adapters (i.e. its index equals the highest assigned interface number + 1).
48def hosts_file_content(net_scheme: NetScheme0, domain_extension: str, included=None, ips=None, 49 separator: str = "\t\t") -> str: 50 """Return /etc/hosts lines for the given machines. 51 52 Args: 53 net_scheme: the NetScheme0 instance 54 domain_extension: domain suffix (e.g. 'example.com') 55 included: list of machine names; defaults to get_visibles_machines() 56 ips: dict {machine_name: [IPv4Interface|IPv4Address, ...]} one ip per network, 57 in the same order as host_interfaces_from_topology(). 58 If None, addresses are read from net_scheme.data.ips.* 59 separator: string placed between fields (default: two tabs) 60 """ 61 if included is None: 62 included = [m.name for m in net_scheme.get_visibles_machines()] 63 64 machine_nets = net_scheme.host_interfaces_from_topology() 65 lines = [] 66 67 for machine_name in included: 68 nets = machine_nets.get(machine_name, []) 69 single = len(nets) == 1 70 71 if ips is not None: 72 addrs = ips.get(machine_name, []) 73 for i, net_name in enumerate(nets): 74 ip = str(addrs[i]).split('/')[0] if i < len(addrs) else None 75 if ip is None: 76 continue 77 if single: 78 lines.append(f"{ip}{separator}{machine_name}{separator}{machine_name}.{domain_extension}") 79 else: 80 lines.append( 81 f"{ip}{separator}{machine_name}_{net_name}{separator}{machine_name}_{net_name}.{domain_extension}") 82 else: 83 for net_name in nets: 84 attr = machine_name if single else f"{machine_name}_{net_name}" 85 ip_obj = getattr(net_scheme.data.ips, attr, None) 86 if ip_obj is None: 87 continue 88 ip = str(ip_obj).split('/')[0] 89 if single: 90 lines.append(f"{ip}{separator}{machine_name}{separator}{machine_name}.{domain_extension}") 91 else: 92 lines.append( 93 f"{ip}{separator}{machine_name}_{net_name}{separator}{machine_name}_{net_name}.{domain_extension}") 94 95 return '\n'.join(lines) + '\n' if lines else ''
Return /etc/hosts lines for the given machines.
Arguments:
- net_scheme: the NetScheme0 instance
- domain_extension: domain suffix (e.g. 'example.com')
- included: list of machine names; defaults to get_visibles_machines()
- ips: dict {machine_name: [IPv4Interface|IPv4Address, ...]} one ip per network, in the same order as host_interfaces_from_topology(). If None, addresses are read from net_scheme.data.ips.*
- separator: string placed between fields (default: two tabs)
98def create_hosts_file(net_scheme: NetScheme0, domain_extension: str, machine_list=None, included=None, ips=None, 99 separator: str = "\t\t"): 100 """Write /etc/hosts to each machine in machine_list. 101 102 Each file starts with the standard loopback entries (127.0.0.1 localhost and 103 127.0.1.1 for the machine itself), followed by the lines produced by 104 hosts_file_content() for the machines in included. 105 106 Args: 107 net_scheme: the NetScheme0 instance 108 domain_extension: domain suffix appended to every hostname (e.g. 'example.com') 109 machine_list: machines that receive the /etc/hosts file; defaults to get_visibles_machines() 110 included: machines whose entries appear in the hosts table; passed through to 111 hosts_file_content() — defaults to get_visibles_machines() when None 112 ips: dict {machine_name: [IPv4Interface|IPv4Address, ...]} — see hosts_file_content() 113 separator: string placed between fields (default: two tabs) 114 """ 115 if machine_list is None: 116 machine_list = included if included is not None else [m.name for m in net_scheme.get_visibles_machines()] 117 118 hosts = hosts_file_content(net_scheme=net_scheme, domain_extension=domain_extension, included=included, ips=ips, 119 separator=separator) 120 121 for m in machine_list: 122 hosts_start = f"127.0.0.1\t\tlocalhost\n127.0.1.1\t\t{m}\t\t{m}.{domain_extension}\n" 123 net_scheme.file(machine=m, filename='/etc/hosts', content=hosts_start + hosts, permissions=0o0644, 124 owner="root:root")
Write /etc/hosts to each machine in machine_list.
Each file starts with the standard loopback entries (127.0.0.1 localhost and 127.0.1.1 for the machine itself), followed by the lines produced by hosts_file_content() for the machines in included.
Arguments:
- net_scheme: the NetScheme0 instance
- domain_extension: domain suffix appended to every hostname (e.g. 'example.com')
- machine_list: machines that receive the /etc/hosts file; defaults to get_visibles_machines()
- included: machines whose entries appear in the hosts table; passed through to hosts_file_content() — defaults to get_visibles_machines() when None
- ips: dict {machine_name: [IPv4Interface|IPv4Address, ...]} — see hosts_file_content()
- separator: string placed between fields (default: two tabs)
127def change_password(net_scheme: NetScheme0, machine: str, username: str, password: str): 128 """Set *username*'s password on *machine* via chpasswd. 129 130 The password is written to a temporary file (never passed on the command line). 131 """ 132 # Write "username:password" to a file so the password never appears in a shell command. 133 net_scheme.file(machine=machine, filename='/tmp/.sre_chpasswd', 134 content=f'{username}:{password}\n', permissions=0o600) 135 net_scheme.cmd(machine, 'sh -c "chpasswd < /tmp/.sre_chpasswd; rm -f /tmp/.sre_chpasswd"')
Set username's password on machine via chpasswd.
The password is written to a temporary file (never passed on the command line).
138def create_user(net_scheme: NetScheme0, machine: str, username: str, password: str, uid: int = None, gid: int = None, shell: str = "/bin/bash"): 139 """Create *username* on *machine* (if not already present) and set its password. 140 141 Uses ``useradd -m`` with optional *uid*/*gid* and login *shell*. The password is written to a 142 temporary file; the username is passed via an environment variable to prevent shell injection. 143 """ 144 # Write "username:password" to a file so the password never appears in a shell command. 145 net_scheme.file(machine=machine, filename='/tmp/.sre_chpasswd', 146 content=f'{username}:{password}\n', permissions=0o600) 147 useradd_opts = f" -s {shlex.quote(shell)}" 148 if uid is not None: 149 useradd_opts += f" -u {int(uid)}" 150 if gid is not None: 151 useradd_opts += f" -g {int(gid)}" 152 # Pass the username via an env var so no user-controlled text appears inside the sh -c string. 153 # "$SRE_USER" is double-quoted in the shell command to prevent word-splitting and glob expansion. 154 net_scheme.cmd(machine, 155 f"env SRE_USER={shlex.quote(username)} " 156 f"sh -c 'id \"$SRE_USER\" >/dev/null 2>&1" 157 f" || useradd{useradd_opts} -m -k /etc/skel \"$SRE_USER\";" 158 f" chpasswd < /tmp/.sre_chpasswd; rm -f /tmp/.sre_chpasswd'")
Create username on machine (if not already present) and set its password.
Uses useradd -m with optional uid/gid and login shell. The password is written to a
temporary file; the username is passed via an environment variable to prevent shell injection.
161def setup_simple_tcp_server(net_scheme: NetScheme0, machine: str, port: int, answer: str, 162 ip: "str | IPv4Interface | IPv4Address" = None): 163 """Setup and (re)launch an idempotent TCP server on *machine*. 164 165 The server listens on *port* — bound to *ip* if provided (the network prefix of an 166 ``IPv4Interface`` is stripped), or to ``0.0.0.0`` otherwise. On each client connection 167 it sends *answer* (UTF-8) and closes the socket. Calling this function again for the 168 same *port* kills the previous instance before relaunching. 169 """ 170 if ip is None: 171 bind_addr = "0.0.0.0" 172 elif isinstance(ip, IPv4Interface): 173 bind_addr = str(ip.ip) 174 else: 175 bind_addr = str(ip).split('/')[0] 176 177 script_path = f"/usr/local/sbin/sre_tcp_server_{port}.py" 178 answer_file = f"/var/lib/sre_tcp_server_{port}.answer" 179 log_file = f"/var/log/sre_tcp_server_{port}.log" 180 pid_file = f"/run/sre_tcp_server_{port}.pid" 181 182 net_scheme.file(machine=machine, filename=answer_file, content=answer, permissions=0o644) 183 184 # The script double-forks AND closes the stdio fds inherited from docker exec — 185 # otherwise exec_run keeps streaming and the state op hangs forever. After the 186 # second fork, fds 0/1/2 are reopened on /dev/null (stdin) and the per-port log 187 # file (stdout/stderr), so binding/startup failures land in the log. The daemon 188 # also writes its PID to a per-port file so the launcher can kill the previous 189 # instance without using `pkill -f` (which would match the launcher's own sh -c 190 # argument and kill the shell before the python3 command runs). 191 script_content = ( 192 "#!/usr/bin/env python3\n" 193 "import os, socket, traceback\n" 194 "if os.fork() != 0: os._exit(0)\n" 195 "os.setsid()\n" 196 "if os.fork() != 0: os._exit(0)\n" 197 "# detach from docker exec's stdio so exec_run can return\n" 198 "for fd in (0, 1, 2):\n" 199 " try: os.close(fd)\n" 200 " except OSError: pass\n" 201 "os.open(os.devnull, os.O_RDONLY) # fd 0\n" 202 f"_log = os.open({log_file!r}, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644) # fd 1\n" 203 "os.dup2(_log, 2) # fd 2 = log\n" 204 f"with open({pid_file!r}, 'w') as _pf: _pf.write(str(os.getpid()))\n" 205 "try:\n" 206 f" with open({answer_file!r}, 'rb') as f:\n" 207 " answer = f.read()\n" 208 " s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n" 209 " s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n" 210 f" s.bind(({bind_addr!r}, {int(port)}))\n" 211 " s.listen(16)\n" 212 " while True:\n" 213 " conn, _ = s.accept()\n" 214 " try:\n" 215 " conn.sendall(answer)\n" 216 " finally:\n" 217 " conn.close()\n" 218 "except Exception:\n" 219 " traceback.print_exc()\n" 220 " os._exit(1)\n" 221 ) 222 net_scheme.file(machine=machine, filename=script_path, content=script_content, permissions=0o755) 223 224 quoted_script = shlex.quote(script_path) 225 quoted_pidfile = shlex.quote(pid_file) 226 # If a previous instance left a PID file, kill it and give the kernel a moment 227 # to release the port; ignore stale PIDs. Then launch the new daemon. The script 228 # daemonizes itself, so this command returns immediately. 229 net_scheme.cmd(machine, 230 f"sh -c '[ -f {quoted_pidfile} ] && kill $(cat {quoted_pidfile}) 2>/dev/null; " 231 f"sleep 0.2; " 232 f"python3 {quoted_script}'")
Setup and (re)launch an idempotent TCP server on machine.
The server listens on port — bound to ip if provided (the network prefix of an
IPv4Interface is stripped), or to 0.0.0.0 otherwise. On each client connection
it sends answer (UTF-8) and closes the socket. Calling this function again for the
same port kills the previous instance before relaunching.