pingpongpear 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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