weft-qda 0.9.6 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/lib/weft.rb +16 -1
  2. data/lib/weft/WEFT-VERSION-STRING.rb +1 -1
  3. data/lib/weft/application.rb +17 -74
  4. data/lib/weft/backend.rb +6 -32
  5. data/lib/weft/backend/sqlite.rb +222 -164
  6. data/lib/weft/backend/sqlite/category_tree.rb +52 -48
  7. data/lib/weft/backend/sqlite/database.rb +57 -0
  8. data/lib/weft/backend/sqlite/upgradeable.rb +7 -0
  9. data/lib/weft/broadcaster.rb +90 -0
  10. data/lib/weft/category.rb +139 -47
  11. data/lib/weft/codereview.rb +160 -0
  12. data/lib/weft/coding.rb +74 -23
  13. data/lib/weft/document.rb +23 -10
  14. data/lib/weft/exceptions.rb +10 -0
  15. data/lib/weft/filters.rb +47 -224
  16. data/lib/weft/filters/indexers.rb +137 -0
  17. data/lib/weft/filters/input.rb +118 -0
  18. data/lib/weft/filters/output.rb +101 -0
  19. data/lib/weft/filters/templates.rb +80 -0
  20. data/lib/weft/filters/win32backtick.rb +246 -0
  21. data/lib/weft/query.rb +169 -0
  22. data/lib/weft/wxgui.rb +349 -294
  23. data/lib/weft/wxgui/constants.rb +43 -0
  24. data/lib/weft/wxgui/controls.rb +6 -0
  25. data/lib/weft/wxgui/controls/category_dropdown.rb +192 -0
  26. data/lib/weft/wxgui/controls/category_tree.rb +314 -0
  27. data/lib/weft/wxgui/controls/document_list.rb +97 -0
  28. data/lib/weft/wxgui/controls/multitype_control.rb +37 -0
  29. data/lib/weft/wxgui/{inspectors → controls}/textcontrols.rb +235 -64
  30. data/lib/weft/wxgui/dialogs.rb +144 -41
  31. data/lib/weft/wxgui/error_handler.rb +116 -36
  32. data/lib/weft/wxgui/exceptions.rb +7 -0
  33. data/lib/weft/wxgui/inspectors.rb +61 -208
  34. data/lib/weft/wxgui/inspectors/category.rb +19 -16
  35. data/lib/weft/wxgui/inspectors/codereview.rb +90 -132
  36. data/lib/weft/wxgui/inspectors/document.rb +12 -8
  37. data/lib/weft/wxgui/inspectors/imagedocument.rb +56 -56
  38. data/lib/weft/wxgui/inspectors/query.rb +284 -0
  39. data/lib/weft/wxgui/inspectors/script.rb +147 -23
  40. data/lib/weft/wxgui/lang/en.rb +69 -0
  41. data/lib/weft/wxgui/sidebar.rb +90 -432
  42. data/lib/weft/wxgui/utilities.rb +70 -91
  43. data/lib/weft/wxgui/workarea.rb +150 -43
  44. data/share/icons/category.ico +0 -0
  45. data/share/icons/category.xpm +109 -0
  46. data/share/icons/codereview.ico +0 -0
  47. data/share/icons/codereview.xpm +54 -0
  48. data/share/icons/d_and_c.xpm +126 -0
  49. data/share/icons/document.ico +0 -0
  50. data/share/icons/document.xpm +70 -0
  51. data/share/icons/project.ico +0 -0
  52. data/share/icons/query.ico +0 -0
  53. data/share/icons/query.xpm +56 -0
  54. data/{lib/weft/wxgui → share/icons}/search.xpm +0 -0
  55. data/share/icons/weft.ico +0 -0
  56. data/share/icons/weft.xpm +62 -0
  57. data/share/icons/weft16.ico +0 -0
  58. data/share/icons/weft32.ico +0 -0
  59. data/share/templates/category_plain.html +18 -0
  60. data/share/templates/codereview_plain.html +18 -0
  61. data/share/templates/document_plain.html +13 -0
  62. data/share/templates/document_plain.txt +7 -0
  63. data/test/001-document.rb +55 -36
  64. data/test/002-category.rb +81 -6
  65. data/test/003-code.rb +8 -4
  66. data/test/004-application.rb +13 -34
  67. data/test/005-query_review.rb +139 -0
  68. data/test/006-filters.rb +54 -42
  69. data/test/007-output_filters.rb +113 -0
  70. data/test/009a-backend_sqlite_basic.rb +95 -24
  71. data/test/009b-backend_sqlite_complex.rb +43 -62
  72. data/test/009c_backend_sqlite_bench.rb +5 -10
  73. data/test/053-doc_inspector.rb +46 -0
  74. data/test/055-query_window.rb +50 -0
  75. data/test/all-tests.rb +1 -0
  76. data/test/test-common.rb +19 -0
  77. data/test/testdata/empty.qdp +0 -0
  78. data/test/testdata/simple with space.pdf +0 -0
  79. data/test/testdata/simple.pdf +0 -0
  80. data/weft-qda.rb +40 -7
  81. metadata +74 -14
  82. data/lib/weft/wxgui/category.xpm +0 -26
  83. data/lib/weft/wxgui/document.xpm +0 -25
  84. data/lib/weft/wxgui/inspectors/search.rb +0 -265
  85. data/lib/weft/wxgui/mondrian.xpm +0 -44
  86. data/lib/weft/wxgui/weft16.xpm +0 -31
@@ -0,0 +1,137 @@
1
+ module QDA
2
+ class Indexer
3
+ attr_reader :cursor
4
+ def initialize()
5
+ @cursor = 0
6
+ end
7
+
8
+ def index(str)
9
+ prepare(str)
10
+ str.each_line { | line | feed(line) }
11
+ end
12
+
13
+ def terminate()
14
+ end
15
+
16
+ def prepare(content)
17
+ end
18
+
19
+ def feed(line)
20
+ @cursor += line.length
21
+ end
22
+ end
23
+ # An indexer which records the position of words for later reverse
24
+ # retrieval
25
+ class WordIndexer < Indexer
26
+ attr_reader :words
27
+ # includes accented latin-1 characters
28
+ WORD_TOKENIZER = /[\w\xC0-\xD6\xD8-\xF6\xF8-\xFF][\w\xC0-\xD6\xD8-\xF6\xF8-\xFF\']+/
29
+ def initialize()
30
+ super
31
+ @words = Hash.new { | h, k | h[k] = [] }
32
+ end
33
+
34
+ def feed(line)
35
+ line.scan( WORD_TOKENIZER ) do | word |
36
+ next if word.length == 1
37
+ if word.respond_to?(:offset)
38
+ offset = cursor + word.offset
39
+ else
40
+ offset = cursor + Regexp.last_match.begin(0)
41
+ end
42
+ @words[word.to_s].push(offset)
43
+ end
44
+ super
45
+ end
46
+ end
47
+
48
+ # An indexer that uses text patterns to identify, for example,
49
+ # passages by a particular speaker, or text headings.
50
+ # The indexer can recognise a number of different types of codes,
51
+ # each denoted by a pattern of punctuation in a line of text. A
52
+ # default coder recognises the following
53
+ # A 'Heading', marked by a line **NAME OF HEADING**
54
+ # A 'Speaker', marked by a line SpeakerName:
55
+ #
56
+ # After the filter has run, the results of the coding can be
57
+ # retrieved by calling Autocoder#codes
58
+ # This is a hash of codetype names to inner hashes of codevalue names
59
+ # (strings) to QDA::Codesets corresponding to them.
60
+ class AutoCoder < Indexer
61
+ STANDARD_TRIGGER_RULES = {
62
+ /^(\w+)\:\s*$/ => 'Speaker',
63
+ /^\*\*(.*)\*\*$/ => 'Heading'
64
+ }
65
+
66
+ attr_reader :codes
67
+ # +rules+ should be a hash of string keys, naming types of autocode
68
+ # (e.g. "Speaker", "Heading", "Topic") mapped to values, which
69
+ # should be regular expressions specifying how the start of such a
70
+ # code should be recognised.
71
+ # For example, to find topics marked by the characters '##' at the
72
+ # start of the line:
73
+ # 'Heading' => /^##(.*)$/
74
+ def initialize(rules = STANDARD_TRIGGER_RULES)
75
+ super()
76
+ @trigger_rules = rules
77
+ @codes = Hash.new { | h, k | h[k] = {} }
78
+ @curr_codes = {}
79
+ end
80
+
81
+ # check a line of document content for triggers
82
+ def feed(line)
83
+ @trigger_rules.each do | rule, type |
84
+ if match = rule.match(line)
85
+ trigger(cursor, type, match[1])
86
+ end
87
+ end
88
+ super
89
+ end
90
+
91
+ # take action on finding a autocode marker
92
+ def trigger(cursor, group, codename)
93
+ # save any previous code that was being done for this group
94
+ store(group) if @curr_codes[group]
95
+ new_codeset = get_code(group, codename)
96
+ @curr_codes[group] = [ new_codeset, cursor ]
97
+ end
98
+ private :trigger
99
+
100
+ # returns the code name +codename+ within the group +group+,
101
+ # creating a new empty category
102
+ def get_code(group, codename)
103
+ return @codes[group][codename] if @codes[group][codename]
104
+ @codes[group][codename] = QDA::CodeSet.new()
105
+ end
106
+
107
+ # Returns the names and codesets for autocodes in group +group+
108
+ # in a series of pairs
109
+ def each_autocode(group)
110
+ @codes[group].each { | name, codeset | yield name, codeset }
111
+ end
112
+
113
+ # alters all the stored coding in this autocoder so that it refers
114
+ # to the document identified by +docid+
115
+ def apply(docid)
116
+ @codes.values.each do | group |
117
+ group.values.each do | codeset |
118
+ codeset.map! { | x | x.docid = docid; x }
119
+ end
120
+ end
121
+ end
122
+
123
+ # finish up all currently active coding in this autocoder
124
+ def terminate()
125
+ @curr_codes.each_key { | group | store(group) }
126
+ end
127
+
128
+ # finish the coding for the current code being used among +group+
129
+ def store(group)
130
+ codeset, start = @curr_codes[group]
131
+ # -1 here is a placeholder
132
+ terminus = cursor - start
133
+ codeset.add( Code.new(-1, start, terminus) )
134
+ end
135
+ private :store
136
+ end
137
+ end
@@ -0,0 +1,118 @@
1
+ module QDA
2
+ module Filters
3
+ class DocumentTextFilter
4
+ IMPORT_CLASS = Document
5
+ MEDIA_TYPE = 'text/plain'
6
+
7
+ def run(content_or_file)
8
+ content = respond_to?(:read) ? read(content_or_file) :
9
+ content_or_file
10
+ doc = QDA::Document.new('', '')
11
+ # signal to indexers we're about to start
12
+ @indexers.each { | indexer | indexer.prepare(content) }
13
+ content.each_line do | line |
14
+ doc.append(line.to_s.chomp)
15
+ @indexers.each { | indexer | indexer.feed(line) }
16
+ end
17
+ @indexers.each { | indexer | indexer.terminate() }
18
+ doc.create
19
+ doc
20
+ end
21
+
22
+
23
+ attr_reader :cursor
24
+
25
+ def initialize()
26
+ @cursor = 0
27
+ @indexers = []
28
+ end
29
+
30
+ def add_indexer(indexer)
31
+ unless indexer.respond_to?(:feed)
32
+ raise "Document indexers should have a feed method"
33
+ end
34
+ @indexers.push(indexer)
35
+ end
36
+ end
37
+
38
+ module TextReader
39
+ def read(filename)
40
+ File.read(filename)
41
+ end
42
+ end
43
+
44
+ class TextFileDocumentFilter < DocumentTextFilter
45
+ EXTENSIONS = ['txt']
46
+ DESCRIPTION = 'Plain Text Files'
47
+ include TextReader
48
+ Filters.register_filter(self)
49
+ end
50
+
51
+ module PDFReader
52
+ # This tests if we are on windows - will raise an exception if used
53
+ # on *nix platforms
54
+ begin
55
+ require 'weft/filters/win32backtick.rb'
56
+ rescue LoadError
57
+ end
58
+
59
+
60
+ maybe_pdf = 'pdftotext'
61
+ begin
62
+ if defined? Win32Process
63
+ # the -v option for pdftotext writes to STDERR
64
+ ignore, test = Win32Process.backtick("#{maybe_pdf} -v")
65
+ else
66
+ test = `#{maybe_pdf} -v 2>&1`
67
+ end
68
+
69
+ if test =~ /pdftotext version 3/
70
+ PDF_TO_TEXT_EXEC = maybe_pdf
71
+ else
72
+ warn 'PDFtotext Version 3 not found in path; ' <<
73
+ 'PDF Filters will not be available'
74
+ end
75
+ end
76
+
77
+ def read(file)
78
+ file = "\"#{file}\"" if file =~ /\s/
79
+ cmd = "#{PDF_TO_TEXT_EXEC} -nopgbrk #{file} -"
80
+ if defined? Win32Process
81
+ do_read_win32(cmd)
82
+ else
83
+ do_read_unixy(cmd)
84
+ end
85
+ end
86
+
87
+ private
88
+ def do_read_win32(cmd)
89
+ out, err = Win32Process.backtick(cmd)
90
+ return out unless out.empty?
91
+ if err =~ /Copying of text from this document is not allowed/
92
+ raise IOError.new("Could not extract PDF text: this PDF is locked")
93
+ else
94
+ raise IOError.new("Could not extract PDF text: #{err}")
95
+ end
96
+ end
97
+
98
+ def do_read_unixy(cmd)
99
+ content = `#{cmd} 2>&1`
100
+ case $CHILD_STATUS
101
+ when 0
102
+ return content
103
+ when 3
104
+ raise IOError.new("Could not extract PDF text: this PDF is locked")
105
+ else
106
+ raise IOError.new("Could not extract PDF text: #{content}")
107
+ end
108
+ end
109
+ end
110
+
111
+ class PDFFileDocumentFilter < DocumentTextFilter
112
+ EXTENSIONS = ['pdf']
113
+ DESCRIPTION = 'PDF Files'
114
+ include PDFReader
115
+ Filters.register_filter(self) if defined?(PDFReader::PDF_TO_TEXT_EXEC)
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,101 @@
1
+ module QDA
2
+ module Filters
3
+
4
+ begin
5
+ require 'csv'
6
+ rescue LoadError
7
+ end
8
+
9
+ begin
10
+ require 'PageTemplate/parser'
11
+ require 'PageTemplate/commands'
12
+ require 'weft/filters/templates.rb'
13
+ # These features won't be used if not available
14
+ rescue LoadError
15
+ end
16
+
17
+ class OutputFilter
18
+ def initialize(app = nil)
19
+ @app = app
20
+ end
21
+ end
22
+
23
+ # Inherit, don't instantiate this class directly
24
+ class DocumentOutputFilter < OutputFilter
25
+ EXPORT_CLASS = Document
26
+ def DocumentOutputFilter.def_name(doc)
27
+ 'document-' << doc.title.gsub(/\s+/, '_').gsub(/\W/,'')
28
+ end
29
+ end
30
+
31
+ # A filter that formats a Category for output, including supplying the
32
+ # text marked by the category
33
+ class CategoryOutputFilter < OutputFilter
34
+ EXPORT_CLASS = Category
35
+ def CategoryOutputFilter.def_name(cat)
36
+ 'category-' << cat.name.gsub(/\s+/, '_').gsub(/\W/,'')
37
+ end
38
+
39
+ def variables(obj)
40
+ { 'text' => @app.get_text_at_category(obj) }
41
+ end
42
+ end
43
+
44
+ class CodeReviewOutputFilter < OutputFilter
45
+ EXPORT_CLASS = CodeReview
46
+ def self.def_name(cr)
47
+ 'codereview-' << cr.dbid.to_s
48
+ end
49
+ end
50
+
51
+ class CodeReviewCSVOutput < CodeReviewOutputFilter
52
+ EXTENSION = 'csv'
53
+ DESCRIPTION = 'Code Review as spreadsheet'
54
+ def write(cr, file = STDOUT, mode = nil )
55
+ mode ||= cr.count_method
56
+ CSV::Writer.generate(file) do | csv |
57
+ cr.output_rows.each { | r | csv << r }
58
+ end
59
+ end
60
+ Filters.register_filter(self) if defined?(CSV)
61
+ end
62
+
63
+ tfiles = File.join(TemplatedOutput::TEMPLATES_DIR, '*.*')
64
+
65
+ # autoload template-based output filters from the shared templates dir
66
+ # this is located in /path/to/weft/share/templates.
67
+ # Each of these special template files should contain a number of comment
68
+ # header lines, followed by a text template.
69
+ # Specifications for the template are given as comment lines as follows:
70
+ # DESCRIPTION [required] : short description of template's use, for UI
71
+ # EXPORT_CLASS [required] : QDA class templated by this file
72
+ # EXTENSION [optional ] : what file extension exported; default,
73
+ # same extension as the template file
74
+ # CLASSNAME [ optional ] : make a reference to this class in the module
75
+ # QDA::Filters
76
+ Dir.glob( tfiles ).each do | tfile |
77
+ tpl_class = nil
78
+ File.foreach(tfile) do | line |
79
+ case line
80
+ when /^#\s*EXPORT_CLASS\s*:(.*)$/
81
+ base_class = Filters.const_get( $1.strip << 'OutputFilter' )
82
+ tpl_class = Class.new( base_class )
83
+ when /^#\s*(DESCRIPTION|EXTENSION)\s*:(.*)$/
84
+ tpl_class.const_set($1.strip, $2)
85
+ when /^#\s*CLASSNAME\s*:(.*)$/
86
+ Filters.const_set($1.strip, tpl_class)
87
+ when /^#/
88
+ # ignore
89
+ else
90
+ break
91
+ end
92
+ end
93
+ next unless tpl_class
94
+ tpl_class.const_set( 'TEMPLATE_FILE', File.basename(tfile) )
95
+ if not defined? tpl_class.EXTENSION
96
+ tpl_class.const_set( 'EXTENSION', File.extname(tfile) )
97
+ end
98
+ tpl_class.instance_eval { include TemplatedOutput }
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,80 @@
1
+ module QDA::Filters
2
+ module Templates
3
+ class CommentStrippingFileSource < PageTemplate::FileSource
4
+ # return the template content of the file named +name+. Uses the standard
5
+ # PageTemplate FileSource mechanism for searching directories etc, but
6
+ # removes any comment lines (all lines at the beginning of the document
7
+ # starting with a comment marker '#') before returning the content.
8
+ def get(name)
9
+ # all stuff starting from the beginning up to the first newline that is
10
+ # not followed by a comment character
11
+ super(name).sub(/\A.*?\n(?!#)/m, '')
12
+ end
13
+ end
14
+
15
+ class ExtendedPreprocessor < PageTemplate::DefaultPreprocessor
16
+ class << self
17
+ # shorter and more ruby-ish names for the PageTemplate methods
18
+ alias :html :escapeHTML
19
+ alias :uri :escapeURI
20
+
21
+ # Takes string +string+ and splits it into HTML paragraphs, taking a blank
22
+ # line and a newline as marking the start of a paragraph.
23
+ # Does HTML escaping of the paragraph contents
24
+ def html_paras(string)
25
+ string.split(/\n\s*\n/).inject('') do | out, para |
26
+ out << "<p>" << html(para) << "</p>\n"
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+
35
+ # Included in output classes to enable them to be output via template (using
36
+ # PageTemplate under the hood).
37
+ module TemplatedOutput
38
+ include Templates
39
+ TEMPLATES_DIR = File.join( WEFT_SHAREDIR, 'templates' )
40
+ # write templated output of the QDA object +obj+ (eg a Category, a
41
+ # Document or a CodeReview) to the file or io +file+.
42
+ def write(obj, file = STDOUT)
43
+ tpl = PageTemplate::Parser.new('source' => CommentStrippingFileSource,
44
+ 'include_paths' => TEMPLATES_DIR,
45
+ 'preprocessor' => ExtendedPreprocessor )
46
+ tpl.load(self.class::TEMPLATE_FILE)
47
+ variables(obj).each { | k, v | tpl[k] = v }
48
+ file.puts( tpl.output() )
49
+ end
50
+
51
+ # Returns a hash containing the named variables that should be templated.
52
+ # By default simply returns a hash containing the named variable 'obj'
53
+ # which contains the object being templated.
54
+ # If a variables() method is defined in the superclass (eg CategoryOutput)
55
+ # then the results from that are merged with the default. For example,
56
+ # CategoryOutput includes the named variable "text" which contains all
57
+ # the text marked by the category.
58
+ def variables(obj)
59
+ super.merge( 'obj' => obj )
60
+ rescue NoMethodError
61
+ { 'obj' => obj }
62
+ end
63
+
64
+
65
+ # Whenever this module is included in another class, it automatically
66
+ # checks whether a valid filter has been defined. If one has (by defining
67
+ # what QDA class it can output, what template file it should use, and
68
+ # what file extension it creates), then the filter is registered for use
69
+ # with the app (so it can be queried through +Filters.export_filters()+
70
+ def TemplatedOutput.included(other)
71
+ if other.is_a?(Class) and
72
+ defined?(other::EXPORT_CLASS) and
73
+ defined?(other::TEMPLATE_FILE) and
74
+ defined?(other::EXTENSION) and
75
+ defined?(PageTemplate)
76
+ QDA::Filters.register_filter(other)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,246 @@
1
+ require 'Win32API'
2
+
3
+ module QDA
4
+ module Filters
5
+ # Used only on windows to enable calling other executables without the
6
+ # annoying command-prompt box that pops up when using Ruby backticks in
7
+ # a script running under rubyw.
8
+ #
9
+ # Note - most of this code written by S Kroeger, see
10
+ # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/155684
11
+ module Win32Process
12
+ NORMAL_PRIORITY_CLASS = 0x00000020
13
+ STARTUP_INFO_SIZE = 68
14
+ PROCESS_INFO_SIZE = 16
15
+ SECURITY_ATTRIBUTES_SIZE = 12
16
+
17
+ ERROR_SUCCESS = 0x00
18
+ FORMAT_MESSAGE_FROM_SYSTEM = 0x1000
19
+ FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x2000
20
+
21
+ HANDLE_FLAG_INHERIT = 1
22
+ HANDLE_FLAG_PROTECT_FROM_CLOSE =2
23
+
24
+ STARTF_USESHOWWINDOW = 0x00000001
25
+ STARTF_USESTDHANDLES = 0x00000100
26
+
27
+ class << self
28
+ def raise_last_win_32_error
29
+ errorCode = Win32API.new("kernel32", "GetLastError", [], 'L').call
30
+ if errorCode != ERROR_SUCCESS
31
+ params = [
32
+ 'L', # IN DWORD dwFlags,
33
+ 'P', # IN LPCVOID lpSource,
34
+ 'L', # IN DWORD dwMessageId,
35
+ 'L', # IN DWORD dwLanguageId,
36
+ 'P', # OUT LPSTR lpBuffer,
37
+ 'L', # IN DWORD nSize,
38
+ 'P', # IN va_list *Arguments
39
+ ]
40
+
41
+ formatMessage = Win32API.new("kernel32", "FormatMessage", params, 'L')
42
+ msg = ' ' * 255
43
+ msgLength = formatMessage.call(FORMAT_MESSAGE_FROM_SYSTEM +
44
+ FORMAT_MESSAGE_ARGUMENT_ARRAY, '', errorCode, 0, msg, 255, '')
45
+
46
+ msg.gsub!(/\000/, '')
47
+ msg.strip!
48
+ raise msg
49
+ else
50
+ raise 'GetLastError returned ERROR_SUCCESS'
51
+ end
52
+ end
53
+
54
+ def create_pipe # returns read and write handle
55
+ params = [
56
+ 'P', # pointer to read handle
57
+ 'P', # pointer to write handle
58
+ 'P', # pointer to security attributes
59
+ 'L'] # pipe size
60
+
61
+ createPipe = Win32API.new("kernel32", "CreatePipe", params, 'I')
62
+
63
+ read_handle, write_handle = [0].pack('I'), [0].pack('I')
64
+ sec_attrs = [SECURITY_ATTRIBUTES_SIZE, 0, 1].pack('III')
65
+
66
+ raise_last_win_32_error if createPipe.Call(read_handle,
67
+ write_handle, sec_attrs, 0).zero?
68
+
69
+ [read_handle.unpack('I')[0], write_handle.unpack('I')[0]]
70
+ end
71
+
72
+ def set_handle_information(handle, flags, value)
73
+ params = [
74
+ 'L', # handle to an object
75
+ 'L', # specifies flags to change
76
+ 'L'] # specifies new values for flags
77
+
78
+ setHandleInformation = Win32API.new("kernel32",
79
+ "SetHandleInformation", params, 'I')
80
+ raise_last_win_32_error if setHandleInformation.Call(handle,
81
+ flags, value).zero?
82
+ nil
83
+ end
84
+
85
+ def close_handle(handle)
86
+ closeHandle = Win32API.new("kernel32", "CloseHandle", ['L'], 'I')
87
+ raise_last_win_32_error if closeHandle.call(handle).zero?
88
+ end
89
+
90
+ def create_process(command, stdin, stdout, stderror)
91
+ params = [
92
+ 'L', # IN LPCSTR lpApplicationName
93
+ 'P', # IN LPSTR lpCommandLine
94
+ 'L', # IN LPSECURITY_ATTRIBUTES lpProcessAttributes
95
+ 'L', # IN LPSECURITY_ATTRIBUTES lpThreadAttributes
96
+ 'L', # IN BOOL bInheritHandles
97
+ 'L', # IN DWORD dwCreationFlags
98
+ 'L', # IN LPVOID lpEnvironment
99
+ 'L', # IN LPCSTR lpCurrentDirectory
100
+ 'P', # IN LPSTARTUPINFOA lpStartupInfo
101
+ 'P'] # OUT LPPROCESS_INFORMATION lpProcessInformation
102
+
103
+ startupInfo = [STARTUP_INFO_SIZE, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
104
+ STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW, 0,
105
+ 0, 0, stdin, stdout, stderror].pack('IIIIIIIIIIIISSIIII')
106
+
107
+ processInfo = [0, 0, 0, 0].pack('IIII')
108
+ command << 0
109
+
110
+ createProcess = Win32API.new("kernel32", "CreateProcess", params, 'I')
111
+ cp_args = [ 0, command, 0, 0, 1, 0, 0, 0, startupInfo, processInfo ]
112
+ raise_last_win_32_error if createProcess.call(*cp_args).zero?
113
+
114
+ hProcess, hThread,
115
+ dwProcessId, dwThreadId = processInfo.unpack('LLLL')
116
+
117
+ close_handle(hProcess)
118
+ close_handle(hThread)
119
+
120
+ [dwProcessId, dwThreadId]
121
+ end
122
+
123
+ def write_file(hFile, buffer)
124
+ params = [
125
+ 'L', # handle to file to write to
126
+ 'P', # pointer to data to write to file
127
+ 'L', # number of bytes to write
128
+ 'P', # pointer to number of bytes written
129
+ 'L'] # pointer to structure for overlapped I/O
130
+
131
+ written = [0].pack('I')
132
+ writeFile = Win32API.new("kernel32", "WriteFile", params, 'I')
133
+
134
+ raise_last_win_32_error if writeFile.call(hFile, buffer, buffer.size,
135
+ written, 0).zero?
136
+
137
+ written.unpack('I')[0]
138
+ end
139
+
140
+ def read_file(hFile)
141
+ params = [
142
+ 'L', # handle of file to read
143
+ 'P', # pointer to buffer that receives data
144
+ 'L', # number of bytes to read
145
+ 'P', # pointer to number of bytes read
146
+ 'L'] #pointer to structure for data
147
+
148
+ number = [0].pack('I')
149
+ buffer = ' ' * 255
150
+
151
+ readFile = Win32API.new("kernel32", "ReadFile", params, 'I')
152
+ return '' if readFile.call(hFile, buffer, 255, number, 0).zero?
153
+
154
+ buffer[0...number.unpack('I')[0]]
155
+ end
156
+
157
+ def peek_named_pipe(hFile)
158
+ params = [
159
+ 'L', # handle to pipe to copy from
160
+ 'L', # pointer to data buffer
161
+ 'L', # size, in bytes, of data buffer
162
+ 'L', # pointer to number of bytes read
163
+ 'P', # pointer to total number of bytes available
164
+ 'L'] # pointer to unread bytes in this message
165
+
166
+ available = [0].pack('I')
167
+ peekNamedPipe = Win32API.new("kernel32", "PeekNamedPipe", params, 'I')
168
+
169
+ return -1 if peekNamedPipe.Call(hFile, 0, 0, 0, available, 0).zero?
170
+
171
+ available.unpack('I')[0]
172
+ end
173
+ end
174
+
175
+ class Win32popenIO
176
+ def initialize (hRead, hWrite, hError)
177
+ @hRead = hRead
178
+ @hWrite = hWrite
179
+ @hError = hError
180
+ end
181
+
182
+ def write data
183
+ Win32Process::write_file(@hWrite, data.to_s)
184
+ end
185
+
186
+ def read
187
+ sleep(0.01) while Win32Process::peek_named_pipe(@hRead).zero?
188
+ Win32Process::read_file(@hRead)
189
+ end
190
+
191
+ def read_all
192
+ all = ''
193
+ until (buffer = read).empty?
194
+ all << buffer
195
+ end
196
+ all
197
+ end
198
+
199
+ def read_err
200
+ sleep(0.01) while Win32Process::peek_named_pipe(@hError).zero?
201
+ Win32Process::read_file(@hError)
202
+ end
203
+
204
+ def read_all_err
205
+ all = ''
206
+ until (buffer = read_err).empty?
207
+ all << buffer
208
+ end
209
+ all
210
+ end
211
+ end
212
+
213
+ # The only useful public method in this class - receives a command line,
214
+ # and returns the output content and error content as a pair of strings.
215
+ # No shell expansion is carried out on the command line string.
216
+ #
217
+ # output, errors = Win32Process::backtick('ls')
218
+ def self.backtick(command)
219
+ # create 3 pipes
220
+ child_in_r, child_in_w = create_pipe
221
+ child_out_r, child_out_w = create_pipe
222
+ child_error_r, child_error_w = create_pipe
223
+
224
+ # Ensure the write handle to the pipe for STDIN is not inherited.
225
+ set_handle_information(child_in_w, HANDLE_FLAG_INHERIT, 0)
226
+ set_handle_information(child_out_r, HANDLE_FLAG_INHERIT, 0)
227
+ set_handle_information(child_error_r, HANDLE_FLAG_INHERIT, 0)
228
+
229
+ processId, threadId = create_process( command,
230
+ child_in_r,
231
+ child_out_w,
232
+ child_error_w )
233
+ # we have to close the handles, so the pipes terminate with the process
234
+ close_handle(child_in_r)
235
+ close_handle(child_out_w)
236
+ close_handle(child_error_w)
237
+ close_handle(child_in_w)
238
+ io = Win32popenIO.new(child_out_r, child_in_w, child_error_r)
239
+
240
+ out = io.read_all().gsub(/\r/, '')
241
+ err = io.read_all_err().gsub(/\r/, '')
242
+ return out, err
243
+ end
244
+ end
245
+ end
246
+ end