#!/usr/bin/env ruby
require 'rubygems'
require 'daemons'
require 'geoip'
require 'socket'
require "optparse"
ENV["RAILS_ENV"] ||= "production"
require File.dirname(__FILE__)+'/../config/environment'
Rails.configuration.log_level = :info # Disable debug
ActiveRecord::Base.allow_concurrency = true
BASE_PATH = File.expand_path(GitoriousConfig['repository_base_path'])
TIMEOUT = 30
MAX_CHILDREN = 30
$children_reaped = 0
$children_active = 0
module Git
class Daemon
include Daemonize
SERVICE_REGEXP = /(\w{4})(git\-upload\-pack)\s(.+)\x0host=([\w\.\-]+)/.freeze
def initialize(options)
@options = options
end
def start
if @options[:daemonize]
daemonize(@options[:logfile])
File.open(@options[:pidfile], "w") do |f|
f.write(Process.pid)
end
end
@socket = TCPServer.new(@options[:host], @options[:port])
log(Process.pid, "Listening on #{@options[:host]}:#{@options[:port]}...")
run
end
def run
while session = @socket.accept
connections = $children_active - $children_reaped
if connections > MAX_CHILDREN
log(Process.pid, "too many active children #{connections}/#{MAX_CHILDREN}")
session.close
next
end
run_service(session)
end
end
def run_service(session)
$children_active += 1
ip_family, port, name, ip = session.peeraddr
line = session.recv(1000)
if line =~ SERVICE_REGEXP
start_time = Time.now
code = $1
service = $2
base_path = $3
host = $4
path = File.expand_path("#{BASE_PATH}/#{base_path}")
if !path.index(BASE_PATH) == 0 || !File.directory?(path)
log(Process.pid, "Invalid path: #{base_path}")
session.close
$children_active -= 1
return
end
if !File.exist?(File.join(path, "git-daemon-export-ok"))
session.close
$children_active -= 1
return
end
Dir.chdir(path) do
cmd = "git-upload-pack --strict --timeout=#{TIMEOUT} ."
fork do
repository = nil
begin
repository = ::Repository.find_by_path(path)
rescue => e
log(Process.pid, "AR error: #{e.class.name} #{e.message}:\n #{e.backtrace.join("\n ")}")
end
pid = Process.pid
log(pid, "Connection from #{ip} for #{path.inspect}")
$stdout.reopen(session)
$stdin.reopen(session)
session.close
if repository
if ip_family == "AF_INET6"
repository.cloned_from(ip)
else
geoip = GeoIP.new(File.join(RAILS_ROOT, "data", "GeoIP.dat"))
localization = geoip.country(ip)
repository.cloned_from(ip, localization[3], localization[5])
end
else
log(pid, "Cannot find repository: #{path}")
end
log(Process.pid, "Deferred in #{'%0.5f' % (Time.now - start_time)}s")
exec(cmd)
# FIXME; we don't ever get here since we exec(), so reaped count may be incorrect
$children_reaped += 1
exit!
end
end rescue Errno::EAGAIN
else
$stderr.puts "Invalid request from #{ip}: #{line}"
session.close
$children_active -= 1
end
end
def handle_stop(signal)
log(Process.pid, "Received #{signal}, exiting..")
exit 0
end
def handle_cld
loop do
pid = nil
begin
pid = Process.wait(-1, Process::WNOHANG)
rescue Errno::ECHILD
break
end
if pid && $?
$children_reaped += 1
log(pid, "Disconnected. (status=#{$?.exitstatus})") if pid > 0
if $children_reaped == $children_active
$children_reaped = 0
$children_active = 0
end
next
end
break
end
end
def log(pid, msg)
$stderr.puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} [#{pid}] #{msg}"
end
end
end
options = {
:port => 9418,
:host => "0.0.0.0",
:logfile => File.join(RAILS_ROOT, "log", "git-daemon.log"),
:pidfile => File.join(RAILS_ROOT, "log", "git-daemon.pid"),
:daemonize => false
}
OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options]"
opts.on("-p", "--port=[port]", Integer, "Port to listen on", "Default: #{options[:port]}") do |o|
options[:port] = o
end
opts.on("-a", "--address=[host]", "Host to listen on", "Default: #{options[:host]}") do |o|
options[:host] = o
end
opts.on("-l", "--logfile=[file]", "File to log to", "Default: #{options[:logfile]}") do |o|
options[:logfile] = o
end
opts.on("-P", "--pidfile=[file]", "PID file to use (if daemonized)", "Default: #{options[:pidfile]}") do |o|
options[:pidfile] = o
end
opts.on("-d", "--daemonize", "Daemonize or run in foreground", "Default: #{options[:daemonize]}") do |o|
options[:daemonize] = o
end
opts.on_tail("-h", "--help", "Show this help message.") do
puts opts
exit
end
# opts.on("-e", "--environment", "RAILS_ENV to use") do |o|
# options[:port] = o
# end
end.parse!
@git_daemon = Git::Daemon.new(options)
trap("SIGKILL") { @git_daemon.handle_stop("SIGKILL") }
trap("TERM") { @git_daemon.handle_stop("TERM") }
trap("SIGINT") { @git_daemon.handle_stop("SIGINT") }
trap("CLD") { @git_daemon.handle_cld }
@git_daemon.start