0

I am trying to run a simple command that initiates a port forward before the execution of my automated tests but it hangs every time.

The end goal was to setup the port forward, get the PID and terminate the port forward, at the end of the session.

I am on macOS and using Python 3.9.7 and trying to execute this inside of PyCharm IDE.

Here is the code snippet:

def setup_port_forward():

     # command
     command = 'kubectl port-forward api_service 8080:80 -n service_name'

     # shell
     shell_script = subprocess.Popen(command,
                                shell=True,
                                stdin=subprocess.PIPE,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                start_new_session=True)

     # extract
     try:
         stdout, stderr = shell_script.communicate(timeout=15)
         pid = int(stdout.decode().strip().split(' ')[-1])
     except subprocess.TimeoutExpired:
         shell_script.kill()

     yield

     # kill session
     os.kill(pid, signal.SIGTERM)

I don't pretend to know what this does or how it works, because I am still learning python.

Here's a few threads I have looked at:

Python Script execute commands in Terminal

python subprocess.Popen hanging

Python Script execute commands in Terminal

Python hangs when executing a shell script that runs a process as a daemon

https://docs.python.org/3/library/subprocess.html#subprocess.Popen.communicate

Many threads say that using subprocess.PIPE in the shell script could cause issues, but again, on a different thread on how to get the PID, this was the method used.

I have tried using different ways as suggested in the different threads:

command = 'kubectl port-forward api_service 8080:80 -n service_name'

# 1
os.system(command)

# 2
subprocess.Popen(command).communicate

# 3
subprocess.run(command)

# 4
subprocess.call(command)

# 5
commands.getstatusoutput(command)

With all of them, they hang. Running this is terminal, it works fine.

Eitel Dagnin
  • 959
  • 4
  • 24
  • 61
  • What does "PF" mean? – mkrieger1 Nov 16 '21 at 14:37
  • @mkrieger1 It stands for port forward. I will change it :) – Eitel Dagnin Nov 16 '21 at 14:38
  • Where does the `yield` belong to? You can't just write `yield` outside a function. – mkrieger1 Nov 16 '21 at 14:39
  • @mkrieger1 That's correct, it was inside a function. I have updated the question again. :) – Eitel Dagnin Nov 16 '21 at 14:43
  • It's not clear what the timeout is supposed to achieve here. Does the process output information that you need? `Popen` already knows the `pid` of the process it creates. – tripleee Nov 16 '21 at 14:55
  • @tripleee I suppose the `timeout` was to allow sufficient time for the port forward session to start? I honestly don't know.. But nonetheless, I didn't know that I don't need to use `communicate` since the `subprocess.open` already 'initiates' the command. – Eitel Dagnin Nov 16 '21 at 15:17

1 Answers1

1

The main problem here is with the communicate. You just want to Popen the process, then leave it running until you kill it.

You will definitely want to avoid shell=True when you can; see also Actual meaning of shell=True in subprocess

I don't see that the stdout and stderr redirections are useful either. If you just want to run it quietly, probably just redirect to and from subprocess.DEVNULL instead.

Creating a separate session seems dubious here; I would perhaps drop that too.

Running Bash commands in Python has some guidance for which subprocess method to prefer under what circumstances. TLDR is subprocess.run for situations where you want to wait for the process to finish, subprocess.Popen when you don't (but then understand your responsibilities around managing the object).

def setup_port_forward():
    proc = subprocess.Popen(
        ['kubectl', 'port-forward', 'api_service', '8080:80', '-n',  'service_name'],
        stdin=subprocess.DEVNULL,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        start_new_session=True)
    yield
    # When done
    proc.kill()

The design with this function as a generator is also slightly weird; I would perhaps suggest you make this into a context manager instead.

tripleee
  • 175,061
  • 34
  • 275
  • 318
  • I'm not familiar with `kubectl` so it's possible that I'm misunderstanding your use case. Maybe [edit] your question to clarify it in that case. – tripleee Nov 16 '21 at 14:58
  • Thank you for this answer, truly appreciate it. :) I do at least understand a bit more now of what's happening and why. What do you mean to make it into context manager instead? I'm not familiar with the terminology unfortunately. – Eitel Dagnin Nov 16 '21 at 15:20
  • 1
    A context manager is something you can use in a `with` statement. Easy to google: https://docs.python.org/3/library/contextlib.html – tripleee Nov 16 '21 at 15:21
  • @tripleee, FYI, if you use `[edit]` in a comment below your answer, it will create a link to edit the answer, not the question. – wovano Nov 16 '21 at 15:27
  • Oh duh, I'm so used to using it in response to questions ... Thanks for the feedback. – tripleee Nov 16 '21 at 15:30
  • @tripleee I was wondering how to refactor more complex commands for this case? I see you split the string command into a list, but I have a more complex command and splitting it doesn't seem to work. It works if I say `shell=True`, but as you suggested (and as discussed in the linked question) I'd prefer not to use `shell=True`. – Eitel Dagnin Nov 17 '21 at 11:37
  • Example of the more complex command: `'kubectl port-forward $(kubectl get pod --selector="app=myapp" --field-selector=status.phase=Running --output jsonpath='"{.items[0].metadata.name}"') 8080:8080'` – Eitel Dagnin Nov 17 '21 at 11:38
  • The command substitution is a shell feature, you need to run that in a separate process if you want to avoid `shell=True`. Like `where = subprocess.run(['kubectl', 'get', 'pod', ...], capture=True, check=True); proc = subprocess.Popen(['kubectl', 'port-forward', where, ...])` – tripleee Nov 17 '21 at 12:29
  • Also maybe try `shlex.split()` if you are unsure how to transcribe the quoting correctly. In brief, the ones in the command substitution with quotes in them should be `... '--selector=app=myapp', ..., '--output', 'jsonpath="{.items[0].metadata.name}"']` – tripleee Nov 17 '21 at 12:32
  • Oh and you want `text=True` in the `where` call too. – tripleee Nov 17 '21 at 12:34
  • @tripleee alright, thank you so very much! :) I will look into it – Eitel Dagnin Nov 17 '21 at 14:33