Blob of script/git-daemon (raw blob data)

1 #!/usr/bin/env ruby
2
3 require 'rubygems'
4 require 'daemons'
5 require 'geoip'
6 require 'socket'
7 require "optparse"
8
9 ENV["RAILS_ENV"] ||= "production"
10 require File.dirname(__FILE__)+'/../config/environment'
11
12 Rails.configuration.log_level = :info # Disable debug
13 ActiveRecord::Base.allow_concurrency = true
14
15 BASE_PATH = File.expand_path(GitoriousConfig['repository_base_path'])
16
17 TIMEOUT = 30
18 MAX_CHILDREN = 30
19 $children_reaped = 0
20 $children_active = 0
21
22 module Git
23 class Daemon
24 include Daemonize
25
26 SERVICE_REGEXP = /(\w{4})(git\-upload\-pack)\s(.+)\x0host=([\w\.\-]+)/.freeze
27
28 def initialize(options)
29 @options = options
30 end
31
32 def start
33 if @options[:daemonize]
34 daemonize(@options[:logfile])
35 File.open(@options[:pidfile], "w") do |f|
36 f.write(Process.pid)
37 end
38 end
39 @socket = TCPServer.new(@options[:host], @options[:port])
40 log(Process.pid, "Listening on #{@options[:host]}:#{@options[:port]}...")
41 run
42 end
43
44 def run
45 while session = @socket.accept
46 connections = $children_active - $children_reaped
47 if connections > MAX_CHILDREN
48 log(Process.pid, "too many active children #{connections}/#{MAX_CHILDREN}")
49 session.close
50 next
51 end
52
53 run_service(session)
54 end
55 end
56
57 def run_service(session)
58 $children_active += 1
59 ip_family, port, name, ip = session.peeraddr
60
61 line = session.recv(1000)
62
63 if line =~ SERVICE_REGEXP
64 start_time = Time.now
65 code = $1
66 service = $2
67 base_path = $3
68 host = $4
69
70 path = File.expand_path("#{BASE_PATH}/#{base_path}")
71 if !path.index(BASE_PATH) == 0 || !File.directory?(path)
72 log(Process.pid, "Invalid path: #{base_path}")
73 session.close
74 $children_active -= 1
75 return
76 end
77
78 if !File.exist?(File.join(path, "git-daemon-export-ok"))
79 session.close
80 $children_active -= 1
81 return
82 end
83
84 Dir.chdir(path) do
85 cmd = "git-upload-pack --strict --timeout=#{TIMEOUT} ."
86
87 fork do
88 repository = nil
89
90 begin
91 repository = ::Repository.find_by_path(path)
92 rescue => e
93 log(Process.pid, "AR error: #{e.class.name} #{e.message}:\n #{e.backtrace.join("\n ")}")
94 end
95
96 pid = Process.pid
97 log(pid, "Connection from #{ip} for #{path.inspect}")
98
99 $stdout.reopen(session)
100 $stdin.reopen(session)
101 session.close
102
103 if repository
104 if ip_family == "AF_INET6"
105 repository.cloned_from(ip)
106 else
107 geoip = GeoIP.new(File.join(RAILS_ROOT, "data", "GeoIP.dat"))
108 localization = geoip.country(ip)
109 repository.cloned_from(ip, localization[3], localization[5])
110 end
111 else
112 log(pid, "Cannot find repository: #{path}")
113 end
114 log(Process.pid, "Deferred in #{'%0.5f' % (Time.now - start_time)}s")
115
116 exec(cmd)
117 # FIXME; we don't ever get here since we exec(), so reaped count may be incorrect
118 $children_reaped += 1
119 exit!
120 end
121 end rescue Errno::EAGAIN
122 else
123 $stderr.puts "Invalid request from #{ip}: #{line}"
124 session.close
125 $children_active -= 1
126 end
127 end
128
129 def handle_stop(signal)
130 log(Process.pid, "Received #{signal}, exiting..")
131 exit 0
132 end
133
134 def handle_cld
135 loop do
136 pid = nil
137 begin
138 pid = Process.wait(-1, Process::WNOHANG)
139 rescue Errno::ECHILD
140 break
141 end
142
143 if pid && $?
144 $children_reaped += 1
145 log(pid, "Disconnected. (status=#{$?.exitstatus})") if pid > 0
146 if $children_reaped == $children_active
147 $children_reaped = 0
148 $children_active = 0
149 end
150
151 next
152 end
153 break
154 end
155 end
156
157 def log(pid, msg)
158 $stderr.puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} [#{pid}] #{msg}"
159 end
160
161 end
162 end
163
164 options = {
165 :port => 9418,
166 :host => "0.0.0.0",
167 :logfile => File.join(RAILS_ROOT, "log", "git-daemon.log"),
168 :pidfile => File.join(RAILS_ROOT, "log", "git-daemon.pid"),
169 :daemonize => false
170
171 }
172
173 OptionParser.new do |opts|
174 opts.banner = "Usage: #{$0} [options]"
175
176 opts.on("-p", "--port=[port]", Integer, "Port to listen on", "Default: #{options[:port]}") do |o|
177 options[:port] = o
178 end
179
180 opts.on("-a", "--address=[host]", "Host to listen on", "Default: #{options[:host]}") do |o|
181 options[:host] = o
182 end
183
184 opts.on("-l", "--logfile=[file]", "File to log to", "Default: #{options[:logfile]}") do |o|
185 options[:logfile] = o
186 end
187
188 opts.on("-P", "--pidfile=[file]", "PID file to use (if daemonized)", "Default: #{options[:pidfile]}") do |o|
189 options[:pidfile] = o
190 end
191
192 opts.on("-d", "--daemonize", "Daemonize or run in foreground", "Default: #{options[:daemonize]}") do |o|
193 options[:daemonize] = o
194 end
195
196 opts.on_tail("-h", "--help", "Show this help message.") do
197 puts opts
198 exit
199 end
200
201 # opts.on("-e", "--environment", "RAILS_ENV to use") do |o|
202 # options[:port] = o
203 # end
204 end.parse!
205
206 @git_daemon = Git::Daemon.new(options)
207
208 trap("SIGKILL") { @git_daemon.handle_stop("SIGKILL") }
209 trap("TERM") { @git_daemon.handle_stop("TERM") }
210 trap("SIGINT") { @git_daemon.handle_stop("SIGINT") }
211 trap("CLD") { @git_daemon.handle_cld }
212
213 @git_daemon.start