Class: Singed::CLI

Inherits:
Object
  • Object
show all
Defined in:
lib/singed/cli.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(argv) ⇒ CLI

Returns a new instance of CLI.



12
13
14
15
16
17
# File 'lib/singed/cli.rb', line 12

def initialize(argv)
  @argv = argv
  @opts = OptionParser.new

  parse_argv!
end

Instance Attribute Details

#argvObject

Returns the value of attribute argv.



10
11
12
# File 'lib/singed/cli.rb', line 10

def argv
  @argv
end

#filenameObject

Returns the value of attribute filename.



10
11
12
# File 'lib/singed/cli.rb', line 10

def filename
  @filename
end

#optsObject

Returns the value of attribute opts.



10
11
12
# File 'lib/singed/cli.rb', line 10

def opts
  @opts
end

Class Method Details

.chdir_rails_rootObject



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/singed/cli.rb', line 160

def self.chdir_rails_root
  original_cwd = Dir.pwd

  loop do
    if File.file?("config/environment.rb")
      return Dir.pwd
    end

    if Pathname.new(Dir.pwd).root?
      Dir.chdir(original_cwd)
      return
    end

    # Otherwise keep moving upwards in search of an executable.
    Dir.chdir("..")
  end
end

Instance Method Details

#adjust_ownership!Object



133
134
135
# File 'lib/singed/cli.rb', line 133

def adjust_ownership!
  sudo ["chown", ENV["USER"], filename], reason: "Adjusting ownership of #{filename}, but need root."
end

#parse_argv!Object



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
# File 'lib/singed/cli.rb', line 19

def parse_argv!
  opts.banner = "Usage: singed [options] <command>"

  opts.on("-h", "--help", "Show this message") do
    @show_help = true
  end

  opts.on("-o", "--output-directory DIRECTORY", "Directory to write flamegraph to") do |directory|
    @output_directory = directory
  end

  opts.order(@argv) do |arg|
    opts.terminate if arg == "--"
    break
  end

  if @argv.empty?
    @show_help = true
    @error_message = "missing command to profile"
    return
  end

  return if @show_help

  begin
    @opts.parse!(argv)
  rescue OptionParser::InvalidOption => e
    @show_help = true
    @error_message = e
  end
end

#password_needed?Boolean

Returns:

  • (Boolean)


125
126
127
# File 'lib/singed/cli.rb', line 125

def password_needed?
  !system("sudo --non-interactive true >/dev/null 2>&1")
end

#prompt_passwordObject



129
130
131
# File 'lib/singed/cli.rb', line 129

def prompt_password
  system("sudo true")
end

#runObject



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
# File 'lib/singed/cli.rb', line 51

def run
  require "singed"

  if @error_message
    puts @error_message
    puts
    puts @opts.help
    exit 1
  end

  if show_help?
    puts @opts.help
    exit 0
  end

  Singed.output_directory = @output_directory if @output_directory
  Singed.output_directory ||= Dir.tmpdir
  FileUtils.mkdir_p Singed.output_directory
  @filename = Singed::Flamegraph.generate_filename(label: "cli")

  options = {
    format: "speedscope",
    file: filename.to_s,
    silent: nil
  }

  rbspy_args = [
    "record",
    *options.map { |k, v| ["--#{k}", v].compact }.flatten,
    "--",
    *argv
  ]

  loop do
    break unless password_needed?

    puts "🔥📈 Singed needs to run as root, but will drop permissions back to your user. Prompting with sudo now..."
    prompt_password
  end

  rbspy = lambda do
    # don't run things with spring, because it forks and rbspy won't see it
    sudo ["rbspy", *rbspy_args], reason: "Singed needs to run as root, but will drop permissions back to your user.", env: {"DISABLE_SPRING" => "1"}
  end

  if defined?(Bundler)
    Bundler.with_unbundled_env do
      rbspy.call
    end
  else
    rbspy.call
  end

  unless filename.exist?
    puts "#{filename} doesn't exist. Maybe rbspy had a failure capturing it? Check the scrollback."
    exit 1
  end

  unless adjust_ownership!
    puts "#{filename} isn't writable!"
    exit 1
  end

  # clean the report, similar to how Singed::Report does
  json = JSON.parse(filename.read)
  json["shared"]["frames"].each do |frame|
    frame["file"] = Singed.filter_line(frame["file"])
  end
  filename.write(JSON.dump(json))

  flamegraph = Singed::Flamegraph.new(filename: filename)
  flamegraph.open
end

#show_help?Boolean

Returns:

  • (Boolean)


137
138
139
# File 'lib/singed/cli.rb', line 137

def show_help?
  @show_help
end

#sudo(system_args, reason:, env: {}) ⇒ Object



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/singed/cli.rb', line 141

def sudo(system_args, reason:, env: {})
  loop do
    break unless password_needed?

    puts "🔥📈 #{reason} Prompting with sudo now..."
    prompt_password
  end

  sudo_args = [
    "sudo",
    "--preserve-env",
    *system_args.map(&:to_s)
  ]

  puts "$ #{Shellwords.join(sudo_args)}"

  system(env, *sudo_args, exception: true)
end