hell 0.0.1
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.
- data/.document +5 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +108 -0
- data/Rakefile +38 -0
- data/VERSION +1 -0
- data/bin/hell +12 -0
- data/lib/hell.rb +0 -0
- data/lib/hell/app.rb +128 -0
- data/lib/hell/config.ru +4 -0
- data/lib/hell/hell.rb +0 -0
- data/lib/hell/lib/helpers.rb +119 -0
- data/lib/hell/lib/monkey_patch.rb +79 -0
- data/lib/hell/public/assets/css/bootstrap-responsive.css +1088 -0
- data/lib/hell/public/assets/css/bootstrap-responsive.min.css +9 -0
- data/lib/hell/public/assets/css/bootstrap.css +5893 -0
- data/lib/hell/public/assets/css/bootstrap.min.css +9 -0
- data/lib/hell/public/assets/css/hell.css +158 -0
- data/lib/hell/public/assets/ico/favicon.ico +0 -0
- data/lib/hell/public/assets/ico/favicon.png +0 -0
- data/lib/hell/public/assets/img/glyphicons-halflings-white.png +0 -0
- data/lib/hell/public/assets/img/glyphicons-halflings.png +0 -0
- data/lib/hell/public/assets/js/backbone-localstorage.js +84 -0
- data/lib/hell/public/assets/js/backbone.js +1431 -0
- data/lib/hell/public/assets/js/backbone.min.js +38 -0
- data/lib/hell/public/assets/js/bankersbox.js +768 -0
- data/lib/hell/public/assets/js/bootstrap.growl.js +2 -0
- data/lib/hell/public/assets/js/bootstrap.js +2025 -0
- data/lib/hell/public/assets/js/bootstrap.min.js +6 -0
- data/lib/hell/public/assets/js/hashchange.min.js +9 -0
- data/lib/hell/public/assets/js/hell.js +444 -0
- data/lib/hell/public/assets/js/jquery.min.js +4 -0
- data/lib/hell/public/assets/js/timeago.js +152 -0
- data/lib/hell/public/assets/js/underscore.min.js +1 -0
- data/lib/hell/views/index.erb +146 -0
- data/test/helper.rb +18 -0
- data/test/test_hell.rb +7 -0
- metadata +201 -0
data/.document
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Jose Diaz-Gonzalez
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# Hell
|
2
|
+
|
3
|
+
An interactive web ui for Capistrano that also provides a json api
|
4
|
+
|
5
|
+

|
6
|
+
|
7
|
+
## Requirements
|
8
|
+
|
9
|
+
- Ruby 1.9.2
|
10
|
+
- Bundler
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Simply add `hell` as a dependency in your Gemfile:
|
15
|
+
|
16
|
+
source 'http://rubygems.org'
|
17
|
+
source 'http://gems.github.com'
|
18
|
+
|
19
|
+
gem 'hell'
|
20
|
+
|
21
|
+
And then run the following in your app directory:
|
22
|
+
|
23
|
+
bundle install
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Once installed, you'll want to run the `hell` binary:
|
28
|
+
|
29
|
+
hell
|
30
|
+
|
31
|
+
Note that if you have multiple capistrano versions installed for multiple repositories, you may need to instead use:
|
32
|
+
|
33
|
+
bundle exec hell
|
34
|
+
|
35
|
+
Then open `localhost:4567` in your browser to get an interactive interface around your capistrano recipes.
|
36
|
+
|
37
|
+
### Running commands
|
38
|
+
|
39
|
+
Hell provides an autocompleted list of all capistrano tasks you have setup, and you can run this against a specific environment as necessary. By default, hell will start the command in the background, and then use an `EventSource` to read the generated logfile.
|
40
|
+
|
41
|
+
When you run a task, that task is added to your history so that you may re-read the logs at a later date, as well as re-run the tasks as necessary. This history is done using `localstorage`, and as such is not shared amongst different users of your application.
|
42
|
+
|
43
|
+
Note that Hell does not provide persistence of generated tasks, nor does it perform locking of individual tasks. For now, suggest using tools such as [Logstash](http://logstash.net/) to persist the logs, and using locking techniques within your capistrano tooling instead.
|
44
|
+
|
45
|
+
|
46
|
+
### Capistrano as an API
|
47
|
+
|
48
|
+
Inspired by Github's internal [Heaven](https://github.com/blog/1241-deploying-at-github) app, Hell provides a simple, rest-like json api around Capistrano. XML-RPC implementation to come.
|
49
|
+
|
50
|
+
Available endpoints:
|
51
|
+
|
52
|
+
- `/tasks`: List all available tasks
|
53
|
+
- `/tasks/search/:pattern`: Search for a given, non-regex pattern
|
54
|
+
- `/tasks/:name/background`: Kicks off the execution of a task in the background and writes the output to a log file. Will respond with an id for future log file recovery.
|
55
|
+
- `/tasks/:name/exists`: Checks if a task exists
|
56
|
+
- `/tasks/:name/execute`: Kicks off the execution of a task and responds with the results using the sinatra streaming api. EventSource-compatible.
|
57
|
+
- `/logs/:id/tail`: Using the id provided by `/tasks/:name/background`, will start a sinatra stream on the log file in question.
|
58
|
+
- `/logs/:id/view`: Using the id provided by `/tasks/:name/background`, will output the current contents of a logfile. Useful for later recovery of the logs through the web api.
|
59
|
+
|
60
|
+
Note that the current response is subject to change, and as such is not documented, though we will attempt to augment rather than change them.
|
61
|
+
|
62
|
+
### Configuration
|
63
|
+
|
64
|
+
The following environment variables are available for your use:
|
65
|
+
|
66
|
+
- `HELL_APP_ROOT`: Path from which capistrano should execute. Defaults to `Dir.pwd`.
|
67
|
+
- `HELL_ENVIRONMENTS`: Comma-separated list of environments, Default: `production,staging`.
|
68
|
+
- `HELL_REQUIRE_ENV`: Whether or not to require specifying an environment. Default: `1`.
|
69
|
+
- `HELL_LOG_PATH`: Path to which logs should be written to. Defaults to `Dir.pwd + '/log'`.
|
70
|
+
- `HELL_BASE_DIR`: Base directory to use in web ui. Useful for subdirectories. Defaults to `/`.
|
71
|
+
- `HELL_SENTINEL_STRINGS`: Sentinel string used to denote the end of a task run. Defaults to `Hellish Task Completed`.
|
72
|
+
|
73
|
+
## TODO
|
74
|
+
|
75
|
+
* ~~Finish the execute task so that it sends output to the browser~~
|
76
|
+
* ~~Figure out where/how to store deploy logs on disk~~
|
77
|
+
* ~~Blacklist tasks from being displayed~~
|
78
|
+
* ~~Add support for environment variables~~
|
79
|
+
* ~~Add support for deployment environments~~
|
80
|
+
* ~~Add support for ad-hoc deploy callbacks~~
|
81
|
+
* Save favorite commands as buttons
|
82
|
+
* Save defaults in a cookie
|
83
|
+
* Use a slide-up element for task output
|
84
|
+
* Add optional task locking so that deploys cannot interfere with one-another
|
85
|
+
* Add ability to use pusher instead of sinatra streaming
|
86
|
+
|
87
|
+
## License
|
88
|
+
|
89
|
+
Copyright (c) 2012 Jose Diaz-Gonzalez
|
90
|
+
|
91
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
92
|
+
a copy of this software and associated documentation files (the
|
93
|
+
"Software"), to deal in the Software without restriction, including
|
94
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
95
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
96
|
+
permit persons to whom the Software is furnished to do so, subject to
|
97
|
+
the following conditions:
|
98
|
+
|
99
|
+
The above copyright notice and this permission notice shall be
|
100
|
+
included in all copies or substantial portions of the Software.
|
101
|
+
|
102
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
103
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
104
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
105
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
106
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
107
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
108
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
|
6
|
+
begin
|
7
|
+
Bundler.setup(:default, :development)
|
8
|
+
rescue Bundler::BundlerError => e
|
9
|
+
$stderr.puts e.message
|
10
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
11
|
+
exit e.status_code
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'rake'
|
15
|
+
require 'jeweler'
|
16
|
+
|
17
|
+
Jeweler::Tasks.new do |gem|
|
18
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
19
|
+
gem.name = "hell"
|
20
|
+
gem.homepage = "http://github.com/seatgeek/hell"
|
21
|
+
gem.license = "MIT"
|
22
|
+
gem.summary = %Q{A web interface and api wrapper around Capistrano}
|
23
|
+
gem.description = %Q{Hell is an open source web interface that exposes a set of capistrano recipes as a json api, for usage within large teams}
|
24
|
+
gem.email = "jose@seatgeek.com"
|
25
|
+
gem.authors = ["Jose Diaz-Gonzalez"]
|
26
|
+
# dependencies defined in Gemfile
|
27
|
+
end
|
28
|
+
Jeweler::RubygemsDotOrgTasks.new
|
29
|
+
|
30
|
+
require 'rdoc/task'
|
31
|
+
Rake::RDocTask.new do |rdoc|
|
32
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
33
|
+
|
34
|
+
rdoc.rdoc_dir = 'rdoc'
|
35
|
+
rdoc.title = "hell #{version}"
|
36
|
+
rdoc.rdoc_files.include('README*')
|
37
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
38
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/bin/hell
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
begin
|
4
|
+
require 'hell/app.rb'
|
5
|
+
rescue LoadError => e
|
6
|
+
require 'rubygems'
|
7
|
+
path = File.expand_path '../../lib', __FILE__
|
8
|
+
$:.unshift(path) if File.directory?(path) && !$:.include?(path)
|
9
|
+
require 'hell/app.rb'
|
10
|
+
end
|
11
|
+
|
12
|
+
Hell::App.run!
|
data/lib/hell.rb
ADDED
File without changes
|
data/lib/hell/app.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
|
3
|
+
require 'sinatra'
|
4
|
+
require 'sinatra/base'
|
5
|
+
require 'sinatra/json'
|
6
|
+
require 'sinatra/streaming'
|
7
|
+
|
8
|
+
require 'json'
|
9
|
+
require 'securerandom'
|
10
|
+
require 'websocket'
|
11
|
+
|
12
|
+
HELL_DIR = Dir.pwd
|
13
|
+
APP_ROOT = ENV.fetch('HELL_APP_ROOT', HELL_DIR)
|
14
|
+
ENVIRONMENTS = ENV.fetch('HELL_ENVIRONMENTS', 'production,staging').split(',')
|
15
|
+
BLACKLIST = ['invoke', 'shell', 'internal:ensure_env', 'internal:setup_env']
|
16
|
+
REQUIRE_ENV = ENV.fetch('HELL_REQUIRE_ENV', '1') == '1'
|
17
|
+
HELL_LOG_PATH = ENV.fetch('HELL_LOG_PATH', File.join(HELL_DIR, 'log'))
|
18
|
+
HELL_BASE_DIR = ENV.fetch('HELL_BASE_DIR', '/')
|
19
|
+
SENTINEL_STRINGS = ENV.fetch('HELL_SENTINEL_STRINGS', 'Hellish Task Completed').split(',')
|
20
|
+
|
21
|
+
require 'hell/lib/monkey_patch'
|
22
|
+
require 'hell/lib/helpers'
|
23
|
+
|
24
|
+
|
25
|
+
module Hell
|
26
|
+
class App < Sinatra::Base
|
27
|
+
helpers Sinatra::JSON
|
28
|
+
helpers Sinatra::Streaming
|
29
|
+
helpers Hell::Helpers
|
30
|
+
|
31
|
+
cap = Capistrano::CLI.parse(["-T"])
|
32
|
+
|
33
|
+
set :public_folder, File.join(File.expand_path('..', __FILE__), 'public')
|
34
|
+
set :root, HELL_DIR
|
35
|
+
set :server, :thin
|
36
|
+
set :static, true
|
37
|
+
set :views, File.join(File.expand_path('..', __FILE__), 'views')
|
38
|
+
|
39
|
+
configure :production, :development do
|
40
|
+
enable :logging
|
41
|
+
end
|
42
|
+
|
43
|
+
get '/' do
|
44
|
+
@tasks = cap.task_index.keys
|
45
|
+
@require_env = REQUIRE_ENV
|
46
|
+
@www_base_dir = HELL_BASE_DIR
|
47
|
+
@environments = ENVIRONMENTS
|
48
|
+
erb :index
|
49
|
+
end
|
50
|
+
|
51
|
+
get '/tasks' do
|
52
|
+
tasks = cap.task_index
|
53
|
+
json tasks
|
54
|
+
end
|
55
|
+
|
56
|
+
get '/tasks/search/:pattern' do
|
57
|
+
tasks = cap.task_index(params[:pattern])
|
58
|
+
json tasks
|
59
|
+
end
|
60
|
+
|
61
|
+
get '/tasks/:name/exists' do
|
62
|
+
tasks = cap.task_index(params[:name], {:exact => true})
|
63
|
+
response = { :exists => !tasks.empty?, :task => params[:name]}
|
64
|
+
json response
|
65
|
+
end
|
66
|
+
|
67
|
+
get '/tasks/:name/background' do
|
68
|
+
tasks, original_cmd = verify_task(cap, params[:name])
|
69
|
+
verbose = ""
|
70
|
+
verbose = "LOGGING=debug" if params[:verbose] == true
|
71
|
+
|
72
|
+
task_id = run_in_background!("bundle exec cap -l STDOUT %s %s" % [original_cmd, verbose]) unless tasks.empty?
|
73
|
+
response = {}
|
74
|
+
response[:status] = tasks.empty? ? 404 : 200,
|
75
|
+
response[:message] = tasks.empty? ? "Task not found" : "Running task in background",
|
76
|
+
response[:task_id] = task_id unless tasks.empty?
|
77
|
+
json response
|
78
|
+
end
|
79
|
+
|
80
|
+
get '/logs/:id/tail' do
|
81
|
+
content_type "text/event-stream"
|
82
|
+
if valid_log params[:id]
|
83
|
+
_stream_success("tail -f %s" % File.join(HELL_LOG_PATH, params[:id] + ".log"))
|
84
|
+
else
|
85
|
+
_stream_error("log file '#{params[:id]}' not found")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
get '/logs/:id/view' do
|
90
|
+
log_path = File.join(HELL_LOG_PATH, params[:id] + ".log")
|
91
|
+
logger.info log_path
|
92
|
+
ansi_escape(File.read(log_path))
|
93
|
+
end
|
94
|
+
|
95
|
+
get '/tasks/:name/execute' do
|
96
|
+
tasks, original_cmd = verify_task(cap, params[:name])
|
97
|
+
content_type "text/event-stream"
|
98
|
+
if tasks.empty?
|
99
|
+
_stream_error("cap task '#{original_cmd}' not found")
|
100
|
+
else
|
101
|
+
_stream_success("bundle exec cap -l STDOUT #{original_cmd} LOGGING=debug 2>&1", {:prepend => true})
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def _stream_error(message)
|
106
|
+
stream do |out|
|
107
|
+
out << "event: start\ndata:\n\n" unless out.closed?
|
108
|
+
out << "data: " + ws_message("<p>#{message}</p>") unless out.closed?
|
109
|
+
out << "event: end\ndata:\n\n" unless out.closed?
|
110
|
+
out.close
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def _stream_success(command, opts = {})
|
115
|
+
opts = {:prepend => false}.merge(opts)
|
116
|
+
stream do |out|
|
117
|
+
out << "event: start\ndata:\n\n" unless out.closed?
|
118
|
+
out << "data: " + ws_message("<p>#{command}</p>") unless out.closed? or opts[:prepend] == false
|
119
|
+
IO.popen(command, 'rb') do |io|
|
120
|
+
io.each do |line|
|
121
|
+
process_line(line, out, io)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
close_stream(out)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/hell/config.ru
ADDED
data/lib/hell/hell.rb
ADDED
File without changes
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Hell
|
2
|
+
class TailDone < StandardError; end
|
3
|
+
|
4
|
+
module Helpers
|
5
|
+
def escape_to_html(data)
|
6
|
+
{
|
7
|
+
1 => :nothing,
|
8
|
+
2 => :nothing,
|
9
|
+
4 => :nothing,
|
10
|
+
5 => :nothing,
|
11
|
+
7 => :nothing,
|
12
|
+
8 => :backspace,
|
13
|
+
30 => "#303030",
|
14
|
+
31 => "#D10915",
|
15
|
+
32 => "#53A948",
|
16
|
+
33 => "#CD7D3D",
|
17
|
+
34 => "#3582E0",
|
18
|
+
35 => :magenta,
|
19
|
+
36 => "#30EFEF",
|
20
|
+
37 => :white,
|
21
|
+
40 => :nothing,
|
22
|
+
41 => :nothing,
|
23
|
+
43 => :nothing,
|
24
|
+
44 => :nothing,
|
25
|
+
45 => :nothing,
|
26
|
+
46 => :nothing,
|
27
|
+
47 => :nothing,
|
28
|
+
}.each do |key, value|
|
29
|
+
if value == :nothing
|
30
|
+
data.gsub!(/\e\[#{key}m/,"<span>")
|
31
|
+
elsif value == :backspace
|
32
|
+
data.gsub!(/.[\b]/, '')
|
33
|
+
else
|
34
|
+
data.gsub!(/\e\[#{key}m/,"<span style=\"color:#{value}\">")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
data.gsub!(/\e\[0m/, '</span>')
|
38
|
+
data.gsub!(/\e\[0/, '</span>')
|
39
|
+
# data.gsub!(' ', ' ')
|
40
|
+
data
|
41
|
+
end
|
42
|
+
|
43
|
+
def ansi_escape(message)
|
44
|
+
escape_to_html(utf8_dammit(message))
|
45
|
+
end
|
46
|
+
|
47
|
+
def ws_message(message)
|
48
|
+
message = {:message => ansi_escape(message)}.to_json + "\n\n"
|
49
|
+
end
|
50
|
+
|
51
|
+
def process_line(line, out, io)
|
52
|
+
begin
|
53
|
+
out << "data: " + ws_message(line) unless out.closed?
|
54
|
+
raise TailDone if SENTINEL_STRINGS.any? { |w| line =~ /#{w}/ }
|
55
|
+
rescue
|
56
|
+
Process.kill("KILL", io.pid)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def close_stream(out)
|
61
|
+
out << "event: end\ndata:\n\n" unless out.closed?
|
62
|
+
out.close
|
63
|
+
end
|
64
|
+
|
65
|
+
def run_in_background!(background_cmd)
|
66
|
+
log_file = Time.now.to_i.to_s + '.' + SecureRandom.hex(2)
|
67
|
+
cmd = [
|
68
|
+
"cd #{APP_ROOT}",
|
69
|
+
"echo '#{background_cmd}' >> #{HELL_LOG_PATH}/#{log_file}.log 2>&1",
|
70
|
+
"#{background_cmd} >> #{HELL_LOG_PATH}/#{log_file}.log 2>&1",
|
71
|
+
"echo 'Hellish Task Completed' >> #{HELL_LOG_PATH}/#{log_file}.log 2>&1",
|
72
|
+
].join(" && ")
|
73
|
+
system("sh -c \"#{cmd}\" &")
|
74
|
+
|
75
|
+
# Wait up to three seconds in case of server load
|
76
|
+
i = 0
|
77
|
+
while i < 3
|
78
|
+
i += 1
|
79
|
+
break if File.exists?(File.join(HELL_LOG_PATH, log_file + ".log"))
|
80
|
+
sleep 1
|
81
|
+
end
|
82
|
+
|
83
|
+
log_file
|
84
|
+
end
|
85
|
+
|
86
|
+
def verify_task(cap, name)
|
87
|
+
original_cmd = name.gsub('+', ' ')
|
88
|
+
cmd = original_cmd.split(' ')
|
89
|
+
cmd.shift if ENVIRONMENTS.include?(cmd.first)
|
90
|
+
cmd = cmd.join(' ')
|
91
|
+
|
92
|
+
tasks = cap.task_index(cmd, {:exact => true})
|
93
|
+
return tasks, original_cmd
|
94
|
+
end
|
95
|
+
|
96
|
+
def valid_log(id)
|
97
|
+
File.exists?(File.join(HELL_LOG_PATH, id + ".log"))
|
98
|
+
end
|
99
|
+
|
100
|
+
def utf8_dammit(s)
|
101
|
+
# Converting ASCII-8BIT to UTF-8 based domain-specific guesses
|
102
|
+
if s.is_a? String
|
103
|
+
begin
|
104
|
+
# Try it as UTF-8 directly
|
105
|
+
cleaned = s.dup.force_encoding('UTF-8')
|
106
|
+
unless cleaned.valid_encoding?
|
107
|
+
# Some of it might be old Windows code page
|
108
|
+
cleaned = s.encode( 'UTF-8', 'Windows-1252' )
|
109
|
+
end
|
110
|
+
s = cleaned
|
111
|
+
rescue EncodingError
|
112
|
+
# Force it to UTF-8, throwing out invalid bits
|
113
|
+
s.encode!('UTF-8', :invalid => :replace, :undef => :replace)
|
114
|
+
end
|
115
|
+
s
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|