file_get_contents obtendo resultados incorretos

10

Atualizar

Resolvi o problema e postei uma resposta. No entanto, minha solução não é 100% ideal. Eu preferiria apenas remover o symlinkdo cachewith clearstatcache(true, $target)ou clearstatcache(true, $link)mas isso não funciona.

Também prefiro evitar o cache de links simbólicos em primeiro lugar ou remover o link simbólico do cache imediatamente após gerá-lo. Infelizmente, não tive sorte com isso. Por algum motivo, clearstatcache(true)após a criação de um link simbólico não funcionar, ele ainda é armazenado em cache.

Felizmente, darei a recompensa a qualquer pessoa que possa melhorar minha resposta e resolver esses problemas.

Editar

Tentei otimizar meu código gerando um arquivo sempre que clearstatcachefor executado, para que eu só precise limpar o cache uma vez para cada link simbólico. Por alguma razão, isso não funciona. clearstatcacheprecisa ser chamado toda vez que um symlinké incluído no caminho, mas por quê? Deve haver uma maneira de otimizar a solução que tenho.


Eu estou usando PHP 7.3.5com nginx/1.16.0. Às vezes, file_get_contentsretorna o valor errado ao usar a symlink. O problema ocorre após excluir e recriar um link simbólico; seu valor antigo permanece no cache. Às vezes, o valor correto é retornado, às vezes o valor antigo. Parece aleatório.

Eu tentei limpar o cache ou impedir o cache com:

function symlink1($target, $link)
{
    realpath_cache_size(0);
    symlink($target, $link);
    //clearstatcache(true);
}

Eu realmente não quero desativar o cache, mas ainda preciso de 100% de precisão com file_get_contents.

Editar

Não consigo postar meu código-fonte, pois é muito longo e complexo, por isso criei um exemplo mínimo e reproduzível (index.php) que recria o problema:

<h1>Symlink Problem</h1>
<?php
    $dir = getcwd();
    if (isset($_POST['clear-all']))
    {
        $nos = array_values(array_diff(scandir($dir.'/nos'), array('..', '.')));
        foreach ($nos as $no)
        {
            unlink($dir.'/nos/'.$no.'/id.txt');
            rmdir($dir.'/nos/'.$no);
        }
        foreach (array_values(array_diff(scandir($dir.'/ids'), array('..', '.'))) as $id)
            unlink($dir.'/ids/'.$id);
    }
    if (!is_dir($dir.'/nos'))
        mkdir($dir.'/nos');
    if (!is_dir($dir.'/ids'))
        mkdir($dir.'/ids');
    if (isset($_POST['submit']) && !empty($_POST['id']) && ctype_digit($_POST['insert-after']) && ctype_alnum($_POST['id']))
    {
        $nos = array_values(array_diff(scandir($dir.'/nos'), array('..', '.')));
        $total = count($nos);
        if ($total <= 100)
        {
            for ($i = $total; $i >= $_POST['insert-after']; $i--)
            {
                $id = file_get_contents($dir.'/nos/'.$i.'/id.txt');
                unlink($dir.'/ids/'.$id);
                symlink($dir.'/nos/'.($i + 1), $dir.'/ids/'.$id);
                rename($dir.'/nos/'.$i, $dir.'/nos/'.($i + 1));
            }
            echo '<br>';
            mkdir($dir.'/nos/'.$_POST['insert-after']);
            file_put_contents($dir.'/nos/'.$_POST['insert-after'].'/id.txt', $_POST['id']);
            symlink($dir.'/nos/'.$_POST['insert-after'], $dir.'/ids/'.$_POST['id']);
        }
    }
    $nos = array_values(array_diff(scandir($dir.'/nos'), array('..', '.')));
    $total = count($nos) + 1;
    echo '<h2>Ids from nos directory</h2>';
    foreach ($nos as $no)
    {
        echo ($no + 1).':'.file_get_contents("$dir/nos/$no/id.txt").'<br>';
    }
    echo '<h2>Ids from using symlinks</h2>';
    $ids = array_values(array_diff(scandir($dir.'/ids'), array('..', '.')));
    if (count($ids) > 0)
    {
        $success = true;
        foreach ($ids as $id)
        {
            $id1 = file_get_contents("$dir/ids/$id/id.txt");
            echo $id.':'.$id1.'<br>';
            if ($id !== $id1)
                $success = false;
        }
        if ($success)
            echo '<b><font color="blue">Success!</font></b><br>';
        else
            echo '<b><font color="red">Failure!</font></b><br>';
    }
?>
<br>
<h2>Insert ID after</h2>
<form method="post" action="/">
    <select name="insert-after">
        <?php
            for ($i = 0; $i < $total; $i++)
                echo '<option value="'.$i.'">'.$i.'</option>';
        ?>
    </select>
    <input type="text" placeholder="ID" name="id"><br>
    <input type="submit" name="submit" value="Insert"><br>
</form>
<h2>Clear all</h2>
<form method="post" action="/">
    <input type="submit" name="clear-all" value="Clear All"><br>
</form>
<script>
    if (window.history.replaceState)
    {
        window.history.replaceState( null, null, window.location.href );
    }
</script>

Parecia muito provável que fosse um problema com a Nginxconfiguração. Não ter essas linhas pode causar o problema:

fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;

Aqui está minha Nginxconfiguração (você pode ver que eu incluí as linhas acima):

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name www.websemantica.co.uk;
    root "/path/to/site/root";
    index index.php;

    location / {
        try_files $uri $uri/ $uri.php$is_args$query_string;
    }

    location ~* \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_param   QUERY_STRING            $query_string;
        fastcgi_param   REQUEST_METHOD          $request_method;
        fastcgi_param   CONTENT_TYPE            $content_type;
        fastcgi_param   CONTENT_LENGTH          $content_length;

        fastcgi_param   SCRIPT_FILENAME         $realpath_root$fastcgi_script_name;
        fastcgi_param   SCRIPT_NAME             $fastcgi_script_name;
        fastcgi_param   PATH_INFO               $fastcgi_path_info;
        fastcgi_param   PATH_TRANSLATED         $realpath_root$fastcgi_path_info;
        fastcgi_param   REQUEST_URI             $request_uri;
        fastcgi_param   DOCUMENT_URI            $document_uri;
        fastcgi_param   DOCUMENT_ROOT           $realpath_root;
        fastcgi_param   SERVER_PROTOCOL         $server_protocol;

        fastcgi_param   GATEWAY_INTERFACE       CGI/1.1;
        fastcgi_param   SERVER_SOFTWARE         nginx/$nginx_version;

        fastcgi_param   REMOTE_ADDR             $remote_addr;
        fastcgi_param   REMOTE_PORT             $remote_port;
        fastcgi_param   SERVER_ADDR             $server_addr;
        fastcgi_param   SERVER_PORT             $server_port;
        fastcgi_param   SERVER_NAME             $server_name;

        fastcgi_param   HTTPS                   $https;

        # PHP only, required if PHP was built with --enable-force-cgi-redirect
        fastcgi_param   REDIRECT_STATUS         200;

        fastcgi_index index.php;
        fastcgi_read_timeout 3000;
    }

    if ($request_uri ~ (?i)^/([^?]*)\.php($|\?)) {
        return 301 /$1$is_args$args;
    }
    rewrite ^/index$ / permanent;
    rewrite ^/(.*)/$ /$1 permanent;
}

Atualmente, tenho o exemplo acima ao vivo em https://www.websemantica.co.uk .

Tente adicionar alguns valores no formulário. Ele deve aparecer Success!em azul sempre. Às vezes é mostra Failure!em vermelho. Pode levar algumas atualizações de página para mudar de Success!para Failure!ou vice-versa. Eventualmente, ele será exibido Success!sempre, portanto, deve haver algum tipo de problema de armazenamento em cache.

Dan Bray
fonte
Eu estava olhando em torno do mesmo caso e encontrei um comentário muito útil na realpathpágina de funções . Talvez isso possa te ajudar.
precisa saber é o seguinte
@ marv255 Tentei usar realpathcom file_get_conentse sem sorte. Às vezes, ainda é carregado do cache.
Dan Bray
2
Quero dizer não apenas realpath, mas algo comoclearstatcache(true); file_get_conents(realpath($fileName));
marv255 6/11/19
Tente linux.die.net/man/8/updatedb e execute o comando entre chamadas consecutivas. Embora eu não tenha certeza de como resolver o problema no php, se este for o caso.
Janes Botis

Respostas:

3

Depende demais do nível do sistema operacional. Então, que tal tentar pensar fora da caixa. Que tal tentar ler a localização real do arquivo readlinke usar esse caminho de localização real?

$realPath = shell_exec("readlink " . $yourSymlink);
$fileContent = file_get_contents($realPath);
Vo Kim Nguyen
fonte
Eu não acho que isso seja suficiente (afinal de contas), afinal, o readlink também depende das chamadas no nível do SO e é afetado pelo cache.
Bahram Ardalan
3

Este é o comportamento desejado do PHP, você pode ver isso aqui, porque o PHP usa realpath_cachepara armazenar os caminhos de arquivo devido a aprimoramentos de desempenho, para que possa reduzir as Operações de Disco.

Para evitar esse comportamento, talvez você possa tentar limpar o realpath_cacheantes de usar a get_file_contentsfunção

Você pode tentar algo como isto:


clearstatcache();
$data = file_get_contents("Your File");

Você pode ler mais sobre o clearstatcache no PHP doc.

Touqeer Shafi
fonte
2

Existem dois caches.

Primeiro, o cache do sistema operacional e, em seguida, o cache do PHP.

Na maioria dos casos, clearstatcache(true)antes file_get_contents(...)faz o trabalho.

Mas às vezes você também precisa limpar o cache do SO. No caso do Linux, lá posso pensar em dois lugares para limpar. PageCache (1) e dentries / inodes (2).

Isso limpa os dois:

shell_exec('echo 3 > /proc/sys/vm/drop_caches')

Nota: Isso é bom para solucionar problemas, mas não para chamadas frequentes na produção, pois limpa todo o cache do SO e custa ao sistema alguns momentos de re-preenchimento do cache.

Bahram Ardalan
fonte
Isso não funciona, às vezes ainda carrega o valor em cache e eu preciso de uma solução que seja boa para chamadas frequentes na produção.
Dan Bray
2
@ DanBray, você poderia registrar coisas para descobrir mais sobre a natureza das vezes ?
Bahram Ardalan
11
@ DanBray, e como você detecta a aparência do valor antigo? Será que o seu teste retorna o valor antigo devido a outras condições de teste enquanto o valor lá realmente mudou?
Bahram Ardalan
2

"O problema é após excluir e recriar um link simbólico"

Como você exclui o link simbólico? A exclusão de um arquivo (ou um link simbólico) deve limpar automaticamente o cache.

Caso contrário, você poderá ver o que acontece se:

// This has "race condition" written all around it
unlink($link);
touch($link);
unlink($link); // Remove the empty file
symlink($target, $link);

Se isso não resolver o problema, poderia ser um problema com o nginx, como nesta edição ?

Tente registrar todas as operações em um arquivo de log para ver o que realmente acontece.

ou talvez...

... você poderia fazer sem links simbólicos completamente ? Por exemplo, armazene em um banco de dados, memcache, arquivo SQLite ou mesmo em um arquivo JSON o mapeamento entre "nome do arquivo" e "destino real do link simbólico". Usando, por exemplo, redis ou outras lojas de chaves, você pode associar o "nome do arquivo" ao destino real do link simbólico e ignorar completamente a resolução do SO.

Dependendo do caso de uso, isso pode até ser mais rápido do que usar links simbólicos.

LSerni
fonte
Não pude ver como isso pode estar relacionado ao nginx, pois parece que não há nada de http entre o processo php e o sistema de arquivos local. Ser o processo pai torna o nginx de alguma forma relevante?
Bahram Ardalan
@BahramArdalan o fato é que não sabemos como o problema foi diagnosticado ou quais são os links simbólicos ou como eles são usados. Portanto, é concebível que a incompatibilidade de conteúdo tenha sido detectada a jusante do nginx e possa, na verdade, não estar relacionada ao PHP. Um SCCCE seria de grande ajuda.
LSerni
Sim. Temos que nos aprofundar um pouco nessa coisa de "como".
Bahram Ardalan
1

Houve dois problemas que causaram o problema.

Primeira edição

Eu já postei como e editei na pergunta. É um problema com a configuração do Nginx.

Estas linhas:

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $document_root;

necessário substituído por:

fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;

Segunda edição

A segunda questão era que eu precisava ligar clearstatcacheantes de ligar file_get_contents. Eu só quero chamar clearstatcachequando for absolutamente necessário, então escrevi uma função que apenas limpa o cache quando o diretório inclui a symlink.

function file_get_contents1($dir)
{
    $realPath = realpath($dir);
    if ($realPath === false)
        return '';
    if ($dir !== $realPath)
    {
        clearstatcache(true);
    }
    return file_get_contents($dir);
}
Dan Bray
fonte
1

Estou deixando minha primeira resposta, pois ainda é uma resposta válida. Estou melhorando a resposta @DanBray implementando clearstatcache (true, $ filename).

Houve dois problemas que causaram o problema.

Primeira edição

Eu já postei como e editei na pergunta. É um problema com a configuração do Nginx.

Estas linhas:

fastcgi_param SCRIPT_FILENAME $ document_root $ fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $ document_root;

necessário substituído por:

fastcgi_param SCRIPT_FILENAME $ realpath_root $ fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $ realpath_root;

Segunda edição

O segundo problema foi que eu precisava ligar para clearstatcache antes de chamar file_get_contents. Eu só quero chamar clearstatcache quando for absolutamente necessário, então escrevi uma função que limpa o cache apenas quando o diretório inclui um link simbólico.

function file_get_contents1234_hard_drives($dir_go_1){
    $realPath = realpath($dir_go_1);
        $myDirectory=opendir(dirname($realPath));        
        while($entryName=readdir($myDirectory)) {
          $dirArray[]=$entryName;
        }

        /* Finds extensions of files used for my site theelectronichandbook.tech
        function findexts ($filename) {
          $filename=strtolower($filename);
          $exts=split("[/\\.]", $filename);
          $n=count($exts)-1;
          $exts=$exts[$n];
          return $exts;
        }*/

        // Closes directory
        closedir($myDirectory);

        // Counts elements in array
        $indexCount=count($dirArray);
        for($ArPos=1;$ArPos<=$indexCount;$ArPos++){
            /*used for my site theelectronichandbook.tech
            if($_SERVER['QUERY_STRING']=="hidden"){
                $H="";
                $af="./";
                $atext="Hide";
            }else{
                $H=".";
                $af="./?hidden";
                $at="Show";
            }*/
            if(strpos($dirArray[$ArPos], "Symlink") !== false){
                clearstatcache(true,$dir_go_1);
            }
        }
    return file_get_contents($dir_go_1);
}

Testei o código acima com meu servidor web e funcionou.

JTS
fonte
11
Infelizmente, ele não funciona para mim no meu servidor web.
21919 Dan Bray
Bem, eu vou voltar para o tabuleiro. @DanBray
JTS
11
Muito obrigado, mas, infelizmente, há muito pouco tempo até o período de recompensa expirar. No entanto, se você pensar em uma solução com a qual eu estou 100% satisfeito, concederei uma recompensa extra. Além disso, file_get_contents1faz parte da estrutura que criei, por isso é muito utilizada, o que torna a otimização importante.
Dan Bray
$dir_go=readdir("$realPath")retorna nulo.
Dan Bray
Isso pode precisar ser alterado para While($dir_go!==null)@DanBray
JTS
0

Tente colocar o código dentro de um elemento que é continuamente atualizado usando o Jquery, além de forçar a revalidação e limpar a captura estática. Este código foi modificado a partir da resposta original @naveed .

form.php:

 <meta http-equiv="Cache-Control" content="no-store, must-revalidate" />
 <meta http-equiv="Expires" content="0"/>
 <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
 <script> 
 jQuery(document).ready(function(){
    jQuery('.ajaxform').submit( function() {
        $.ajax({
            url     : $(this).attr('action'),
            type    : $(this).attr('method'),
            dataType: 'json',
            data    : $(this).serialize(),
            success : function( data ) {
                        // loop to set the result(value)
                        // in required div(key)
                        for(var id in data) {
                            jQuery('#' + id).html( data[id] );
                        }
                      }
        });
        return false;
    });
});
var timer, delay = 30;
timer = setInterval(function(){
    $.ajax({
      type    : 'POST',
      url     : 'profile.php',
      dataType: 'json',
      data    : $('.ajaxform').serialize(),
      success : function(data){
                  for(var id in data) {
                    jQuery('#' + id).html( data[id] );
                  }
                }
    }); }, delay);
 </script>
 <form action='profile.php' method='post' class='ajaxform'></form>
 <div id='result'></div>

profile.php:

 <?php
       // All form data is in $_POST
       // Now perform actions on form data here and create an result array something like this
       clearstatcache();
       $arr = array( 'result' => file_get_contents("./myfile.text") );
       echo json_encode( $arr );
 ?>
JTS
fonte