Description
Scenario
I stumbled upon this issue when using fabric2 where I had a task which would execute alembic to upgrade/downgrade multiple local databases. When executing the task it would sometimes (completely randomly) fail with the following exception.
Please note the following is only happening when pty
is set to False
.
Version of invoke used 1.3.0.
Encountered a bad command exit code! Command: './env/bin/alembic upgrade head' Exit code: None Stdout: already printed Stderr: already printed
Reproduction of the bug
I replicated the fabric task to a pure invoke task like shown here. When executing the following code I get randomly the error mentioned above. I will execute the task multiple times to see that the error occurs in different runs every time.
Code
from invoke import task
@task
def upgrade(ctx):
for index in range(1, 5):
try:
print("Run upgrade")
ctx.run("./env/bin/alembic upgrade head")
except Exception as exc:
print(exc)
First run
$ ./env/bin/invoke upgrade
Run upgrade 1
Run upgrade 2
Run upgrade 3
Run upgrade 4
Run upgrade 5
Encountered a bad command exit code!
Command: './env/bin/alembic upgrade head'
Exit code: None
Stdout: already printed
Stderr: already printed
Run upgrade 6
Encountered a bad command exit code!
Command: './env/bin/alembic upgrade head'
Exit code: None
Stdout: already printed
Stderr: already printed
Run upgrade 7
Encountered a bad command exit code!
Command: './env/bin/alembic upgrade head'
Exit code: None
Stdout: already printed
Stderr: already printed
Run upgrade 8
Run upgrade 9
Second run
$ ./env/bin/invoke upgrade
Run upgrade 1
Encountered a bad command exit code!
Command: './env/bin/alembic upgrade head'
Exit code: None
Stdout: already printed
Stderr: already printed
Run upgrade 2
Run upgrade 3
Encountered a bad command exit code!
Command: './env/bin/alembic upgrade head'
Exit code: None
Stdout: already printed
Stderr: already printed
Run upgrade 4
Run upgrade 5
Encountered a bad command exit code!
Command: './env/bin/alembic upgrade head'
Exit code: None
Stdout: already printed
Stderr: already printed
Run upgrade 6
Encountered a bad command exit code!
Command: './env/bin/alembic upgrade head'
Exit code: None
Stdout: already printed
Stderr: already printed
Run upgrade 7
Run upgrade 8
Run upgrade 9
Investigation
After investigating the issue with one of my colleagues we might have identified the issue. However this is merely an assumption rather than a certainty. It seems that the threads that are being spawned for the Runners are trying to access Local.returncode
function before the process has had a chance to finish without waiting on the process to finish.
I've tried changing the code of returncode
by adding Popen wait call which seems to fix the issue I had.
def returncode(self):
if self.using_pty:
# No subprocess.returncode available; use WIFEXITED/WIFSIGNALED to
# determine whch of WEXITSTATUS / WTERMSIG to use.
# TODO: is it safe to just say "call all WEXITSTATUS/WTERMSIG and
# return whichever one of them is nondefault"? Probably not?
# NOTE: doing this in an arbitrary order should be safe since only
# one of the WIF* methods ought to ever return True.
code = None
if os.WIFEXITED(self.status):
code = os.WEXITSTATUS(self.status)
elif os.WIFSIGNALED(self.status):
code = os.WTERMSIG(self.status)
# Match subprocess.returncode by turning signals into negative
# 'exit code' integers.
code = -1 * code
return code
# TODO: do we care about WIFSTOPPED? Maybe someday?
else:
self.process.wait(timeout=3)
return self.process.returncode
$ ./env/bin/invoke upgrade
Run upgrade 1
Run upgrade 2
Run upgrade 3
Run upgrade 4
Run upgrade 5
Run upgrade 6
Run upgrade 7
Run upgrade 8
Run upgrade 9
Please consider the following pull request #661