Как НЕ прекратить чтение файла при встрече с EOF?

Я пытаюсь реализовать подпрограмму для Node.js, которая позволила бы открыть файл, к которому в данный момент добавляется какой-то другой процесс, а затем сразу же возвращать порции данных, когда они добавляются в файл. Считается, что она похожа на tail -f UNIX tail -f, однако действует немедленно, когда доступны фрагменты, а не опрашивает изменения во времени. В качестве альтернативы, вы можете думать об этом как о работе с файлом, так же как и с сокетом - ожидая, что время on('data') будет срабатывать до тех пор, пока файл не будет закрыт явно.

В C land, если бы я это реализовал, я бы просто открыл файл, передал его дескриптор файла в select() (или любую альтернативную функцию с аналогичным обозначением), а затем просто прочитал куски, так как дескриптор файла помечен как "читаемый". Таким образом, когда нечего читать, оно не будет читаемым, а когда что-то добавляется в файл, оно снова читается.

Я несколько ожидал такого поведения для следующего примера кода в Javascript:

function readThatFile(filename) {
    const stream = fs.createReadStream(filename, {
        flags: 'r',
        encoding: 'utf8',
        autoClose: false // I thought this would prevent file closing on EOF too
    });

    stream.on('error', function(err) {
        // handle error
    });

    stream.on('open', function(fd) {
        // save fd, so I can close it later
    });

    stream.on('data', function(chunk) {
        // process chunk
        // fs.close() if I no longer need this file
    });
}

Тем не менее, этот пример кода просто вылетает при обнаружении EOF, поэтому я не могу дождаться появления нового чанка. Конечно, я мог бы переопределить это, используя fs.open и fs.read, но это несколько fs.read цели Node.js В качестве альтернативы я мог бы fs.watch() файл fs.watch() для изменений, но он не будет работать по сети, и мне не нравится идея постоянно открывать файл вместо того, чтобы просто держать его открытым.

Я пытался сделать это:

const fd = fs.openSync(filename, 'r'); // sync for readability' sake
const stream = net.Socket({ fd: fd, readable: true, writable: false });

Но не повезло - net.Socket не устраивает и выдает TypeError: Unsupported fd type: FILE.

Итак, какие-либо решения?

Ответ 1

Я не изучал внутренности потоков чтения для файлов, но возможно, что они не поддерживают ожидание того, что файл будет иметь больше данных, написанных на нем. Однако пакет fs определенно поддерживает это с его самой базовой функциональностью.

Чтобы объяснить, как работает хвостохранилище, я написал несколько хакерских tail функций, которые будут читать весь файл и вызывать обратный вызов для каждой строки (только через \n), а затем ждать, пока файл будет иметь на него написано больше строк. Обратите внимание, что более эффективным способом сделать это будет иметь буфер фиксированного размера и просто перетасовывать байты в него (со специальным случаем для чрезвычайно длинных строк), а не изменять строки JavaScript.

var fs = require('fs');

function tail(path, callback) {
  var descriptor, bytes = 0, buffer = new Buffer(256), line = '';

  function parse(err, bytesRead, buffer) {
    if (err) {
      callback(err, null);
      return;
    }
    // Keep track of the bytes we have consumed already.
    bytes += bytesRead;
    // Combine the buffered line with the new string data.
    line += buffer.toString('utf-8', 0, bytesRead);
    var i = 0, j;
    while ((j = line.indexOf('\n', i)) != -1) {
      // Callback with a single line at a time.
      callback(null, line.substring(i, j));
      // Skip the newline character.
      i = j + 1;
    }
    // Only keep the unparsed string contents for next iteration.
    line = line.substr(i);
    // Keep reading in the next tick (avoids CPU hogging).
    process.nextTick(read);
  }

  function read() {
    var stat = fs.fstatSync(descriptor);
    if (stat.size <= bytes) {
      // We're currently at the end of the file. Check again in 500 ms.
      setTimeout(read, 500);
      return;
    }
    fs.read(descriptor, buffer, 0, buffer.length, bytes, parse);
  }

  fs.open(path, 'r', function (err, fd) {
    if (err) {
      callback(err, null);
    } else {
      descriptor = fd;
      read();
    }
  });

  return {close: function close(callback) {
    fs.close(descriptor, callback);
  }};
}

// This will tail the system log on a Mac.
var t = tail('/var/log/system.log', function (err, line) {
  console.log(err, line);
});

// Unceremoniously close the file handle after one minute.
setTimeout(t.close, 60000);

Все, что сказано, вы должны также попытаться использовать сообщество NPM. При некотором поиске я нашел tail-stream пакет, который может делать то, что вы хотите, с потоками.

Ответ 2

Предыдущие ответы упоминали tail-stream подход, который использует fs.watch, fs.read и fs.stat вместе, чтобы создать эффект потоковой передачи содержимое файла. Вы можете увидеть этот код в действии здесь.

Другим, возможно, хакерским подходом может быть просто использование хвоста путем нереста дочернего процесса с ним. Это, конечно же, связано с ограничением, что хвост должен существовать на целевой платформе, но одна из сильных сторон node использует его для разработки асинхронных систем через spawn и даже в окнах, вы можете выполнить node в альтернативной оболочке, например, msysgit или cygwin, чтобы получить доступ к утилите хвоста.

Код для этого:

var spawn = require('child_process').spawn;

var child = spawn('tail',
    ['-f', 'my.log']);

child.stdout.on('data',
    function (data) {
        console.log('tail output: ' + data);
    }
);

child.stderr.on('data',
    function (data) {
        console.log('err data: ' + data);
    }
);

Ответ 3

То, что вы пытаетесь сделать, - это файл FIFO (аббревиатура для First In First Out), который, как вы сказали, работает как сокет.

Там есть node.js модуль, который позволяет вам работать с файлами fifo.

Я не знаю, для чего вам это нужно, но есть лучшие способы работы с сокетами на node.js. Вместо этого попробуйте socket.io.

Вы также можете посмотреть на этот предыдущий вопрос: Чтение файла в режиме реального времени с помощью node.js

Обновление 1

Я не знаком ни с одним модулем, который будет делать то, что вы хотите, с обычным файлом, а не с сокетным типом. Но, как вы сказали, вы можете использовать tail -f для выполнения трюка:

// filename must exist at the time of running the script
var filename = 'somefile.txt';

var spawn = require('child_process').spawn;
var tail = spawn('tail', ['-f', filename]);

tail.stdout.on('data', function (data) {
    data = data.toString().replace(/^[\s]+/i,'').replace(/[\s]+$/i,'');
    console.log(data);
});

Затем из командной строки попробуйте echo someline > somefile.txt и посмотрите на консоли.

Вы также можете взглянуть на это: https://github.com/layerssss/node-tailer