weft-qda 0.9.6 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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