pingpongpear 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/bin/pingpongpear +1 -1
  3. data/lib/ping_pong_pear.rb +140 -116
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 398675b7d66d9103e11e13a4c5e16d6dc4084dac
4
- data.tar.gz: ebe8152549dae3433f437f563af4f6abe22b49b6
3
+ metadata.gz: 7c0839e52767ce0d7c55c2089253fba002345785
4
+ data.tar.gz: c83a92f8743ae021c67edb43022946df58eadce4
5
5
  SHA512:
6
- metadata.gz: 2794b44623036b5b525c332fe52e676e9b751585c0406ba63c77fb5fcc9b712b7956131114c00a12794a4de425be655a144b8202dc7a2d39e95a20a496a4a9bb
7
- data.tar.gz: b61c396a9d1fd66ca26d9604e3f508f5969e7377c671378b690c0ea1abe72140d09c01fc5755511fd2e1e0a5baaca80f655a4ebf1d660d4d1fb66d9e5323de53
6
+ metadata.gz: ab94cabb5d3a0ec278810519fa08f9416c2baac554819ce3eafdd33819dd8b247c3e99edb14b66ffba62339ef7fefe2e765c030a72c785eeb745b1f9ca7dca14
7
+ data.tar.gz: 7294e1f3f09f9dca66f6ff5b317d8f95e59b6a6abc15015714e86afa0ee51c1195de12cbaf8a786eb73665170cd5065b7be7f18edeac2122afff61ca0b0a22da
data/bin/pingpongpear CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'ping_pong_pear'
4
- PingPongPear::Commands.run ARGV
4
+ PingPongPear.run(ARGV)
@@ -1,143 +1,167 @@
1
- require 'socket'
2
- require 'ipaddr'
3
- require 'json'
4
-
5
- module PingPongPear
6
- VERSION = '1.0.0'
7
- UDP_PORT = 4545
8
- MULTICAST_ADDR = "224.0.0.1"
9
-
10
- class Broadcaster
11
- def self.my_public_address
12
- Socket.ip_address_list.reject { |addr|
13
- addr.ipv4_loopback? || addr.ipv6_loopback? || addr.ipv6_linklocal?
14
- }.first
15
- end
1
+ require 'dnssd'
2
+ require 'webrick'
3
+ require 'securerandom'
4
+ require 'set'
5
+ require 'net/http'
6
+ require 'logger'
7
+ require 'shellwords'
8
+
9
+ class PingPongPear
10
+ VERSION = '2.0.0'
11
+
12
+ SERVICE = "_http._tcp,pingpongpear"
13
+
14
+ def self.run args
15
+ new.public_send args.first, *args.drop(1)
16
+ rescue
17
+ cmds = public_instance_methods(false).find_all { |x|
18
+ instance_method(x).arity < 0
19
+ }
20
+ $stderr.puts "USAGE: pingpongpear #{cmds}"
21
+ exit 1
22
+ end
16
23
 
17
- def self.send_update args
18
- new.send ['commit'] + args
19
- end
24
+ attr_reader :pull_requests, :peers, :logger
20
25
 
21
- def self.send_location name, address, port
22
- new.send ['source', name, address, port]
23
- end
26
+ def initialize
27
+ @pull_requests = Queue.new
28
+ @send_pull_requests = Queue.new
29
+ @peers = Set.new
30
+ @logger = Logger.new $stdout
31
+ end
24
32
 
25
- def self.where_is? name
26
- listener_t = Thread.new {
27
- listener = Listener.new
28
- listener.start { |cmd, n, *rest|
29
- break(rest) if cmd == 'source' && n == name
30
- }
31
- }
32
- broadcast = new
33
- while listener_t.alive?
34
- broadcast.send ['locate', name]
35
- end
36
- addr, port = listener_t.value
37
- "http://#{addr}:#{port}/"
38
- end
33
+ def start name = File.basename(Dir.pwd)
34
+ post_commit_hook = '.git/hooks/post-commit'
35
+ pidfile = '.git/pingpongpear.pid'
39
36
 
40
- def initialize
41
- @multicast_addr = MULTICAST_ADDR
42
- @port = UDP_PORT
43
- @socket = UDPSocket.open
44
- @socket.setsockopt :IPPROTO_IP, :IP_MULTICAST_TTL, 1
37
+ if File.exist? pidfile
38
+ raise "Another instance of Ping Pong Pear is running"
39
+ else
40
+ File.open(pidfile, 'w') { |f| f.write $$ }
45
41
  end
46
42
 
47
- def send message
48
- @socket.send(JSON.dump(message), 0, @multicast_addr, @port)
49
- end
50
- end
43
+ File.open(post_commit_hook, 'w') { |f|
44
+ f.write <<-eof
45
+ #!/bin/sh
46
+
47
+ git update-server-info
48
+ kill -INFO $(cat #{pidfile})
49
+ eof
50
+ }
51
+ File.chmod 0755, post_commit_hook
52
+
53
+ system "git update-server-info"
54
+
55
+ at_exit {
56
+ File.unlink pidfile
57
+ File.unlink post_commit_hook
58
+ }
59
+
60
+ identifier = make_ident name
61
+
62
+ logger.info "SEND THIS TO YOUR PEAR `git clone #{name}`"
63
+ logger.info "CTRL-T sends pull requests to all peers"
64
+ logger.debug "MY PROJECT NAME: #{name} IDENT: #{identifier}"
51
65
 
52
- class Listener
53
- def initialize
54
- @multicast_addr = MULTICAST_ADDR
55
- @bind_addr = "0.0.0.0"
56
- @port = UDP_PORT
66
+ server = start_server pull_requests
67
+ http_port = server.listeners.map { |x| x.addr[1] }.first
68
+ hostname = Socket.gethostname
57
69
 
58
- @socket = UDPSocket.new
59
- membership = IPAddr.new(@multicast_addr).hton + IPAddr.new(@bind_addr).hton
70
+ discover identifier, name, peers
71
+ process_pull_requests pull_requests
72
+ process_send_pull_requests @send_pull_requests
60
73
 
61
- @socket.setsockopt :IPPROTO_IP, :IP_ADD_MEMBERSHIP, membership
62
- @socket.setsockopt :SOL_SOCKET, :SO_REUSEPORT, 1
74
+ trap('INFO') { send_pull_requests peers, hostname, http_port }
63
75
 
64
- @socket.bind @bind_addr, @port
76
+ advertise(identifier, name, hostname, http_port).each { |x| x }
77
+ end
78
+
79
+ def clone name, dir = nil
80
+ browser = DNSSD::Service.browse SERVICE
81
+ browser.each do |response|
82
+ r = response.resolve
83
+ if r.text_record['project'] == name
84
+ url = "http://#{r.target}:#{r.port}"
85
+ system "git clone #{Shellwords.escape(url)} #{dir || Shellwords.escape(name)}"
86
+ break
87
+ end
65
88
  end
89
+ end
66
90
 
67
- def start
68
- loop do
69
- message, _ = @socket.recvfrom 1024
70
- yield JSON.parse(message)
91
+ private
92
+
93
+ def start_server pull_requests
94
+ server = WEBrick::HTTPServer.new Port: 0,
95
+ DocumentRoot: '.git',
96
+ Logger: logger
97
+ server.mount_proc '/pull' do |req, res|
98
+ host = req.query['host']
99
+ port = req.query['port']
100
+ if host && port
101
+ logger.info "ADDED PR: #{host}:#{port}"
102
+ pull_requests << [host, port.to_i]
71
103
  end
104
+ res.body = ''
72
105
  end
106
+ Thread.new { server.start }
107
+ server
108
+ end
109
+
110
+ def advertise ident, name, hostname, http_port
111
+ txt = DNSSD::TextRecord.new 'project' => name
112
+ DNSSD::Service.register ident, SERVICE, nil, http_port, hostname, txt
113
+ end
114
+
115
+ def make_ident name
116
+ "#{name} (#{SecureRandom.hex.slice(0, 4)})"
73
117
  end
74
118
 
75
- module Commands
76
- def self.start args
77
- require 'webrick'
78
- require 'securerandom'
79
- require 'shellwords'
80
-
81
- project_name = args.first || File.basename(Dir.pwd)
82
- post_commit_hook = '.git/hooks/post-commit'
83
- uuid = SecureRandom.uuid
84
-
85
- system 'git update-server-info'
86
-
87
- server = WEBrick::HTTPServer.new Port: 0, DocumentRoot: '.git'
88
- http_port = server.listeners.map { |x| x.addr[1] }.first
89
- my_address = Broadcaster.my_public_address.ip_address
90
-
91
- File.open(post_commit_hook, 'w') { |f|
92
- f.puts "#!/usr/bin/env ruby"
93
- f.puts "ARGV[0] = 'update'"
94
- f.puts "ARGV[1] = '#{project_name}'"
95
- f.puts "ARGV[2] = '#{uuid}'"
96
- f.puts "ARGV[3] = '#{my_address}'"
97
- f.puts "ARGV[4] = '#{http_port}'"
98
- f.write File.read __FILE__
99
- }
100
- File.chmod 0755, post_commit_hook
101
-
102
- Thread.new {
103
- listener = PingPongPear::Listener.new
104
- listener.start { |cmd, name, *rest|
105
- next unless name == project_name
106
-
107
- case cmd
108
- when 'locate' then Broadcaster.send_location project_name, my_address, http_port
109
- when 'commit'
110
- unless rest.first == uuid
111
- url = "http://#{rest.drop(1).join(':')}"
112
- system "git pull #{Shellwords.escape(url)}"
113
- end
119
+ def discover ident, name, peers
120
+ browser = DNSSD::Service.browse SERVICE
121
+ browser.async_each do |response|
122
+ if response.flags.to_i > 0
123
+ logger.debug "SAW: #{response.name}"
124
+ unless response.name == ident
125
+ r = response.resolve
126
+ if r.text_record['project'] == name
127
+ logger.info "PEER: #{response.name}"
128
+ peers << [response.name, r.target, r.port]
114
129
  end
115
- }
116
- }
117
-
118
- trap('INT') {
119
- File.unlink post_commit_hook
120
- server.shutdown
121
- }
122
- server.start
130
+ end
131
+ else
132
+ peers.delete_if { |id, _, _| id == response.name }
133
+ logger.info "REMOVED: #{response.name}"
134
+ end
123
135
  end
136
+ end
124
137
 
125
- def self.clone args
126
- require 'shellwords'
127
- name = args.first
128
- url = PingPongPear::Broadcaster.where_is? name
129
- system "git clone #{Shellwords.escape(url)} #{Shellwords.escape(name)}"
138
+ def send_pull_requests peers, http_host, http_port
139
+ peers.each do |_, host, port|
140
+ @send_pull_requests << [host, port, http_host, http_port]
130
141
  end
142
+ end
131
143
 
132
- def self.update args
133
- system 'git update-server-info'
134
- PingPongPear::Broadcaster.send_update args
144
+ def process_send_pull_requests requests
145
+ Thread.new do
146
+ while pr = requests.pop
147
+ host, port, http_host, http_port = *pr
148
+ http = Net::HTTP.new host, port
149
+ request = Net::HTTP::Post.new '/pull'
150
+ request.set_form_data 'host' => http_host, 'port' => http_port
151
+ http.request request
152
+ end
135
153
  end
154
+ end
136
155
 
137
- def self.run args
138
- send args.first, args.drop(1)
156
+ def process_pull_requests pull_requests
157
+ Thread.new do
158
+ while pr = pull_requests.pop
159
+ url = "http://#{pr.join(':')}"
160
+ logger.debug "git pull #{Shellwords.escape(url)}"
161
+ system "git pull #{Shellwords.escape(url)}"
162
+ end
139
163
  end
140
164
  end
141
165
  end
142
166
 
143
- PingPongPear::Commands.run(ARGV) if $0 == __FILE__
167
+ PingPongPear.run(ARGV) if $0 == __FILE__
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pingpongpear
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Patterson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-01-06 00:00:00.000000000 Z
11
+ date: 2015-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest