File: runner.rb

package info (click to toggle)
ruby-launchy 2.5.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 292 kB
  • sloc: ruby: 1,285; makefile: 6
file content (152 lines) | stat: -rw-r--r-- 4,505 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
require 'shellwords'
require 'stringio'

module Launchy::Detect
  class Runner
    class NotFoundError < Launchy::Error; end

    extend ::Launchy::DescendantTracker

    # Detect the current command runner
    #
    # This will return an instance of the Runner to be used to do the
    # application launching.
    #
    # If a runner cannot be detected then raise Runner::NotFoundError
    #
    # The runner rules are, in order:
    #
    # 1) If you are on windows, you use the Windows Runner no matter what
    # 2) If you are using the jruby engine, use the Jruby Runner. Unless rule
    #    (1) took effect
    # 3) Use Forkable (barring rules (1) and (2))
    def self.detect
      host_os_family = Launchy::Detect::HostOsFamily.detect
      ruby_engine    = Launchy::Detect::RubyEngine.detect

      return Windows.new if host_os_family.windows?
      if ruby_engine.jruby? then
        return Jruby.new
      end
      return Forkable.new
    end

    #
    # cut it down to just the shell commands that will be passed to exec or
    # posix_spawn. The cmd argument is split according to shell rules and the
    # args are not escaped because they whole set is passed to system as *args
    # and in that case system shell escaping rules are not done.
    #
    def shell_commands( cmd, args )
      cmdline = [ cmd.to_s.shellsplit ]
      cmdline << args.flatten.collect{ |a| a.to_s }
      return commandline_normalize( cmdline )
    end

    def commandline_normalize( cmdline )
      c = cmdline.flatten!
      c = c.find_all { |a| (not a.nil?) and ( a.size > 0 ) }
      Launchy.log "commandline_normalized => #{c.join(' ')}"
      return c
    end

    def dry_run( cmd, *args )
      shell_commands(cmd, args).join(" ")
    end

    def run( cmd, *args )
      raise Launchy::CommandNotFoundError, "No command found to run with args '#{args.join(' ')}'. If this is unexpected, #{Launchy.bug_report_message}" unless cmd
      if Launchy.dry_run? then
        $stdout.puts dry_run( cmd, *args )
      else
        wet_run( cmd, *args )
      end
    end


    #---------------------------------------
    # The list of known runners
    #---------------------------------------

    class Windows < Runner

      def all_args( cmd, *args )
        args = [ 'cmd', '/c', *shell_commands( cmd, *args ) ]
        Launchy.log "Windows: all_args => #{args.inspect}"
        return args
      end

      def dry_run( cmd, *args )
        all_args( cmd, *args ).join(" ")
      end

      # escape the reserved shell characters in windows command shell
      # http://technet.microsoft.com/en-us/library/cc723564.aspx
      #
      # Also make sure that the item after 'start' is guaranteed to be quoted.
      # https://github.com/copiousfreetime/launchy/issues/62
      def shell_commands( cmd, *args )
        parts = cmd.shellsplit

        if start_idx = parts.index('start') then
          title_idx = start_idx + 1
          title     = parts[title_idx]
          title     = title.sub(/^/,'"') unless title[0] == '"'
          title     = title.sub(/$/,'"') unless title[-1] == '"'
          parts[title_idx] = title
        end

        cmdline = [ parts ]
        cmdline << args.flatten.collect { |a| a.to_s.gsub(/([&|()<>^])/, "^\\1") }
        return commandline_normalize( cmdline )
      end

      def wet_run( cmd, *args )
        system( *all_args( cmd, *args ) )
      end
    end

    class Jruby < Runner
      def wet_run( cmd, *args )
        child_pid = spawn( *shell_commands( cmd, *args ) )
        Process.detach( child_pid )
      end
    end

    class Forkable < Runner
      attr_reader   :child_pid

      def wet_run( cmd, *args )
        @child_pid = fork do
          close_file_descriptors unless Launchy.debug?
          Launchy.log("wet_run: before exec in child process")
          exec_or_raise( cmd, *args )
          exit!
        end
        Process.detach( @child_pid )
      end

      private

      # attaching to a StringIO instead of reopening so we don't loose the
      # STDERR, needed for exec_or_raise.
      def close_file_descriptors
        $stdin.reopen( "/dev/null")

        @saved_stdout = $stdout
        @saved_stderr = $stderr

        $stdout       = StringIO.new
        $stderr       = StringIO.new
      end

      def exec_or_raise( cmd, *args )
        exec( *shell_commands( cmd, *args ))
      rescue Exception => e
        $stderr = @saved_stderr
        $stdout = @saved_stdout
        raise e
      end
    end
  end
end