Search code examples
phpxmllibxml2php-7

DOMDocument->save() fails after user_abort is handled


In PHP, its possible to register shutdown functions, which (sometimes gets ignored, however) is definetely called in my scenario, see below.

PHP/libxml supported by the DOMDocument class in PHP does not play along well w/ my registered shutdown functions, if I want to call ->save() (->saveXML() works fine) after user abort (e.g. from registered shutdown function or a class instance destructor). Related is also the PHP connection handling.

Let the examples speak:

PHP version:

php --version
PHP 7.1.4 (cli) (built: Apr 25 2017 09:48:36) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies

To reproduce user_abort I'm running the php through python2.7 run.py:

import subprocess

cmd = ["/usr/bin/php", "./user_aborted.php"]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)

# As this process exists here and the user_aborted.php
# has sleeps/blocks in for-cycle thus simulating a user-abort
# for the php subprocess.

The php script user_aborted.php to try saving XML in shutdown function :

<?php

ignore_user_abort(false);
libxml_use_internal_errors(true);

$xml = new DOMDocument();
$xml_track = $xml->createElement( "Track", "Highway Blues" );
$xml->appendChild($xml_track);

function shutdown() {
    global $xml;

    $out_as_str = $xml->saveXML();

    fwrite(STDERR, "\nout_as_str: " . var_export($out_as_str, true) . "\n");

    $out_as_file = $xml->save('out.xml');

    fwrite(STDERR, "\nout_as_file: >" . var_export($out_as_file, true) . "<\n");
    fwrite(STDERR, "\nerrors: \n" . var_export(libxml_get_errors(), true) . "\n");
}

register_shutdown_function('shutdown');

$i = 2;

while ($i > 0) {
    fwrite(STDERR, "\n PID: " . getmypid() . " aborted: " . connection_aborted());
    echo("\nmaking some output on stdout"); // so user_abort will be checked
    sleep(1);
    $i--;
}

Now, if I run this script w/o user abort (simply calling PHP) with:
php user_aborted.php the XML gets saved properly.

However when calling this through python2.7 (which simulates the user abort by exiting the parent process), python2.7 run.py the weirdest things happen:

  • the out_as_str value is fine, looks the XML I wanted
  • BUT the file out.xml is an empty file
  • ALSO the libxml_get_errors reports FLUSH problems

The output w/ python looks: python2.7 run.py

PID: 16863 aborted: 0
out_as_str: '<?xml version="1.0"?>
<Track>Highway Blues</Track>
'

out_as_file: >false<

errors:
array (
  0 =>
  LibXMLError::__set_state(array(
     'level' => 2,
     'code' => 1545,
     'column' => 0,
     'message' => 'flush error',
     'file' => '',
     'line' => 0,
  ))
) 

Sorry for the long post, but I was looking through PHP/libxml2 code the whole day w/o any success. :/


Solution

  • Reason:

    It turns out this is due to a fix for a previous bug.

    Links:

    The linked php_libxml_streams_IO_write function is the writecallback (set in ext/libxml/libxml.c) for the buffer of the docp object, which is handed over for the libxml call on ext/dom/document.c. Ending up in libxml xmlIO.c where the buffer is NULL hence the file given over for ->save(*) does not get written.

    Workaround:

    Use the ->saveXML() to get the XML representation in string and write it using file_put_contents(*) by "hand":

    $xml_as_str = $xml->saveXML();
    file_put_contents('/tmp/my.xml', $xml_as_str);