hv has asked for the wisdom of the Perl Monks concerning the following question:

Last week I rolled out to our bleeding edge website an update to our application with new logging code, and it was good. Yesterday, for the first time since then, someone logged on and tried to update their email address, which caused the application to try an Email::Valid check, and it all went horribly wrong.

It turns out that Email::Valid implements its MX check using a fork-and-exec, and in the forked child it starts with:

open OLDERR, '>&STDERR' or croak "cannot dup stderr: $!"; open STDERR, '>&STDOUT' or croak "cannot redirect stderr to stdout: +$!";

Now our new logging code ties STDERR, and as a result this fails with the error q{Can't locate object method "OPEN" via package "NVC::Log::STDERR"}. And that's a good thing - I don't want the logging to leak into the child. But then I don't really want it dying either.

So, what should I do? It would be sorta handy if I could register a callback for fork(), but I don't think that's easily possible (and even overriding CORE::fork is unlikely to work, since the fork is actually implicit in the Email::Valid code within:

if (my $fh = new IO::File '-|') { ...

For the moment I've rolled up a new routine that suspends logging for the duration of the call to Email::Valid->address, but that doesn't feel like an ideal solution - I've no idea what other CPAN modules we're using that might have a fork() hidden under the machinery, and even if I did I don't really want to have to write individual protection for each one of them.

And I really like the effect I get by tying STDERR - I tried to write the logging code without that initially, and I was finding it very hard to get correct clean code; what I now have seems like a much more desirable approch if I can get over this one hurdle.

My tie class is very simple right now:

package NVC::Log::STDERR; use strict; sub TIEHANDLE { my $class = shift; bless {}, $class; } sub PRINT { my $self = shift; $log->die(join '', @_); }
and perhaps I can fix the problem by adding some methods (such as OPEN) to do the right thing, but I can't think what the right thing might be - I feel that the ideal solution would be to suspend logging just before any fork, and reenable it in the parent only just after, but I can't see how to get there from here.

Suggestions gratefully received,

Update: it doesn't solve the general problem, but I did discover that Email::Valid forks only as a fallback (using nslookup(1)) when it can't find Net::DNS; installing Net::DNS was enough to avoid the fork, and therefore allowed me to avoid suspending logging for the duration of the call.

Hugo

Replies are listed 'Best First'.
Re: Catching fork() with tied STDERR
by bluto (Curate) on Apr 30, 2004 at 15:29 UTC
    One heavyweight alternative, almost not worth mentioning, is to have a grandparent process fork off the parent and just sit around and log all of the parent's STDERR output.

    If you find a way of not patching this on a case-by-case basis, I'd be interested. I tend to go the other direction and log output from warn/die/croak/etc and the error codes I get back, since I find there is a lot of marginal stuff, outside of my control, that goes to stderr. If something turns up that I need to monitor from stderr, then I resort to trapping it on a case-by-case basis (i.e. by sending it to a temp file, forking a child and reading the stderr output from it, etc).

Re: Catching fork() with tied STDERR
by bart (Canon) on Apr 30, 2004 at 20:10 UTC
    Tieing STDERR doesn't always work out right. Some of perl's internal calls will ignore it, and continue to send their messages over the old STDERR. A separate program launched with system will commonly ignore it, too. In short, I don't think this is the last of your problems.

    For a quick fix, for this particular problem, look into IPC::Open3, and run the check as a separate script. It'll send its STDOUT/STDERR nicely over a couple of handles you can read from, and forward to your own STDERR.

      Hmm, if any of perl's internal calls are ignoring it without a good reason (I guess a "panic: interpreter is broken" would be a good reason) then that's probably a bug that should be fixed; in any case, the underlying *STDERR still points to the standard Apache error log, so such messages won't be lost entirely. (And in fact that seems like the real benefit of the tie - if anything goes horribly wrong in the logging code, the fallback is as simple as untie *STDERR.)

      In general, if anything is calling out to a separate process I'd expect it to be handling STDERR from that itself - exactly as Email::Valid is trying to do - so I don't think that's a problem either.

      The issue as I see it is making sure I get out of the way of such child processes, so that they are actually able to handle STDERR themselves. And to do that, either I've got to know in advance that the child process will be created (as I now do for the MX check), or I have to get perl to tell me that it's about to happen (or has just happened).

      Hugo

Re: Catching fork() with tied STDERR
by Anonymous Monk on Apr 30, 2004 at 16:51 UTC
    You can now catch the open at STDERR. That's a Good Thing. On the other hand you're now bound to do it. Before it's gone wrong too, but you didn't feel it, because Email::Validate got the heat. If you rebless your tied()-objekt to IO::File or so in the OPEN-sub you should be in the clear. Condition is: Your object is a glob.

      Thanks for the suggestion. The thing tied is \*STDERR, so it may be enough just to untie it at that point; I'll have to try that and see.

      Hugo