Select Git revision
-
cleemy desu wayo authored
If the version of Psych was 4.0.0 or later, an error occurred with hookcc. Even a simple example like this will cause an error: $ ./outvoke.rb -e 'hookcc("every-sec") { e }' This bug appeared in version 0.0.99.20241123 (1c328546). Outvoke used Object#to_yaml and YAML.load for deep copying. This commit makes the change to use YAML.unsafe_load instead of YAML.load. Since YAML.unsafe_load is executed immediately after executing Object#to_yaml, the security risk is considered small. However, please note that this has not been carefully verified to ensure it is safe in all cases. In Outvoke, the approach for deep copying may be radically changed in the future.cleemy desu wayo authoredIf the version of Psych was 4.0.0 or later, an error occurred with hookcc. Even a simple example like this will cause an error: $ ./outvoke.rb -e 'hookcc("every-sec") { e }' This bug appeared in version 0.0.99.20241123 (1c328546). Outvoke used Object#to_yaml and YAML.load for deep copying. This commit makes the change to use YAML.unsafe_load instead of YAML.load. Since YAML.unsafe_load is executed immediately after executing Object#to_yaml, the security risk is considered small. However, please note that this has not been carefully verified to ensure it is safe in all cases. In Outvoke, the approach for deep copying may be radically changed in the future.
outvoke.rb 28.84 KiB
#!/usr/bin/env ruby
#
# ==========================================================================
# Outvoke -- version 0.0.99.20250614
#
# written by cleemy desu wayo / Licensed under CC0 1.0
#
# official repository: https://gitlab.com/cleemy-desu-wayo/outvoke
# ==========================================================================
#
# * requirements:
# * Ruby 3.0 or later
#
require 'time'
require 'yaml'
require 'json'
require 'bigdecimal'
require 'bigdecimal/util'
require 'bigdecimal/math'
class Outvoke
attr_accessor :version, :ds, :hooks, :hooks_first_time, :wait, :nodup_list,
:mutex_nodup, :init_time
attr_reader :last_msgid
alias_method :sources, :ds # eventually "sources" will be removed
def initialize
@version = Struct.new(:branch, :body).new(
'0.1',
'0.0.99.20250614'
)
@ds = Hash.new
@hooks = []
@hooks_first_time = []
@wait = 0.5
@mutex_msgid = Mutex.new
@last_msgid = 0
@hookcnt = 0
@nodup_list = Hash.new
@mutex_nodup = Mutex.new
@init_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@is_quiet_mode = false
end
def [](str)
@ds[str]
end
def quiet_mode?
@is_quiet_mode
end
def quiet_mode=(x)
@is_quiet_mode = x
end
def elapsed
Process.clock_gettime(Process::CLOCK_MONOTONIC) - @init_time
end
#
# TODO: experimental and incomplete implementation #
# (specifications are subject to sudden change without notice)
#
def nodup(time, cond)
unless block_given?
@mutex_nodup.synchronize do
register_nodup(time, cond)
end
return
end
is_nodup = false
@mutex_nodup.synchronize do
is_nodup = sweep_and_check_nodup(cond)
register_nodup(time, cond)
end
return unless is_nodup
yield time, cond
end
def nodup?(x)
@mutex_nodup.synchronize do
sweep_and_check_nodup(x)
end
end
#
# TODO: experimental and incomplete implementation
#
# (specifications are subject to sudden change without notice)
#
def loputs(body = "", attachment = nil)
@ds['lo'] << { "body" => body.to_s.dup, "attachment" => attachment }
nil
end
def mainloop
loop do
@ds.each do |ds_label, ds|
next unless ds.enabled?
ds.before_each_event
ds.each_event do |event|
hooks = @hooks
@hookcnt = 0
if ds.is_first_time_log_check
ds.first_time_log_check(ds, event)
hooks = @hooks_first_time
end
hooks.each do |hook|
hooks_exec(ds, hook, event)
end
end
ds.after_each_event
end
sleep @wait
end
end
def register_proc(ds_cond, event_cond, hookby, &block)
hooks = @hooks
if hookby.end_with?("ft")
hooks = @hooks_first_time end
extract_ds(ds_cond).each do |ds_label|
@ds[ds_label].enabled = true
hooks << {
'ds_label' => ds_label,
'event_cond' => event_cond,
'hookby' => hookby,
'proc' => block
}
end
end
# extracts only the labels of the appropriate data sources from $sources
def extract_ds(ds_cond)
return @ds.keys if ds_cond == true
ds_cond_array = [ds_cond]
if ds_cond.respond_to?(:each)
ds_cond_array = ds_cond
end
result = []
ds_cond_array.each do |label_cond|
next unless label_cond.respond_to?(:===)
# TODO (automatically generate a new data source)
if label_cond.is_a?(String)
label_cond.match(/\Aauxin:(fifo)?:([1-9]+[0-9]*)\z/)&.then do |m|
new_ds_label = "auxin:fifo:#{m[2]}"
unless @ds[new_ds_label]
@ds[new_ds_label] = OutvokeDataSourceAuxinFifo.new(self, new_ds_label)
end
end
end
@ds.keys.each do |label|
result << label if label_cond === label
end
end
result.uniq
end
def generate_new_msgid
new_msgid = nil
@mutex_msgid.synchronize do
@last_msgid += 1
new_msgid = @last_msgid
end
new_msgid
end
private
#
# TODO: experimental and incomplete implementation
#
# (specifications are subject to sudden change without notice)
#
def register_nodup(time, obj)
expiration_time = Time.now + time
@nodup_list.each_key do |key|
@nodup_list[key] = expiration_time if key === obj
end
@nodup_list[obj] = expiration_time
end
def sweep_and_check_nodup(x)
is_nodup = true
tmp_now = Time.now
# sweep (remove expired items)
@nodup_list = @nodup_list.reject do |_, exp|
tmp_now > exp
end
# immediately after sweep, the list can be considered as a simple blocklist
@nodup_list.each_key do |key|
if key === x
is_nodup = false
break
end
end
is_nodup
end
def hooks_exec(ds, hook, event)
# check data source
return unless hook['ds_label'] == ds.label
event_cond_array = [hook['event_cond']]
if hook['event_cond'].respond_to?(:each)
event_cond_array = hook['event_cond']
end
event.is_quiet_mode = @is_quiet_mode # TODO (is_quiet_mode is deprecated)
event.status.quiet_mode = @is_quiet_mode
# check condition
event_cond_array.detect do |event_cond|
event.m = nil
if event_cond == true
event.m = [event.body]
elsif event_cond.is_a?(String)
event.m = [event.body] if event.body.start_with?(event_cond)
elsif event_cond.respond_to?(:match)
event.m = event_cond.match(event.body)
elsif event_cond.respond_to?(:call)
proc_result = event_cond.call(event)
if proc_result
if proc_result.respond_to?(:each)
event.m = proc_result.each
else
event.m = [proc_result]
end
end
end
event.m
end
return unless event.m
event.m = event.m.to_a # with YAML.unsafe_load and to_yaml in mind
event.hookby = hook["hookby"]
@hookcnt += 1
event.hookcnt = @hookcnt
hook_result = nil hook_result_for_post_process = nil
# pre process
ds.pre_procs.each do |proc|
proc.call(event)
end
# execute
if hook['proc']
tmp_context = ds.generate_context(event)
hook_result = tmp_context.instance_exec(event, &hook['proc'])
else
hook_result_for_post_process = event
end
if hook_result == true
hook_result_for_post_process = event
elsif hook_result.nil? || hook_result == false
# nop
else
hook_result_for_post_process = hook_result
end
# post process
ds.post_procs.each do |proc|
proc.call(event, hook_result_for_post_process)
end
end
end
#
# this class provided only for instance_exec
#
class OutvokeProcExecContext
def initialize(outvoke, ds, event)
@outvoke = outvoke
@ds = ds
@event = event
@event_body = @event.body.dup
end
def to(ds_cond, attachment = nil)
@outvoke.extract_ds(ds_cond).map{ @outvoke.ds[_1] }.each do |ds|
ds << { "body" => @event_body.to_s.dup, "attachment" => attachment }
end
nil
end
def e = @event
def loputs(x = @event)
@outvoke["lo"] << { "body" => x.to_s.dup, "attachment" => nil }
nil
end
end
module OutvokeDSL
refine Kernel do
def hook(ds_cond, event_cond = true, &block)
$outvoke.register_proc(ds_cond, event_cond, "hook", &block)
end
def hookft(ds_cond, event_cond = true, &block)
$outvoke.register_proc(ds_cond, event_cond, "hookft", &block)
end
# if block is given, execute in another thread (concurrent computing)
def hookcc(ds_cond, event_cond = true, &block)
register_hookcc(ds_cond, event_cond, "hookcc", &block) end
# if block is given, execute in another thread (concurrent computing)
def hookccft(ds_cond, event_cond = true, &block)
register_hookcc(ds_cond, event_cond, "hookccft", &block)
end
def register_hookcc(ds_cond, event_cond, hookby, &block)
if block_given?
$outvoke.register_proc(ds_cond, event_cond, hookby) do |e; tmp_event|
tmp_event = YAML.unsafe_load(e.to_yaml) # deep copy
Thread.start do
Thread.current["hookcc_result"] = instance_exec(tmp_event , &block)
if Thread.current["hookcc_result"] == true
Thread.current["hook_result_for_post_process"] = tmp_event
elsif Thread.current["hookcc_result"].nil? || Thread.current["hookcc_result"] == false
Thread.current["hook_result_for_post_process"] = nil
else
Thread.current["hook_result_for_post_process"] = Thread.current["hookcc_result"]
end
@ds.post_procs_cc.each do |proc|
proc.call(e, Thread.current["hook_result_for_post_process"])
end
end
nil
end
else
hook(ds_cond, event_cond)
end
end
def hooklo(event_cond = true, &block)
hook("lo", event_cond, &block)
end
def hookstd(event_cond = true, &block)
hook("stdin", event_cond, &block)
end
def hookaux(event_cond = true, &block)
hook("auxin:fifo:1", event_cond, &block)
end
def hooksec(event_cond = true, &block)
hook("every-sec", event_cond, &block)
end
def hookweb(event_cond = true, &block)
hook("web-001", event_cond, &block)
end
def hookvr(event_cond = true, &block)
hook(/^vr/, event_cond, &block)
end
def loputs(x = "", attachment = nil)
$outvoke.loputs(x, attachment)
end
#
# TODO: experimental and incomplete implementation
#
# (specifications are subject to sudden change without notice)
#
def nodup(time, cond)
raise '$outvoke is not an Outvoke object' unless $outvoke.is_a?(Outvoke)
if block_given?
$outvoke.nodup(time, cond) {|time, cond| yield time, cond }
else
$outvoke.nodup(time, cond)
end
end
def nodup?(x)
raise '$outvoke is not an Outvoke object' unless $outvoke.is_a?(Outvoke)
$outvoke.nodup?(x)
end
end
#
# TODO: experimental and incomplete implementation
#
# (specifications are subject to sudden change without notice)
#
refine BasicObject do
def to(ds_cond, attachment = nil)
$outvoke.extract_ds(ds_cond).map{ $outvoke.ds[_1] }.each do |ds|
ds << { "body" => self.to_s.dup, "attachment" => attachment }
end
nil
end
end
end
class OutvokeEvent < String
attr_accessor :status, :msgid, :time, :attachment, :m
attr_accessor :hookby, :hookcnt
attr_accessor :is_quiet_mode # deprecated
def initialize(x)
super x.to_s
@time = Time.now
@hookcnt = 0
end
def body = self.to_s
end
class OutvokeDataSource
attr_accessor :label, :status, :log_lines, :mutex_lo, :is_first_time_log_check,
:pre_procs, :post_procs, :post_procs_cc
attr_reader :lo_data
def initialize(outvoke, label)
@outvoke = outvoke
@label = label
@status = nil
@log_lines = []
@lo_data = []
@mutex_lo = Mutex.new
@is_first_time_log_check = false
@is_enabled = false
#
# Proc objects for pre process
# (Procs to process before the execution of a Proc registered by hook)
#
@pre_procs = []
#
# Proc objects for post process
# (Procs to process the value returned by Proc registered by hook) #
@post_procs = []
@post_procs << ->(e, hook_result) {
return unless hook_result
if e.status.quiet_mode
puts hook_result.to_s
else
puts "[#{e.time}] #{hook_result}".gsub("\n", " ")
end
}
#
# Proc objects for post process
# (Procs to process the value returned by Proc registered by hook)
#
@post_procs_cc = []
@post_procs_cc << ->(e, hook_result) {
return unless hook_result
if e.status.quiet_mode
puts hook_result.to_s
else
puts "[#{Time.now}] #{hook_result}".gsub("\n", " ")
end
}
end
def enabled?
@is_enabled
end
def enabled=(x)
@is_enabled = !!x
end
def <<(x)
new_msgid = @outvoke.generate_new_msgid
x["msgid"] = new_msgid
@mutex_lo.synchronize do
@lo_data << x
end
new_msgid
end
def each_event
raise NotImplementedError
end
def before_each_event
raise NotImplementedError
end
def after_each_event
raise NotImplementedError
end
def first_time_log_check
raise NotImplementedError
end
def generate_context(event)
OutvokeProcExecContext.new(@outvoke, self, event)
end
end
#
# TODO: experimental and incomplete implementation#
# (specifications are subject to sudden change without notice)
#
StructDSStdinStatus = Struct.new(:now, :quiet_mode, :last_time)
class OutvokeDataSourceStdin < OutvokeDataSource
def initialize(outvoke, label)
super
@status = StructDSStdinStatus.new
@status.last_time = Time.now.floor
@stdin = $stdin.each_line
@stdin_new_lines = []
@mutex_stdin = Mutex.new
@thread_stdin = Thread.start do
Thread.stop
loop do
Thread.stop unless @is_enabled
Thread.current["tmp"] = @stdin.next
Thread.stop unless @is_enabled
Thread.current["new_msgid"] = @outvoke.generate_new_msgid
@mutex_stdin.synchronize do
@stdin_new_lines << {
"msgid" => Thread.current["new_msgid"],
"body" => Thread.current["tmp"].chomp
}
end
end
end
end
def enabled=(x)
return true if @is_enabled == true && x
@is_enabled = !!x
return false unless @is_enabled
sleep 0.05 # TODO
tmp_status = @thread_stdin.status
if tmp_status == "run" || tmp_status == "sleep"
begin
@thread_stdin.run
rescue ThreadError
return false
end
end
true
end
def each_event
@log_lines.each do |line|
event = OutvokeEvent.new(line["body"])
event.msgid = line["msgid"]
event.status = @status
event.status.now = event.time
yield event
@status.last_time = event.time
end
self
end
def before_each_event
@log_lines = []
return unless @is_enabled
@mutex_stdin.synchronize do
@log_lines = @stdin_new_lines
@stdin_new_lines = []
end
end
def after_each_event
nil
end
def first_time_log_check(ds, event)
nil
end
end
#
# TODO: experimental and incomplete implementation
#
# (specifications are subject to sudden change without notice)
#
StructDSAuxinStatus = Struct.new(:now, :quiet_mode, :last_time)
class OutvokeDataSourceAuxinFifo < OutvokeDataSource
def initialize(outvoke, label)
super
@status = StructDSAuxinStatus.new
@status.last_time = Time.now.floor
@new_lines = []
@mutex_new_lines = Mutex.new
m = label.match(/\Aauxin:fifo:([1-9]+[0-9]*)\z/)
@fifo_file_name = "./.outvoke.auxin.#{m[1]}.fifo"
@thread_auxin = Thread.start do
Thread.stop
loop do
next if File.symlink?(@fifo_file_name)
@fifo_file = File.open(@fifo_file_name).each_line
loop do
Thread.stop unless @is_enabled
Thread.current["tmp"] = @fifo_file.next
Thread.stop unless @is_enabled
Thread.current["new_msgid"] = @outvoke.generate_new_msgid
@mutex_new_lines.synchronize do
@new_lines << {
"msgid" => Thread.current["new_msgid"],
"body" => Thread.current["tmp"].chomp
}
end
end
end
end
end
def enabled=(x)
return true if @is_enabled == true && x
@is_enabled = !!x
return false unless @is_enabled
sleep 0.05 # TODO
# create fifo file (named pipe) unless File.pipe?(@fifo_file_name)
unless File.exist?(@fifo_file_name)
File.mkfifo(@fifo_file_name, 0606)
end
end
unless File.pipe?(@fifo_file_name)
@is_enabled = false
return
end
tmp_status = @thread_auxin.status
if tmp_status == "run" || tmp_status == "sleep"
begin
@thread_auxin.run
rescue ThreadError
return false
end
end
true
end
def each_event
@log_lines.each do |line|
event = OutvokeEvent.new(line["body"])
event.msgid = line["msgid"]
event.status = @status
event.status.now = event.time
yield event
@status.last_time = event.time
end
self
end
def before_each_event
@log_lines = []
return unless @is_enabled
@mutex_new_lines.synchronize do
@log_lines = @new_lines
@new_lines = []
end
end
def after_each_event
nil
end
def first_time_log_check(ds, event)
nil
end
end
#
# TODO: experimental and incomplete implementation
#
# (specifications are subject to sudden change without notice)
#
StructDSLoopbackStatus = Struct.new(:now, :quiet_mode, :last_time)
class OutvokeDataSourceLoopback < OutvokeDataSource
def initialize(outvoke, label)
super @status = StructDSLoopbackStatus.new
@status.last_time = Time.now.floor
end
def each_event
@log_lines.each do |line|
event = OutvokeEvent.new(line["body"])
event.msgid = line["msgid"]
event.attachment = line["attachment"]
event.status = @status
event.status.now = event.time
yield event
@status.last_time = event.time
end
self
end
def before_each_event
@log_lines = []
@mutex_lo.synchronize do
@log_lines = @lo_data
@lo_data = []
end
end
def after_each_event
nil
end
def first_time_log_check(ds, event)
nil
end
end
#
# TODO: experimental and incomplete implementation
#
# (specifications are subject to sudden change without notice)
#
StructDSWebStatus = Struct.new(:now, :quiet_mode, :last_time, :ds_label)
class OutvokeEventWeb < OutvokeEvent
attr_accessor :req, :get, :post, :res
end
class OutvokeDataSourceWeb < OutvokeDataSource
attr_accessor :port, :document_root, :mount_proc_dir, :timeout
attr_reader :first_res_list
def initialize(outvoke, label)
super
@status = StructDSWebStatus.new
@status.last_time = Time.now.floor
@websrv = nil
@port = 8080
@document_root = nil
@mount_proc_dir = "/"
@timeout = 5
@first_res_list = Hash.new
@mutex_first_res = Mutex.new
@post_procs = []
@post_procs << ->(e, hook_result) {
register_first_res(e.msgid, hook_result)
} end
# TODO
def enabled=(x)
@is_enabled = !!x
end
def websrv_start
require 'uri'
require 'webrick'
websrv = WEBrick::HTTPServer.new({:Port => @port,
:DocumentRoot => @document_root})
if @mount_proc_dir
websrv.mount_proc @mount_proc_dir do |req, res|
req.query # TODO
msgid = self << { "body" => req.path.to_s.dup, "attachment" => [req, res] }
res_found = false
loop_start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
loop do
break if loop_start_time + @timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC)
# TODO (delete from @first_res_list when done)
tmp_res = nil
@mutex_first_res.synchronize do
tmp_res = @first_res_list[msgid]
end
if tmp_res
if res == tmp_res
# nop
elsif tmp_res.is_a?(WEBrick::HTTPResponse) # maybe hookcc
res.body = tmp_res.body
tmp_res.header.each do |key, value|
res.header[key] = value
end
else
res.body = tmp_res.to_s
res.body += "\n" if res.body[-1] != "\n"
end
res_found = true
break
end
end
unless res_found
res.status = 500
res.body = "error\n"
end
end
end
Thread.start { websrv.start }
end
def each_event
@log_lines.each do |line|
event = OutvokeEventWeb.new(line["body"])
event.msgid = line["msgid"]
event.attachment = line["attachment"]
event.req = line["attachment"][0]
event.res = line["attachment"][1]
if event.req.request_method == "GET"
event.get = event.req.query
event.post = Hash.new
elsif event.req.request_method == "POST"
event.get = Hash[URI::decode_www_form(event.req.request_uri.query)]
event.post = event.req.query
end
event.status = @status
event.status.now = event.time
event.status.ds_label = @label
yield event
@status.last_time = event.time
end
self
end
def before_each_event
@log_lines = []
@mutex_lo.synchronize do
@log_lines = @lo_data
@lo_data = []
end
end
def after_each_event
nil
end
def first_time_log_check(ds, event)
nil
end
def register_first_res(msgid, res)
return if res.nil?
@mutex_first_res.synchronize do
if @first_res_list[msgid].nil?
@first_res_list[msgid] = res
end
end
end
def generate_context(event)
context = OutvokeProcExecContext.new(@outvoke, self, event)
context.extend(Module.new do
def res(hook_result)
@ds.register_first_res(@event.msgid, hook_result)
end
end)
end
end
StructDSVrchatStatus = Struct.new(:now, :quiet_mode, :logfile, :file_size,
:last_joined_time, :elapsed, :count, :lineno)
class OutvokeEventVrchat < OutvokeEvent
attr_accessor :raw, :type
end
class OutvokeDataSourceVrchat < OutvokeDataSource
attr_accessor :log_dir
def initialize(outvoke, label)
super
@status = StructDSVrchatStatus.new
@status.logfile = nil
@status.file_size = 0
@status.count = 0
@status.lineno = 0
@status.last_joined_time = Time.now
@is_first_time_log_check = true
@log_dir = ''
@has_new_log_lines = false
end
def each_event
return unless @has_new_log_lines
loop do
cnt = 0
line_raw = ''
m = nil
@log_lines[@status.lineno..].each do |line|
cnt += 1
line_raw = line
m = line.chomp.match(/\A([0-9]{4}\.[0-9]{2}\.[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}) +([-_a-zA-Z0-9]+) +(.*)\z/)
break if m # if found
end
@status.lineno += cnt
@has_new_log_lines = false
return self unless m
event = OutvokeEventVrchat.new(m[3].sub(/\A *- */, ''))
event.raw = line_raw
event.time = Time.parse(m[1]) # unlike event.status.now, a value taken from log data
event.type = m[2]
event.msgid = @outvoke.generate_new_msgid
event.status = @status
event.status.now = Time.now
if event.body.start_with?('[Behaviour] Joining or Creating Room: ')
event.status.last_joined_time = event.time
end
event.status.elapsed = event.status.now - event.status.last_joined_time
yield event
end
self
end
def before_each_event
tmp_now = Time.now
has_file_changed = false
# check if it is a new file
Dir.glob("#{@log_dir}/output_log_*.txt").last.then do
if @status.logfile != _1
has_file_changed = true
@status.logfile = _1
@status.lineno = 0
if not $outvoke.quiet_mode? # TODO
puts "[#{tmp_now}] [outvoke-system] #{@label}: a new log file was found." if @status.logfile
end
end
end
# return if not found
unless @status.logfile
if @is_first_time_log_check
puts "[#{tmp_now}] [outvoke-system] #{@label}: ERROR: VRChat log file not found" end
return
end
# return if not readable
return unless File.readable?(@status.logfile)
# check if file size has increased
unless has_file_changed
if @status.file_size < File.size(@status.logfile)
has_file_changed = true
end
end
# return if the log file has not changed
return unless has_file_changed
if @is_first_time_log_check
tmp = @status.logfile.sub(
/\A.*output_log_([0-9]{4}-[0-9]{2}-[0-9]{2})_([0-9]{2})-([0-9]{2})-([0-9]{2})\.txt\z/,
"\\1 \\2:\\3:\\4")
@status.last_joined_time = Time.parse(tmp)
end
# read VRChat log file
File.read(@status.logfile).then do |file_body|
@log_lines = file_body.split("\n")
@status.file_size = file_body.bytesize
end
@has_new_log_lines = true
end
def after_each_event
if @is_first_time_log_check
if not $outvoke.quiet_mode? # TODO
puts "[#{Time.now}] [outvoke-system] #{@label}: first time log check has done."
end
@is_first_time_log_check = false
end
end
def first_time_log_check(ds, event)
return unless event.body.start_with?('[B')
if event.body.start_with?('[Behaviour] Entering Room: ')
@status.count = 0
elsif event.body.start_with?('[Behaviour] OnPlayerJoined ')
@status.count += 1
elsif event.body.start_with?('[Behaviour] OnPlayerLeft ')
@status.count -= 1
@status.count = 0 if @status.count < 0
end
end
end
StructDSEverySecStatus = Struct.new(:now, :quiet_mode, :last_time)
class OutvokeDataSourceEverySec < OutvokeDataSource
def initialize(outvoke, label)
super
@status = StructDSEverySecStatus.new
@status.last_time = Time.now.floor
end
def each_event
@log_lines.each do |line|
event = OutvokeEvent.new(line["body"])
event.time = line["body"] # TODO
event.msgid = line["msgid"] event.status = @status
event.status.now = Time.now
yield event
@status.last_time = line["body"]
end
self
end
def before_each_event
@log_lines = []
tmp_time = @status.last_time.floor
loop do
tmp_time += 1
return if tmp_time >= Time.now
@log_lines << {"msgid" => @outvoke.generate_new_msgid, "body" => tmp_time}
end
end
def after_each_event
nil
end
def first_time_log_check(ds, event)
nil
end
end
# --------------------------------------------------------------------
# from this point forward, it will only be executed when this file run
# as a command, not when this file included by require.
if $0 == __FILE__
$stdout.sync = true
def log_puts(str, line_head = '')
return if $outvoke.quiet_mode?
if line_head == ''
puts "#{str.gsub("\n", " ")}"
else
puts "#{line_head}#{str.gsub("\n", "\n" + line_head)}"
end
end
$outvoke = Outvoke.new
version_str = "#{$outvoke.version.branch} (version #{$outvoke.version.body})"
require 'optparse'
is_ruby_code_given = false
ruby_code = nil
mainloop_interval = nil
opts = OptionParser.new
opts.default_argv = ['main.rb']
opts.on('-e CODE') { |optvalue| ruby_code = optvalue }
opts.on('-q', '--quiet') { $outvoke.quiet_mode = true }
opts.on('--wait INTERVAL') do |optvalue|
raise "--wait option allows only numeric" if optvalue !~ /\A[0-9]+(\.[0-9]+)?\z/
mainloop_interval = optvalue.to_f
end
if ARGV[0]
specified_file = opts.parse!(ARGV)[0]
else specified_file = opts.parse![0]
end
log_puts "# starting Outvoke #{version_str} ---- #{Time.now}"
log_puts "# ----"
$outvoke.wait = mainloop_interval if mainloop_interval
'stdin'.then do
$outvoke.ds[_1] = OutvokeDataSourceStdin.new($outvoke, _1)
end
'auxin:fifo:1'.then do
$outvoke.ds[_1] = OutvokeDataSourceAuxinFifo.new($outvoke, _1)
end
'lo'.then do
$outvoke.ds[_1] = OutvokeDataSourceLoopback.new($outvoke, _1)
end
'every-sec'.then do
$outvoke.ds[_1] = OutvokeDataSourceEverySec.new($outvoke, _1)
end
'web-001'.then do
$outvoke.ds[_1] = OutvokeDataSourceWeb.new($outvoke, _1)
end
'vrchat-001'.then do
vrchat_log_dir = "#{Dir.home}/.steam/debian-installation/steamapps/compatdata/438100/pfx/drive_c/users/steamuser/AppData/LocalLow/VRChat/VRChat"
$outvoke.ds[_1] = OutvokeDataSourceVrchat.new($outvoke, _1)
$outvoke.ds[_1].log_dir = vrchat_log_dir
end
'./outvoke.conf.rb'.then do
if File.readable?(_1)
log_puts "# loading #{_1} ..."
require _1
end
end
if ruby_code
log_puts '# given ruby code:'
log_puts ruby_code, '# '
eval "using OutvokeDSL;#{ruby_code}"
else
file_prefix = ''
file_prefix = './' if not specified_file.start_with?('/')
specified_file = "#{file_prefix }#{specified_file}"
log_puts "# loading #{specified_file} ..."
if File.readable?(specified_file)
require specified_file
else
log_puts "# ERROR: #{specified_file} does not exist or is not readable"
exit 1
end
end
'web-001'.then do
if $outvoke[_1].enabled?
log_puts "# =========================== websrv start ==========================="
$outvoke[_1].websrv_start
end
end
log_puts '# ----'
log_puts ''
# execute mainloop