Skip to content

Child mode: report the child's exit status, and say why it failed - #158

Open
rlerdorf wants to merge 1 commit into
adsr:masterfrom
rlerdorf:pr3-child-exit
Open

Child mode: report the child's exit status, and say why it failed#158
rlerdorf wants to merge 1 commit into
adsr:masterfrom
rlerdorf:pr3-child-exit

Conversation

@rlerdorf

Copy link
Copy Markdown

Three things that make phpspy -- cmd awkward, all in the same area.

1. The child's exit status is discarded

main_fork calls waitpid(fork_pid, NULL, 0), so:

$ phpspy -- php -r 'exit(3);'; echo $?
0
$ phpspy -- php broken.php; echo $?     # php exits 255
0

That makes phpspy hard to drop into a pipeline or a CI job. It now reports the child's status, as strace(1) and time(1) do.

I went back and forth on precedence and settled on the child's status winning over phpspy's own error. The reason is practical: a short-lived child routinely exits before phpspy finishes resolving symbols, so main_pid returns an error for exactly the quick commands most likely to be scripted. Preferring phpspy's error would mean the status is lost in the common case. Profiling failures are still reported on stderr.

2. The normal end of a run looks like an error

copy_proc_mem prints process_vm_readv: No such process on ESRCH — which is what happens at the end of every successful child-mode run. ESRCH is now silent there, and the sampling loop reports the exit once instead:

phpspy: pid 10652 exited

3. A failed stdio redirect doesn't say what it couldn't open

The defaults -O phpspy.%d.out / -E phpspy.%d.err are relative to the working directory, so running from somewhere unwritable gives a bare fopen: Permission denied with no path. Worse, phpspy then carries on into main_pid against a child that never execed, so the next thing on screen is an awk/objdump cascade that points at ptrace rather than at file permissions:

fopen: Permission denied
main_fork: Expected SIGTRAP from child
awk: fatal: cannot open file `/proc/350159/maps' for reading: No such file or directory
get_php_bin_path: Failed

The message now names the path and which stream it was for, and main_fork distinguishes a child that died before exec from the expected SIGTRAP and stops there. I left the defaults alone — relocating people's output files silently seemed worse than the bad message.

Also: an unchecked short read

copy_proc_mem only treated -1 as failure, so a partial process_vm_readv left the tail of the destination struct holding whatever the caller had memset it to. zend_execute_data and zend_function are largely pointers, which are then dereferenced as addresses in the target. Now checked.

Compatibility

The exit-status change is user-visible: phpspy -- cmd goes from always exiting 0 to exiting what cmd exited. No existing test depended on it (every child-mode target in the suite exits 0), but it is a CLI contract change and worth a release note if you take it.

Testing

make test passes (15/15) under both make and USE_ZEND=1 make. tests/test_child_exit_status.sh is written standalone rather than through test.sh, since the harness has no exit-code assertion — that avoids colliding with #156, which adds one.

Independent of #156 and #157.

Three things make `phpspy -- cmd` awkward to use, all in the same area.

The child's exit status is discarded. `waitpid` is called with NULL, so
`phpspy -- php -r 'exit(3);'` exits 0, and so does a script that fails to
parse. That makes phpspy hard to drop into a pipeline or CI job. It now
reports the child's status, the way strace(1) and time(1) do: a profiling
failure is still reported on stderr, but it does not mask the exit code of the
command being profiled -- not least because a short-lived child routinely
exits before phpspy has finished attaching, so preferring phpspy's own error
would mean the status is lost precisely for the quick commands most likely to
be scripted.

The normal end of a run looks like an error. copy_proc_mem prints
"process_vm_readv: No such process" when the target exits, which is what
happens at the end of every successful child-mode run. ESRCH is now silent
there and the loop says "phpspy: pid N exited" once instead.

A failure to redirect child stdio does not say what it could not open. The
default `-O phpspy.%d.out` / `-E phpspy.%d.err` are relative to the working
directory, so running from somewhere unwritable produces a bare
"fopen: Permission denied" with no path -- and then phpspy carries on, so the
next thing on screen is an awk/objdump cascade from probing a child that
never execed. The message now names the path and the stream, and main_fork
distinguishes a child that died before exec from the expected SIGTRAP and
stops there.

Also fixes an unchecked short read while in copy_proc_mem: only -1 was
treated as failure, so a partial read left the tail of the destination struct
holding whatever the caller had memset it to. zend_execute_data and
zend_function are largely pointers, which are then dereferenced as addresses
in the target.

Note the exit-status change is user-visible: `phpspy -- cmd` goes from always
exiting 0 to exiting what cmd exited. No existing test depended on it (every
child-mode target in the suite exits 0), but it is a CLI contract change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant