appear 1.1.1 → 1.2.0
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.
- checksums.yaml +4 -4
- data/.doc-coverage +1 -1
- data/CHANGELOG.md +11 -0
- data/README.md +1 -1
- data/bin/appear +6 -0
- data/lib/appear/command.rb +28 -0
- data/lib/appear/config.rb +7 -0
- data/lib/appear/constants.rb +1 -4
- data/lib/appear/editor/nvim.rb +251 -0
- data/lib/appear/editor.rb +262 -0
- data/lib/appear/instance.rb +4 -0
- data/lib/appear/lsof.rb +84 -51
- data/lib/appear/mac_os.rb +12 -2
- data/lib/appear/output.rb +16 -0
- data/lib/appear/processes.rb +4 -6
- data/lib/appear/revealers.rb +65 -62
- data/lib/appear/runner.rb +25 -5
- data/lib/appear/terminal.rb +127 -0
- data/lib/appear/tmux.rb +285 -39
- data/lib/appear/util/command_builder.rb +148 -0
- data/lib/appear/util/join.rb +144 -0
- data/lib/appear/util/memoizer.rb +83 -0
- data/lib/appear/util/value_class.rb +57 -0
- data/lib/appear/util.rb +6 -0
- data/lib/appear.rb +55 -1
- data/scripts/console +9 -1
- data/tools/macOS-helper.js +24 -16
- data/tools/unix-dropper.applescript +167 -0
- metadata +11 -3
- data/lib/appear/join.rb +0 -134
data/lib/appear/lsof.rb
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
require 'appear/service'
|
|
2
|
+
require 'appear/util/memoizer'
|
|
3
|
+
require 'appear/util/value_class'
|
|
2
4
|
|
|
3
5
|
module Appear
|
|
6
|
+
# raised if we can't parse a connection from an output line of the `lsof`
|
|
7
|
+
# command.
|
|
8
|
+
class LsofParseError < Appear::Error; end
|
|
9
|
+
|
|
4
10
|
# The LSOF service co-ordinates access to the `lsof` system utility. LSOF
|
|
5
11
|
# stands for "list open files". It can read the "connections" various
|
|
6
12
|
# programs have to a given file, eg, what programs have a file descriptor for
|
|
@@ -12,18 +18,46 @@ module Appear
|
|
|
12
18
|
|
|
13
19
|
# A connection of a process to a file.
|
|
14
20
|
# Created from one output row of `lsof`.
|
|
15
|
-
class Connection
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
class Connection < ::Appear::Util::ValueClass
|
|
22
|
+
# @return [String]
|
|
23
|
+
property :command_name
|
|
24
|
+
|
|
25
|
+
# @return [Fixnum]
|
|
26
|
+
property :pid
|
|
27
|
+
|
|
28
|
+
# @return [String]
|
|
29
|
+
property :fd
|
|
30
|
+
|
|
31
|
+
# @return [String]
|
|
32
|
+
property :fd
|
|
33
|
+
|
|
34
|
+
# @return [String]
|
|
35
|
+
property :type
|
|
36
|
+
|
|
37
|
+
# @return [String]
|
|
38
|
+
property :device
|
|
39
|
+
|
|
40
|
+
# @return [String]
|
|
41
|
+
property :size
|
|
42
|
+
|
|
43
|
+
# @return [String]
|
|
44
|
+
property :node
|
|
45
|
+
|
|
46
|
+
# @return [String]
|
|
47
|
+
property :file_name
|
|
22
48
|
end
|
|
23
49
|
|
|
24
50
|
# Represents a pane's connection to a TTY.
|
|
25
51
|
class PaneConnection
|
|
26
|
-
|
|
52
|
+
# @return [#tty] a Terminal emulator pane.
|
|
53
|
+
attr_reader :pane
|
|
54
|
+
|
|
55
|
+
# @return [Connection] an LSOF connection
|
|
56
|
+
attr_reader :connection
|
|
57
|
+
|
|
58
|
+
# @return [Appear::Processes::ProcessInfo] the process
|
|
59
|
+
attr_reader :process
|
|
60
|
+
|
|
27
61
|
# @param pane [#tty] a pane in a terminal emulator
|
|
28
62
|
# @param connection [Appear::Lsof::Connection] a connection of a process
|
|
29
63
|
# to a file -- usually a TTY device.
|
|
@@ -34,10 +68,12 @@ module Appear
|
|
|
34
68
|
@process = process
|
|
35
69
|
end
|
|
36
70
|
|
|
71
|
+
# @return [String] the TTY this connection is to
|
|
37
72
|
def tty
|
|
38
73
|
connection.file_name
|
|
39
74
|
end
|
|
40
75
|
|
|
76
|
+
# @return [Fixnum] pid of the process making the connection
|
|
41
77
|
def pid
|
|
42
78
|
connection.pid
|
|
43
79
|
end
|
|
@@ -45,7 +81,7 @@ module Appear
|
|
|
45
81
|
|
|
46
82
|
def initialize(*args)
|
|
47
83
|
super(*args)
|
|
48
|
-
@
|
|
84
|
+
@lsof_memo = Util::Memoizer.new
|
|
49
85
|
end
|
|
50
86
|
|
|
51
87
|
# find any intersections where a process in the given tree is present in
|
|
@@ -87,63 +123,60 @@ module Appear
|
|
|
87
123
|
hits.values
|
|
88
124
|
end
|
|
89
125
|
|
|
90
|
-
# list connections to files
|
|
126
|
+
# list connections to files.
|
|
91
127
|
#
|
|
92
128
|
# @param files [Array<String>] files to query
|
|
93
129
|
# @return [Hash<String, Array<Connection>>] map of filename to connections
|
|
94
130
|
def lsofs(files, opts = {})
|
|
95
|
-
|
|
96
|
-
uncached = files.reject { |f| @cache[f] }
|
|
97
|
-
|
|
98
|
-
result = parallel_lsof(uncached, opts)
|
|
99
|
-
result.each do |file, data|
|
|
100
|
-
@cache[file] = data
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
cached.each do |f|
|
|
104
|
-
result[f] = @cache[f]
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
result
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
private
|
|
111
|
-
|
|
112
|
-
# lsof takes a really long time, so parallelize lookups when you can.
|
|
113
|
-
def parallel_lsof(files, opts = {})
|
|
131
|
+
mutex = Mutex.new
|
|
114
132
|
results = {}
|
|
115
133
|
threads = files.map do |file|
|
|
116
134
|
Thread.new do
|
|
117
|
-
|
|
135
|
+
single_result = lsof(file, opts)
|
|
136
|
+
mutex.synchronize do
|
|
137
|
+
results[file] = single_result
|
|
138
|
+
end
|
|
118
139
|
end
|
|
119
140
|
end
|
|
120
141
|
threads.each { |t| t.join }
|
|
121
142
|
results
|
|
122
143
|
end
|
|
123
144
|
|
|
145
|
+
private
|
|
146
|
+
|
|
124
147
|
def lsof(file, opts = {})
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
@lsof_memo.call(file, opts) do
|
|
149
|
+
pids = opts[:pids]
|
|
150
|
+
if pids
|
|
151
|
+
output = run(['lsof', '-ap', pids.join(','), file], :allow_failure => true)
|
|
152
|
+
else
|
|
153
|
+
output = run(['lsof', file], :allow_failure => true)
|
|
154
|
+
end
|
|
155
|
+
next [] if output.empty?
|
|
156
|
+
rows = output.lines.map do |line|
|
|
157
|
+
line = line.strip
|
|
158
|
+
fields = line.split(/\s+/)
|
|
159
|
+
if fields.length < 9
|
|
160
|
+
raise LsofParseError.new("Not enough fields in line #{line.inspect}")
|
|
161
|
+
end
|
|
162
|
+
command, pid, user, fd, type, device, size, node, name = fields
|
|
163
|
+
Connection.new({
|
|
164
|
+
command_name: command,
|
|
165
|
+
pid: pid.to_i,
|
|
166
|
+
user: user,
|
|
167
|
+
fd: fd,
|
|
168
|
+
type: type,
|
|
169
|
+
device: device,
|
|
170
|
+
size: size,
|
|
171
|
+
node: node.to_i,
|
|
172
|
+
file_name: name
|
|
173
|
+
})
|
|
174
|
+
end
|
|
175
|
+
# drop header row
|
|
176
|
+
rows[1..-1]
|
|
144
177
|
end
|
|
145
|
-
|
|
146
|
-
|
|
178
|
+
rescue LsofParseError => err
|
|
179
|
+
log("lsof: parse error: #{err}")
|
|
147
180
|
[]
|
|
148
181
|
end
|
|
149
182
|
end
|
data/lib/appear/mac_os.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'appear/constants'
|
|
|
4
4
|
require 'appear/service'
|
|
5
5
|
|
|
6
6
|
module Appear
|
|
7
|
+
# Raised if our helper program returns an error.
|
|
7
8
|
class MacToolError < Error
|
|
8
9
|
def initialize(message, stack)
|
|
9
10
|
super("Mac error #{message.inspect}\n#{stack}")
|
|
@@ -19,6 +20,10 @@ module Appear
|
|
|
19
20
|
SCRIPT = Appear::TOOLS_DIR.join('macOS-helper.js').realpath.to_s
|
|
20
21
|
|
|
21
22
|
# call a method in our helper script. Communicates with JSON!
|
|
23
|
+
# @param method_name [String, Symbol] check the source of macOS-helper.js for method names.
|
|
24
|
+
# @param data [Any, nil] json-able data to pass to the named method.
|
|
25
|
+
# @return [Any] json data returned from the helper
|
|
26
|
+
# @raise [MacToolError] if an error occured
|
|
22
27
|
def call_method(method_name, data = nil)
|
|
23
28
|
command = [SCRIPT, method_name.to_s]
|
|
24
29
|
command << data.to_json unless data.nil?
|
|
@@ -32,8 +37,13 @@ module Appear
|
|
|
32
37
|
end
|
|
33
38
|
end
|
|
34
39
|
|
|
35
|
-
#
|
|
36
|
-
#
|
|
40
|
+
# Return true if the given process is a macOS GUI process, false otherwise.
|
|
41
|
+
#
|
|
42
|
+
# @todo: ask Applescript if this a GUI application instead of just looking
|
|
43
|
+
# at the path
|
|
44
|
+
#
|
|
45
|
+
# @param process [Appear::Processes::ProcessInfo]
|
|
46
|
+
# @return [Boolean]
|
|
37
47
|
def has_gui?(process)
|
|
38
48
|
executable = process.command.first
|
|
39
49
|
executable =~ /\.app\/Contents\//
|
data/lib/appear/output.rb
CHANGED
|
@@ -5,16 +5,29 @@ module Appear
|
|
|
5
5
|
# The Output service encapsulates writing logging information to log files
|
|
6
6
|
# and STDERR, and writing output to STDOUT.
|
|
7
7
|
class Output
|
|
8
|
+
# Create a new Output service.
|
|
9
|
+
#
|
|
10
|
+
# @param log_file_name [String, nil] if a string, log to the file at this path
|
|
11
|
+
# @param silent [Boolean] if true, output to STDERR
|
|
8
12
|
def initialize(log_file_name, silent)
|
|
13
|
+
@file_logger = nil
|
|
14
|
+
@stderr_logger = nil
|
|
15
|
+
|
|
9
16
|
@file_logger = Logger.new(log_file_name.to_s) if log_file_name
|
|
10
17
|
@stderr_logger = Logger.new(STDERR) unless silent
|
|
11
18
|
end
|
|
12
19
|
|
|
20
|
+
# Log a message.
|
|
21
|
+
#
|
|
22
|
+
# @param any [Array<Any>]
|
|
13
23
|
def log(*any)
|
|
14
24
|
@stderr_logger.debug(*any) if @stderr_logger
|
|
15
25
|
@file_logger.debug(*any) if @file_logger
|
|
16
26
|
end
|
|
17
27
|
|
|
28
|
+
# Log an error
|
|
29
|
+
#
|
|
30
|
+
# @param err [Error]
|
|
18
31
|
def log_error(err)
|
|
19
32
|
log("Error #{err.inspect}: #{err.to_s.inspect}")
|
|
20
33
|
if err.backtrace
|
|
@@ -22,6 +35,9 @@ module Appear
|
|
|
22
35
|
end
|
|
23
36
|
end
|
|
24
37
|
|
|
38
|
+
# Output a message to STDOUT, and also to the log file.
|
|
39
|
+
#
|
|
40
|
+
# @param any [Array<Any>]
|
|
25
41
|
def output(*any)
|
|
26
42
|
STDOUT.puts(*any)
|
|
27
43
|
@file_logger.debug(*any) if @file_logger
|
data/lib/appear/processes.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'appear/constants'
|
|
2
2
|
require 'appear/service'
|
|
3
|
+
require 'appear/util/memoizer'
|
|
3
4
|
|
|
4
5
|
module Appear
|
|
5
6
|
# Raised if Processes tries to get info for a dead process, or a PID that is
|
|
@@ -24,7 +25,7 @@ module Appear
|
|
|
24
25
|
|
|
25
26
|
def initialize(*args)
|
|
26
27
|
super(*args)
|
|
27
|
-
@
|
|
28
|
+
@get_info_memo = Util::Memoizer.new
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
# Get info about a process by PID, including its command and parent_pid.
|
|
@@ -32,12 +33,9 @@ module Appear
|
|
|
32
33
|
# @param pid [Integer]
|
|
33
34
|
# @return [ProcessInfo]
|
|
34
35
|
def get_info(pid)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
result = fetch_info(pid)
|
|
38
|
-
@cache[pid] = result
|
|
36
|
+
@get_info_memo.call(pid) do
|
|
37
|
+
fetch_info(pid)
|
|
39
38
|
end
|
|
40
|
-
result
|
|
41
39
|
end
|
|
42
40
|
|
|
43
41
|
# Is the given process alive?
|
data/lib/appear/revealers.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require 'appear/service'
|
|
2
|
-
require 'appear/join'
|
|
2
|
+
require 'appear/util/join'
|
|
3
|
+
require 'appear/terminal'
|
|
4
|
+
require 'ostruct'
|
|
3
5
|
|
|
4
6
|
module Appear
|
|
5
7
|
# stores all the ways we can appear something
|
|
@@ -11,6 +13,11 @@ module Appear
|
|
|
11
13
|
module Revealers
|
|
12
14
|
# extend to implement more revealers
|
|
13
15
|
class BaseRevealer < Service
|
|
16
|
+
# Reveal `tree` if supported by this revealer. You can get a tree from
|
|
17
|
+
# {Processes#process_tree}.
|
|
18
|
+
#
|
|
19
|
+
# @param tree [Array<Processes::ProcessInfo>]
|
|
20
|
+
# @return [true, nil] return true if we revealed something, otherwise nil
|
|
14
21
|
def call(tree)
|
|
15
22
|
target, *rest = tree
|
|
16
23
|
if supports_tree?(target, rest)
|
|
@@ -18,98 +25,82 @@ module Appear
|
|
|
18
25
|
end
|
|
19
26
|
end
|
|
20
27
|
|
|
21
|
-
#
|
|
28
|
+
# Reveal `tree`. Should be implemented by subclasses.
|
|
29
|
+
#
|
|
30
|
+
# @abstract subclasses must implement this method.
|
|
31
|
+
# @param tree [Array<Processes::ProcessInfo>]
|
|
32
|
+
# @return [true, nil] return true if we revealed something, otherwise nil
|
|
22
33
|
def reveal_tree(tree)
|
|
23
34
|
raise "not implemented"
|
|
24
35
|
end
|
|
25
36
|
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
37
|
+
# Returns true if this revealer may be able to reveal something in the
|
|
38
|
+
# tree. For this method, the caller splits the tree into the target and
|
|
39
|
+
# the rest of the tree, which sometimes simplifies the implementation of
|
|
40
|
+
# this method.
|
|
41
|
+
#
|
|
42
|
+
# @abstract subclasses must implement this method.
|
|
43
|
+
# @param target [Processes::ProcessInfo] bottom (child-most) item in the process
|
|
44
|
+
# tree
|
|
45
|
+
# @param rest [Array<Processes::ProcessInfo>] the rest of the tree
|
|
46
|
+
# @return [Boolean]
|
|
29
47
|
def supports_tree?(target, rest)
|
|
30
48
|
raise "not implemented"
|
|
31
49
|
end
|
|
32
50
|
|
|
51
|
+
# Register this class as a revealer so it will be called by
|
|
52
|
+
# {Instance#call}
|
|
33
53
|
def self.register!
|
|
34
54
|
Appear::REVEALERS.push(self)
|
|
35
55
|
end
|
|
36
56
|
end
|
|
37
57
|
|
|
58
|
+
# Base class for Mac-terminal revealers.
|
|
38
59
|
class MacRevealer < BaseRevealer
|
|
39
|
-
delegate :join_via_tty, :lsof
|
|
40
60
|
require_service :mac_os
|
|
61
|
+
require_service :mac_term
|
|
62
|
+
require_service :lsof
|
|
41
63
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def reveal_hit(hit)
|
|
47
|
-
raise "not implemented"
|
|
48
|
-
end
|
|
49
|
-
|
|
64
|
+
# Implementation.
|
|
65
|
+
# @see BaseRevealer#reveal_tree
|
|
50
66
|
def reveal_tree(tree)
|
|
51
|
-
hits = join_via_tty(tree, panes)
|
|
67
|
+
hits = services.lsof.join_via_tty(tree, services.mac_term.panes)
|
|
52
68
|
actual_hits = hits.uniq {|hit| hit.tty }.
|
|
53
69
|
reject {|hit| services.mac_os.has_gui?(hit.process) }.
|
|
54
|
-
each { |hit|
|
|
70
|
+
each { |hit| services.mac_term.reveal_pane(hit) }
|
|
55
71
|
|
|
56
72
|
return actual_hits.length > 0
|
|
57
73
|
end
|
|
58
74
|
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
process.name ==
|
|
75
|
+
# Implementation
|
|
76
|
+
# @see BaseRevealer#supports_tree?
|
|
77
|
+
def supports_tree?(target, rest)
|
|
78
|
+
rest.any? do |process|
|
|
79
|
+
process.name == services.mac_term.app_name && services.mac_os.has_gui?(process)
|
|
64
80
|
end
|
|
65
81
|
end
|
|
66
82
|
end
|
|
67
83
|
|
|
84
|
+
# Iterm2 revealer support
|
|
68
85
|
class Iterm2 < MacRevealer
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def panes
|
|
76
|
-
pids = services.processes.pgrep('iTerm2')
|
|
77
|
-
services.mac_os.call_method('iterm2_panes').map do |hash|
|
|
78
|
-
hash[:pids] = pids
|
|
79
|
-
OpenStruct.new(hash)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def reveal_hit(hit)
|
|
84
|
-
services.mac_os.call_method('iterm2_reveal_tty', hit.tty)
|
|
86
|
+
def initialize(services)
|
|
87
|
+
super(services.merge(
|
|
88
|
+
:mac_term => Appear::Terminal::Iterm2.new(services)
|
|
89
|
+
))
|
|
85
90
|
end
|
|
86
91
|
end
|
|
87
92
|
|
|
93
|
+
# TerminalApp revealer support
|
|
88
94
|
class TerminalApp < MacRevealer
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def panes
|
|
96
|
-
pids = services.processes.pgrep('Terminal.app')
|
|
97
|
-
services.mac_os.call_method('terminal_panes').map do |hash|
|
|
98
|
-
hash[:pids] = pids
|
|
99
|
-
OpenStruct.new(hash)
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def reveal_hit(hit)
|
|
104
|
-
# iterm2 runs a non-gui server process. Because of implementation
|
|
105
|
-
# details of MacOs#has_gui?, we don't *techinically* have to worry
|
|
106
|
-
# about this, but we should in case I ever implement real mac
|
|
107
|
-
# gui-or-not lookup.
|
|
108
|
-
return if hit.process.name == 'iTerm2'
|
|
109
|
-
services.mac_os.call_method('terminal_reveal_tty', hit.tty)
|
|
95
|
+
def initialize(services)
|
|
96
|
+
super(services.merge(
|
|
97
|
+
:mac_term => Appear::Terminal::TerminalApp.new(services)
|
|
98
|
+
))
|
|
110
99
|
end
|
|
111
100
|
end
|
|
112
101
|
|
|
102
|
+
# support for the cross-platform Tmux multiplexer. Also reveals a connected
|
|
103
|
+
# tmux client, if possible.
|
|
113
104
|
class Tmux < BaseRevealer
|
|
114
105
|
# TODO: cache services.tmux.panes, services.tmux.clients for this revealer?
|
|
115
106
|
require_service :tmux
|
|
@@ -117,12 +108,16 @@ module Appear
|
|
|
117
108
|
require_service :revealer
|
|
118
109
|
require_service :processes
|
|
119
110
|
|
|
111
|
+
# Implementation
|
|
112
|
+
# @see BaseRevealer#supports_tree?
|
|
120
113
|
def supports_tree?(target, rest)
|
|
121
114
|
rest.any? { |p| p.name == 'tmux' }
|
|
122
115
|
end
|
|
123
116
|
|
|
117
|
+
# Implementation.
|
|
118
|
+
# @see BaseRevealer#reveal_tree
|
|
124
119
|
def reveal_tree(tree)
|
|
125
|
-
relevent_panes = Join.join(:pid, tree, services.tmux.panes)
|
|
120
|
+
relevent_panes = Util::Join.join(:pid, tree, services.tmux.panes)
|
|
126
121
|
relevent_panes.each do |pane|
|
|
127
122
|
log("#{self.class.name}: revealing pane #{pane}")
|
|
128
123
|
services.tmux.reveal_pane(pane)
|
|
@@ -141,17 +136,21 @@ module Appear
|
|
|
141
136
|
# to find the PID of a tmux client is to lsof() the TTY that the client
|
|
142
137
|
# is connected to, and then deduce the client PID, which will be a tmux
|
|
143
138
|
# process PID that is not the server PID.
|
|
139
|
+
#
|
|
140
|
+
# @param tree [Array<Processes::ProcessInfo>]
|
|
141
|
+
# @return [Number, nil] pid of a tmux client, if one was found. Otherwise
|
|
142
|
+
# nil.
|
|
144
143
|
def tmux_client_for_tree(tree)
|
|
145
144
|
tmux_server = tree.find {|p| p.name == 'tmux'}
|
|
146
145
|
|
|
147
146
|
# join processes on tmux panes by PID.
|
|
148
|
-
proc_and_panes = Join.join(:pid, services.tmux.panes, tree)
|
|
147
|
+
proc_and_panes = Util::Join.join(:pid, services.tmux.panes, tree)
|
|
149
148
|
|
|
150
149
|
# Join the list of tmux clients with process_and_pid on :session.
|
|
151
150
|
# In tmux, every pane is addressed by session_name:window_index:pane_index.
|
|
152
151
|
# This gives us back a list of all the clients that have a pane that
|
|
153
152
|
# contains a process in our given process tree.
|
|
154
|
-
proc_and_clients = Join.join(:session, services.tmux.clients, proc_and_panes)
|
|
153
|
+
proc_and_clients = Util::Join.join(:session, services.tmux.clients, proc_and_panes)
|
|
155
154
|
|
|
156
155
|
# there *should* be only one of these, unless there are two clients
|
|
157
156
|
# connected to the same tmux session. In that case we just choose one
|
|
@@ -167,11 +166,15 @@ module Appear
|
|
|
167
166
|
[tty_of_client],
|
|
168
167
|
:pids => services.processes.pgrep('tmux')
|
|
169
168
|
)[tty_of_client]
|
|
169
|
+
|
|
170
170
|
client_connection = connections_to_tty.find do |conn|
|
|
171
171
|
(conn.command_name =~ /^tmux/) && (conn.pid != tmux_server.pid)
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
if client_connection
|
|
175
|
+
log("tmux_client_for_tree: found client pid=#{client_connection.pid} for tree pid=#{tree.first.pid}")
|
|
176
|
+
return client_connection.pid
|
|
177
|
+
end
|
|
175
178
|
end
|
|
176
179
|
end
|
|
177
180
|
|
data/lib/appear/runner.rb
CHANGED
|
@@ -21,8 +21,17 @@ module Appear
|
|
|
21
21
|
# either be a string, or an array of command name and parameters.
|
|
22
22
|
# Returns the combinded STDERR and STDOUT of the command.
|
|
23
23
|
#
|
|
24
|
-
# @
|
|
25
|
-
|
|
24
|
+
# @param command [String, Array] command to run, as an argv array, or as a
|
|
25
|
+
# sh command string.
|
|
26
|
+
# @param opts [Hash] options
|
|
27
|
+
# @option opts [Boolean] :allow_failure (false) permit running the command
|
|
28
|
+
# to fail. Do not raise an ExecutionFailure error.
|
|
29
|
+
#
|
|
30
|
+
# @raise [ExecutionFailure] if the command exists non-zero
|
|
31
|
+
#
|
|
32
|
+
# @return [String]
|
|
33
|
+
def run(command, opts = {})
|
|
34
|
+
allow_failure = opts[:allow_failure] || false
|
|
26
35
|
start = Time.new
|
|
27
36
|
if command.is_a? Array
|
|
28
37
|
output, status = Open3.capture2e(*command)
|
|
@@ -31,14 +40,20 @@ module Appear
|
|
|
31
40
|
end
|
|
32
41
|
finish = Time.new
|
|
33
42
|
log("Runner: ran #{command.inspect} in #{finish - start}s")
|
|
34
|
-
|
|
43
|
+
if !status.success? && !allow_failure
|
|
44
|
+
raise ExecutionFailure.new(command, output)
|
|
45
|
+
end
|
|
35
46
|
output
|
|
36
47
|
end
|
|
37
48
|
end
|
|
38
49
|
|
|
39
50
|
# Records every command run to a directory; intended to be useful for later integration tests.
|
|
40
51
|
class RunnerRecorder < Runner
|
|
52
|
+
# The location to write recorded runs to. Currently hard-coded to a
|
|
53
|
+
# location inside the gem's spec folder.
|
|
41
54
|
OUTPUT_DIR = MODULE_DIR.join('spec/command_output')
|
|
55
|
+
|
|
56
|
+
# the time that this class file was loaded at
|
|
42
57
|
INIT_AT = Time.new
|
|
43
58
|
|
|
44
59
|
def initialize(*args)
|
|
@@ -46,14 +61,19 @@ module Appear
|
|
|
46
61
|
@command_runs = Hash.new { |h, k| h[k] = [] }
|
|
47
62
|
end
|
|
48
63
|
|
|
49
|
-
|
|
64
|
+
# @see Runner#run
|
|
65
|
+
def run(command, opts = {})
|
|
50
66
|
begin
|
|
51
67
|
result = super(command)
|
|
52
68
|
record_success(command, result)
|
|
53
69
|
return result
|
|
54
70
|
rescue ExecutionFailure => err
|
|
55
71
|
record_error(command, err)
|
|
56
|
-
|
|
72
|
+
if opts[:allow_failure]
|
|
73
|
+
return err.output
|
|
74
|
+
else
|
|
75
|
+
raise err
|
|
76
|
+
end
|
|
57
77
|
end
|
|
58
78
|
end
|
|
59
79
|
|