locport 1.1.2 → 1.2.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/lib/locport/indexer.rb +178 -0
  3. data/lib/locport.rb +51 -146
  4. metadata +60 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34b562cd9ee5712eb6659f3324af7caed450ffe9931e38f054b606d63d544bfe
4
- data.tar.gz: ea1d8b3019ba4715ba1d41e79d2e0267b5912a09346bd72de4cb8ee7027a70eb
3
+ metadata.gz: 3cbc4ad1881fb933fd3582c13cd2ed6c4d881b71d19430451c0ccb4f882c299a
4
+ data.tar.gz: abe62ddb5fa288edbb3376c0137558a90d84ce93906e638697399d3ac5077ad8
5
5
  SHA512:
6
- metadata.gz: 43c5385f8745d6b49456848c0ef3e373276033ecd19c022ba5ffc896fa6624f94c816024332fc6544e8aec316a13206ff80fdf3b44ffe1e7a1eb2de0776864c9
7
- data.tar.gz: ec1eed26d60f600ed3b7e04fe31551d6453306fc145e0f677b9d2ccfc67757177ab1e00ba43ac0e4503712d9393ec23a483293ec9e8d6a4dcffe8d3d95fb4106
6
+ metadata.gz: 568880abea66ec3c00d68ac0a3c5c02300fa348d8a2ba729c0a2cfbe805a2d23e0ccef8d88c71532634e44e03a2c4be97cb9b4fe21d257c81389bda2a67adecc
7
+ data.tar.gz: 8fe1b19ea6f27c437e54302b7b4c06f6aeadfaecfa204f81cd9fb66ec7c8b7db5cc9f2f9c9c0d1cb0a9f6e281d60ca43e8d223aadf1ed585a7568ad55170b41f
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "find"
4
+ require "socket"
5
+ require "fileutils"
6
+ require "pathname"
7
+
8
+ module Locport
9
+ Address = Struct.new(:host, :port, :path, :line_number, :host_conflicts, :port_conflicts)
10
+
11
+ class Indexer
12
+ APP_NAME = "locport"
13
+ DOTFILE = ".localhost"
14
+ DATA_FILE = "projects"
15
+ PORT_RANGE = (30_000..60_000)
16
+
17
+ attr_reader :dotfiles, :projects, :addresses
18
+
19
+ def initialize(home_path: Dir.home, storage_base_dir: default_storage_base_dir)
20
+ @home_path = home_path
21
+ @storage_base_dir = storage_base_dir
22
+ @dotfiles = load_dotfiles
23
+ @projects = load_projects
24
+ end
25
+
26
+ def index(path, recursive: false, shell: nil)
27
+ start_path = File.expand_path path.to_s
28
+
29
+ [].tap do |result|
30
+ begin
31
+ Find.find(start_path) do |path|
32
+ Find.prune if !recursive && File.directory?(path) && path.size > start_path.size
33
+
34
+ if File.basename(path) == DOTFILE && File.file?(path)
35
+ result << Pathname.new(path)
36
+
37
+ shell&.indent do
38
+ shell&.say "#{path} "
39
+ end
40
+ shell&.say "added", :green
41
+ end
42
+ end
43
+ rescue Errno::ENOENT
44
+ end
45
+
46
+ @dotfiles = (@dotfiles + result).uniq.sort
47
+ end
48
+ end
49
+
50
+ def load_dotfiles
51
+ File.read(storage_path).lines.map(&:strip).reject(&:empty?).map { |path| Pathname.new(path).join(DOTFILE) }
52
+ rescue Errno::ENOENT
53
+ []
54
+ end
55
+
56
+ def load_projects
57
+ @projects = {}.tap do |result|
58
+ @addresses = []
59
+
60
+ @dotfiles.each do |path|
61
+ File.read(path).each_line.with_index do |line, index|
62
+ dir = File.dirname path.to_s
63
+
64
+ key, source = cannonize_project_dir dir
65
+ result[key] ||= []
66
+
67
+ if line.strip =~ /^(.+):(\d+)$/
68
+ address = Address.new($1, $2.to_i, source, index + 1)
69
+ result[key] << address
70
+ @addresses << address
71
+ end
72
+ end
73
+ rescue Errno::ENOENT
74
+ end
75
+
76
+ reveal_address_conflicts
77
+ end.sort.to_h
78
+ end
79
+
80
+ def port_listening?(port)
81
+ Socket.tcp("127.0.0.1", port, connect_timeout: 0.01) {} # 10ms timeout
82
+ true
83
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError
84
+ false
85
+ end
86
+
87
+ def save
88
+ FileUtils.mkdir_p storage_dir
89
+ File.write storage_path, @dotfiles.map { |path| File.dirname(path) }.join("\n") + "\n"
90
+ end
91
+
92
+ def default_storage_base_dir
93
+ if Gem.win_platform?
94
+ ENV["APPDATA"] || File.join(Dir.home, "AppData", "Roaming")
95
+ else
96
+ ENV["XDG_DATA_HOME"] || File.join(Dir.home, ".local", "share")
97
+ end
98
+ end
99
+
100
+ def create_address(value, dir: Dir.pwd)
101
+ _, source = cannonize_project_dir dir
102
+
103
+ address = if value.strip =~ /^(.+):(\d+)$/
104
+ Address.new($1, $2.to_i, source)
105
+ else
106
+ Address.new(value, find_unused_port, source)
107
+ end
108
+
109
+ @addresses << address
110
+ reveal_address_conflicts
111
+
112
+ address
113
+ end
114
+
115
+ def append_address_to_dotfile(address, dir: Dir.pwd)
116
+ _, _, fullpath = cannonize_project_dir dir
117
+
118
+ File.open(fullpath, "a") do |file|
119
+ file.puts("#{address.host}:#{address.port}")
120
+ end
121
+
122
+ index dir
123
+ save
124
+ end
125
+
126
+ def storage_path
127
+ File.join storage_dir, DATA_FILE
128
+ end
129
+
130
+ private
131
+ def storage_dir
132
+ File.join @storage_base_dir, APP_NAME
133
+ end
134
+
135
+ def cannonize_project_dir(dir)
136
+ key = if dir.start_with?(@home_path.to_s)
137
+ dir.sub(@home_path.to_s, "~")
138
+ else
139
+ dir
140
+ end
141
+
142
+ source = "#{key}/#{DOTFILE}"
143
+ fullpath = "#{dir}/#{DOTFILE}"
144
+
145
+ [ key, source, fullpath ]
146
+ end
147
+
148
+ def reveal_address_conflicts
149
+ @addresses.each do |address|
150
+ address.host_conflicts = nil
151
+ address.port_conflicts = nil
152
+ end
153
+
154
+ @addresses.each do |address|
155
+ @addresses.each do |other_address|
156
+ next if address == other_address
157
+
158
+ if address.host == other_address.host
159
+ address.host_conflicts ||= []
160
+ address.host_conflicts << other_address
161
+ end
162
+
163
+ if address.port == other_address.port
164
+ address.port_conflicts ||= []
165
+ address.port_conflicts << other_address
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ def find_unused_port
172
+ loop do
173
+ port = rand PORT_RANGE
174
+ return port unless @addresses.any? { |address| address.port == port } || port_listening?(port)
175
+ end
176
+ end
177
+ end
178
+ end
data/lib/locport.rb CHANGED
@@ -1,203 +1,108 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require "fileutils"
5
- require "pathname"
6
-
7
- APP_NAME = "locport"
8
- DATA_FILE = "projects"
9
- DOTFILE = ".localhost"
10
- PORT_RANGE = (30_000..60_000)
4
+ require "locport/indexer"
11
5
 
12
6
  module Locport
13
7
  class Main < Thor
8
+ COLOR_FAINT = "\e[2m"
9
+
14
10
  default_task :list
15
11
 
16
12
  def self.exit_on_failure?
17
13
  true
18
14
  end
19
15
 
20
- desc "index [PATH]", "Index a project"
21
- def index(path = Dir.pwd, silent: false)
22
- project_path = Pathname.new(path).cleanpath
23
- dotfile_path = project_path.join(DOTFILE)
24
-
25
- unless File.exist?(dotfile_path)
26
- say_error "#{dotfile_path} file doesn't exist", :red
27
- say_error "You can create it from within that directory with `locport add`"
28
- exit 1
16
+ desc "index [PATH]", "Add project path(s) to locport"
17
+ method_option :recursive, type: :boolean, default: false, aliases: "-r"
18
+ def index(*paths)
19
+ paths.each do |path|
20
+ say "Indexing "
21
+ say path, :blue
22
+ indexer.index(path, recursive: options.recursive, shell:)
29
23
  end
30
24
 
31
- append_to_projects project_path.to_s
25
+ indexer.save
32
26
 
33
- unless silent
34
- say "Indexing ", :green
35
- say project_path
36
- end
27
+ say "Done ✓", :green
37
28
  end
38
29
 
39
30
  desc "add [HOST[:PORT]]", "Add a new host to .localhost file. \
40
31
  Without arguments current directory name will be used and random port number assigned."
41
32
  def add(host = "#{File.basename(Dir.pwd)}.localhost")
42
- host_with_port, port = ensure_port(host.strip)
43
- host = host_with_port.split(":").first
44
-
45
- if used_ports.include?(port)
46
- say_error "Port #{port} is "
47
- say_error "already used. ", :red
48
- say_error "See `locport list`"
49
- exit 1
50
- end
33
+ address = indexer.create_address host
51
34
 
52
- if used_hosts.include?(host)
53
- say_error "Host '#{host}' is "
54
- say_error "already used. ", :red
55
- say_error "See `locport list`"
35
+ say_address_conflicts address
36
+
37
+ if @conflicts_found
38
+ say "Can't add due to conflicts"
56
39
  exit 1
57
40
  end
58
41
 
59
- append_to_dotfile host_with_port
60
- index silent: true
42
+ indexer.append_address_to_dotfile address
61
43
 
62
- say "#{host_with_port} ", :bold
63
- say "added ", :green
64
- say "to #{DOTFILE}"
44
+ say "#{address.host}:#{address.port} ", :bold
45
+ say "added ", :green
65
46
  end
66
47
 
67
48
  desc "list", "List indexed projects, hosts and ports"
68
49
  def list
69
- table_data = []
70
- used_ports = []
71
- used_hosts = []
72
- conflicts_found = false
50
+ indexer.projects.each do |dir, addresses|
51
+ say dir, :blue
73
52
 
74
- projects.each do |(dir, host, port)|
75
- table_data << [ dir.sub(Dir.home, "~"), "http://#{host}:#{port}" ]
53
+ shell.indent do
54
+ addresses.each do |address|
55
+ port_color = indexer.port_listening?(address.port) ? :green : COLOR_FAINT
56
+ say "• ", port_color
76
57
 
77
- if used_ports.include?(port)
78
- conflicts_found = true
79
- table_data << [ "", "╰ Port used before" ]
80
- else
81
- used_ports << port
82
- end
58
+ shell.indent(-1) do
59
+ say [ display_host(address.host), address.port ].join(":")
60
+ end
83
61
 
84
- if used_hosts.include?(host)
85
- conflicts_found = true
86
- table_data << [ "", "╰ Host used before" ]
87
- else
88
- used_hosts << host
62
+ say_address_conflicts address
63
+ end
89
64
  end
90
65
  end
91
66
 
92
- print_table [ [ "Project", "URL" ] ] + table_data, borders: true
93
-
94
- if conflicts_found
95
- say "Conflicts found!", :red
67
+ if @conflicts_found
68
+ say "Conflicts found", :red
96
69
  exit 1
97
- else
98
- say "All hosts and ports are unique ✓", :green
99
70
  end
100
71
  end
101
72
 
102
73
  desc "info", "Display tool information"
103
74
  def info
104
- say "Projects index: #{projects_file_path}"
75
+ say "Index file: #{indexer.storage_path}"
105
76
  end
106
77
 
107
78
  private
108
- def projects
109
- load_projects
110
- @projects
111
- end
112
-
113
- def used_ports
114
- load_projects
115
- @used_ports
116
- end
117
-
118
- def used_hosts
119
- load_projects
120
- @used_hosts
79
+ def indexer
80
+ @indexer ||= Indexer.new
121
81
  end
122
82
 
123
- def load_projects
124
- @projects = []
125
- @used_ports = []
126
- @used_hosts = []
127
-
128
- File.read(projects_file_path).lines.each do |project_path|
129
- project_path = project_path.strip
130
- dotfile_path = Pathname.new(project_path).join(DOTFILE)
131
- next unless File.exist?(dotfile_path)
132
-
133
- hosts = File.read(dotfile_path).lines
134
-
135
- hosts.each do |host_with_port|
136
- host, port = host_with_port.strip.split(":")
137
- port = port.to_i
138
- @projects << [ project_path, host, port ]
139
-
140
- unless @used_ports.include?(port)
141
- @used_ports << port
142
- end
143
-
144
- unless @used_hosts.include?(host)
145
- @used_hosts << host
146
- end
147
- end
148
- end
149
-
150
- @projects
151
- end
152
-
153
- def ensure_port(host)
154
- if host =~ /:([\d]+)$/
155
- [ host, $1.to_i ]
156
- else
157
- port = find_unused_port
158
- [ "#{host}:#{port}", port ]
159
- end
160
- end
161
-
162
- def find_unused_port
163
- loop do
164
- port = rand PORT_RANGE
165
- return port unless used_ports.include?(port)
166
- end
167
- end
168
-
169
- def storage_base_dir
170
- if Gem.win_platform?
171
- ENV["APPDATA"] || File.join(Dir.home, "AppData", "Roaming")
83
+ def display_host(host)
84
+ if host.include?("://")
85
+ host
172
86
  else
173
- ENV["XDG_DATA_HOME"] || File.join(Dir.home, ".local", "share")
87
+ "http://#{host}"
174
88
  end
175
89
  end
176
90
 
177
- def storage_dir
178
- File.join(storage_base_dir, APP_NAME)
179
- end
180
-
181
- def projects_file_path
182
- FileUtils.mkdir_p(storage_dir)
183
- File.join(storage_dir, DATA_FILE)
184
- end
91
+ def say_address_conflicts(address)
92
+ if address.port_conflicts
93
+ @conflicts_found = true
185
94
 
186
- def append_to_projects(line)
187
- path_present = File.exist?(projects_file_path) && File.read(projects_file_path).lines.any? do
188
- it.strip == line
95
+ address.port_conflicts.each do |conflicting_address|
96
+ say "╰ Port also at #{conflicting_address.path}:#{conflicting_address.line_number}", :red
97
+ end
189
98
  end
190
99
 
191
- return if path_present
100
+ if address.host_conflicts
101
+ @conflicts_found = true
192
102
 
193
- File.open(projects_file_path, "a") do |f|
194
- f.puts(line)
195
- end
196
- end
197
-
198
- def append_to_dotfile(line)
199
- File.open(DOTFILE, "a") do |f|
200
- f.puts(line)
103
+ address.host_conflicts.each do |conflicting_address|
104
+ say "╰ Host also at #{conflicting_address.path}:#{conflicting_address.line_number}", :red
105
+ end
201
106
  end
202
107
  end
203
108
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: locport
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Starsi
@@ -23,6 +23,62 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '1.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: testerobly
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: debug
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.10'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.10'
26
82
  description: Overview of localhost ports used across projects. Prevent conflicts.
27
83
  email: klevo@klevo.sk
28
84
  executables:
@@ -33,6 +89,7 @@ files:
33
89
  - LICENSE
34
90
  - bin/locport
35
91
  - lib/locport.rb
92
+ - lib/locport/indexer.rb
36
93
  homepage: https://github.com/klevo/locport
37
94
  licenses:
38
95
  - MIT
@@ -44,14 +101,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
44
101
  requirements:
45
102
  - - ">="
46
103
  - !ruby/object:Gem::Version
47
- version: '0'
104
+ version: '3.2'
48
105
  required_rubygems_version: !ruby/object:Gem::Requirement
49
106
  requirements:
50
107
  - - ">="
51
108
  - !ruby/object:Gem::Version
52
109
  version: '0'
53
110
  requirements: []
54
- rubygems_version: 3.7.1
111
+ rubygems_version: 3.7.2
55
112
  specification_version: 4
56
113
  summary: localhost port management
57
114
  test_files: []