Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed ngrok persistence support PR #20

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ Ngrok::Tunnel.start(addr: 'foo.dev:80',
authtoken: 'MY_TOKEN',
inspect: false,
log: 'ngrok.log',
config: '~/.ngrok')
config: '~/.ngrok',
persistence: true,
persistence_file: '/Users/user/.ngrok2/ngrok-process.json') # optional parameter

```

Expand Down
83 changes: 75 additions & 8 deletions lib/ngrok/tunnel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,22 @@ def init(params = {})
# map old key 'port' to 'addr' to maintain backwards compatibility with versions 2.0.21 and earlier
params[:addr] = params.delete(:port) if params.key?(:port)

@params = {addr: 3001, timeout: 10, config: '/dev/null'}.merge(params)
@params = {addr: 3001, timeout: 10, config: '/dev/null'}.merge!(params)
@status = :stopped unless @status
end

def start(params = {})
ensure_binary
init(params)

if stopped?
@params[:log] = (@params[:log]) ? File.open(@params[:log], 'w+') : Tempfile.new('ngrok')
@pid = spawn("exec ngrok http " + ngrok_exec_params)
at_exit { Ngrok::Tunnel.stop }
fetch_urls
end
persistent_ngrok = @params[:persistence] == true
# Attempt to read the attributes of an existing process instead of starting a new process.
try_params_from_running_ngrok if persistent_ngrok

spawn_new_ngrok(persistent_ngrok: persistent_ngrok) if stopped?

@status = :running
store_new_ngrok_process if persistent_ngrok
@ngrok_url
end

Expand Down Expand Up @@ -79,6 +79,73 @@ def inherited(subclass)

private

def parse_persistence_file
JSON.parse(File.read(@persistence_file))
rescue StandardError => _e # Catch all possible errors on reading and parsing the file
nil
end

def raise_if_similar_ngroks(pid)
other_similar_ngroks = ngrok_process_status_lines.select do |line|
# If the pid is not nil and the line starts with this pid, do not take this line into account
!(pid && line.start_with?(pid)) && line.include?('ngrok http') && line.end_with?("#{addr}")
end

return if other_similar_ngroks.empty?

raise Ngrok::Error, "ERROR: Other ngrok instances tunneling to port #{addr} found"
end

def ngrok_process_status_lines(refetch: false)
return @ngrok_process_status_lines if defined?(@ngrok_process_status_lines) && !refetch

@ngrok_process_status_lines = (`ps ax | grep "ngrok http"`).split(/\n/)
end

def try_params_from_running_ngrok
@persistence_file = @params[:persistence_file] || "#{File.dirname(@params[:config])}/ngrok-process.json"
state = parse_persistence_file
pid = state&.[]('pid')
raise_if_similar_ngroks(pid)

return unless ngrok_running?(pid)

@status = :running
@pid = pid
@ngrok_url = state['ngrok_url']
@ngrok_url_https = state['ngrok_url_https']
end

def ngrok_running?(pid)
pid && Process.kill(0, pid.to_i) ? true : false
rescue Errno::ESRCH, Errno::EPERM
false
end

def spawn_new_ngrok(persistent_ngrok:)
raise_if_similar_ngroks(nil)
prepare_ngrok_logfile
if persistent_ngrok
Process.spawn("exec nohup ngrok http #{ngrok_exec_params} &")
@pid = ngrok_process_status_lines(refetch: true).find { |line| line.include?('ngrok http -log')}.split[0]
else
@pid = spawn("exec ngrok http #{ngrok_exec_params}")
at_exit { Ngrok::Tunnel.stop }
end

fetch_urls
end

def prepare_ngrok_logfile
# Prepare the log file into which ngrok output will be redirected in `ngrok_exec_params`
@params[:log] = @params[:log] ? File.open(@params[:log], 'w+') : Tempfile.new('ngrok')
end

def store_new_ngrok_process
# Record the attributes of the new process so that it can be reused on a subsequent call.
File.write(@persistence_file, { pid: @pid, ngrok_url: @ngrok_url, ngrok_url_https: @ngrok_url_https }.to_json)
end

def ngrok_exec_params
exec_params = "-log=stdout -log-level=debug "
exec_params << "-bind-tls=#{@params[:bind_tls]} " if @params.has_key? :bind_tls
Expand All @@ -88,7 +155,7 @@ def ngrok_exec_params
exec_params << "-subdomain=#{@params[:subdomain]} " if @params[:subdomain]
exec_params << "-hostname=#{@params[:hostname]} " if @params[:hostname]
exec_params << "-inspect=#{@params[:inspect]} " if @params.has_key? :inspect
exec_params << "-config=#{@params[:config]} #{@params[:addr]} > #{@params[:log].path}"
exec_params << "-config #{@params[:config]} #{@params[:addr]} > #{@params[:log].path}"
end

def fetch_urls
Expand Down
36 changes: 36 additions & 0 deletions spec/tunnel/tunnel_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@
end

describe "Custom host header" do
before { expect(Ngrok::Tunnel).to receive(:fetch_urls) }

it "doesn't include the -host-header parameter when it is not provided" do
Ngrok::Tunnel.start()
expect(Ngrok::Tunnel.send(:ngrok_exec_params)).not_to include("-host-header=")
Expand Down Expand Up @@ -177,4 +179,38 @@
Ngrok::Tunnel.stop
end
end

describe '#start' do
before { allow(Process).to receive(:kill) }
after { Ngrok::Tunnel.stop }

describe 'when persistence param is true' do
it 'tries fetching params of an already running Ngrok and store Ngrok process data into a file ' do
expect(Ngrok::Tunnel).to receive(:try_params_from_running_ngrok)
expect(Ngrok::Tunnel).to receive(:spawn_new_ngrok).with(persistent_ngrok: true)
expect(Ngrok::Tunnel).to receive(:store_new_ngrok_process)

Ngrok::Tunnel.start(persistence: true)
end
end

describe 'when persistence param is not true' do
it "doesn't try to fetch params of an already running Ngrok" do
expect(Ngrok::Tunnel).not_to receive(:try_params_from_running_ngrok)
expect(Ngrok::Tunnel).to receive(:spawn_new_ngrok).with(persistent_ngrok: false)
expect(Ngrok::Tunnel).not_to receive(:store_new_ngrok_process)

Ngrok::Tunnel.start(persistence: false)
end
end

describe 'when Ngrok::Tunnel is already running' do
it "doesn't try to spawn a new Ngrok process" do
allow(Ngrok::Tunnel).to receive(:stopped?).and_return(false)
expect(Ngrok::Tunnel).not_to receive(:spawn_new_ngrok)

Ngrok::Tunnel.start
end
end
end
end