diff options
Diffstat (limited to 'scripts')
50 files changed, 4430 insertions, 125 deletions
diff --git a/scripts/context/perl/mptopdf.pl b/scripts/context/perl/mptopdf.pl index b5d4bc15d..f85c36891 100644 --- a/scripts/context/perl/mptopdf.pl +++ b/scripts/context/perl/mptopdf.pl @@ -88,17 +88,17 @@ if (($pattern eq '')||($Help)) { $rest .= " $mplatexswitch" ; } if ($MetaFun) { - $mpbin = 'mpost --progname=mpost --mem=metafun' ; + $mpbin = "mpost --progname=mpost --mem=metafun" ; } else { - $mpbin = 'mpost --mem=mpost' ; + $mpbin = "mpost --mem=mpost" ; } } else { if ($Latex) { $rest .= " $texlatexswitch" ; } - $mpbin = 'texexec --mptex $PassOn' ; + $mpbin = "texexec --mptex $PassOn" ; } - print "\n$program : running '$command'" ; + print "\n$program : running '$mpbin'\n" ; my $error = system ("$mpbin $rest $pattern") ; if ($error) { print "\n$program : error while processing mp file\n" ; diff --git a/scripts/context/ruby/base/tex.rb b/scripts/context/ruby/base/tex.rb index 165d370f9..1e1b67f0f 100644 --- a/scripts/context/ruby/base/tex.rb +++ b/scripts/context/ruby/base/tex.rb @@ -50,6 +50,10 @@ class Array end end + def join_path + self.join(File::PATH_SEPARATOR) + end + end class TEX @@ -164,16 +168,22 @@ class TEX @@extrastringvars = [] def booleanvars - [@@booleanvars,@@extrabooleanvars].flatten + [@@booleanvars,@@extrabooleanvars].flatten.uniq end def stringvars - [@@stringvars,@@extrastringvars].flatten + [@@stringvars,@@extrastringvars].flatten.uniq end def standardvars - @@standardvars + [@@standardvars].flatten.uniq end def knownvars - @@knownvars + [@@knownvars].flatten.uniq + end + def allbooleanvars + [@@booleanvars,@@extrabooleanvars].flatten.uniq + end + def allstringvars + [@@stringvars,@@extrastringvars,@@standardvars,@@knownvars].flatten.uniq end def setextrastringvars(vars) @@ -517,7 +527,8 @@ class TEX end mpsformats.each do |mpsformat| report("generating mps format #{mpsformat}") - command = [quoted(mpsengine),prognameflag(progname),iniflag,tcxflag,mpsformat,mpsmakeextras(mpsformat)].join(' ') + # command = [quoted(mpsengine),prognameflag(progname),iniflag,tcxflag,mpsformat,mpsmakeextras(mpsformat)].join(' ') + command = [quoted(mpsengine),iniflag,tcxflag,mpsformat,mpsmakeextras(mpsformat)].join(' ') report(command) if getvariable('verbose') system(command) end @@ -1185,7 +1196,7 @@ class TEX if mpsengine && mpsformat && progname then ENV["MPXCOMMAND"] = "0" unless mpx # command = [quoted(mpsengine),prognameflag(progname),formatflag(mpsengine,mpsformat),tcxflag,runoptions(mpsengine),filename,mpsprocextras(mpsformat)].join(' ') - command = [quoted(mpsengine),"-progname=mpost",formatflag(mpsengine,mpsformat),tcxflag,runoptions(mpsengine),mpname,mpsprocextras(mpsformat)].join(' ') + command = [quoted(mpsengine),formatflag(mpsengine,mpsformat),tcxflag,runoptions(mpsengine),mpname,mpsprocextras(mpsformat)].join(' ') report(command) if getvariable('verbose') system(command) true @@ -1285,7 +1296,7 @@ class TEX report("fixing backend map path for #{backend}") if getvariable('verbose') ENV['backend'] = backend ; ENV['progname'] = backend unless validtexengine(backend) - ENV['TEXFONTMAPS'] = ".;\$TEXMF/fonts/map/{#{backend},pdftex,dvips,}//" + ENV['TEXFONTMAPS'] = ['.',"\$TEXMF/fonts/map/{#{backend},pdftex,dvips,}//"].join_path else report("unable to fix backend map path") if getvariable('verbose') end diff --git a/scripts/context/ruby/base/texutil.rb b/scripts/context/ruby/base/texutil.rb index 0529f0ca1..89f5e5385 100644 --- a/scripts/context/ruby/base/texutil.rb +++ b/scripts/context/ruby/base/texutil.rb @@ -51,12 +51,21 @@ class TeXUtil @logger = logger end + def report(str) + @logger.report("fatal error in plugin (#{str}): #{$!}") + puts("\n") + $@.each do |line| + puts(" #{line}") + end + puts("\n") + end + def reset(name) if @plugins.include?(name) then begin eval("#{name}").reset(@logger) rescue Exception - @logger.report("fatal error in resetting plugin") + report("resetting") end else @logger.report("no plugin #{name}") @@ -89,7 +98,7 @@ class TeXUtil begin eval("#{name}").reader(@logger,data.flatten) rescue Exception - @logger.report("fatal error in plugin reader #{name} (#{$!})") + report("reading") end else @logger.report("no plugin #{name}") @@ -107,7 +116,7 @@ class TeXUtil begin eval("#{p}").writer(@logger,handle) rescue Exception - @logger.report("fatal error in plugin writer #{p} (#{$!})") + report("writing") end end end @@ -117,7 +126,7 @@ class TeXUtil begin eval("#{p}").processor(@logger) rescue Exception - @logger.report("fatal error in plugin processor #{p} (#{$!})") + report("processing") end end end @@ -127,7 +136,7 @@ class TeXUtil begin eval("#{p}").finalizer(@logger) rescue Exception - @logger.report("fatal error in plugin finalizer #{p} (#{$!})") + report("finalizing") end end end @@ -211,8 +220,12 @@ class TeXUtil end def replace(str) - str.gsub(@rexa) do - @rep[$1.escaped] + if @rexa then + str.gsub(@rexa) do + @rep[$1.escaped] + end + else + str end end @@ -226,11 +239,11 @@ class TeXUtil def remap(str) s = str.dup -if true then # numbers are treated special - s.gsub!(/(\d+)/o) do - $1.rjust(10,'a') # rest is b .. k - end -end + if true then # numbers are treated special + s.gsub!(/(\d+)/o) do + $1.rjust(10,'a') # rest is b .. k + end + end if @rexa then s.gsub!(@rexa) do @rep[$1.escaped] diff --git a/scripts/context/ruby/ctxtools.rb b/scripts/context/ruby/ctxtools.rb index 2092b346c..2104eebf7 100644 --- a/scripts/context/ruby/ctxtools.rb +++ b/scripts/context/ruby/ctxtools.rb @@ -181,7 +181,7 @@ class Commands if @commandline.option("pipe") then print version else - report("context version: #{version}") + report("context version: #{version} (#{filename})") end end @@ -789,10 +789,10 @@ class String def markbraces level = 0 self.gsub(/([\{\}])/o) do |chr| - if chr == '{' + if chr == '{' then level = level + 1 chr = "((+#{level}))" - elsif chr == '}' + elsif chr == '}' then chr = "((-#{level}))" level = level - 1 end @@ -803,13 +803,13 @@ class String def unmarkbraces self.gsub(/\(\(\+\d+?\)\)/o) do "{" - end.gsub(/\(\(\-\d+?\)\)/o) do + end .gsub(/\(\(\-\d+?\)\)/o) do "}" end end def getargument(pattern) - if self =~ /(#{pattern})\s*\(\(\+(\d+)\)\)(.*?)\(\(\-\2\)\)/ then # no /o + if self =~ /(#{pattern})\s*\(\(\+(\d+)\)\)(.*?)\(\(\-\2\)\)/m then # no /o return $3 else return "" @@ -843,6 +843,7 @@ class Language @language = language @filenames = filenames @remapping = Array.new + @demapping = Array.new @unicode = Hash.new @encoding = encoding @data = '' @@ -867,6 +868,9 @@ class Language def remap(from, to) @remapping.push([from,to]) end + def demap(from, to) + @demapping.push([from,to]) + end def load(filenames=@filenames) begin @@ -906,6 +910,11 @@ class Language @data = @data.withargument(what) do |content| report("converting #{what}") report("") + @demapping.each_index do |i| + content.gsub!(@demapping[i][0], @demapping[i][1]) + end + content.gsub!(/\\delete\{.*?\}/o) do '' end + content.gsub!(/\\keep\{(.*?)\}/o) do $1 end done = false @remapping.each_index do |i| from, to, m = @remapping[i][0], @remapping[i][1], 0 @@ -1242,24 +1251,24 @@ class Language remap(/Y/, "[ostroke]") remap(/Z/, "[aring]") when 'hu' then - + # nothing when 'ca' then - remap(/\\c\{.*?\}/, "") + demap(/\\c\{/, "\\delete{") when 'de', 'deo' then - remap(/\\c\{.*?\}/, "") - remap(/\\n\{\}/, "") + demap(/\\c\{/, "\\delete{") + demap(/\\n\{/, "\\keep{") remap(/\\3/, "[ssharp]") remap(/\\9/, "[ssharp]") remap(/\"a/, "[adiaeresis]") remap(/\"o/, "[odiaeresis]") remap(/\"u/, "[udiaeresis]") when 'fr' then - remap(/\\n\{\}/, "") + demap(/\\n\{/, "\\keep{") remap(/\\ae/, "[adiaeresis]") remap(/\\oe/, "[odiaeresis]") when 'la' then # \lccode`'=`' somewhere else, todo - remap(/\\c\{.*?\}/, "") + demap(/\\c\{/, "\\delete{") remap(/\\a\s*/, "[aeligature]") remap(/\\o\s*/, "[oeligature]") when 'agr' then @@ -2202,6 +2211,47 @@ end class Commands + @@re_utf_bom = /^\357\273\277/o # just utf-8 + + def disarmutfbom + + if @commandline.arguments.empty? then + report("provide filename") + else + @commandline.arguments.each do |filename| + report("checking '#{filename}'") + if FileTest.file?(filename) then + begin + data = IO.read(filename) + if data.sub!(@@re_utf_bom,'') then + if @commandline.option('force') then + if f = File.open(filename,'wb') then + f << data + f.close + report("bom found and removed") + else + report("bom found and removed, but saving file fails") + end + else + report("bom found, use '--force' to remove it") + end + else + report("no bom found") + end + rescue + report("bom found, but removing it fails") + end + else + report("provide valid filename") + end + end + end + end + +end + +class Commands + include CommandBase def updatecontext @@ -2303,6 +2353,7 @@ commandline.registeraction('brandfiles' , 'add context copyright notice [ commandline.registeraction('platformize' , 'replace line-endings [--recurse --force] [pattern]') commandline.registeraction('dependencies' , 'analyze depedencies witin context [--compact] [rootfile]') commandline.registeraction('updatecontext' , 'download latest version and remake formats') +commandline.registeraction('disarmutfbom' , 'remove utf bom [==force]') commandline.registervalue('type','') diff --git a/scripts/context/ruby/rlxtools.rb b/scripts/context/ruby/rlxtools.rb index 7962474eb..addfe9894 100644 --- a/scripts/context/ruby/rlxtools.rb +++ b/scripts/context/ruby/rlxtools.rb @@ -97,13 +97,13 @@ class Commands report("processing record #{nofrecords} (#{variables['file'] || 'noname'}: #{variables.size} entries)") if conversion = variables['conversion'] then report("testing for conversion #{conversion}") - if suffix = variables['suffix'] then + if suffix.downcase = variables['suffix'].downcase then if file = variables['file'] then report("conversion #{conversion} for suffix #{suffix} for file #{file}") else report("conversion #{conversion} for suffix #{suffix}") end - pattern = "@name='#{conversion}' and @suffix='#{suffix}'" + pattern = "@name='#{conversion}' and @suffix='#{suffix.downcase}'" if steps = REXML::XPath.first(proc.root,"/rl:manipulators/rl:manipulator[#{pattern}]") then localsteps = steps.deep_clone ['rl:old','rl:new'].each do |tag| diff --git a/scripts/context/ruby/rscortool.rb b/scripts/context/ruby/rscortool.rb new file mode 100644 index 000000000..c656fed85 --- /dev/null +++ b/scripts/context/ruby/rscortool.rb @@ -0,0 +1,63 @@ +# program : rscortool +# copyright : PRAGMA Publishing On Demand +# version : 1.00 - 2002 +# author : Hans Hagen +# +# project : eXaMpLe +# concept : Hans Hagen +# info : j.hagen@xs4all.nl +# www : www.pragma-pod.com / www.pragma-ade.com + +require 'rexml/document.rb' + +class Array + + def downcase + self.collect { |l| l.to_s.downcase } + end + +end + +class SortedXML + + def initialize (filename) + return nil if not filename or filename.empty? or not test(?e,filename) + @data = REXML::Document.new(File.new(filename), + {:ignore_whitespace_nodes => :all, + :compress_whitespace => :all}) + end + + def save (filename) + # filename += '.xml' unless filename.match(/\..*?$/) + filename += '.xml' unless filename =~ /\..*?$/ + if not filename.empty? and f = open(filename,'w') + @data.write(f,0) + f.close + end + end + + def sort + keys = REXML::XPath.match(@data.root,"/contacts/contact/@label") + return unless keys + keys = keys.downcase + records = @data.elements.to_a("/contacts/contact") + @data.elements.delete_all("/contacts/contact") + keys = keys.collect do |l| # prepare numbers + l.gsub(/(\d+)/) do |d| sprintf('%05d', d) end + end + keys.sort.each do |s| + @data.root.add_element(records[keys.index(s)]) + end + end + +end + +def sortfile (filename) + c = SortedXML.new(filename) + c.sort + c.save('test.xml') +end + +exit if ARGV[0] == nil or ARGV[0].empty? + +sortfile(ARGV[0]) diff --git a/scripts/context/ruby/rsfiltool.rb b/scripts/context/ruby/rsfiltool.rb new file mode 100644 index 000000000..f3abfdfc7 --- /dev/null +++ b/scripts/context/ruby/rsfiltool.rb @@ -0,0 +1,340 @@ +# program : rsfiltool +# copyright : PRAGMA Publishing On Demand +# version : 1.01 - 2002 +# author : Hans Hagen +# +# project : eXaMpLe +# concept : Hans Hagen +# info : j.hagen@xs4all.nl +# www : www.pragma-pod.com / www.pragma-ade.com + +unless defined? ownpath + ownpath = $0.sub(/[\\\/]\w*?\.rb/i,'') + $: << ownpath +end + +# --name=a,b,c.xml wordt names [a.xml, b.xml, c.xml] +# --path=x/y/z/a,b,c.xml wordt [x/y/z/a.xml, x/y/z/b.xml, x/y/z/c.xml] + +# todo : split session stuff from xmpl/base into an xmpl/session module and "include xmpl/session" into base and here and ... + +require 'ftools' +require 'xmpl/base' +require 'xmpl/switch' +require 'xmpl/request' + +session = Example.new('rsfiltool', '1.01', 'PRAGMA POD') + +filterprefix = 'rsfil-' + +commandline = CommandLine.new + +commandline.registerflag('submit') +commandline.registerflag('fetch') +commandline.registerflag('report') +#commandline.registerflag('split') +commandline.registerflag('stamp') +commandline.registerflag('silent') +commandline.registerflag('request') +commandline.registerflag('nobackup') + +commandline.registervalue('filter') + +commandline.registervalue('root') +commandline.registervalue('path') +commandline.registervalue('name') + +commandline.expand + +session.set('log.silent',true) if commandline.option('silent') + +session.inherit(commandline) + +session.identify + +# session.exit unless session.loadenvironment + +def prepare (session) + + # Normally the system provides the file, but a user can provide the rest; in + # order to prevent problems with keying in names, we force lowercase names. + + session.set('option.file',session.get('argument.first')) if session.get('option.file').empty? + + root = session.get('option.root').downcase + path = session.get('option.path').downcase + name = session.get('option.name').downcase + file = session.get('option.file').downcase + + session.error('provide file') if file.empty? + session.error('provide root') if root.empty? + + filter = session.get('option.filter').downcase + trash = session.get('option.trash').downcase + + trash = '' unless FileTest.directory?(trash) + + if not filter.empty? then + begin + require filter + rescue Exception + begin + require filterprefix + filter + rescue Exception + session.error('invalid filter') + end + end + begin + if RSFIL::valid?(file) then + split = RSFIL::split(file,name) + path = if split[0].downcase then split[0] else '' end + file = if split[1].downcase then split[1] else '' end + name = if split[2].downcase then split[2] else '' end + session.report('split result',split.inspect) + session.error('unable to split off path') if path.empty? + session.error('unable to split off file') if file.empty? + session.error('unable to split off name') if name.empty? + session.set('option.path',path) if path + session.set('option.file',file) if file + session.set('option.name',name) if name + else + session.error('invalid filename', file) + unless trash.empty? then + File.copy(file,trash + '/' + file) + end + end + rescue + session.error('unable to split',file,'with filter',filter) + end + end + + session.error('provide path') if path.empty? + + session.error('invalid root') unless test(?d,root) + + exit if session.error? + + session.set('fb.filename',file) + + path.gsub!(/\\/o, '/') + path.gsub!(/\s/o, '') + + path = root + '/' + path + + # multiple paths + + if path =~ /^(.*)\/(.*?)$/o then + prepath = $1 + postpath = $2 + paths = postpath.split(/\,/) + paths.collect! do |p| + prepath + '/' + p + end + else + paths = Array.new + paths.push(path) + end + + paths.collect! do |p| + p.gsub(/[^a-zA-Z0-9\s\-\_\/\.\:]/o, '-') + end + + file.gsub!(/\\/o, '/') + file.gsub!(/[^a-zA-Z0-9\s\-\_\/\.\:]/o, '-') + +# if session.get('option.split') +# if file =~ /(.*)\.(.*?)$/o +# path = path + '/' + $1 +# else +# session.error('nothing to split in filename') +# end +# end + + paths.each do |p| + begin + session.report('creating path', p) + File.makedirs(p) + rescue + session.error('unable to create path', p) + end + end + + name.gsub!(/\s+/,'') + + # can be a,b,c.exa.saved => a.exa.saved,b.exa.saved,c.exa.saved + + if name =~ /(.*?)\.(.*)$/ + name = $1 + suffix = $2 + names = name.split(/\,/) + names.collect! do |n| + n + '.' + suffix + end + name = names.join(',') + else + names = name.split(/\,/) + end + + session.set('fb.path',path) + session.set('fb.paths',paths) + session.set('fb.name',name) + session.set('fb.names',names) + +end + +def thefullname(path,file,name='') + + filename = file.gsub(/.*?\//, '') + + if name.empty? + path + '/' + filename + else + unless name =~ /\..+$/o # unless name.match(/\..+$/o) + if filename =~ /(\..+)$/o # if file.match(/(\..+)$/o) + name = name + $1 + end + end + path + '/' + name + end + +end + +def submitfile (session) + + filename = session.get('fb.filename') + paths = session.get('fb.paths') + names = session.get('fb.names') + + paths.each do |path| + session.report('submitting path',path) + names.each do |name| + session.report('submitting file',filename,'to',name) + submit(session,path,filename,name) + end + end + +end + +def submitlist (session) + + requestname = session.get('fb.filename') + paths = session.get('fb.paths') + + if test(?e,requestname) + session.report('loading request file', requestname) + if request = ExaRequest.new(requestname) + filelist = request.files + if filelist && (filelist.size > 0) + filelist.each do |filename| + paths.each do |path| + session.report('submitting file from list', filename) + submit(session,path,filename,request.naturalname(filename)) + end + end + else + session.warning('no filelist in', requestname) + end + else + session.warning('unable to load', requestname) + end + else + session.warning('no file', requestname) + end + +end + +def submit (session, path, filename, newname) + + fullname = thefullname(path,newname) + + unless test(?e,filename) + session.warning('no file to submit', filename) + return + end + + begin + File.copy(fullname,fullname+'.old') if ! session.get('nobackup') && test(?e,fullname) + if test(?e,filename) + File.copy(filename,fullname) + session.report('submit', filename, 'in', fullname) + if session.get('option.stamp') + f = open(fullname+'.tim','w') + f.puts(Time.now.gmtime.strftime("%a %b %d %H:%M:%S %Y")) + f.close + end + else + session.error('unable to locate', filename) + end + rescue + session.error('unable to move', filename, 'to', fullname) + end + +end + +def fetch (session) + + filename = session.get('fb.filename') + paths = session.get('fb.paths') + name = session.get('fb.name') + + begin + File.copy(filename,filename+'.old') if ! session.get('nobackup') && test(?e,filename) + paths.each do |path| + # fullname = thefullname(path,request.naturalname(filename)) + # fullname = thefullname(path,filename) + fullname = thefullname(path,name) + if test(?e,fullname) + File.copy(fullname,filename) + session.report('fetch', filename, 'from', fullname) + return + else + session.report('file',fullname, 'is not present') + end + end + rescue + session.error('unable to fetch file from path') + end + session.error('no file',filename, 'fetched') unless test(?e,filename) + +end + +def report (session) + + filename = session.get('fb.filename') + paths = session.get('fb.paths') + + paths.each do |path| + fullname = thefullname(path,request.naturalname(filename)) + if test(?e,fullname) + begin + session.report('file', fullname) + session.report('size', test(?s,fullname)) + if test(?e,fullname+'.tim') + str = IO.readlines(fullname+'.tim') + # str = IO.read(fullname+'.tim') + session.report('time', str) + end + rescue + session.error('unable to report about', fullname) + end + end + end + +end + +if session.get('option.submit') + prepare(session) + if session.get('option.request') + submitlist(session) + else + submitfile(session) + end +elsif session.get('option.fetch') + prepare(session) + fetch(session) +elsif session.get('option.report') + prepare(session) + report(session) +else + session.report('provide action') +end diff --git a/scripts/context/ruby/rslibtool.rb b/scripts/context/ruby/rslibtool.rb new file mode 100644 index 000000000..7ae5efb64 --- /dev/null +++ b/scripts/context/ruby/rslibtool.rb @@ -0,0 +1,114 @@ +# program : rslibtool +# copyright : PRAGMA Publishing On Demand +# version : 1.00 - 2002 +# author : Hans Hagen +# +# project : eXaMpLe +# concept : Hans Hagen +# info : j.hagen@xs4all.nl +# www : www.pragma-pod.com / www.pragma-ade.com + +# --add --base=filename --path=directory pattern +# --remove --base=filename --path=directory label +# --sort --base=filename --path=directory +# --purge --base=filename --path=directory +# --dummy --base=filename +# --namespace + +# rewrite + +unless defined? ownpath + ownpath = $0.sub(/[\\\/]\w*?\.rb/i,'') + $: << ownpath +end + +require 'rslb/base' +require 'xmpl/base' +require 'xmpl/switch' + +session = Example.new('rslbtool', '1.0', 'PRAGMA POD') + +session.identify + +commandline = CommandLine.new + +commandline.registerflag('add') +commandline.registerflag('remove') +commandline.registerflag('delete') +commandline.registerflag('sort') +commandline.registerflag('purge') +commandline.registerflag('dummy') +commandline.registerflag('process') +commandline.registerflag('namespace') + +commandline.registervalue('prefix') +commandline.registervalue('base') +commandline.registervalue('path') +commandline.registervalue('result') +commandline.registervalue('texexec') +commandline.registervalue('zipalso') + +commandline.expand + +session.inherit(commandline) + +base = session.get('option.base') +path = session.get('option.path') + +base = 'rslbtool.xml' if base.empty? + +# when path is given, assume that arg list is list of +# suffixes, else assume it is a list of globbed filespec + +if path.empty? + base += '.xml' unless base =~ /\..+$/ + list = commandline.arguments +else + Dir.chdir(File.dirname(path)) + list = Dir.glob("*.{#{commandline.arguments.join(',')}}") +end + +begin + reslib = Resource.new(base,session.get('option.namespace')) + reslib.load(base) +rescue + session.error('problems with loading base') + exit +end + +unless session.get('option.texexec').empty? + reslib.set_texexec(session.get('option.texexec')) +end + +if session.get('option.add') + + session.report('adding records', list) + reslib.add_figures(list,session.get('option.prefix')) + +elsif session.get('option.remove') or session.get('option.delete') + + session.report('removing records') + reslib.delete_figures(list) + +elsif session.get('option.sort') + + session.report('sorting records') + reslib.sort_figures() + +elsif session.get('option.purge') + + session.report('purging records') + reslib.purge_figures() + +elsif session.get('option.dummy') + + session.report('creating dummy records') + reslib.create_dummies(session.get('option.process'),session.get('option.result'),session.get('option.zipalso')) + +else + + session.warning('provide action') + +end + +reslib.save(base) diff --git a/scripts/context/ruby/runtools.rb b/scripts/context/ruby/runtools.rb index 7cb80f4ff..9c504845a 100644 --- a/scripts/context/ruby/runtools.rb +++ b/scripts/context/ruby/runtools.rb @@ -213,7 +213,7 @@ class Job end end - def copy_dir(from,to,pattern='*',exclude=[]) + def copy_dir(from,to,pattern='*',exclude=[]) # recursive pattern = '*' if ! pattern or pattern.empty? if from and to and File.expand_path(from) != File.expand_path(to) then ex = [exclude].flatten @@ -225,6 +225,18 @@ class Job end end + def copy_path(from,to,pattern='*',exclude=[]) # non-recursive + pattern = '*' if ! pattern or pattern.empty? + if from and to and File.expand_path(from) != File.expand_path(to) then + ex = [exclude].flatten + Dir.glob("#{from}/#{pattern}").each do |file| + unless ex.include?(File.extname(file)) then + _do_copy_(file,File.join(to,file.sub(/^#{from}/, ''))) + end + end + end + end + def _do_copy_(file,tofile) if FileTest.file?(file) and File.expand_path(file) != File.expand_path(tofile) then begin diff --git a/scripts/context/ruby/texexec.rb b/scripts/context/ruby/texexec.rb index e4de87b10..2751cef63 100644 --- a/scripts/context/ruby/texexec.rb +++ b/scripts/context/ruby/texexec.rb @@ -603,6 +603,42 @@ if job = TEX.new(logger) then end +class Commands + + alias saved_help help + + def wrap_help(title, vars) + report("") + report(title) + report("") + r, n = '', 0 + vars.sort.each do |s| + if n == 5 then + report(r) + r, n = '', 1 + else + n += 1 + end + r << ' ' + s + end + report(r) unless r.empty? + end + + def help + saved_help + if @commandline.option('all') then + if job = TEX.new(logger) then + wrap_help("boolean switches:", job.allbooleanvars) + wrap_help("string switches:", job.allstringvars) + end + else + report('') + report('--help --all shows all switches') + end + end + +end + # todo: register flags -> first one true commandline.registerflag('pdf') diff --git a/scripts/context/ruby/texmfstart.rb b/scripts/context/ruby/texmfstart.rb index bae921098..569da636c 100644 --- a/scripts/context/ruby/texmfstart.rb +++ b/scripts/context/ruby/texmfstart.rb @@ -58,7 +58,9 @@ end if $kpseerror then $kpsereport << "unable to locate #{$kpsemodules.join('|')} on library paths:\n\n" $kpsereport << " " + $:.join("\n ") + "\n\n" - $kpsereport << "an option is to copy\n\n" + $kpsereport << "an option is to point the RUBYLIB variable to\n\n" + $kpsereport << " <texmf-local or texmf>/scripts/context/ruby\n\n" + $kpsereport << "or to copy\n\n" $kpsereport << " <texmf-local or texmf>/scripts/context/ruby/base/kpse*\n\n" $kpsereport << "(including the kpse subpath) to e.g.\n\n" $kpsereport << " #{$ownpath}/../lib/texmfstart/\n\n" @@ -74,7 +76,7 @@ if $mswindows then require "Win32API" end -exit if defined?(REQUIRE2LIB) +# exit if defined?(REQUIRE2LIB) $stdout.sync = true $stderr.sync = true @@ -135,7 +137,7 @@ $makelist = [ 'exatools', 'runtools', # - 'texmfstart' + # no, 'texmfstart' ] # if ENV['TEXMFSTART_MODE'] = 'experimental' then @@ -785,75 +787,69 @@ def edit(filename) end def make(filename,windows=false,linux=false,remove=false) - basename = filename.dup - basename.sub!(/\.[^.]+?$/, '') - basename.sub!(/^.*[\\\/]/, '') + basename = File.basename(filename).gsub(/\.[^.]+?$/, '') if $stubpath == 'auto' then basename = File.dirname($0) + '/' + basename else basename = $stubpath + '/' + basename unless $stubpath.empty? end - if basename == filename then - report("nothing made (#{filename})") + if filename == 'texmfstart' then + program = 'ruby' + command = 'kpsewhich --format=texmfscripts --progname=context texmfstart.rb' + filename = `#{command}`.chomp.gsub(/\\/, '/') + if filename.empty? then + report("failure: #{command}") + return + elsif not remove then + if windows then + ['bat','exe'].each do |suffix| + if FileTest.file?("#{basename}.#{suffix}") then + report("windows stub '#{basename}.#{suffix}' skipped (already present)") + return + end + end + elsif linux && FileTest.file?(basename) then + report("unix stub '#{basename}' skipped (already present)") + return + end + end else program = nil if filename =~ /[\\\/]/ && filename =~ /\.(#{$scriptlist})$/ then program = $applications[$1] end filename = "\"#{filename}\"" if filename =~ /\s/ - if filename == 'texmfstart' then - program = 'ruby' - command = 'kpsewhich --format=texmfscripts --progname=context texmfstart.rb' - filename = `#{command}`.chomp - if filename.empty? then - report("failure: #{command}") - return - elsif not remove then - if windows then - ['bat','exe'].each do |suffix| - if FileTest.file?("#{basename}.#{suffix}") then - report("windows stub '#{basename}.#{suffix}' skipped (already present)") - return - end - end - elsif linux && FileTest.file?(basename) then - report("unix stub '#{basename}' skipped (already present)") - return - end + program = 'texmfstart' if $indirect || ! program || program.empty? + end + begin + callname = $predefined[filename.sub(/\.*?$/,'')] || filename + if remove then + if windows && (File.delete(basename+'.bat') rescue false) then + report("windows stub '#{basename}.bat' removed (calls #{callname})") + elsif linux && (File.delete(basename) rescue false) then + report("unix stub '#{basename}' removed (calls #{callname})") end else - program = 'texmfstart' if $indirect || ! program || program.empty? - end - begin - callname = $predefined[filename.sub(/\.*?$/,'')] || filename - if remove then - if windows && (File.delete(basename+'.bat') rescue false) then - report("windows stub '#{basename}.bat' removed (calls #{callname})") - elsif linux && (File.delete(basename) rescue false) then - report("unix stub '#{basename}' removed (calls #{callname})") - end - else - if windows && f = open(basename+'.bat','w') then - f.binmode - f.write("@echo off\015\012") - f.write("#{program} #{callname} %*\015\012") - f.close - report("windows stub '#{basename}.bat' made (calls #{callname})") - elsif linux && f = open(basename,'w') then - f.binmode - f.write("#!/bin/sh\012") - f.write("#{program} #{callname} $@\012") - f.close - report("unix stub '#{basename}' made (calls #{callname})") - end + if windows && f = open(basename+'.bat','w') then + f.binmode + f.write("@echo off\015\012") + f.write("#{program} #{callname} %*\015\012") + f.close + report("windows stub '#{basename}.bat' made (calls #{callname})") + elsif linux && f = open(basename,'w') then + f.binmode + f.write("#!/bin/sh\012") + f.write("#{program} #{callname} $@\012") + f.close + report("unix stub '#{basename}' made (calls #{callname})") end - rescue - report("failed to make stub '#{basename}' #{$!}") - else - return true end + rescue + report("failed to make stub '#{basename}' #{$!}") + return false + else + return true end - return false end def process(&block) diff --git a/scripts/context/ruby/www/admin.rb b/scripts/context/ruby/www/admin.rb new file mode 100644 index 000000000..4e85fd830 --- /dev/null +++ b/scripts/context/ruby/www/admin.rb @@ -0,0 +1,215 @@ +require 'fileutils' + +require 'www/lib' +require 'www/dir' +require 'www/common' + +class WWW + + include Common + + # klopt nog niet, twee keer task met een verschillend doel + + def handle_exatask + # case @session.check('task', request_variable('task')) + task, options, option = @session.get('task'), @session.get('option').split(@@re_bar), request_variable('option') + option = (options.first || '') if option.empty? + case task + when 'exaadmin' + @session.set('status', 'admin') # admin: status|dir + touch_session(@session.get('id')) + if options.include?(option) then + case option + when 'status' then handle_exaadmin_status + when 'dir' then handle_exaadmin_dir + else handle_exaadmin_status + end + elsif option.empty? then + message('Status', "unknown option") + else + message('Status', "option '#{option}' not permitted #{options.inspect}") + end + else + message('Status', "unknown task '#{task}") + end + end + + def handle_exaadmin + if id = valid_session() then + handle_exatask + else + message('Status', 'no login') + end + end + + def handle_exaadmin_dir + check_template_file('exalogin','exalogin-template.htm') + @interface.set('path:docroot', work_root) + @interface.set('dir:uri', 'exaadmin') # forces the dir handler into cgi mode + @interface.set('dir:task', 'exaadmin') # forces the dir handler into cgi mode + @interface.set('dir:option', 'dir') # forces the dir handler into cgi mode + filename = "#{@@session_prefix}#{request_variable('path')}" + fullname = File.join(work_root,filename) + if request_variable('path').empty? then + handle_exaadmin_status + elsif FileTest.directory?(fullname) then + handle_dir(filename, [], false) + elsif File.zero?(fullname) then + message('Error', "The file '#{filename}' is empty") + elsif File.size?(fullname) > (4 * 1024 * 1024) then + if FileTest.file?(File.expand_path(File.join(cache_root,filename))) then + str = "<br/><br/>Cached alternative: <a href=\"#{File.join('cache',filename)}\">#{File.basename(filename)}</a>" + else + str = '' + end + message('Error', "The file '#{filename}' is too big to serve over cgi." + str) + else + send_file(fullname) + end + end + + def handle_exaadmin_status + check_template_file('exalogin','exalogin-template.htm') + begin + n, str, lines, list, start, most, least, cached = 0, '', '', Hash.new, Time.now, 0, 0, false + filename = File.join(tmp_path(dirname),'sessions.rbd') + begin + File.open(filename) do |f| + list = Marshal.load(f) + end + rescue + cached, list = false, Hash.new + else + cached = true + end + files = Dir.glob("{#{work_roots.join(',')}}/#{@@session_prefix}*.ses") + list.keys.each do |l| + list.delete(l) unless files.include?(l) # slow + end + files.each do |f| + ctime = File.ctime(f) + stime = list[f][0] == ctime rescue 0 + unless ctime == stime then + begin + hash = load_session_file(f) + rescue + else + list[f] = [ctime,hash] + end + end + end + begin + File.open(filename,'w') do |f| + f << Marshal.dump(list) + end + rescue + # no save + end + begin + keys = list.keys.sort do |a,b| + case list[b][0] <=> list[a][0] + when -1 then -1 + when +1 then +1 + else + a <=> b + end + end + rescue + keys = list.keys.sort + end + totaltime, totaldone = 0.0, 0 + if keys.length > 0 then + keys.each do |entry| + s, t, session = entry, list[entry][0], list[entry][1] + status = session['status'] || '' + runtime = (session['runtime'] || '').to_f rescue 0 + starttime = (start.to_i-session['starttime'].to_i).to_s rescue '' + requesttime = session['endtime'].to_i-session['starttime'].to_i rescue 0 + requesttime = if requesttime > 0 then requesttime.to_s else '' end + if runtime > 0.0 then + totaltime += runtime + totaldone += 1 + if least > 0 then + if runtime < least then least = runtime end + else + least = runtime + end + if most > 0 then + if runtime > most then most = runtime end + else + most = runtime + end + end + if status.empty? then + # skip, garbage + elsif status =~ /^(|exa)admin/o then + # skip, useless + else + begin + lines << "<tr>\n" + lines << td("<a href=\"exaadmin?option=dir&path=#{session['id']}.dir\">#{session['id']}</a>") + lines << td(status) + lines << td(session['timeout']) + lines << td(starttime) + lines << td(session['runtime']) + lines << td(requesttime) + lines << td(t.strftime("%H:%M:%S %Y-%m-%d")) + lines << td(session['domain']) + lines << td(session['project']) + lines << td(session['username']) + lines << td(File.basename(File.dirname(s))) + lines << "</tr>\n" + rescue + else + n += 1 + end + end + end + if n > 0 then + str = "<table cellpadding='0'>\n" + str << "<tr>\n" + str << th('session identifier') + str << th('status') + str << th('timeout') + str << th('time') + str << th('runtime') + str << th('total') + str << th('modification time') + str << th('domain') + str << th('project') + str << th('username') + str << th('process') + str << "</tr>\n" + str << lines + str << "</table>\n" + end + end + rescue + message('Status', "#{$!} There is currently no status available.", false, @@admin_refresh, 'exaadmin') + else + if n > 0 then + # r = if n > 100 then 60 else @@admin_refresh.to_i end # scanning takes long + r = @@admin_refresh + average = "average = #{if totaldone > 0 then sprintf('%.02f',totaltime/totaldone) else '0' end} (#{sprintf('%.02f',least)} .. #{sprintf('%.02f',most)})" + sessions = "sessions = #{n}" + refresh = "refresh = #{r.to_s} sec" + loadtime = "loadtime = #{sprintf('%.04f',Time.now-start)} sec" + cached = if cached then "cached" else "not cached" end + message("Status | #{sessions} | #{refresh} | #{loadtime} - #{cached} | #{average} |", str, false, r, 'exaadmin') + else + message('Status', "There are no sessions registered.", false, @@admin_refresh, 'exaadmin') + end + end + end + + private + + def th(str) + "<th align='left'>#{str} </th>\n" + end + + def td(str) + "<td><code>#{str || ''}  </code></td>\n" + end + +end diff --git a/scripts/context/ruby/www/common.rb b/scripts/context/ruby/www/common.rb new file mode 100644 index 000000000..9c3832294 --- /dev/null +++ b/scripts/context/ruby/www/common.rb @@ -0,0 +1,80 @@ +# We cannot chdir in threads because it is something +# process wide, so we will run into problems with the +# other threads. The same is true for the global ENV +# pseudo hash, so we cannot communicate the runpath +# via an anvironment either. This leaves texmfstart +# in combination with a path directive and an tmf file. + +module Common # can be a mixin + + # we assume that the hash.subset method is defined + + @@re_texmfstart = /^(texmfstart|ruby\s*texmfstart.rb)\s*(.*)$/ + @@re_texmfpath = /^\-\-path\=/ + + def command_string(path,command,log='') + runner = "texmfstart --path=#{File.expand_path(path)}" + if command =~ @@re_texmfstart then + cmd, arg = $1, $2 + if arg =~ @@re_texmfpath then + # there is already an --path (first switch) + else + command = "#{runner} #{arg}" + end + else + command = "#{runner} bin:#{command}" + end + if log && ! log.empty? then + return "#{command} 2>&1 > #{File.expand_path(File.join(path,log))}" + else + return command + end + end + + def set_os_vars + begin + ENV['TEXOS'] = ENV['TEXOS'] || platform + rescue + ENV['TEXOS'] = 'texmf-linux' + else + ENV['TEXOS'] = 'texmf-' + ENV['TEXOS'] unless ENV['TEXOS'] =~ /^texmf\-/ + ensure + ENV['EXA:TEXOS'] = ENV['TEXOS'] + end + end + + def set_environment(hash) + set_os_vars + paths = ENV['PATH'].split(File::PATH_SEPARATOR) + hash.subset('binpath:').keys.each do |key| + begin + paths << File.expand_path(hash[key]) + rescue + end + end + ENV['PATH'] = paths.uniq.join(File::PATH_SEPARATOR) + hash.subset('path:').keys.each do |path| + key, value = "EXA:#{path.upcase}", File.expand_path(hash[path]) + ENV[key] = value + end + end + + def save_environment(hash,path,filename='request.tmf') + begin + File.open(File.join(path,filename),'w') do |f| + set_os_vars + ['EXA:TEXOS','TEXOS'].each do |key| + f.puts("#{key} = #{ENV[key]}") + end + hash.subset('binpath:').keys.each do |key| + f.puts("PATH < #{File.expand_path(@interface.get(key))}") + end + hash.subset('path:').keys.each do |path| + f.puts("EXA:#{path.upcase} = #{File.expand_path(@interface.get(path))}") + end + end + rescue + end + end + +end diff --git a/scripts/context/ruby/www/dir.rb b/scripts/context/ruby/www/dir.rb new file mode 100644 index 000000000..09e088d77 --- /dev/null +++ b/scripts/context/ruby/www/dir.rb @@ -0,0 +1,155 @@ +require 'www/lib' + +# dir handling + +class WWW + + # borrowed code from webrick demo, patched + + @@dir_name_width = 25 + + def handle_dir(dirpath=@variables.get('path'),hidden=[],showdirs=true) + check_template_file('dir','text-template.htm') + docroot = @interface.get('path:docroot') + dirpath = dirpath || '' + hidden = [] unless hidden + local_path = dirpath.dup + title, str = "Index of #{escaped(dirpath)}", '' + begin + local_path.gsub!(/[\/\\]+/,'/') + local_path.gsub!(/\/$/, '') + if local_path !~ /^(\.|\.\.|\/|[a-zA-Z]\:)$/io then # maybe also /... + full_path = File.join(docroot,local_path) + @interface.set('log:dir', full_path) + begin + list = Dir::entries(full_path) + rescue + str << "unable to parse #{local_path}" + else + if list then + list.collect! do |name| + if name =~ /^\.+/o then + nil # no . and .. + else + st = (File::stat(File.join(docroot,local_path,name)) rescue nil) + if st.nil? then + [name, nil, -1, false] + elsif st.directory? then + if showdirs then [name + "/", st.mtime, -1, true] else nil end + elsif hidden.length > 0 then + if hidden.include?(name) then nil else [name, st.mtime, st.size, false] end + else + [name, st.mtime, st.size, false] + end + end + end + list.compact! + n, m, s = @variables.get('n'), @variables.get('m'), @variables.get('s') + if ! n.empty? then + idx, d0 = 0, n + elsif ! m.empty? then + idx, d0 = 1, m + elsif ! s.empty? then + idx, d0 = 2, s + else + idx, d0 = 0, 'a' + end + d1 = if d0 == 'a' then 'd' else 'a' end + if d0 == 'a' then + list.sort! do |a,b| a[idx] <=> b[idx] end + else + list.sort! do |a,b| b[idx] <=> a[idx] end + end + u = dir_uri(@variables.get('path') || '.') + str << "<div class='dir-view'>\n<pre>\n" + str << "<a href=\"#{u}&n=#{d1}\">name</A>".ljust(49+u.length) + str << "<a href=\"#{u}&m=#{d1}\">last modified</A>".ljust(41+u.length) + str << "<a href=\"#{u}&s=#{d1}\">size</A>".rjust(31+u.length) << "\n" << "\n" + # parent path + if showdirs && ! hidden.include?('..') then + dname = "parent directory" + fname = "#{File.dirname(dirpath)}" + time = File::mtime(File.join(docroot,local_path,"/..")) + str << dir_entry(fname,dname,time,-1,true) + str << "\n" + end + # directories + done = false + list.each do |name, time, size, dir| + if dir then + if name.size > @@dir_name_width then + dname = name.sub(/^(.#{@@dir_name_width-2})(.*)/) do $1 + ".." end + else + dname = name + end + fname = "#{escaped(dirpath)}/#{escaped(name)}" + str << dir_entry(fname,dname,time,size,dir) + done = true + end + end + str << "\n" if done + # files + list.each do |name, time, size, dir| + unless dir then + if name.size > @@dir_name_width then + dname = name.sub(/^(.#{@@dir_name_width-2})(.*)/) do $1 + ".." end + else + dname = name + end + fname = "#{escaped(dirpath)}/#{escaped(name)}" + str << dir_entry(fname,dname,time,size,dir) + end + end + str << "\n" + str << '</pre></div>' + else + str << 'no info' + end + end + else + str << 'no access' + end + rescue + str << "error #{$!}<br/><pre>" + str << $@.join("\n") + str << "</pre>" + end + message(title,str) + end + def dir_uri(f='.') + u, t, o = @interface.get('dir:uri'), @interface.get('dir:task'), @interface.get('dir:option') # takes precedence, in case we run under cgi control + if u.empty? then + u, t, o = @interface.get('process:uri'), '', '' + elsif ! t.empty? then + t = "task=#{t}&" + o = "option=#{o}&" + end + if u && ! u.empty? then + u = u.sub(/\?.*$/,'') # frozen string + if f =~ /^\.+$/ then + "#{u}?#{t}#{o}path=" + else + "#{u}?#{t}#{o}path=#{f}" + end + else + '' + end + end + + def dir_entry(fname,dname,time,size,dir=false) + if dir then + f = fname.sub(/\/+$/,'').sub(/^\/+/,'') + s = "<a href=\"#{dir_uri(f)}\">#{dname}</a>" + elsif ! @interface.get('dir:uri').empty? then # takes precedence, in case we run under cgi control + s = "<a href=\"#{dir_uri(fname.gsub(/\/+/,'/'))}\">#{dname}</a>" + else + s = "<a href=\"#{fname.gsub(/\/+/,'/')}\">#{dname}</a>" + end + # s << " " * (30 - dname.size) + s << " " * (@@dir_name_width + 5 - dname.size) + s << (time ? time.strftime("%Y/%m/%d %H:%M ") : " " * 22) + s << (size >= 0 ? size.to_s : "-").rjust(12) << "\n" + return s + end + +end diff --git a/scripts/context/ruby/www/exa.rb b/scripts/context/ruby/www/exa.rb new file mode 100644 index 000000000..6cf7d8a9b --- /dev/null +++ b/scripts/context/ruby/www/exa.rb @@ -0,0 +1,386 @@ +require 'fileutils' +require 'www/lib' +require 'www/dir' +require 'www/common' +require 'www/admin' + +class WWW + + include Common + + def handle_exadefault + check_template_file('exalogin','exalogin-template.htm') + if id = logged_in_session(true) then + finish_login + else + message('Error', 'No default login permitted.') + end + end + + def handle_exalogin + check_template_file('exalogin','exalogin-template.htm') + if id = logged_in_session(false) then + finish_login + else + message('Error', 'No default login permitted.') + end + end + + def finish_login + get_gui() + filename, path, task = @session.get('gui'), @session.checked('path','.'), @session.get('task') + if ! task.empty? then + save_session + handle_exatask + elsif filename and not filename.empty? then + save_session + fullname = filename.gsub(/\.\./,'') + fullname = File.join(path,filename) unless FileTest.file?(fullname) + fullname = File.join(@interface.get('path:interfaces'), filename) unless FileTest.file?(fullname) + fullname = File.join(@interface.get('path:interfaces'), path, filename) unless FileTest.file?(fullname) + if FileTest.file?(fullname) then + send_file(fullname,true) + else + message('Interface', 'Invalid interface request, no valid interface file.' ) + end + else + message('Interface', 'Invalid interface request, no default interface file.') + end + end + + def handle_exainterface() + check_template_file('text','text-template.htm') + if id = valid_session() then + filename = @interface.get('process:uri').to_s # kind of dup + if ! filename.empty? && filename.sub!(/^.*\//,'') then + path = @session.checked('path', '.') + fullname = filename.gsub(/\.\./,'') + fullname = File.join(path,filename) unless FileTest.file?(fullname) + fullname = File.join(@interface.get('path:interfaces'),filename) unless FileTest.file?(fullname) + fullname = File.join(@interface.get('path:interfaces'),path,filename) unless FileTest.file?(fullname) + if FileTest.file?(fullname) then + save_session + send_file(fullname,true) + else + get_file(filename) + filename, path = @session.get('gui'), @session.checked('path','.') + if filename and not filename.empty? then + save_session + fullname = filename.gsub(/\.\./,'') + fullname = File.join(path,filename) unless FileTest.file?(fullname) + fullname = File.join(@interface.get('path:interfaces'),filename) unless FileTest.file?(fullname) + fullname = File.join(@interface.get('path:interfaces'),path,filename) unless FileTest.file?(fullname) + send_file(fullname,true) if FileTest.file?(fullname) + else + message('Interface', 'Invalid interface request, no interface file.') + end + end + else + message('Interface', 'Invalid interface request, no resource file.') + end + else + message('Interface', 'Invalid interface request, no login.') + end + end + + def handle_exarequest() # todo: check if request is 'command' + check_template_file('exalogin','exalogin-template.htm') + if id = client_session() then + client = true + @interface.set('log:kind', "remote client request: #{id}") + elsif id = valid_session() then + client = false + @interface.set('log:kind', "remote browser request: #{id}") + else + client, id = false, nil + @interface.set('log:kind', 'unknown kind of request') + end + if id then + dir, tmp = dirname, tmp_path(dirname) + requestname, replyname = 'request.exa', 'reply.exa' + requestfile, replyfile = File.join(tmp,requestname), File.join(tmp,replyname) + lockfile = File.join(dirname,lckname) + action, filename, command, url, req = '', '', '', '', '' + extract_sent_files(tmp) + @variables.each do |key, value| + case key + when 'exa:request' then + req = value.dup + when 'exa:action' then + action = value.dup + # when 'exa:command' then + # command = value.dup + # when 'exa:url' then + # url = value.dup + when 'exa:filename' then + filename = value.dup + when 'exa:threshold' then + @interface.set('process:threshold', value.dup) + when /^fakename/o then + @variables.set(key, File.basename(value)) + when /^filename\-/o then + @variables.set(key, filename = File.basename(value)) + when /^dataname\-/o then + @variables.set(key) + else # remove varname- prefix from value + @variables.set(key, @variables.get(key).sub(/#{key}\-/,'')) + end + end + @variables.check('exa:filename', filename) + @variables.check('exa:action', action) + if @variables.empty?('exa:filename') then + @variables.set('exa:filename', @interface.get('log:attachments').split('|').first || '') + end + req.gsub!(/<exa:data\s*\/>/i, '') + dat = "<exa:data>\n" + @variables.each do |key, value| + if ['password','exa:request'].include?(key) then + # skip + elsif ! value || value.empty? then + dat << "<exa:variable label='#{key}'/>\n" + else # todo: escape 'm + dat << "<exa:variable label='#{key}'>#{value}</exa:variable>\n" + end + end + dat << "</exa:data>\n" + if req.empty? then + req << "<?xml version='1.0' ?>\n" + req << "<exa:request #{@@namespace}'>\n" + req << "<exa:application>\n" + req << "<exa:action>'#{action}</exa:action>\n" unless action.empty? + # req << "<exa:command>'#{command}</exa:command>\n" unless command.empty? + # req << "<exa:url>'#{url}</exa:url>\n" unless url.empty? + req << "<exa:application>\n" + req << "<exa:comment>constructed request</exa:comment>\n" + req << dat + req << "</exa:request>\n" + else + # better use rexml but slower + if req =~ /<exa:request[^>]*>.*?\s*<exa:threshold>\s*(.*?)\s*<\/exa:threshold>\s*.*?<\/exa:request>/mois then + threshold = $1 + unless threshold.empty? then + @interface.set('process:threshold', threshold) + @session.set('threshold', threshold) + end + end + req.sub!(/(<exa:request[^>]*>.*?)\s*<exa:option>\s*\-\-action\=(.*?)\s*<\/exa:option>\s*(.*?<\/exa:request>)/mois) do + pre, act, pos = $1, $2, $3 + action = act.sub(/\.exa$/,'') if action.empty? + str = "#{pre}<exa:action>#{action}</exa:action>#{pos}" + str.sub(/\s*<exa:command>.*?<\/exa:command>\s*/mois ,'') + end + req.sub!(/(<exa:request[^>]*>.*?)<exa:action>\s*(.*?)\s*<\/exa:action>(.*?<\/exa:request>)/mois) do + pre, act, pos = $1, $2, $3 + action = act.sub(/\.exa$/,'') if action.empty? + str = "#{pre}<exa:action>#{action}</exa:action>#{pos}" + str.sub(/\s*<exa:command>.*?<\/exa:command>\s*/mois ,'') + end + unless req =~ /<exa:data>(.*?)<\/exa:data>/mois then + req.sub!(/(<\/exa:request>)/) do dat + $1 end + end + end + req.sub!(/<exa:filename>.*?<\/exa:filename>/mois, '') + unless @variables.empty?('exa:filename') then + req.sub!(/(<\/exa:application>)/mois) do + "<exa:filename>#{@variables.get('exa:filename')}<\/exa:filename>" + $1 + end + end + @variables.set('exa:action', action) + @interface.set("log:#{requestname}", req) + begin + File.open(requestfile,'w') do |f| + f << req + end + rescue + message('Error', 'There is a problem in handling this request (working path access).') + return + end + File.delete(replyfile) rescue false + @interface.set('log:action',action) + get_command(action) + logdata = '' + begin + command = @session.get('command') + @interface.set('log:command',if command.empty? then '[no command]' else command end) + if ! command.empty? then + @session.set('starttime', Time.now.to_i.to_s) # can be variables and in save list + if @interface.true?('process:background') then + # background + @session.set('status', 'running: background') + @session.set('maxtime', @interface.get('process:timeout')) + @session.set('threshold', @interface.get('process:threshold')) + save_session + timeout(@@watch_delay) do + save_environment(@interface,tmp) + begin + starttime = File.mtime(@session_file) + # crap + loop do + sleep(1) + if starttime != File.mtime(@session_file) then + break unless FileTest.file?(lockfile) + end + end + rescue TimeoutError + if client then + send_reply() + else + message('Status', 'Processing your request takes a while',true,5,'exastatus') + end + return + rescue + end + end + if client then send_reply() else send_result() end + else + # foreground + status = 'running: foreground' + @session.set('status', status) + @session.set('maxtime', @interface.get('process:timeout')) + @session.set('threshold', @interface.get('process:threshold')) + save_session + timeout(@interface.get('process:timeout').to_i) do + begin + status = 'running: foreground' + set_environment(@interface) + save_environment(@interface,tmp) + command = command_string(tmp,command) + logdata = `#{command}` + rescue TimeoutError + status = 'running: timeout' + logdata = "timeout: #{@interface.get('process:timeout')} seconds" + rescue + status = 'running: aborted' + logdata = 'fatal runtime error' + else + @session.set('endtime', Time.now.to_i.to_s) + status = 'running: finished' + end + end + @session.set('status', status) + save_session + case @session.get('status') + when 'running: finished' then + if client then send_reply(logdata) else send_result(logdata) end + when 'running: timeout' then + message('Error', 'There is a problem in handling this request (timeout).') + when 'running: aborted' then + message('Error', 'There is a problem in handling this request (aborted).') + else + message('Error', 'There is a problem in handling this request (unknown).') + end + end + else + message('Error', 'There is a problem in handling this request (no runner).') + end + rescue + message('Error', 'There is a problem in handling this request (no run).' + $!) + end + else + message('Error', 'Invalid session.') + end + end + + def handle_exacommand() # shares code with exarequest + check_template_file('exalogin','exalogin-template.htm') + if id = client_session() then + client = true + @interface.set('log:kind', "remote client request: #{id}") + elsif id = valid_session() then + client = false + @interface.set('log:kind', "remote browser request: #{id}") + else + client, id = false, nil + @interface.set('log:kind', 'unknown kind of request') + end + if id then + dir, tmp = dirname, tmp_path(dirname) + requestname, replyname = 'request.exa', 'reply.exa' + requestfile, replyfile = File.join(tmp,requestname), File.join(tmp,replyname) + req, command, url = '', '', '' + @variables.each do |key, value| + case key + when 'exa:request' then + req = value.dup + when 'exa:command' then + command = value.dup + when 'exa:threshold' then + @interface.set('process:threshold', value.dup) + when 'exa:url' then + url = value.dup + end + end + unless req.empty? then + # better use rexml but slower / reuse these : command = filter_from_request('exa:command') + if req =~ /<exa:request[^>]*>.*?\s*<exa:command>\s*(.*?)\s*<\/exa:command>\s*.*?<\/exa:request>/mois then + command = $1 + end + if req =~ /<exa:request[^>]*>.*?\s*<exa:url>\s*(.*?)\s*<\/exa:url>\s*.*?<\/exa:request>/mois then + url = $1 + end + if req =~ /<exa:request[^>]*>.*?\s*<exa:threshold>\s*(.*?)\s*<\/exa:threshold>\s*.*?<\/exa:request>/mois then + threshold = $1 + unless threshold.empty? then + @interface.set('process:threshold', threshold) + @session.set('threshold', threshold) + end + end + end + @variables.check('exa:command', command) + @variables.check('exa:url', url) + File.delete(replyfile) rescue false + case @variables.get('exa:command') + when 'fetch' then + if @variables.empty?('exa:url') then + message('Error', "Problems with fetching, no file given") + else + # the action starts here + filename = @variables.get('exa:url').to_s # kind of dup + unless filename.empty? then + get_path(filename) # also registers filename as url + path = @session.checked('path', '') + fullname = filename.gsub(/\.\./,'') + fullname = File.join(path,fullname) unless path.empty? + if FileTest.file?(fullname) then + if client then + send_url(fullname) + else + send_file(fullname,true) + end + @session.set('threshold', @interface.get('process:threshold')) + @session.set('url',filename) + save_session + else + message('Error', "Problems with fetching, unknown file #{fullname}.") + # message('Error', "Problems with fetching, unknown file #{filename}.") + end + else + message('Error', "Problems with fetching, invalid file #{filename}.") + end + # and ends here + end + else + message('Error', "Invalid command #{command}.") + end + else + message('Error', 'Invalid session.') + end + end + + def handle_exastatus + if request_variable('id').empty? then + if id = valid_session() then + send_result() + else + message('Error', 'Invalid session.') + end + else + if id = valid_session() then + send_reply() + else + send_reply('invalid session') + end + end + end + +end diff --git a/scripts/context/ruby/www/lib.rb b/scripts/context/ruby/www/lib.rb new file mode 100644 index 000000000..b330c2a97 --- /dev/null +++ b/scripts/context/ruby/www/lib.rb @@ -0,0 +1,1391 @@ +#!/usr/bin/env ruby + +# This is just a simple environment for remote processing of context +# files. It's not a framework, nor an example of how that should be done. +# Nowadays there are environments like Rails or Nitro. Maybe some day I'll +# give one of them a try. + +# <META Http-Equiv="Cache-Control" Content="no-cache"> +# <META Http-Equiv="Pragma" Content="no-cache"> +# <META Http-Equiv="Expires" Content="0"> + +# we make limited use of cgi methods because we also need to handle webrick + +# %var% as well as $(var) are supported + +# paths need to be expanded before they enter apache, since .. is not +# handled by default + +require 'base/variables' + +require 'ftools' +require 'fileutils' +require 'tempfile' +require 'timeout' +require 'md5' +require 'digest/md5' +require 'cgi' # we also need escaping for webrick (could move it here) + +# beware, namespaces have to match ! + +module XML + + def XML::element(tag,attributes=nil) + if attributes.class == Hash then + if block_given? then + XML::element(tag,XML::attributes(attributes)) do yield end + else + XML::element(tag,XML::attributes(attributes)) + end + else + if block_given? then + "<#{tag}#{if attributes && ! attributes.empty? then ' ' + attributes end}>#{yield}</#{tag}>" + else + "<#{tag}#{if attributes && ! attributes.empty? then ' ' + attributes end}/>" + end + end + end + + def XML::attributes(hash) + str = '' + hash.each do |k,v| + str << ' ' unless str.empty? + if v =~ /\'/ then + str << "#{k}=\"#{v}\"" + else + str << "#{k}=\'#{v}\'" + end + end + return str + end + + def XML::create(version='1.0') + "<?version='#{version}'?>#{yield || ''}" + end + + def XML::line + "\n" + end + +end + +# str = + # XML::create do + # XML::element('test') do + # XML::element('test') do + # 'text a' + # end + + # XML::element('test',XML::attributes({'a'=>'b'})) do + # XML::element('nested',XML::attributes({'a'=>'b'})) do + # 'text b-1' + # end + + # XML::element('nested',XML::attributes({'a'=>'b'})) do + # 'text b-2' + # end + # end + + # XML::element('nested',{'a'=>'b'}) do + # 'text c' + # end + # end + # end + +class ExtendedHash + + DEFAULT = 'default' + + @@re_default = /^(default|)$/i + + def default?(key) + self[key] =~ @@re_default rescue true # unset, empty or 'default' + end + + def default(key) + self[key] = DEFAULT + end + + def match?(key,value) + value == '*' || value == self[key] + end + + +end + +class WWW + + @@session_prefix = '' + @@data_file = 'example.cfg' + @@session_max_age = 60*60 + @@watch_delay = 30 + @@send_threshold = 2*1024*1024 + @@admin_refresh = 10 + @@namespace = "http://www.pragma-ade.com/schemas/example.rng" + + @@re_bar = /\s*\|\s*/ + @@re_lst = /\s*\,\s*/ + @@re_var_a = /\%(.*?)\%/ + @@re_var_b = /\$\((.*?)\)/ + + attr_reader :variables + attr_writer :variables + + @@paths = [ + 'configurations', + 'data', + 'distributions', + 'documents', + 'interfaces', + 'logs', + 'resources', + 'runners', + 'scripts', + 'templates', + 'work'] + + @@re_true = /^\s*(YES|ON|TRUE|1)\s*$/io + @@re_false = /^\s*(NO|OFF|FALSE|0)\s*$/io + + def initialize(webrick_daemon=nil,webrick_request=nil,webrick_response=nil) + @session_id, @session_file = '', '' + @cgi, @cgi_cookie = nil, nil + @webrick_daemon, @webrick_request, @webrick_response = webrick_daemon, webrick_request, webrick_response + + @interface = ExtendedHash.new + @variables = ExtendedHash.new + @session = ExtendedHash.new + + @checked = false + + analyze_request() + update_interface() + + @interface.set('template:message' , 'text-template.htm') + @interface.set('template:status' , 'text-template.htm') + @interface.set('template:login' , 'exalogin.htm') + @interface.set('process:timeout' , @@session_max_age) + @interface.set('process:threshold' , @@send_threshold) + @interface.set('process:background', 'yes') # this demands a watchdog being active + @interface.set('process:indirect' , 'no') # indirect download, no direct feed + @interface.set('process:autologin' , 'yes') # provide default interface when applicable + @interface.set('process:exaurl' , '') # this one will be used as replacement in templates + @interface.set('trace:run' , 'no') + @interface.set('trace:errors' , 'no') + @interface.set('process:os' , platform) + @interface.set('process:texos' , 'texmf-' + platform) + + @interface.set('trace:run' , 'yes') if (ENV['EXA:TRACE:RUN'] || '') =~ @@re_true + @interface.set('trace:errors' , 'yes') if (ENV['EXA:TRACE:ERRORS'] || '') =~ @@re_true + + yield self if block_given? + end + + def set(key,value) + @interface.set(key,value) + end + def get(key) + @interface.get(key,value) + end + + def platform + case RUBY_PLATFORM + when /(mswin|bccwin|mingw|cygwin)/i then 'mswin' + when /(linux)/i then 'linux' + when /(netbsd|unix)/i then 'unix' + when /(darwin|rhapsody|nextstep)/i then 'macosx' + else 'unix' + end + end + + def check_cgi + # when mod_ruby is used, we need to close + # the cgi session explicitly + unless @webrick_request then + unless @cgi then + @cgi = CGI.new('html4') + at_exit do + begin + @cgi.close + rescue + end + end + end + end + end + + def request_variable(key) + begin + if @webrick_request then + [@webrick_request.query[key]].flatten.first.to_s + else + check_cgi + [@cgi.params[key]].flatten.first.to_s + end + rescue + '' + end + end + + def request_cookie(key) + begin + if @cgi then + if str = @cgi.cookies[key] then + return str.first || '' + end + elsif @webrick_request then + @webrick_request.cookies.flatten.each do |cookie| + if cookie.name == key then + return cookie.value unless cookie.value.empty? + end + end + end + rescue + end + return '' + end + + def analyze_request + if @webrick_request then + @interface.set('path:docroot', @webrick_daemon.config[:DocumentRoot] || './documents') + @interface.set('process:uri', @webrick_request.request_uri.to_s) + # @interface.set('process.url', [@webrick_request.host,@webrick_request.request_port].join(':')) + @cgi = nil + @webrick_request.query.each do |key, value| + # todo: filename + @variables.set(key, [value].flatten.first) + end + else + @interface.set('path:docroot', ENV['DOCUMENT_ROOT'] || './documents') + @interface.set('process:uri', ENV['REQUEST_URI'] || '') + # @interface.set('process.url', [ENV['SERVER_NAME'],ENV['SERVER:PORT']].join(':')) + ARGV[0] = '' # get rid of terminal mode + check_cgi + # quite fragile, due to changes between 1.6 and 1.8 + @cgi.params.keys.each do |p| + if @cgi[p].respond_to?(:original_filename) then + @interface.set('log:method','post') + if @cgi[p].original_filename && ! @cgi[p].original_filename.empty? then + @variables.set(p, File.basename(@cgi[p].original_filename)) + else + case @cgi.params[p].class + when StringIO.class then @variables.set(p, @cgi[p].read) + when Array.class then @variables.set(p, @cgi[p].first.to_s) + when String.class then @variables.set(p, @cgi[p]) + when Tempfile.class then @variables.set(p, '[data blob]') + end + end + else + @interface.set('log:method','get') unless @interface.get('log:method') == 'post' + @variables.set(p, [@cgi.params[p]].flatten.first.to_s) + end + end + end + end + + # name in calling script takes precedence + # one can set template:whatever as well + # todo: in config + + def check_template_file(tag='',filename='exalogin-template.htm') + @interface.set('file:template', filename) if @interface.get('file:template').empty? + @interface.set('tag:template', tag) + @interface.set('file:template', @interface.get('tag:template')) unless @interface.get('tag:template').empty? + end + + def update_interface() + root = @interface.get('path:docroot') + @interface.set('path:docroot', File.expand_path("#{root}")) + @@paths.each do |path| + @interface.set("path:#{path}", File.expand_path("#{root}/../#{path}")) + end + @interface.set('file:template', @interface.get('tag:template')) unless @interface.get('tag:template').empty? + end + + def indirect?(result) + size = FileTest.size?(result) || 0 + @interface.true?('trace:errors') || @interface.true?('trace:run') || @interface.true?('process:indirect') || + ((! @interface.empty?('process:threshold')) && (size > @interface.get('process:threshold').to_i)) || + ((! @session.empty?('threshold')) && (size > @session.get('threshold').to_i)) + end + +end + +# files + +class WWW + + def sesname + File.basename(@session_file) + end + def dirname + File.basename(@session_file.sub(/ses$/,'dir')) + end + def lckname + File.basename(@session_file.sub(/ses$/,'lck')) + end + + def work_root(expand=true) + p = if expand then File.expand_path(@interface.get('path:work')) else @interface.get('path:work') end + if @interface.true?('process:background') then + File.join(@interface.get('path:work'),'watch') + else + File.join(@interface.get('path:work'),'direct') + end + end + + def work_roots(expand=true) + p = if expand then File.expand_path(@interface.get('path:work')) else @interface.get('path:work') end + [File.join(@interface.get('path:work'),'watch'),File.join(@interface.get('path:work'),'direct')] + end + + def cache_root(expand=true) + p = if expand then File.expand_path(@interface.get('path:work')) else @interface.get('path:work') end + File.join(@interface.get('path:work'),'cache') + end + + def cleanup_path(dir) + FileUtils::rm_r(pth) rescue false + end + + def tmp_path(dir) + @interface.set('path:templates', File.expand_path(@interface.get('path:templates'))) # to be sure; here ? ? ? + pth = File.join(work_root,dir) + File.makedirs(pth) rescue false + pth + end + + def locked?(lck) + FileTest.file?(lck) + end + +end + +# sessions + +class WWW + + @@session_tags = ['id','domain','project','username','password','gui','path','process','command','filename','action','status', 'starttime','endtime','runtime','task','option','threshold','url'].sort + @@session_keep = ['id','domain','project','username','password','process'].sort + @@session_reset = @@session_tags - @@session_keep + + def new_session() + if @variables.empty?('exa:session') then + @session_id = new_session_id + else + @session_id = @variables.get('exa:session') + end + if @session_id == 'default' then # ??? + @session_id = new_session_id + end + @session_file = File.join(work_root,"#{@@session_prefix}#{@session_id}.ses") + register_session + return @session_id + end + + def reset_session(all=false) + (if all then @@session_tags else @@session_reset end).each do |k| + @session.set(k) + end + end + + def valid_session + @session_id = request_variable('id') + if @session_id.empty? then + begin + if @cgi then + if @session_id = @cgi.cookies['session_id'] then + @session_id = @session_id.first || '' + else + @session_id = '' + end + elsif @webrick_request then + @webrick_request.cookies.flatten.each do |cookie| + if cookie.name == 'session_id' then + unless cookie.value.empty? then + @session_id = cookie.value + # break + end + end + end + else + @session_id = '' + end + rescue + @interface.set('log:session',"[error in request #{$!}]") + return false + end + end + if @session_id.empty? then + @interface.set('log:session','[no id, check work dir permissions]') + return false + else + @interface.set('log:session',@session_id) + load_session + if ! @session.empty?('domain') && ! @session.empty?('project') && ! @session.empty?('username') then + register_session + return @session_id + else + return false + end + end + end + + def touch_session(id=nil) + begin + t = Time.now + File.utime(t,t,File.join(work_root,"#{@@session_prefix}#{id || @session_id}.ses")) rescue false + rescue + false + end + end + + def forced_session + @session_id = new_session + if @session_id.empty? then + @interface.set('log:session','[no id, check work dir permissions]') + return false + else + return check_session + end + end + + def client_session + request, done = @variables.get('exa:request'), false + request.sub!(/(^.*<exa:request[^>]*>.*?)\s*<exa:client>\s*(.*)\s*<\/exa:client>\s*(.*?<\/exa:request>.*$)/mio) do + pre, client, post = $1, $2, $3 + client.scan(/<exa:(domain|project|username|password)>(.*?)<\/exa:\1>/mio) do + @variables.set($1, $2) + end + done = true + pre + post + end + if done then + return forced_session + else + return nil + end + end + + def register_session + if @cgi then + @cgi_cookie = CGI::Cookie::new( + 'name' => 'session_id', + 'value' => @session_id, + 'expires' => Time.now + @interface.get('process:timeout').to_i + ) + # @cgi_cookie = CGI::Cookie::new('session_id',@session_id) + elsif @webrick_response then + if cookie = WEBrick::Cookie.new('session_id', @session_id) then + cookie.expires = Time.now + @interface.get('process:timeout').to_i + cookie.max_age = @interface.get('process:timeout').to_i + cookie.comment = 'exa identifier' + @webrick_response.cookies.clear + @webrick_response.cookies << cookie + end + end + end + + def new_session_id # taken from cgi + md5 = Digest::MD5::new + now = Time::now + md5.update(now.to_s) + md5.update(String(now.usec)) + md5.update(String(rand(0))) + md5.update(String($$)) + md5.update('foobar') + @new_session = true + md5.hexdigest[0,32] # was 16 + end + + @@hide_passwords = true + HIDDEN = 'hidden' + + def same_passwords(password) # password in cfg file + if @@hide_passwords && (@session.get('password') == HIDDEN) && (@session_id == @session.get('id')) then + # this condition is only true when a same session id is found and + # the password is checked once and set to HIDDEN + same = true + elsif password =~ /^MD5:/ then + # so, one cannot send a known encrypted password since it will be + # encrypted twice then + same = (password == "MD5:" + MD5.new(@session.get('password')).hexdigest.upcase) + else + if (@session.default?('domain') && @session.default?('project') && @session.default?('username')) then + @session.default('password') # is this safe enough? + end + same = (password == @session.get('password')) + end + if @@hide_passwords && same then + @session.set('password', HIDDEN) + save_session # next time this session is ok anyway + end + return same + end + + @@session_line = /^\s*(?![\#\%])(.*?)\s*\=\s*(.*?)\s*$/o + @@session_begin = 'begin exa session' + @@session_end = 'end exa session' + + def loaded_session_data(filename) + begin + if data = IO.readlines(filename) then + return data if (data.first =~ /^[\#\%]\s*#{@@session_begin}/o) && (data.last =~ /^[\#\%]\s*#{@@session_end}/o) + end + rescue + end + return nil + end + + def load_session() + begin + @session_file = File.join(work_root,"#{@@session_prefix}#{@session_id}.ses") + if data = loaded_session_data(@session_file) then + data.each do |line| + if line =~ @@session_line then + @session.set($1, $2 || '') + end + end + else + return false + end + rescue + return false + else + return true + end + end + + def load_session_file(filename) + begin + if data = loaded_session_data(filename) then + session = Hash.new + data.each do |line| + if line =~ @@session_line then + session[$1] = $2 || '' + end + end + else + Hash.new + end + rescue + Hash.new + else + session + end + end + + def save_session + begin + unless @session_id.empty? then + @session_file = File.join(work_root,"#{@@session_prefix}#{@session_id}.ses") + @session_file = File.join(work_root,"#{@@session_prefix}#{@session_id}.ses") + File.open(@session_file,'w') do |f| + f << "\# #{@@session_begin}\n" + @@session_tags.each do |tag| + if @session && @session.key?(tag) then + if ! @session.get(tag).empty? then # no one liner, fails + f << "#{tag}=#{@session.get(tag)}\n" + end + elsif @variables.key?(tag) && ! @variables.empty?(key) then + f << "#{tag}=#{@variables.get(tag)}\n" + end + end + @session.subset("ENV").keys.each do |tag| + f << "#{tag}=#{@session.get(tag)}\n" + end + f << "\# #{@@session_end}\n" + end + end + rescue + return false + else + return true + end + end + + def logged_in_session(force_default=false) + if force_default || (@variables.default?('domain') && @variables.default?('project') && @variables.default?('username')) then + id = default_session + else + id = check_session + end + end + + def default_session + if @interface.true?('process:autologin') then + @variables.default('domain') + @variables.default('project') + @variables.default('username') + @variables.default('password') + check_session + else + @session_id = nil + end + end + + def check_session + @session.set('domain', @variables.get('domain').downcase) + @session.set('project', @variables.get('project').downcase) + @session.set('username', @variables.get('username').downcase) + @session.set('password', @variables.get('password').downcase) + new_session + @session.set('id', @session_id) + save_session + return @session_id + end + + def delete_session(id=nil) + File.delete(work_root,"#{@@session_prefix}#{id || @session_id}.ses") rescue false + end + + def cleanup_sessions(max_age=nil) + begin + now, age = Time.now, (max_age||@interface.get('process:timeout')).to_i + Dir.glob("{#{work_root},#{cache_root}/#{@@session_prefix}*").each do |s| + begin + if (now - File.mtime(s)) > age then + if FileTest.directory?(s) then + FileUtils::rm_r(s) + else + File.delete(s) + end + end + rescue + # maybe purged in the meantime + end + end + rescue + # maybe another process is busy + end + end + +end + +# templates + +class WWW + + def filled_template(title,text,showtime=false,refresh=0,refreshurl=nil) + template = @interface.get("template:#{@interface.get('tag:template')}") + template = @interface.get("template:status") if template.empty? + fullname = File.join(@interface.get('path:templates'),template) + @interface.set('log:templatename',template) + @interface.set('log:templatefile',fullname) + append_status(text) + htmreply = '' + if FileTest.file?(fullname) then + begin + htmreply = IO.read(fullname) + rescue + htmreply = '' + end + end + if refresh>0 then + if refreshurl then + metadata = "<meta http-equiv='refresh' content='#{refresh};#{refreshurl}'>" + else + metadata = "<meta http-equiv='refresh' content='#{refresh}'>" + end + else + metadata = '' + end + if ! htmreply || htmreply.empty? then + # in head: <link rel='stylesheet' href='/exaresource/exastyle.css'> + htmreply = <<-EOD + <html> + #{metadata} + <head> + <title>#{title}</title> + </head> + <body> + <h2>#{title}</h2> + <h4>#{Time.now}</h4> + #{text} + </body> + </html> + EOD + else + if showtime then + exa_template = "<h1>#{title}</h1>\n<h2>#{Time.now}</h2>\n#{text}\n" + else + exa_template = "<h1>#{title}</h1>#{text}\n" + end + htmreply = replace_template_placeholder(htmreply,exa_template,metadata) + end + htmreply + end + + def message(title,str='',showtime=false,refresh=0,refreshurl=nil) + if @cgi then + @cgi.out("cookie"=>[@cgi_cookie]) do + filled_template(title,str,showtime,refresh,refreshurl) + end + elsif @webrick_response then + @webrick_response['content-type'] = 'text/html' + @webrick_response.body = filled_template(title,str,showtime,refresh,refreshurl) + else + filled_template(title,str,showtime,refresh,refreshurl) + end + end + + def plaintext(str) + if @cgi then + @cgi.out('cookie'=>[@cgi_cookie],'content-type'=>'text/plain') do + str + end + elsif @webrick_response then + @webrick_response['content-type'] = 'text/plain' + @webrick_response.body = str + else + str + end + end + + def exareply(status='',url='',size='',comment='') + exaurl = @interface.get('process:exaurl') + str = "<?xml version='1.0'?>\n\n" + str << "<exa:reply xmlns:exa='#{@@namespace}'>\n" + str << " <exa:session>#{@session_id}</exa:session>\n" unless @session_id.empty? + str << " <exa:status>#{status}</exa:status>\n" unless (status || '').empty? + str << " <exa:url>#{exaurl}/#{url}</exa:url>\n" unless (url || '').empty? + str << " <exa:size>#{size}</exa:size>\n" unless (size || '').empty? + str << " <exa:comment>#{comment}</exa:comment>\n" unless (comment|| '').empty? + str << "</exa:reply>\n" + return str + end + + def append_status(str='') + if @interface.true?('trace:errors') then + if $! && $@ then + str << "<br/><br/><br/><em>Error:</em><br/><pre>#{$!}</pre><pre>" + str << $@.join("\n") + end + str << '<br/><br/><br/>' + str << status_data + str << '<em>Paths</em><br/>' + str << '<pre>' + @interface.subset('path:').each do |k,v| + if FileTest.directory?(v) then + if FileTest.writable?(v) then + str << "#{v} exists and is writable\n" + else + str << "#{v} is not writable\n" + end + else + str << "#{v} does not exist\n" + end + end + str << '</pre>' + end + str + end + + def simpleurl(url) + if url then url.sub(/(:80|:443)$/,'') else '' end + end + + def replace_exa_placeholders(data) + data.gsub(/([\"\'])\@exa\_([a-zA-Z0-9\-\_]+)\1/) do + quot, key, value = $1, $2, '' + begin + value = @variables.get(key) + rescue + value = '' + end + quot + value + quot + end + end + + def replace_url_placeholder(data) + data.gsub!(/(http:\/\/|\/+)*\@exa\_main\_url/, @interface.get('process:exaurl')) + replace_exa_placeholders(data) + end + + def replace_template_placeholder(data,template='',metadata='') + data.gsub!(/(http:\/\/|\/+)*\@exa\_main\_url/, @interface.get('process:exaurl')) + data.gsub!(/\@exa\_template/, template) + data.gsub!(/\@exa\_metadata/, metadata) + replace_exa_placeholders(data) + end + + def escaped(str) + str + end + +end + +# send files + +class WWW + + def send_file(filename,parse=false) # this can take a lot of memory, look for alternative (fastcgi ?) + begin + if filename =~ /\.pdf$/ then + mimetype, parse = 'application/pdf', false + elsif filename =~ /\.(html|htm)$/ then + mimetype, parse = 'text/html', true + else + mimetype, parse = 'text/plain', false + end + if FileTest.file?(filename) then + if @webrick_response then + begin + @webrick_response['content-type'] = mimetype + @webrick_response['content-length'] = FileTest.size?(filename) + if parse then + File.open(filename, 'rb') do |f| + @webrick_response.body = replace_url_placeholder(f.read) + end + else + @webrick_response.body = File.open(filename, 'rb') + end + rescue + else + return + end + elsif @cgi then + begin + # the following works ok, but stores the whole file in memory (see @cgi.out) + # + # File.open(filename, 'rb') do |f| + # @cgi.out('cookie'=>[@cgi_cookie],'connection'=>'close', 'length'=>File.size(filename), 'type'=>mimetype) do + # if parse then replace_url_placeholder(f.read) else f.read end + # end + # end + if parse then + File.open(filename, 'rb') do |f| + @cgi.out('cookie'=>[@cgi_cookie],'connection'=>'close', 'length'=>File.size(filename), 'type'=>mimetype) do + replace_url_placeholder(f.read) + end + end + else + @cgi.print(@cgi.header('cookie'=>[@cgi_cookie],'connection'=>'close', 'length'=>File.size(filename), 'type'=>mimetype)) + File.open(filename, 'rb') do |f| + while str = f.gets do + @cgi.print(str) + end + end + end + rescue + else + return + end + end + end + rescue + end + message('Error', "There is a problem with sending file #{File.basename(filename)}.") + end + + def send_htmlfile(filename,parse=false) + send_file(filename,parse) + end + def send_pdffile(filename) # this can take a lot of memory, look for alternative (fastcgi ?) + send_file(filename,false) + end + +end + +# tracing + +class WWW + + def show_vars(a=@variables,title='') + if a && a.length > 0 then + if title.empty? then + str = '' + else + str = "<em>#{title}</em>" + end + str << "<br/><pre>\n" + a.keys.sort.each do |k| + if k && a[k] && ! a[k].empty? then + if k == 'password' then + val = if a[k] == 'default' then 'default' else '******' end + else + # str << "#{k} => #{a[k].sub(/^\s+/moi,'').sub(/\s+$/moi,'')}\n" + val = a[k].to_s.strip + val.gsub!("&","&") + val.gsub!("<","<") + val.gsub!(">",">") + val.gsub!("\n","\n ") + end + str << "#{k} => #{val}\n" + end + end + str << "</pre><br/>\n" + return str + else + return '' + end + end + + def status_data + show_vars(@session , 'Session' ) + + show_vars(@variables, 'Variables' ) + + show_vars(@interface, 'Interface' ) + + show_vars(ENV , 'Environment') + end + + def report_status + check_template_file('status') + message('Status',status_data) + end + +end + +# attachments + +class WWW + + def extract_sent_files(dir) + files = Array.new + if @cgi then + @cgi.params.keys.each do |tag| + begin + if filename = @cgi[tag].original_filename then + files << extract_file_content(dir,filename,@cgi[tag]) unless filename.empty? + end + rescue + end + end + elsif @webrick_request then + @webrick_request.query.keys.each do |tag| + begin + if filename = @webrick_request.query[tag].filename then + files << extract_file_content(dir,filename,@webrick_request.query[tag]) unless filename.empty? + end + rescue + end + end + end + @interface.set('log:attachments', files.compact.uniq.join('|')) + end + + def extract_file_content(dir,filename,data) + filename = File.join(dir,File.basename(filename)) + begin + @interface.set('log:attachclass', data.class.inspect) + if data.class == Tempfile then + begin + File.copy(data.path,filename) + rescue + begin + File.open(filename,'wb') do |f| + File.open(data.path,'rb') do |g| + while str = g.gets do + f.write(str) + end + end + end + rescue + @interface.set('log:attachstate', "saving tempfile #{filename} failed (#{$!})") + else + @interface.set('log:attachstate', "tempfile #{filename} has been saved") + end + else + @interface.set('log:attachstate', "#{data.path} copied to #{filename}") + end + elsif data.class == String then + begin + File.open(filename,'wb') do |f| + f.write(data) + end + rescue + @interface.set('log:attachstate', "saving string #{filename} failed (#{$!})") + else + @interface.set('log:attachstate', "string #{filename} has been saved") + end + elsif data.class == StringIO then + begin + File.open(filename,'wb') do |f| + f.write(data.read) + end + rescue + @interface.set('log:attachstate', "saving stringio #{filename} failed (#{$!})") + else + @interface.set('log:attachstate', "stringio #{filename} has been saved") + end + else + @interface.set('log:attachstate', "unknown attachment class #{data.class.to_s}") + end + rescue + begin File.delete(filename) ; rescue ; end + else + begin File.delete(filename) if FileTest.size(filename) == 0 ; rescue ; end + end + return File.basename(filename) + end + +end + +# configuration + +class WWW + + def interface_base_name(str) + str.sub(/\.(pdf|htm|html)$/, '') + end + + def located_interface_file(filename) + ['configurations', 'runners', 'scripts'].each do |tag| + datafile = File.join(@interface.get("path:#{tag}"),filename) + if FileTest.file?(datafile+'.encrypted') then + return datafile + '.encrypted' + elsif FileTest.file?(datafile) then + return datafile + end + end + return nil + end + + def load_interface_file(filename=@@data_file) + reset_session() # no save yet + if datafile = located_interface_file(filename) then + nestedfiles = Array.new + begin + data = IO.read(datafile) || '' + unless data.empty? then + loop do # we need to load them recursively + done = false + data.gsub!(/^include\s*:\s*(.*?)\s*$/) do + includedname, done = $1, true + if nestedname = located_interface_file(includedname) then + begin + str = ("\n" + IO.read(nestedname) + "\n") || '' + rescue + nestedfiles << File.basename('-'+includedname) + '' + else + nestedfiles << File.basename('+'+includedname) + str + end + else + nestedfiles << File.basename('-'+includedname) + '' + end + end + break unless done + end + end + @interface.set('log:configurationfile', datafile + ' [' + nestedfiles.join(' ') + ']') + return data + rescue + end + end + @interface.set('log:configurationfile', filename + ' [not loaded]') + return nil + end + + def fetch_session_interface_variables(data) + data.scan(/^variable\s*:\s*(.*?)\s*\=\s*(.*?)\s*$/) do + @interface.set($1, $2) + end + return true + end + + def fetch_session_project_list(data) + projectlist, permitted = Array.new, false + data.scan(/^user\s*:\s*(.*?)\s*\,\s*(.*?)\s*\=\s*(.*?)\s*\,\s*(.*?)\s*$/) do + domain, username, password, projects = $1, $2, $3, $4 + if @session.match?('domain',domain) && @session.match?('username',username) then + if same_passwords(password) then + projectlist, permitted = @interface.resolved(projects).split(@@re_bar), true + break + end + end + end + if permitted then + @interface.set('log:projectlist', '['+projectlist.join(' ')+']') + if projectlist.length == 0 then + return nil + else + return projectlist + end + else + @interface.set('log:projectlist', '[no projects]') + return nil + end + end + + def fetch_session_command(data) + data.scan(/^process\s*:\s*(.*?)\s*\,\s*(.*?)\s*\=\s*(.*?)\s*$/) do + domain, process, command = $1, $2, $3 + if @session.match?('domain',domain) && @session.match?('process',process) then + @session.set('command', @interface.resolved(command)) + end + end + return @session.get('command') + end + + def fetch_session_settings(data) + data.scan(/^setting\s*:\s*(.*?)\s*\,\s*(.*?)\s*\,\s*(.*?)\s*\=\s*(.*?)\s*$/) do + domain, process, variable, value = $1, $2, $3, $4 + if @session.match?('domain',domain) && @session.match?('process',process) then + @interface.set(variable,value) + end + end + end + + def get_command(action) + # @session.set('action', action) + # if @session.get('process') == 'none' then + # @interface.set('log:child','yes') + # @session.set('process', action) + # end + if data = load_interface_file() then + fetch_session_interface_variables(data) + if projectlist = fetch_session_project_list(data) then + data.scan(/^project\s*:\s*(.*?)\s*\,\s*(.*?)\s*\=\s*(.*?)\s*,\s*(.*?)\s*,\s*(.*?)\s*$/) do + domain, project, gui, path, process = $1, $2, $3, $4, $5 + if @session.match?('domain',domain) then + if @session.match?('project',project) then + if projectlist.include?(project) then + @session.set('process', @interface.resolved(process)) + # break # no, else we end up in the parent (e.g. examplap instead of impose) + end + elsif ! action.empty? && project == action then + if projectlist.include?(action) then + @session.set('process', @interface.resolved(process)) + # break # no, else we end up in the parent (e.g. examplap instead of impose) + end + end + end + end + fetch_session_command(data) + fetch_session_settings(data) + end + end + return ! @session.nothing?('command') + end + + def get_file(filename) + @session.set('filename', filename) + if data = load_interface_file() then + fetch_session_interface_variables(data) + if projectlist = fetch_session_project_list(data) then + data.scan(/^project\s*:\s*(.*?)\s*\,\s*(.*?)\s*\=\s*(.*?)\s*,\s*(.*?)\s*,\s*(.*?)\s*$/) do + domain, project, gui, path, process = $1, $2, $3, $4, $5 + if @session.match?('domain',domain) then + guilist = @interface.resolved(gui).split(@@re_bar) + guilist.each do |g| + if /#{filename}$/ =~ g then + @session.set('gui', File.expand_path(@interface.resolved(g))) + @session.set('path', File.expand_path(@interface.resolved(path))) + @session.set('process', process) + break # take first matching interface + end + end + end + end + end + end + return ! (@session.nothing?('gui') && @session.nothing?('path') && @session.nothing?('process')) + end + + def get_path(url='') + if data = load_interface_file() then + fetch_session_interface_variables(data) + if projectlist = fetch_session_project_list(data) then + data.scan(/^project\s*:\s*(.*?)\s*\,\s*(.*?)\s*\=\s*(.*?)\s*,\s*(.*?)\s*,\s*(.*?)\s*$/) do + domain, project, gui, path, process = $1, $2, $3, $4, $5 + if @session.match?('domain',domain) && @session.match?('project',project) then + @session.set('url', url) + @session.set('gui', '') + @session.set('path', File.expand_path(@interface.resolved(path))) + @session.set('process', '') + end + end + end + end + return ! @session.nothing?('path') + end + + def get_gui() + if data = load_interface_file() then + fetch_session_interface_variables(data) + if projectlist = fetch_session_project_list(data) then + data.scan(/^project\s*:\s*(.*?)\s*\,\s*(.*?)\s*\=\s*(.*?)\s*,\s*(.*?)\s*,\s*(.*?)\s*$/) do + domain, project, gui, path, process = $1, $2, $3, $4, $5 + if @session.match?('domain',domain) && @session.match?('project',project) && projectlist.include?(project) then + @session.set('gui', File.expand_path(@interface.resolved(gui))) + @session.set('path', File.expand_path(@interface.resolved(path))) + @session.set('process', process) unless process == 'none' + break # take first matching interface + end + end + data.scan(/^admin\s*:\s*(.*?)\s*\,\s*(.*?)\s*\=\s*(.*?)\s*\,\s*(.*?)\s*$/) do + domain, project, task, option = $1, $2, $3, $4 + if @session.match?('domain',domain) && @session.match?('project',project) && projectlist.include?(project) then + @session.set('task', task) + @session.set('option', option) + break # take first matching task + end + end + end + end + return ! (@session.nothing?('gui') && @session.nothing?('path') && @session.nothing?('process')) + end + +end + +class WWW + + def send_reply(logdata='') + if @interface.true?('trace:run') then + send_result(logdata) + else + dir, tmp = dirname, tmp_path(dirname) + case @session.get('status') + when 'running: finished' then + resultname, replyname = 'result.pdf', 'reply.exa' + replyfile = File.join(tmp,replyname) + if FileTest.file?(replyfile) then + begin + data = IO.read(replyfile) + resultname = if data =~ /<exa:output>(.*?)<\/exa:output>/ then $1 else resultname end + rescue + plaintext(exareply('error in reply')) + return + end + end + resultfile = File.join(tmp,resultname) + if FileTest.file?(resultfile) then + if indirect?(resultfile) then + begin + File.makedirs(File.join(cache_root,dir)) + FileUtils::mv(resultfile,File.join(cache_root,dir,resultname)) + rescue + plaintext(exareply('unable to access cache')) + else + plaintext(exareply('big file', "cache/#{dir}/#{resultname}", "#{File.size?(resultfile)}")) + end + else + send_file(resultfile) + end + else + plaintext(exareply('no result')) + end + else # background, running, aborted + plaintext(exareply(@session.get('status'))) + end + end + end + + def send_url(fullname) + dir, tmp = dirname, tmp_path(dirname) + resultname, replyname = 'result.pdf', 'reply.exa' + replyfile = File.join(tmp,replyname) + resultfile = File.join(tmp,resultname) + if FileTest.file?(fullname) then + if indirect?(fullname) then + begin + File.makedirs(File.join(cache_root,dir)) + targetname = File.join(cache_root,dir,resultname) + File.delete(targetname) rescue false # left overs + File.symlink(fullname,targetname) rescue message('Status',$!) + unless FileTest.file?(targetname) then + FileUtils::cp(fullname,targetname) rescue false + end + rescue + plaintext(exareply('unable to access cache')) + else + plaintext(exareply('big file', "cache/#{dir}/#{resultname}", "#{File.size?(fullname)}")) + end + else + send_file(fullname) + end + else + message('Status', 'The file is not found') + end + end + + def send_result(logdata='') + check_template_file('exalogin','exalogin-template.htm') + dir, tmp = dirname, tmp_path(dirname) + resultname, replyname, logname = 'result.pdf', 'reply.exa', 'log.htm' + case @session.get('status') + when 'running: background' then + if st = @session.get('starttime') then # fuzzy + st = Time.now.to_i if st.empty? + if (Time.now.to_i - st.to_i) > @interface.get('process:timeout').to_i then + message('Status', 'Your request has been aborted (timeout)',true) + else + message('Status', 'Your request is queued',true,5,'exastatus') + end + end + when 'running: busy' then + if st = @session.get('starttime') then # fuzzy + st = Time.now.to_i if st.empty? + if (Time.now.to_i - st.to_i) > @interface.get('process:timeout').to_i then + message('Status', 'Your request has been aborted (timeout)',true) + else + message('Status', 'Your request is being processed',true,5,'exastatus') + end + end + when 'running: aborted' then + message('Status', 'Your request has been aborted (timeout)',true) + when 'running: finished' then + if @interface.true?('trace:run') then + logfile = File.join(tmp,logname) + begin + if f = File.open(logname,'w') then + if logdata.empty? then + begin + logdata = IO.read('www-watch.out') + rescue + logdata = 'no log data' + end + end + f << filled_template('Log',"<pre>#{CGI::escapeHTML(logdata)}</pre>") + f.close + end + rescue + message('Error', '') + end + if FileTest.file?(logfile) then + begin + File.makedirs(File.join(cache_root,dir)) + FileUtils::mv(logfile,File.join(cache_root,dir,logname)) + rescue + logdata = "<br/><br/>unable to access cache</a>" + else + logdata = "<br/><br/><a href='/cache/#{dir}/#{logname}'>#{logname}</a>" + end + else + logdata = '' + end + else + logdata = '' + end + # todo: generate reply.exa if no reply + replyfile = File.join(tmp,replyname) + if FileTest.file?(replyfile) then + begin + data = IO.read(replyfile) + resultname = if data =~ /<exa:output>(.*?)<\/exa:output>/ then $1 else resultname end + rescue + message('Error','There is a problem in handling this request (invalid reply).') + return + end + end + resultfile = File.join(tmp,resultname) + if FileTest.file?(resultfile) then + if indirect?(resultfile) then + begin + File.makedirs(File.join(cache_root,dir)) + FileUtils::mv(resultfile,File.join(cache_root,dir,resultname)) + rescue + str = "<br/><br/>unable to access cache</a>" + else + str = "<br/><br/><a href='/cache/#{dir}/#{resultname}'>#{resultname}</a> (#{File.size?(resultname)} bytes)" + end + message('Result', 'You can pick up the result here:' + str + logdata) + else + send_file(resultfile) + end + else + message('Error', 'There is a problem in handling this request (no result file).' + logdata) + end + end + end + +end diff --git a/scripts/context/ruby/www/login.rb b/scripts/context/ruby/www/login.rb new file mode 100644 index 000000000..1c88a97e6 --- /dev/null +++ b/scripts/context/ruby/www/login.rb @@ -0,0 +1,13 @@ +require 'www/lib' + +# basic login + +class WWW + + def handle_login() + check_template_file('login','exalogin.htm') + set('password', '') + message('Login','') + end + +end diff --git a/scripts/context/ruby/wwwclient.rb b/scripts/context/ruby/wwwclient.rb new file mode 100644 index 000000000..8f5451a8d --- /dev/null +++ b/scripts/context/ruby/wwwclient.rb @@ -0,0 +1,677 @@ +#!/usr/bin/env ruby + +# a direct request is just passed on +# +# exaclient --direct --request=somerequest.exa --result=somefile.pdf +# +# in an extended request the filename in the template file is replaced by the filename +# given on the command line; templates are located on the current path and at parent +# directories (two levels); the filename is expanded to a full path +# +# exaclient --extend --template=tmicare-l-h.exa --file=somefile.xml --result=somefile.pdf +# +# a constructed request is build out of the provided filename and action; the filename is +# expanded to a full path +# +# exaclient --construct --action=tmicare-s-h.exa --file=somefile.xml --result=somefile.pdf +# +# in all cases, the result is either determined by a switch or taken from a reply file + +banner = ['WWWClient', 'version 1.0.0', '2003-2006', 'PRAGMA ADE/POD'] + +$: << File.dirname(File.expand_path($0)) + +require 'base/switch' +require 'base/logger' + +require 'timeout' +require 'thread' +require 'rexml/document' +require 'net/http' + +class File + + def File.backtracked(filename,level=3) + if level > 0 && filename && ! filename.empty? then + if FileTest.file?(filename) then + filename + else + File.backtracked('../'+filename,level-1) + end + else + filename + end + end + + def File.expanded(filename) + File.expand_path(filename) + end + +end + +class Commands + + include CommandBase + +end + +class Commands + + @@namespace = "xmlns:exa='http://www.pragma-ade.com/schemas/example.rng'" + @@randchars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "abcdefghijklmnopqrstuvwxyz" + + def traceback + "(error: #{$!})" + "\n -- " + $@.join("\n >>") + end + + def pdf(action,filename,enabled) + if enabled && FileTest.file?(filename) then + begin + report("pdf action #{action} on #{filename}") + case action + when 'close' then system("pdfclose --all") + when 'open' then system("pdfopen --file #{filename}") + end + rescue + # forget about it + end + end + end + + def status(replyfile,str) # when block, then ok + begin + # def status(*whatever) + # end + File.open(replyfile,'w') do |f| + report("saving reply info in '#{replyfile}'") + f.puts("<?xml version='1.0'?>\n\n") + f.puts("<exa:reply #{@@namespace}>\n") + if block_given? then + f.puts(" <exa:status>ok</exa:status>\n") + f.puts(" #{yield}\n") + else + f.puts(" <exa:status>error</exa:status>\n") + end + f.puts(" <exa:comment>" + str + "</exa:comment>\n") + f.puts("</exa:reply>\n") + f.close + report("saving status: #{str}") + end + rescue + report("saving reply info in '#{replyfile}' fails") + ensure + exit + end + exit # to be real sure + end + + + def boundary_string (length) # copied from webrick/utils + rand_max = @@randchars.size + ret = "" + length.times do + ret << @@randchars[rand(rand_max)] + end + ret.upcase + end + +end + +class Commands + + @@connecttimeout = 10*60 # ten minutes + @@processtimeout = 60*60 # an hour + @@polldelay = 5 # 5 seconds + + def main + + datatemplate = @commandline.option('template') + datafile = @commandline.option('file') + dataaction = @commandline.option('action') + + if ! datatemplate.empty? then + report("template '#{datatemplate}' specified without --construct") + report("aborting") + elsif ! dataaction.empty? then + report("action data '#{dataaction}' specified without --construct or --extend") + report("aborting") + elsif ! datafile.empty? then + report("action file '#{datafile}' specified without --construct or --extend") + report("aborting") + else + report("assuming --direct") + direct() + end + + end + + def construct + + requestfile = @commandline.option('request') + replyfile = @commandline.option('reply') + + datatemplate = @commandline.option('template') + datafile = @commandline.option('file') + dataaction = @commandline.option('action') + + domain = @commandline.option('domain') + project = @commandline.option('project') + username = @commandline.option('username') + password = @commandline.option('password') + + threshold = @commandline.option('threshold') + + datablob = '' + + begin + datablob = IO.read(datatemplate) + rescue + datablob = '' + else + begin + request = REXML::Document.new(datablob) + if e = REXML::XPath.match(request.root,"/exa:request/exa:data") then + datablob = e.to_s.chomp + end + rescue + datablob = '' + end + end + + begin + File.open(requestfile,'w') do |f| + f.puts "<?xml version='1.0'?>\n" + f.puts "<exa:request #{@@namespace}>\n" + f.puts " <exa:application>\n" + f.puts " <exa:action>#{dataaction}</exa:action>\n" unless dataaction.empty? + f.puts " <exa:filename>#{datafile}</exa:filename>\n" unless datafile.empty? + f.puts " <exa:threshold>#{threshold}</exa:threshold>\n" unless threshold.empty? + f.puts " </exa:application>\n" + f.puts " <exa:client>\n" + f.puts " <exa:domain>#{domain}</exa:domain>\n" + f.puts " <exa:project>#{project}</exa:project>\n" + f.puts " <exa:username>#{username}</exa:username>\n" + f.puts " <exa:password>#{password}</exa:password>\n" + f.puts " </exa:client>\n" + if datablob.empty? then + f.puts " <exa:data/>\n" + else + f.puts " #{datablob.chomp}\n" + end + f.puts "</exa:request>" + end + rescue + status(replyfile,"unable to create '#{requestfile}'") + end + + direct() + + end + + def extend + + requestfile = @commandline.option('request') + replyfile = @commandline.option('reply') + + datatemplate = @commandline.option('template') + datafile = @commandline.option('file') + dataaction = @commandline.option('action') + + threshold = @commandline.option('threshold') + + if datatemplate.empty? then + status(replyfile,"invalid data template '#{datatemplate}'") + else + begin + if FileTest.file?(datatemplate) && oldrequest = IO.read(datatemplate) then + request, done = REXML::Document.new(oldrequest), false + if ! threshold.empty? && e = REXML::XPath.match(request.root,"/exa:request/exa:application/exa:threshold") then + e.text, done = threshold, true + end + if ! dataaction.empty? && e = REXML::XPath.match(request.root,"/exa:request/exa:application/exa:action") then + e.text, done = dataaction, true + end + if ! datafile.empty? && e = REXML::XPath.match(request.root,"/exa:request/exa:application/exa:filename") then + e.text, done = datafile, true + end + # + if ! threshold.empty? && e = REXML::XPath.match(request.root,"/exa:request/exa:application") then + e = e.add_element('exa:threshold') + e.add_text(threshold.to_s) + done = true + end + # + report("nothing replaced in template file") unless done + begin + File.open(requestfile,'w') do |f| + f.puts(newrequest.to_s) + end + rescue + status(replyfile,"unable to create '#{requestfile}'") + end + else + status(replyfile,"unable to read data template '#{datatemplate}'") + end + rescue + status(replyfile,"unable to handle data template '#{datatemplate}'") + end + end + + direct() + + end + + def direct + + requestpath = @commandline.option('path') + requestfile = @commandline.option('request') + replyfile = @commandline.option('reply') + resultfile = @commandline.option('result') + datatemplate = @commandline.option('template') + datafile = @commandline.option('file') + threshold = @commandline.option('threshold') + address = @commandline.option('address') + port = @commandline.option('port') + session_id = @commandline.option('session') + exaurl = @commandline.option('exaurl') + + exaurl = "/#{exaurl}" unless exaurl =~ /^\// + + address.sub!(/^http\:\/\//io) do + '' + end + address.sub!(/\:(\d+)$/io) do + port = $1 + '' + end + + autopdf = @commandline.option('autopdf') + + dialogue = nil + + resultfile.sub!(/\.[a-z]+?$/, '') # don't overwrite the source + + unless requestpath.empty? then + begin + if FileTest.directory?(requestpath) then + if Dir.chdir(requestpath) then + report("gone to path '#{requestpath}'") + else + status(replyfile,"unable to go to path '#{requestpath}") + end + else + status(replyfile,"unable to locate '#{requestpath}'") + end + rescue + status(replyfile,"unable to handle '#{requestpath}'") + end + end + + datafile = File.expand_path(datafile) unless datafile.empty? + datatemplate = File.backtracked(datatemplate,3) unless datatemplate.empty? + + # request must be valid + + status(replyfile,'no request file') if requestfile.empty? + status(replyfile,"invalid request file '#{requestfile}'") unless FileTest.file?(requestfile) + + begin + request = IO.readlines(requestfile).join('') + request = REXML::Document.new(request) + status(replyfile,'invalid request (no request)') unless request.root.fully_expanded_name=='exa:request' + status(replyfile,'invalid request (no application block)') unless request.elements['exa:request'].elements['exa.application'] == nil # explicit nil test needed + rescue REXML::ParseException + status(replyfile,'invalid request (invalid xml file)') + rescue + status(replyfile,'invalid request (invalid file)') + else + report("using request file '#{requestfile}'") + end + + # request can force session_id + + if session_id && session_id.empty? then + begin + id = request.elements['exa:request'].elements['exa:application'].elements['exa:session'].text + rescue Exception + id = '' + ensure + if id && ! id.empty? then + session_id = id + end + end + end + + # request can overload reply name + + begin + rreplyfile = request.elements['exa:request'].elements['exa:application'].elements['exa:output'].text + rescue Exception + rreplyfile = nil + ensure + if rreplyfile && ! rreplyfile.empty? then + replyfile = rreplyfile + report("reply file '#{replyfile} set by request'") + else + report("using reply file '#{replyfile}'") + end + end + + # request can overload result name + + begin + rresultfile = request.elements['exa:request'].elements['exa:application'].elements['exa:result'] + rescue Exception + rresultfile = nil + ensure + if rresultfile && ! rresultfile.empty? then + resultfile = rresultfile + report("result file '#{resultfile}' set by request") + else + report("using result file '#{resultfile}'") + end + end + + # try to connect to server + + start_time = Time.now + + processtimeout = begin @commandline.option('timeout').to_i rescue @@processtimeout end + processtimeout = @@processtimeout if processtimeout == 0 # 'xx'.to_i => 0 + + dialogue = start_dialogue(address, port, processtimeout) + + if dialogue then + # continue + else + status(replyfile,'no connection') + end + + # post request + + timeout (@@processtimeout-10) do # -10 so that we run into this one first + begin + report("posting request of type '#{exaurl}'") + report("using session id '#{session_id}'") if session_id && ! session_id.empty? + firstline, chunks, total = nil, 0, 0 + body, boundary, crlf = '', boundary_string(32), "\x0d\x0a" + body << '--' + boundary + crlf + body << "Content-Disposition: form-data; name=\"exa:request\"" + body << crlf + body << "Content-Type: text/plain" + body << crlf + crlf + body << request.to_s + body << crlf + '--' + boundary + crlf +if session_id && ! session_id.empty? then + body << "Content-Disposition: form-data; name=\"exa:session\"" + body << "Content-Type: text/plain" + body << crlf + crlf + body << session_id + body << crlf + '--' + boundary + crlf +end + begin + File.open(datafile,'rb') do |df| + body << "Content-Disposition: form-data; name=\"filename\"" + body << "Content-Type: text/plain" + body << crlf + crlf + body << datafile + body << crlf + '--' + boundary + crlf + body << "Content-Disposition: form-data; name=\"fakename\" ; filename=\"#{datafile}\"" + body << "Content-Type: application/octetstream" + body << "Content-Transfer-Encoding: binary" + body << crlf + crlf + body << df.read + body << crlf + '--' + boundary + '--' + crlf + end + rescue + # skip + end + headers = Hash.new + headers['content-type'] = "multipart/form-data; boundary=#{boundary}" + headers['content-length'] = body.length.to_s + begin + File.open(resultfile,'wb') do |rf| + begin + # firstline is max 1024 but ok for reply + dialogue.post(exaurl,body,headers) do |str| + if ! firstline || firstline.empty? then + report('receiving result') if total == 0 + firstline = str + end + total += 1 + rf.write(str) + end + rescue + report("forced close #{traceback}") + end + end + rescue + status(replyfile,'cannot open file') + end + begin + File.delete(resultfile) if File.zero?(resultfile) + rescue + end + unless FileTest.file?(resultfile) then + report("deleting empty resultfile") + begin + File.delete(resultfile) + rescue + # nice try, an error anyway + end + status(replyfile,'empty file') + else + n, id, status = 0, '', '' + loop do + again = false + if ! dialogue then + again = true + elsif firstline =~ /(\<exa:reply)/moi then + begin + reply = REXML::Document.new(firstline) + id = (REXML::XPath.match(reply.root,"/exa:reply/exa:session/text()") || '').to_s + status = (REXML::XPath.match(reply.root,"/exa:reply/exa:status/text()") || '').to_s + rescue + report("error in parsing reply #{traceback}") + break + else + report("status: #{status}") + if (status =~ /^running\s*\:\s*(background|busy)$/i) && (! id.empty?) then + report("waiting for status reply (#{n*@@polldelay})") + again = true + end + end + end + if again then + n += 1 + sleep(@@polldelay) # todo: duplicate when n > 1 + unless dialogue then + report('reestablishing connection') + dialogue = start_dialogue(address, port, processtimeout) + end + if dialogue then + begin + File.open(resultfile,'wb') do |rf| + begin + body = "id=#{id}" + headers = Hash.new + headers['content-type'] = "application/x-www-form-urlencoded" + headers['content-length'] = body.length.to_s + total, firstline = 0, '' + dialogue.post("/exastatus",body,headers) do |str| + if ! firstline || firstline.empty? then + firstline = str + end + total += 1 + rf.write(str) + end + rescue + report("forced close #{traceback}") + dialogue = nil + again = true + end + end + begin + File.delete(resultfile) if File.zero?(resultfile) + rescue + end + rescue + report("error in opening file #{traceback}") + status(replyfile,'cannot open file') + end + else + report("unable to make a connection") + status(replyfile,'unable to make a connection') # exit + end + else + break + end + end + case firstline + when /<\?xml\s*version=.*?\?>\s*<exa:reply/moi then + begin + File.delete(replyfile) if FileTest.file?(replyfile) + resultfile = replyfile if File.rename(resultfile,replyfile) + rescue + end + report("reply saved in '#{resultfile}'") + when /\%PDF\-/io then + report("done, file #{resultfile}, type pdf, #{total} chunks, #{File.size? rescue 0} bytes") + if resultfile =~ /\.pdf$/i then + report("file identified as 'pdf'") + elsif resultfile =~ /\..*$/o + report("result file suffix should be 'pdf'") + else + newresultfile = resultfile + '.pdf' + newresultfile.sub!(/\.pdf\.pdf/io, '.pdf') + pdf('close',newresultfile,autopdf) + begin + File.delete(newresultfile) if FileTest.file?(newresultfile) + resultfile = newresultfile if File.rename(resultfile,newresultfile) + rescue + report("adding 'pdf' suffix to result name failed") + else + report("'pdf' suffix added to result name") + end + end + report("result saved in '#{resultfile}'") + pdf('open',resultfile,autopdf) + status(replyfile,'ok') do + "<exa:filename>#{resultfile}</exa:filename>" + end + when /html/io then + report("done, file #{resultfile}, type html, #{total} chunks, #{File.size? rescue 0} bytes") + if resultfile =~ /\.(htm|html)$/i then + report("file identified as 'html'") + elsif resultfile =~ /\..*$/o + report("result file suffix should be 'htm'") + else + newresultfile = resultfile + '.htm' + begin + File.delete(newresultfile) if FileTest.file?(newresultfile) + resultfile = newresultfile if File.rename(resultfile,newresultfile) + rescue + report("adding 'htm' suffix to result name failed") + else + report("'htm' suffix added to result name") + end + end + report("result saved in '#{resultfile}'") + status(replyfile,'ok') do + "<exa:filename>#{resultfile}</exa:filename>" + end + else + report("no result file, first line #{firstline}") + status(replyfile,'no result file') + end + end + rescue TimeoutError + report("aborted due to time out") + status(replyfile,'time out') + rescue + report("aborted due to some problem #{traceback}") + status(replyfile,"no answer #{traceback}") + end + end + + begin + report("run time: #{Time.now-start_time} seconds") + rescue + end + + end + + def start_dialogue(address, port, processtimeout) + timeout(@@connecttimeout) do + report("trying to connect to #{address}:#{port}") + begin + begin + if dialogue = Net::HTTP.new(address, port) then + # dialogue.set_debug_output $stderr + dialogue.read_timeout = processtimeout # set this before start + if dialogue.start then + report("connected to #{address}:#{port}, timeout: #{processtimeout}") + else + retry + end + else + retry + end + rescue + sleep(2) + retry + else + return dialogue + end + rescue TimeoutError + return nil + rescue + return nil + end + end + end + +end + +logger = Logger.new(banner.shift) +commandline = CommandLine.new + +commandline.registerflag('autopdf') + +commandline.registervalue('path' , '') + +commandline.registervalue('request' , 'request.exa') +commandline.registervalue('reply' , 'reply.exa') +commandline.registervalue('result' , 'result') + +commandline.registervalue('template' , '') +commandline.registervalue('file' , '') +commandline.registervalue('action' , '') +commandline.registervalue('timeout' , '') + +commandline.registervalue('domain' , 'default') +commandline.registervalue('project' , 'default') +commandline.registervalue('username' , 'guest') +commandline.registervalue('password' , 'anonymous') +commandline.registervalue('exaurl' , 'exarequest') +commandline.registervalue('threshold' , '0') +commandline.registervalue('session' , '') + +commandline.registervalue('address' , 'localhost') +commandline.registervalue('port' , '80') + +commandline.registeraction('direct' , '[--path --request --reply --result --autopdf]') +commandline.registeraction('construct', '[--path --request --reply --result --autopdf] --file --action') +commandline.registeraction('extend' , '[--path --request --reply --result --autopdf] --file --action --template') + +commandline.registeraction('direct') +commandline.registeraction('construct') +commandline.registeraction('extend') + +commandline.registerflag('verbose') +commandline.registeraction('help') +commandline.registeraction('version') + +commandline.expand + +Commands.new(commandline,logger,banner).send(commandline.action || 'main') diff --git a/scripts/context/ruby/wwwserver.rb b/scripts/context/ruby/wwwserver.rb new file mode 100644 index 000000000..aa6352183 --- /dev/null +++ b/scripts/context/ruby/wwwserver.rb @@ -0,0 +1,292 @@ +#!/usr/env ruby + +banner = ['WWWServer', 'version 1.0.0', '2003-2006', 'PRAGMA ADE/POD'] + +$: << File.dirname(File.expand_path($0)) + +require 'base/switch' +require 'base/logger' + +require 'monitor' + +# class WWW < Monitor +# end +# class Server < Monitor +# end + +require 'www/lib' +require 'www/dir' +require 'www/login' +require 'www/exa' + +require 'tempfile' +require 'ftools' +require 'webrick' + +class Server + + attr_accessor :document_root, :work_path, :logs_path, :port_number, :exa_url, :verbose, :trace, :direct + + def initialize(logger) + @httpd = nil + @document_root = '' + @work_path = '' + @logs_path = '' + @port_number = 8061 + @exa_url = 'http://localhost:8061' + @logger = logger + @n_of_clients = 500 + @request_timeout = 5*60 + @verbose = false + @trace = false + @direct = false + end + + def report(str) + @logger.report(str) if @logger + end + + def setup + if @document_root.empty? then + rootpath = File.expand_path($0) + @document_root = File.expand_path(File.join(File.dirname(rootpath),'..','documents')) + unless FileTest.directory?(@document_root) then # todo: optional + loop do + prevpath = rootpath.dup + rootpath = File.dirname(rootpath) + if prevpath == rootpath then + break + else + checkpath = File.join(rootpath,'documents') + # report("locating: #{checkpath}") + if FileTest.directory?(checkpath) then + @document_root = checkpath + break + else + checkpath = File.join(rootpath,'docroot/documents') + # report("locating: #{checkpath}") + if FileTest.directory?(checkpath) then + @document_root = checkpath + break + end + end + end + end + end + end + @document_root = File.join(Dir.pwd, 'documents') unless FileTest.directory?(@document_root) + unless FileTest.directory?(@document_root) then + report("invalid document root: #{@document_root}") + exit + else + report("using document root: #{@document_root}") + end + # + @work_path = File.expand_path(File.join(@document_root,'..','work')) if @work_path.empty? + # begin File.makedirs(@work_path) ; rescue ; end # no, let's auto-temp + if ! FileTest.directory?(@work_path) || ! FileTest.writable?(@work_path) then + @work_path = File.expand_path(File.join(Dir.tmpdir,'exaserver','work')) + begin File.makedirs(@logs_path) ; rescue ; end + end + report("using work path: #{@work_path}") + # + @logs_path = File.expand_path(File.join(@document_root,'..','logs')) if @logs_path.empty? + # begin File.makedirs(@logs_path) ; rescue ; end # no, let's auto-temp + if ! FileTest.directory?(@logs_path) || ! FileTest.writable?(@logs_path) then + @logs_path = File.expand_path(File.join(Dir.tmpdir,'exaserver','logs')) + begin File.makedirs(@logs_path) ; rescue ; end + end + report("using log path: #{@logs_path}") + # + if @logs_path.empty? then + @logfile = $stderr + @accfile = $stderr + else + @logfile = File.join(@logs_path,'exa-info.log') + @accfile = File.join(@logs_path,'exa-access.log') + begin File.delete(@logfile) ; rescue ; end + begin File.delete(@accfile) ; rescue ; end + end + # + begin + @httpd = WEBrick::HTTPServer.new( + :DocumentRoot => @document_root, + :DirectoryIndex => ['index.html','index.htm','showcase.pdf'], + :Port => @port_number.to_i, + :Logger => WEBrick::Log.new(@logfile, WEBrick::Log::INFO), # DEBUG + :RequestTimeout => @request_timeout, + :MaxClients => @n_of_clients, + :AccessLog => [ + [ @accfile, WEBrick::AccessLog::COMMON_LOG_FORMAT ], + [ @accfile, WEBrick::AccessLog::REFERER_LOG_FORMAT ], + [ @accfile, WEBrick::AccessLog::AGENT_LOG_FORMAT ], + # :CGIPathEnv => ENV["PATH"] # PATH environment variable for CGI. + ] + ) + rescue + report("starting server at port: #{@port_number} failed") + exit + else + report("running server at port: #{@port_number}") + end + + begin + # + @httpd.mount_proc("/dir") do |request,reply| + report("accepting /dir") if @verbose + web_session(request,reply).handle_dir + end + @httpd.mount_proc("/login") do |request,reply| + report("accepting /login") if @verbose + web_session(request,reply).handle_login + end + @httpd.mount("/cache", WEBrick::HTTPServlet::FileHandler, File.join(@work_path,'cache')) + # @httpd.mount_proc("/cache") do |request,reply| + # WEBrick::HTTPServlet::FileHandler(@httpd,@work_path) # not ok + # end + @httpd.mount_proc("/exalogin") do |request,reply| + report("accepting /exalogin") if @verbose + web_session(request,reply).handle_exalogin + end + @httpd.mount_proc("/exadefault") do |request,reply| + report("accepting /exadefault") if @verbose + web_session(request,reply).handle_exadefault + end + @httpd.mount_proc("/exainterface") do |request,reply| + report("accepting /exainterface") if @verbose + web_session(request,reply).handle_exainterface + end + @httpd.mount_proc("/exarequest") do |request,reply| + report("accepting /exarequest") if @verbose + web_session(request,reply).handle_exarequest + end + @httpd.mount_proc("/exacommand") do |request,reply| + report("accepting /exacommand") if @verbose + web_session(request,reply).handle_exacommand + end + @httpd.mount_proc("/exastatus") do |request,reply| + report("accepting /exastatus") if @verbose + web_session(request,reply).handle_exastatus + end + @httpd.mount_proc("/exaadmin") do |request,reply| + report("accepting /exaadmin") if @verbose + web_session(request,reply).handle_exaadmin + end + # + rescue + report("problem in starting server: #{$!}") + end + [:INT, :TERM, :EXIT].each do |signal| + trap(signal) do + @httpd.shutdown + end + end + end + + def start + unless @httpd then + setup + @httpd.start + end + end + + def stop + @httpd.shutdown if @httpd + end + + def restart + stop + start + end + + private + + def web_session(request,reply) + www = WWW.new(@httpd,request,reply) + www.set('path:work', @work_path) + www.set('path:logs', @logs_path) + www.set('path:root', File.dirname(@document_root)) + www.set('process:exaurl', @exa_url) + www.set('trace:errors','yes') if @trace + www.set('process:background', 'no') if @direct + return www + end + +end + +class Commands + + include CommandBase + + def start + if server = setup then server.start end + end + + def stop + if server = setup then server.stop end + end + + def restart + if server = setup then server.restart end + end + + private + + def setup + server = Server.new(logger) + server.document_root = @commandline.option('root') + server.verbose = @commandline.option('verbose') + if @commandline.option('forcetemp') then + server.work_path = Dir.tmpdir + '/exa/work' + server.logs_path = Dir.tmpdir + '/exa/logs' + [server.work_path,server.logs_path].each do |d| + begin + File.makedirs(d) unless FileTest.directory?(d) + rescue + report("unable to create #{d}") + exit + end + unless FileTest.writable?(d) then + report("unable to access #{d}") + exit + end + end + else + server.work_path = @commandline.option('work') + server.logs_path = @commandline.option('logs') + end + server.port_number = @commandline.option('port') + server.exa_url = @commandline.option('url') + server.trace = @commandline.option('trace') + server.direct = @commandline.option('direct') + return server + end + +end + +logger = Logger.new(banner.shift) +commandline = CommandLine.new + +commandline.registervalue('root' , '') +commandline.registervalue('work' , '') +commandline.registervalue('logs' , '') +commandline.registervalue('address', 'localhost') +commandline.registervalue('port' , '8061') +commandline.registervalue('url' , 'http://localhost:8061') + +commandline.registeraction('start' , 'start the server [--root --forcetemp --work --logs --address --port --url]') +commandline.registeraction('stop' , 'stop the server') +commandline.registeraction('restart', 'restart the server') + +commandline.registerflag('forcetemp') +commandline.registerflag('direct') +commandline.registerflag('verbose') +commandline.registerflag('trace') + +commandline.registeraction('help') +commandline.registeraction('version') + +commandline.expand + +Commands.new(commandline,logger,banner).send(commandline.action || 'start') + diff --git a/scripts/context/ruby/wwwwatch.rb b/scripts/context/ruby/wwwwatch.rb new file mode 100644 index 000000000..1f61ef479 --- /dev/null +++ b/scripts/context/ruby/wwwwatch.rb @@ -0,0 +1,464 @@ +#!/usr/bin/env ruby + +banner = ['WWWWatch', 'version 1.0.0', '2003-2006', 'PRAGMA ADE/POD'] + +$: << File.dirname(File.expand_path($0)) + +require 'base/switch' +require 'base/logger' + +require 'www/common' + +require 'monitor' +require 'fileutils' +require 'ftools' +require 'tempfile' +require 'timeout' +require 'thread' + +class Watch < Monitor + + include Common + + @@session_prefix = '' + @@check_factor = 4 + @@process_timeout = 1*60*60 + @@fast_wait_loop = false + + @@session_line = /^\s*(?![\#\%])(.*?)\s*\=\s*(.*?)\s*$/o + @@session_begin = 'begin exa session' + @@session_end = 'end exa session' + + attr_accessor :root_path, :work_path, :delay, :max_threads, :max_age, :verbose + + def initialize(logger) # we need to register all @vars here becase of the monitor + @threads = Hash.new + @files = Array.new + @stats = Hash.new + @skips = Hash.new + @root_path = '' + @work_path = Dir.tmpdir + @cache_path = @work_path + @last_action = Time.now + @delay = 1 + @max_threads = 5 + @max_age = @@process_timeout + @logger = logger + @verbose = false + [:INT, :TERM, :EXIT].each do |signal| + trap(signal) do + kill + exit + end + end + at_exit do + kill + end + end + + def trace + if @verbose && @logger then + @logger.report("exception: #{$!})") + $@.each do |t| + @logger.report(">> #{t}") + end + end + end + + def report(str) + @logger.report(str) if @logger + end + + def setup + @threads = Hash.new + @files = Array.new + @stats = Hash.new + @skips = Hash.new + @root_path = File.expand_path(File.join(File.dirname($0),'..')) if @root_path.empty? + @work_path = File.expand_path(File.join(@root_path,'work','watch')) if @work_path.empty? + @cache_path = File.expand_path(File.join(@root_path,'work','cache')) if @work_path.empty? + begin File.makedirs(@work_path) ; rescue ; end + begin File.makedirs(@cache_path) ; rescue ; end + unless File.writable?(@work_path) then + @work_path = File.expand_path(File.join(Dir.tmpdir,'work','watch')) + begin File.makedirs(@work_path) ; rescue ; end + end + unless File.writable?(@cache_path) then + @cache_path = File.expand_path(File.join(Dir.tmpdir,'work','cache')) + begin File.makedirs(@cache_path) ; rescue ; end + end + unless File.writable?(@work_path) then + puts "no valid work path: #{@work_path}" ; exit + end + unless File.writable?(@cache_path) then + puts "no valid work path: #{@work_path}" ; # no reason to exit + end + @last_action = Time.now + report("watching path #{@work_path}") if @verbose + end + + def lock(lck) + begin + report("watchdog: locking #{lck}") if @verbose + File.open(lck,'w') do |f| + f << Time.now + end + rescue + trace + end + end + + def unlock(lck) + begin + report("watchdog: unlocking #{lck}") if @verbose + File.delete(lck) + rescue + trace + end + end + + def kill + @threads.each do |t| + t.kill rescue false + end + end + + def restart + @files = Array.new + @skips = Hash.new + @stats = Hash.new + kill # threads + end + + def collect + begin + @files = Array.new + Dir.glob("#{@work_path}/#{@@session_prefix}*.ses").each do |sessionfile| + sessionfile = File.expand_path(sessionfile) + begin + if @threads.key?(sessionfile) then + # leave alone + elsif (Time.now - File.mtime(sessionfile)) > @max_age.to_i then + # delete + FileUtils::rm_r(sessionfile) rescue false + FileUtils::rm_r(sessionfile.sub(/ses$/,'dir')) rescue false + FileUtils::rm_r(sessionfile.sub(/ses$/,'lck')) rescue false + begin + FileUtils::rm_r(File.join(@cache_path, File.basename(sessionfile.sub(/ses$/,'dir')))) + rescue + report("watchdog: problems in cache cleanup #{$!}") # if @verbose + end + @stats.delete(sessionfile) rescue false + @skips.delete(sessionfile) rescue false + report("watchdog: removing session #{sessionfile}") if @verbose + elsif ! @skips.key?(sessionfile) then + @files << sessionfile + report("watchdog: checking session #{sessionfile}") if @verbose + end + rescue + # maybe purged in the meantime + end + end + rescue + if File.directory?(@work_path) then + @files = Array.new + else + # maybe dir is deleted (manual cleanup) + restart + end + end + begin + Dir.glob("#{@cache_path}/*.dir").each do |dirname| + begin + if (Time.now - File.mtime(dirname)) > @max_age.to_i then + begin + FileUtils::rm_r(dirname) + rescue + report("watchdog: problems in cache cleanup #{$!}") # if @verbose + end + end + rescue + # maybe purged in the meantime + end + end + rescue + end + end + + def purge + begin + Dir.glob("#{@work_path}/#{@@session_prefix}*").each do |sessionfile| + sessionfile = File.expand_path(sessionfile) + begin + if (Time.now - File.mtime(sessionfile)) > @max_age.to_i then + begin + if FileTest.directory?(sessionfile) then + FileUtils::rm_r(sessionfile) + else + File.delete(sessionfile) + end + rescue + end + begin + @stats.delete(sessionfile) + @skips.delete(sessionfile) + rescue + end + report("watchdog: purging session #{sessionfile}") if @verbose + end + rescue + # maybe purged in the meantime + end + end + rescue + end + end + + def loaded_session_data(filename) + begin + if data = IO.readlines(filename) then + return data if (data.first =~ /^[\#\%]\s*#{@@session_begin}/o) && (data.last =~ /^[\#\%]\s*#{@@session_end}/o) + end + rescue + trace + end + return nil + end + + def load(sessionfile) + # we assume that we get an exception when the file is locked + begin + if data = loaded_session_data(sessionfile) then + report("watchdog: loading session #{sessionfile}") if @verbose + vars = Hash.new + data.each do |line| + begin + if line.chomp =~ /^(.*?)\s*\=\s*(.*?)\s*$/o then + key, value = $1, $2 + vars[key] = value + end + rescue + end + end + return vars + else + return nil + end + rescue + trace + return nil + end + end + + def save(sessionfile, vars) + begin + report("watchdog: saving session #{sessionfile}") if @verbose + if @stats.key?(sessionfile) then + @stats[sessionfile] = File.mtime(sessionfile) + elsif @stats[sessionfile] == File.mtime(sessionfile) then + else + # construct data first + str = "\# #{@@session_begin}\n" + for k,v in vars do + str << "#{k}=#{v}\n" + end + str << "\# #{@@session_end}\n" + # save as fast as possible + File.open(sessionfile,'w') do |f| + f.puts(str) + end + end + rescue + report("watchdog: unable to save session #{sessionfile}") if @verbose + trace + return false + else + return true + end + end + + def launch + begin + @files.each do |sessionfile| + if @threads.length < @max_threads then + begin + if ! @skips.key?(sessionfile) && (vars = load(sessionfile)) then + if (id = vars['id']) && vars['status'] then + if vars['status'] == 'running: background' then + @last_action = Time.now + @threads[sessionfile] = Thread.new(vars, sessionfile) do |vars, sessionfile| + begin + report("watchdog: starting thread #{sessionfile}") if @verbose + dir = File.expand_path(sessionfile.sub(/ses$/,'dir')) + lck = File.expand_path(sessionfile.sub(/ses$/,'lck')) + start_of_run = Time.now + start_of_job = start_of_run.dup + max_runtime = @max_age + begin + start_of_job = vars['starttime'].to_i || start_of_run + start_of_job = start_of_run if start_of_job == 0 + rescue + start_of_job = Time.now + end + begin + max_runtime = vars['maxtime'].to_i || @max_age + max_runtime = @max_age if max_runtime == 0 + max_runtime = max_runtime - (Time.now.to_i - start_of_job.to_i) + rescue + max_runtime = @max_age + end + lock(lck) + if max_runtime > 0 then + command = vars['command'] || '' + if ! command.empty? then + vars['status'] = 'running: busy' + vars['timeout'] = max_runtime.to_s + save(sessionfile,vars) + timeout(max_runtime) do + begin + command = command_string(dir,command,'process.log') + report("watchdog: #{command}") if @verbose + system(command) + rescue TimeoutError + vars['status'] = 'running: timeout' + rescue + trace + vars['status'] = 'running: aborted' + else + vars['status'] = 'running: finished' + vars['runtime'] = sprintf("%.02f",(Time.now - start_of_run)) + vars['endtime'] = Time.now.to_i.to_s + end + end + else + vars['status'] = 'running: aborted' # no command + end + else + vars['status'] = 'running: aborted' # not enough time + end + save(sessionfile,vars) + unlock(lck) + report("watchdog: ending thread #{sessionfile}") if @verbose + @threads.delete(sessionfile) + rescue + trace + end + end + else + report("watchdog: skipping - id (#{vars['id']}) / status (#{vars['status']})") if @verbose + end + @skips[sessionfile] = true + else + # not yet ok + end + else + # maybe a lock + end + rescue + trace + end + else + break + end + end + rescue + trace + end + end + + def wait + begin + # report(Time.now.to_s) if @verbose + loop do + if @threads.length == @max_threads then + if @delay > @max_threads then + sleep(@delay) + else + sleep(@max_threads) + end + break if @@fast_wait_loop + else + sleep(@delay) + break + end + end + rescue + trace + end + end + + def check + begin + time = Time.now + if (time - @last_action) > @@check_factor*@max_age then + report("watchdog: cleanup") if @verbose + @stats = Hash.new + @last_action = time + kill + end + rescue + trace + end + end + + def cycle + loop do + begin + collect + launch + wait + check + rescue + trace + report("watchdog: some problem, restarting loop") + end + end + end + +end + +class Commands + + include CommandBase + + def watch + if watch = setup then watch.cycle end + end + def main + watch + end + + private + + def setup + watch = Watch.new(logger) + watch.root_path = @commandline.option('root') + watch.work_path = @commandline.option('work') + watch.verbose = @commandline.option('verbose') + begin + watch.max_threads = @commandline.option('threads').to_i + rescue + watch.max_threads = 5 + end + watch.setup + return watch + end + +end + +logger = Logger.new(banner.shift) +commandline = CommandLine.new + +commandline.registervalue('root' , '') +commandline.registervalue('work' , '') +commandline.registervalue('threads' , '5') + +commandline.registeraction('watch', '[--work=path] [--root=path]') + +commandline.registerflag('verbose') +commandline.registeraction('help') +commandline.registeraction('version') + +commandline.expand + +Commands.new(commandline,logger,banner).send(commandline.action || 'main') diff --git a/scripts/context/stubs/mswin/texmfstart.bat b/scripts/context/stubs/mswin/texmfstart.bat deleted file mode 100755 index 287f9c4fa..000000000 --- a/scripts/context/stubs/mswin/texmfstart.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -ruby c:\data\develop\context/ruby/texmfstart.rb %* diff --git a/scripts/context/stubs/unix/ctxtools b/scripts/context/stubs/unix/ctxtools new file mode 100755 index 000000000..5a6a1feb5 --- /dev/null +++ b/scripts/context/stubs/unix/ctxtools @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart ctxtools.rb $@ diff --git a/scripts/context/stubs/unix/ctxtools.bat b/scripts/context/stubs/unix/ctxtools.bat deleted file mode 100755 index f1f5e019e..000000000 --- a/scripts/context/stubs/unix/ctxtools.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart ctxtools.rb %* diff --git a/scripts/context/stubs/unix/exatools b/scripts/context/stubs/unix/exatools new file mode 100755 index 000000000..cededbb57 --- /dev/null +++ b/scripts/context/stubs/unix/exatools @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart exatools.rb $@ diff --git a/scripts/context/stubs/unix/exatools.bat b/scripts/context/stubs/unix/exatools.bat deleted file mode 100755 index 57f798e82..000000000 --- a/scripts/context/stubs/unix/exatools.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart exatools.rb %* diff --git a/scripts/context/stubs/unix/makempy b/scripts/context/stubs/unix/makempy new file mode 100755 index 000000000..c30a261f0 --- /dev/null +++ b/scripts/context/stubs/unix/makempy @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart makempy.pl $@ diff --git a/scripts/context/stubs/unix/makempy.bat b/scripts/context/stubs/unix/makempy.bat deleted file mode 100755 index e339058c6..000000000 --- a/scripts/context/stubs/unix/makempy.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart makempy.pl %* diff --git a/scripts/context/stubs/unix/mpstools b/scripts/context/stubs/unix/mpstools new file mode 100755 index 000000000..68fd485bc --- /dev/null +++ b/scripts/context/stubs/unix/mpstools @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart mpstools.rb $@ diff --git a/scripts/context/stubs/unix/mpstools.bat b/scripts/context/stubs/unix/mpstools.bat deleted file mode 100755 index df1732e17..000000000 --- a/scripts/context/stubs/unix/mpstools.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart mpstools.rb %* diff --git a/scripts/context/stubs/unix/mptopdf b/scripts/context/stubs/unix/mptopdf new file mode 100755 index 000000000..a29448782 --- /dev/null +++ b/scripts/context/stubs/unix/mptopdf @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart mptopdf.pl $@ diff --git a/scripts/context/stubs/unix/mptopdf.bat b/scripts/context/stubs/unix/mptopdf.bat deleted file mode 100755 index 242854337..000000000 --- a/scripts/context/stubs/unix/mptopdf.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart mptopdf.pl %* diff --git a/scripts/context/stubs/unix/pdftools b/scripts/context/stubs/unix/pdftools new file mode 100755 index 000000000..fc6b9e864 --- /dev/null +++ b/scripts/context/stubs/unix/pdftools @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart pdftools.rb $@ diff --git a/scripts/context/stubs/unix/pdftools.bat b/scripts/context/stubs/unix/pdftools.bat deleted file mode 100755 index adc48eacf..000000000 --- a/scripts/context/stubs/unix/pdftools.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart pdftools.rb %* diff --git a/scripts/context/stubs/unix/pstopdf b/scripts/context/stubs/unix/pstopdf new file mode 100755 index 000000000..e1f0375e5 --- /dev/null +++ b/scripts/context/stubs/unix/pstopdf @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart pstopdf.rb $@ diff --git a/scripts/context/stubs/unix/pstopdf.bat b/scripts/context/stubs/unix/pstopdf.bat deleted file mode 100755 index 248e34caf..000000000 --- a/scripts/context/stubs/unix/pstopdf.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart pstopdf.rb %* diff --git a/scripts/context/stubs/unix/runtools b/scripts/context/stubs/unix/runtools new file mode 100755 index 000000000..3e4e2c505 --- /dev/null +++ b/scripts/context/stubs/unix/runtools @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart runtools.rb $@ diff --git a/scripts/context/stubs/unix/runtools.bat b/scripts/context/stubs/unix/runtools.bat deleted file mode 100755 index 68a7b4f97..000000000 --- a/scripts/context/stubs/unix/runtools.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart runtools.rb %* diff --git a/scripts/context/stubs/unix/texexec b/scripts/context/stubs/unix/texexec new file mode 100755 index 000000000..a8e297307 --- /dev/null +++ b/scripts/context/stubs/unix/texexec @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart texexec.rb $@ diff --git a/scripts/context/stubs/unix/texexec.bat b/scripts/context/stubs/unix/texexec.bat deleted file mode 100755 index 02b297b9c..000000000 --- a/scripts/context/stubs/unix/texexec.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart texexec.rb %* diff --git a/scripts/context/stubs/unix/texfont b/scripts/context/stubs/unix/texfont new file mode 100755 index 000000000..ec1bd57d5 --- /dev/null +++ b/scripts/context/stubs/unix/texfont @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart texfont.pl $@ diff --git a/scripts/context/stubs/unix/texfont.bat b/scripts/context/stubs/unix/texfont.bat deleted file mode 100755 index 3134bf14c..000000000 --- a/scripts/context/stubs/unix/texfont.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart texfont.pl %* diff --git a/scripts/context/stubs/unix/texmfstart.bat b/scripts/context/stubs/unix/texmfstart.bat deleted file mode 100755 index 287f9c4fa..000000000 --- a/scripts/context/stubs/unix/texmfstart.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -ruby c:\data\develop\context/ruby/texmfstart.rb %* diff --git a/scripts/context/stubs/unix/textools b/scripts/context/stubs/unix/textools new file mode 100755 index 000000000..5078ebce0 --- /dev/null +++ b/scripts/context/stubs/unix/textools @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart textools.rb $@ diff --git a/scripts/context/stubs/unix/textools.bat b/scripts/context/stubs/unix/textools.bat deleted file mode 100755 index 727b4a36d..000000000 --- a/scripts/context/stubs/unix/textools.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart textools.rb %* diff --git a/scripts/context/stubs/unix/texutil b/scripts/context/stubs/unix/texutil new file mode 100755 index 000000000..773176da5 --- /dev/null +++ b/scripts/context/stubs/unix/texutil @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart texutil.rb $@ diff --git a/scripts/context/stubs/unix/texutil.bat b/scripts/context/stubs/unix/texutil.bat deleted file mode 100755 index 1e63639bb..000000000 --- a/scripts/context/stubs/unix/texutil.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart texutil.rb %* diff --git a/scripts/context/stubs/unix/tmftools b/scripts/context/stubs/unix/tmftools new file mode 100755 index 000000000..9b52a5e53 --- /dev/null +++ b/scripts/context/stubs/unix/tmftools @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart tmftools.rb $@ diff --git a/scripts/context/stubs/unix/tmftools.bat b/scripts/context/stubs/unix/tmftools.bat deleted file mode 100755 index c9c0c08bd..000000000 --- a/scripts/context/stubs/unix/tmftools.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart tmftools.rb %* diff --git a/scripts/context/stubs/unix/xmltools b/scripts/context/stubs/unix/xmltools new file mode 100755 index 000000000..7e8c174ca --- /dev/null +++ b/scripts/context/stubs/unix/xmltools @@ -0,0 +1,2 @@ +#!/bin/sh +texmfstart xmltools.rb $@ diff --git a/scripts/context/stubs/unix/xmltools.bat b/scripts/context/stubs/unix/xmltools.bat deleted file mode 100755 index 2de0e4457..000000000 --- a/scripts/context/stubs/unix/xmltools.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -texmfstart xmltools.rb %* |