Why does fopen fail within a register shutdown fun

2019-02-25 19:47发布

问题:

I am working on a site in a shared host. I don't have access to the PHP ini file, or the PHP ini_set function, and I can't use php_flag or php_value directives.

I'm using the set_error_handler function to log most errors, and trying to use the register_shutdown_function to log fatal ones. I've tried several of the solutions for catching fatal errors here: How do I catch a PHP Fatal Error . I can successfully use the solutions there to either display the errors on screen or bring up a custom error page, and to log non-fatal errors, but I can't seem to log the fatal errors.

This is simplest version of my attempts:

    function errorHandler($num, $str, $file, $line) {

        $now        = strftime("%B %d, %Y at %I:%M %p", time());
        $text       = "{$now} → error {$num} in {$file}, line {$line}: {$str}\n";
        $errorlog   = "my_path/my_log";

        $new        = file_exists($errorlog) ? false : true;
        if($handle  = fopen($errorlog, 'a')) {
            fwrite($handle, $text);
            fclose($handle);
            if($new) { chmod($errorlog, 0755); }
        } else {
            print("Could not open log file for writing"); //I'm using this to test
        }

    }

    function fatalHandler() {
        $error  = error_get_last();
        if($error) {
            errorHandler($error["type"], $error["message"], $error["file"], $error["line"]);
        }
    }

    set_error_handler("errorHandler");
    register_shutdown_function("fatalHandler");    

When I test this with a non-fatal error, like echo $undefined_variable, it works and the error is logged properly. However if I test with a fatal error, like undefined_function(), it does not log anything and prints "could not open log file". If I test with both errors present, it logs the non-fatal one, but not the fatal one.

Your question has been identified as a possible duplicate of another question. If the answers there do not address your problem, please edit to explain in detail the parts of your question that are unique.

@Vic Seedoubleyew -Well, the most obvious thing is the warning addressed in the question you mentioned failed to open stream: ...etc has nothing to do with my situation. That warning never happened. The test message: "could not open log file for writing" is my own message that I wrote to myself, so I would have some evidence that the if statement was being processed. I made that message up. It's noted very clearly in my question that the message is of my own making. Anyone experiencing the problem I was will never get the message failed to open stream...etc, so they won't be helped by a question addressing a message they never got. I assume the point of all this is to actually help dummies like me.

The title: "Why does fopen fail within a register shutdown function" was not my title. That is someone else's edit. My question was why I couldn't write to a log within the register_shutdown_function. I didn't know whether fopen was failing or there was some other problem. I was trying to log something out of the function and it wasn't working. Removing the stated goal (logging out of the function) from the question, and adding someone else's assessment "why does fopen fail", actually makes it much less useful to anyone coming here for a solution to the situation I was experiencing. Had there already been a question present which addressed writing to a file within the shutdown function, I would have found it. "Why does fopen fail" is too specific to be useful in a search by someone who doesn't know why they can't write to a file in the function.

I wasn't going to say anything about the edit, but since you have asked me to explain, I'm explaining. I realize everyone probably gets points for edits, but take a minute to consider the effect your edit has on the usefulness of the question to someone who comes here with the situation I had.

回答1:

Are you using a relative path to your custom error log? If so, this note on the register_shutdown_function page might be relevant:

Working directory of the script can change inside the shutdown function under some web servers, e.g. Apache.

In fact, the second comment says:

If you want to do something with files in function, that registered in register_shutdown_function(), use ABSOLUTE paths to files instead of relative. Because when script processing is complete current working directory chages to ServerRoot (see httpd.conf)

Other things I'll mention:

Your variable $error, and the logic if ($error) { ... is misleading. See, when you register a function using register_shutdown_function, you are telling PHP to invoke that function every single time your script finishes executing - regardless of whether or not there was an error. This means that fatalHandler is invoked even when your script finishes with a non-fatal error, or even no error at all!

Since you have an alternative method for dealing with non-fatal errors (using set_error_handler), you should specifically check for fatal errors in fatalHandler:

function fatalHandler() {
    if( $error !== NULL && $error['type'] == E_ERROR) {
        errorHandler($error["type"], $error["message"], $error["file"], $error["line"]);
        header("HTTP/1.1 500 Internal Server Error");
        exit();
    }
}

See the list of PHP error levels here.

You may not be aware of this, but PHP has a built-in error-logging function. By default, it writes to the file specified for error_log in your php.ini.

If you are interested in more advanced logging, and generally taking your PHP-fu to the next level, I suggest looking into the Monolog package. It is more or less considered the universal standard for logging in professional PHP communities.



回答2:

For this situations I just include file
that has content like that:

function ErrorHandler($errno, $errstr, $errfile, $errline) {
    if(in_array($errno, [E_NOTICE, E_USER_NOTICE, E_WARNING])) { //, E_WARNING, E_CORE_WARNING, E_DEPRECATED, E_USER_DEPRECATED])) {
        return;
    }

    $err_type='ERROR  ';

    switch ($errno)
    {
        case E_ERROR:
        case E_CORE_ERROR:
        case E_COMPILE_ERROR:
        case E_PARSE:
            $err_type='FATAL  ';
            break;

        case E_USER_ERROR:
        case E_RECOVERABLE_ERROR:
            $err_type='ERROR  ';
            break;

        case E_WARNING:
        case E_CORE_WARNING:
        case E_COMPILE_WARNING:
        case E_USER_WARNING:
            $err_type='WARNING  ';
            break;

        case E_NOTICE:
        case E_USER_NOTICE:
            $err_type='NOTICE  ';
            break;

        case E_STRICT:
            $err_type='STRICT  ';
            break;
    }

    $log_text = $err_type.$errfile.'('.$errline.')'.' :  ('.$errno.')  '.$errstr;
    Logger::log('ErrorHandler', $log_text); // Logger is custom class that appends to log file or sends email, You can write same one
}

function ShutdownHandler() {
    $last_error=error_get_last();
    if(!empty($last_error)) {
        if(in_array($last_error['type'], [E_NOTICE, E_USER_NOTICE])) { //, E_WARNING, E_CORE_WARNING, E_DEPRECATED, E_USER_DEPRECATED])) {
            return;
        }

        $error = [];
        foreach($last_error AS $key => $val) {
            $error[] = $key.' : '.$val."\n";
        }
        Logger::log('ShutdownHandler', implode(', ', $error));
        exit(-1);
    }
}

function ExceptionHandler($exception) {
    Logger::log('ExceptionHandler', $exception->getFile().'('.$exception->getLine().')'.' :  '.$exception->getMessage());
}

// here I register handlers for different types of situations
set_error_handler("ErrorHandler", E_ALL);
set_exception_handler("ExceptionHandler");
register_shutdown_function("ShutdownHandler");

And my Logger class:

class Logger {
  public static $logDir;
  public static setLogDir($dir) {
    self::$logDir = realpath($dir);
  }

  public static logFile($label = '') {
    return 
      self::$logDir.'/'
        .date('Ymd')
        .(($label!='')?'.'.$label:'')
        .'.log'; // example: /somepath/logs/20160906.ExceptionHandler.log
  }

  public static function log($label, $data) {
    @file_put_contents(self::logFile($label), $data, FILE_APPEND);
  }
}

Logger::setLogDir(__DIR__.'/logs'); // modify logs path as You wish

p.s. usually no need to have special logs, if You've full access to system You can enable error_log in php settings.