hell 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![http://cl.ly/image/0R2H1J0t0c35](http://cl.ly/image/0R2H1J0t0c35/s10e11_480.jpg)
|
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
|