Skip to content
Snippets Groups Projects
  • cleemy desu wayo's avatar
    Modify: add new feature "pre_procs" and "post_procs" · 20f8d9c1
    cleemy desu wayo authored
    "pre_procs" is for the process before the execution of a Proc
    registered by hook.
    
    When overwriting e.body with pre_procs, it should be noted that
    it will be executed as many times as the number of hooks.
    
    "post_procs" treats the value returned by Proc registered by hook.
    
    This new feature "post_procs" is related to 04be13b4 and 00b2fb7c.
    20f8d9c1
outvoke.rb 18.41 KiB
#!/usr/bin/env ruby
#
# ==========================================================================
# Outvoke -- version 0.0.99.20241111.1
#
# 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'
class Outvoke
include Enumerable
attr_accessor :sources, :wait, :version, :nodup_list, :mutex_nodup
def initialize
@version = Struct.new(:branch, :body).new(
'0.1',
'0.0.99.20241111.1'
)
@sources = Hash.new
@wait = 0.5
@hooks = []
@is_quiet_mode = false
@nodup_list = Hash.new
@mutex_nodup = Mutex.new
end
def quiet_mode?
@is_quiet_mode
end
def quiet_mode=(x)
@is_quiet_mode = x
end
def each
return @hooks.enum_for unless block_given?
@hooks.each {|x| yield x }
end
def <<(x)
@hooks << x
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(x)
@sources['lo'].mutex_lo.synchronize do
@sources['lo'].lo_data << x
end
nil
end
def mainloop
loop do
@sources.each do |dslabel, ds|
ds.before_each_event
ds.each_event do |event|
next unless ds.enabled?
if ds.is_first_time_log_check
ds.first_time_log_check(ds, event)
else
hooks_exec(ds, event)
end
end
ds.after_each_event
end
sleep @wait
end
end
# extracts only the labels of the appropriate data sources from $sources
def extract_ds(dslabel)
return @sources.keys if dslabel == true
label_cond_array = [dslabel]
label_cond_array = dslabel if dslabel.respond_to?(:each)
result = []
label_cond_array.each do |label_cond|
next unless label_cond.respond_to?(:===)
@sources.keys.each do |label|
result << label if label_cond === label
end
end
result.uniq
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, event)
@hooks.each do |hook|
# check data source
next unless hook['source'] == ds.label # TODO
hook_cond_array = [hook['cond']]
if hook['cond'].respond_to?(:each)
hook_cond_array = hook['cond']
end
event.is_quiet_mode = @is_quiet_mode # TODO
# check condition
hook_cond_array.detect do |cond|
event.m = nil
if cond == true
event.m = [event.body]
elsif cond.is_a?(String)
event.m = [event.body] if event.body.start_with?(cond)
elsif cond.respond_to?(:match)
event.m = cond.match(event.body)
elsif cond.respond_to?(:call)
proc_result = 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
next unless event.m
event.m = event.m.to_a # with Marshal.#dump in mind
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 = OutvokeProcExecContext.new(self, event)
hook_result = tmp_context.instance_exec(event, &hook['proc'])
else
hook_result_for_post_process = event.body
end
if hook_result == true
hook_result_for_post_process = event.body
elsif (hook_result != nil && hook_result != false)
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
end
#
# this class provided only for instance_exec
#
class OutvokeProcExecContext
def initialize(outvoke, event)
@outvoke = outvoke
@event = event
@event_body = @event.body.dup
end
def to(ds_cond)
@outvoke.extract_ds(ds_cond).map{ @outvoke.sources[_1] }.each do |ds|
ds.mutex_lo.synchronize do
ds.lo_data << @event_body
end
end
nil
end
end
module OutvokeDSL
refine Kernel do
def hook(ds_cond, event_cond = true, &block)
raise '$outvoke is not an Outvoke object' unless $outvoke.is_a?(Outvoke)
$outvoke.extract_ds(ds_cond).each do |label|
if label == 'stdin'
$outvoke.sources['stdin'].enabled = true
end
$outvoke << {
'source' => label, # TODO
'cond' => event_cond,
'proc' => block
}
end
end
# if block is given, execute in another thread (concurrent computing)
def hookcc(ds_cond, event_cond = true, &block)
if block_given?
hook(ds_cond, event_cond) do |e; tmp_event|
tmp_event = Marshal.load(Marshal.dump(e)) # deep copy
Thread.start { instance_exec(tmp_event , &block) }
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 hooksec(event_cond = true, &block)
hook("every-sec", event_cond, &block)
end
def hookvr(event_cond = true, &block)
hook(/^vr/, event_cond, &block)
end
def loputs(x)
$outvoke.loputs(x)
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)
$outvoke.extract_ds(ds_cond).map{ $outvoke.sources[_1] }.each do |ds|
ds.mutex_lo.synchronize do
ds.lo_data << self.to_s
end
end
nil
end
end
end
class OutvokeDataSource
attr_accessor :label, :status, :log_lines, :lo_data, :mutex_lo, :is_first_time_log_check,
:pre_procs, :post_procs
def initialize(label)
@label = label
@is_enabled = true
@log_lines = []
@lo_data = []
@mutex_lo = Mutex.new
@is_first_time_log_check = 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.is_quiet_mode
puts hook_result.to_s
else
puts "[#{e.time}] #{hook_result}".gsub("\n", " ")
end
}
end
def enabled?
@is_enabled
end
def enabled=(x)
@is_enabled = x
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
end
#
# TODO: experimental and incomplete implementation
#
# (specifications are subject to sudden change without notice)
#
StructDSStdinStatus = Struct.new(:now, :last_time)
StructDSStdinEvent = Struct.new(:status, :time, :body, :m, :is_quiet_mode)
class OutvokeDataSourceStdin < OutvokeDataSource
def initialize(label)
super
@status = StructDSStdinStatus.new
@status.last_time = Time.now.floor
@stdin = $stdin.each_line
@is_enabled = false
@stdin_new_lines = []
@mutex_stdin = Mutex.new
@thread_stdin = Thread.new do
Thread.stop
loop do
Thread.stop unless @is_enabled
Thread.current["tmp"] = @stdin.next
Thread.stop unless @is_enabled
@mutex_stdin.synchronize do
@stdin_new_lines << Thread.current["tmp"].chomp
end
end
end
end
def enabled=(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 = StructDSStdinEvent.new
event.time = Time.now
event.body = line.to_s
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.map { _1.dup } # something like deep_dup
@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)
#
StructDSLoopbackStatus = Struct.new(:now, :last_time)
StructDSLoopbackEvent = Struct.new(:status, :time, :body, :m, :is_quiet_mode)
class OutvokeDataSourceLoopback < OutvokeDataSource
def initialize(label)
super
@status = StructDSLoopbackStatus.new
@status.last_time = Time.now.floor
end
def each_event
@log_lines.each do |line|
event = StructDSLoopbackEvent.new
event.time = Time.now
event.body = line.to_s
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.map { _1.dup } # something like deep_dup
@lo_data = []
end
end
def after_each_event
nil
end
def first_time_log_check(ds, event)
nil
end
end
StructDSVrchatStatus = Struct.new(:now, :logfile, :file_size,
:last_joined_time, :elapsed, :count, :lineno)
StructDSVrchatEvent = Struct.new(:status, :raw, :time, :type, :body, :m, :is_quiet_mode)
class OutvokeDataSourceVrchat < OutvokeDataSource
attr_accessor :log_dir
def initialize(label)
super
@status = StructDSVrchatStatus.new
@status.logfile = nil
@status.file_size = 0
@status.count = 0
@status.lineno = 0
@log_dir = ''
@is_first_time_log_check = true
@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 = StructDSVrchatEvent.new
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.body = m[3].sub(/\A *- */, '')
event.status = @status
event.status.now = Time.now
if event.body =~ /\A\[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
Dir.glob("#{@log_dir}/output_log_*.txt").last.then do
if _1 != @status.logfile
@status.logfile = _1
@status.lineno = 0
if not $outvoke.quiet_mode? # TODO
puts "[#{Time.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 "[#{Time.now}] [outvoke-system] #{@label}: ERROR: VRChat log file not found"
end
return
end
# return if not readable
return unless File.readable?(@status.logfile)
# return if the file size has not changed
return if @status.file_size == File.size(@status.logfile)
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)
if event.body =~ /\A\[Behaviour\] entering room:/i
@status.count = 0
elsif event.body =~ /\A\[Behaviour\] onplayerjoined /i
@status.count += 1
elsif event.body =~ /\A\[Behaviour\] onplayerleft /i
@status.count -= 1
@status.count = 0 if @status.count < 0
end
end
end
StructDSEverySecStatus = Struct.new(:now, :last_time)
StructDSEverySecEvent = Struct.new(:status, :time, :body, :m, :is_quiet_mode)
class OutvokeDataSourceEverySec < OutvokeDataSource
def initialize(label)
super
@status = StructDSEverySecStatus.new
@status.last_time = Time.now.floor
end
def each_event
@log_lines.each do |line|
event = StructDSEverySecEvent.new
event.time = line
event.body = line.to_s
event.status = @status
event.status.now = Time.now
yield event
@status.last_time = line
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 << 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
webui_port = 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('--webui-port PORT') { |optvalue| webui_port = optvalue }
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.sources[_1] = OutvokeDataSourceStdin.new(_1)
end
'lo'.then do
$outvoke.sources[_1] = OutvokeDataSourceLoopback.new(_1)
end
'every-sec'.then do
$outvoke.sources[_1] = OutvokeDataSourceEverySec.new(_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.sources[_1] = OutvokeDataSourceVrchat.new(_1)
$outvoke.sources[_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
log_puts '# ----'
log_puts ''
# execute mainloop
begin
$outvoke.mainloop
rescue Interrupt
if not $outvoke.quiet_mode?
puts "[#{Time.now}] [outvoke-system] interrupted"
end
end
end