Eu encontrei esta questão enquanto escrevia meu próprio script para mais ou menos o mesmo propósito e sua idéia sobre o armazenamento em cache de arquivos com seu dev + inode + size + mtime pareceu muito útil, então eu o adicionei. A sua ideia e a minha implementação diferem, uma vez que tropecei tarde nesta página e decidi não reescrever tudo:
-
O script armazena entradas de cache em um único arquivo YAML para simplificar. Você ainda pode compartilhar esse arquivo em várias máquinas, mas há um risco de um RCE, além de precisar gravar um wrapper de bloqueio por causa do TOCTOU no arquivo YAML.
-
Ele provavelmente funcionará apenas no Linux e, com sorte, em outros Unixes.
-
Use por sua conta e risco. Seu conteúdo de cache não será protegido.
Execute gem install chronic_duration
primeiro.
#!/usr/bin/env ruby
# Usage: memoize [-D DATABASE] [-T TIMEOUT] [-F] [--] COMMAND [ARG]...
# or memoize [-D DATABASE] --cleanup
#
# OPTIONS
# -D DATABASE Store entries in YAML format in DATABASE file.
# -T TIMEOUT Invalidate memoized entries older than TIMEOUT.
# -F Track file changes (dev+inode+size+mtime).
# --cleanup Remove all stale entries.
require 'date'
require 'optparse'
require 'digest'
require 'yaml'
require 'chronic_duration'
require 'open3'
MYSELF = File.basename(__FILE__)
DEFAULT_DBFILE = "#{Dir.home}/.config/memoize.yml"
DEFAULT_TIMEOUT = '1 week'
def fc(fpath) # File characteristic
return [:dev, :ino, :size, :mtime].map do |s|
Digest::SHA1.digest(Integer(File.stat(fpath).send(s)).to_s.b)
end.join
end
def cmdline_checksum(cmdline, fchanges)
pre_cksum_bytes = "".b
cmdline.each do |c|
characteristic = (File.exists?(c) and fchanges) ? fc(c) : c
pre_cksum_bytes += Digest::SHA1.digest(characteristic)
end
return Digest::SHA1.digest(pre_cksum_bytes)
end
def timed_out?(entry)
return (entry[:timestamp] + Integer(entry[:timeout])) < Time.now
end
def pluralize(n, singular, plural)
return (n % 100 == 11 || n % 10 != 1) ? plural : singular
end
fail "memoize: FATAL: this is a script, not a library" unless __FILE__ == $0
$dbfile = DEFAULT_DBFILE
$timeout = DEFAULT_TIMEOUT
$fchanges = false
$cleanup = false
$retcode = 0
$replay = false
ARGV.options do |o|
o.version = '2018.06.23'
o.banner = "Usage: memoize [OPTION]... [--] COMMAND [ARG]...\n"+
"Cache results of COMMAND and replay its output"
o.separator ""
o.separator "OPTIONS"
o.summary_indent = " "
o.summary_width = 17
o.on('-D=DATABASE', "Default: #{DEFAULT_DBFILE}") { |d| $dbfile = d }
o.on('-T=TIMEOUT', "Default: #{DEFAULT_TIMEOUT}") { |t| $timeout = t }
o.on('-F', "Track file changes (dev+inode+size+mtime)") { $fchanges = true }
o.on('--cleanup', "Remove all stale entries") { $cleanup = true }
end.parse!
begin
File.open($dbfile, 'a') {}
File.chmod(0600, $dbfile)
end unless File.exists?($dbfile)
db = (YAML.load(File.read($dbfile)) or {})
cmdline = ARGV
cksum = cmdline_checksum(cmdline, $fchanges)
entry = {
cmdline: cmdline,
timestamp: Time.now,
timeout: '1 week',
stdout: "",
stderr: "",
retcode: 0,
}
if $cleanup
entries = db.keys.select{|k| timed_out?(db[k]) }
c = entries.count
entries.each do |k|
db.delete(k)
end
STDERR.puts "memoize: NOTE: #{c} stale #{pluralize(c, "entry", "entries")} removed"
File.open($dbfile, 'w') { |f| f << YAML.dump(db) }
exit
end
$replay = db.key?(cksum) && (not timed_out?(db[cksum]))
if $replay
entry = db[cksum]
else
Open3.popen3(*cmdline) do |i, o, e, t|
i.close
entry[:stdout] = o.read
entry[:stderr] = e.read
entry[:retcode] = t.value.exitstatus
end
entry[:timestamp] = Time.now
entry[:timeout] = Integer(ChronicDuration.parse($timeout))
db[cksum] = entry
end
$retcode = entry[:retcode]
STDOUT.write(entry[:stdout]) # NOTE: we don't record or replay stream timing
STDERR.write(entry[:stderr])
STDOUT.flush
STDERR.flush
File.open($dbfile, 'w') { |f| f << YAML.dump(db) }
exit! $retcode