kastner / passengerpane forked from alloy/passengerpane

A Mac OS X preference pane for easily configuring Rails applications with Passenger.

This URL has Read+Write access

cehoffman (author)
Tue Apr 21 18:46:11 -0700 2009
Erik Kastner (committer)
Thu Jun 18 14:56:20 -0700 2009
passengerpane / PassengerApplication.rb
100644 264 lines (214 sloc) 7.393 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
require 'osx/cocoa'
include OSX
 
require 'fileutils'
require 'yaml'
require File.expand_path('../passenger_pane_config', __FILE__)
require File.expand_path('../shared_passenger_behaviour', __FILE__)
 
class PassengerApplication < NSObject
  include SharedPassengerBehaviour
  
  CONFIG_UNINSTALLER = File.expand_path('../config_uninstaller.rb', __FILE__)
  CONFIG_INSTALLER = File.expand_path('../config_installer.rb', __FILE__)
  
  RAILS = 'rails'
  RACK = 'rack'
  
  DEVELOPMENT = 0
  PRODUCTION = 1
  
  class << self
    include SharedPassengerBehaviour
    
    def existingApplications
      Dir.glob(File.join(PassengerPaneConfig::PASSENGER_APPS_DIR, "*.#{PassengerPaneConfig::PASSENGER_APPS_EXTENSION}")).map do |app|
        PassengerApplication.alloc.initWithFile(app)
      end
    end
    
    def startApplications(apps)
      data = serializedApplicationsData(apps)
      p "Starting Rails applications:\n#{data}"
      execute '/usr/bin/ruby', CONFIG_INSTALLER, data
      apps.each { |app| app.apply(false) }
    end
    
    def removeApplications(apps)
      data = serializedApplicationsData(apps)
      p "Removing applications: #{data}"
      execute '/usr/bin/ruby', CONFIG_UNINSTALLER, data
    end
    
    def serializedApplicationsData(apps)
      apps.to_ruby.map { |app| app.to_hash }.to_yaml
    end
  end
  
  kvc_accessor :host, :path, :aliases, :dirty, :valid, :revertable, :environment, :bonjour, :valid_bonjour_host
  attr_accessor :user_defined_data, :vhostname
  
  def host_with_bonjour_validation(*args)
    host_without_bonjour_validation(*args)
    
    self.valid_bonjour_host = (@host.to_s =~ /\.local\.?$/)
    
    # Don't want bonjour to be enabled if not valid, also want to renable if originally set while editing
    if !@valid_bonjour_host
      self.bonjour = false
    else
      self.bonjour = @bonjour || @original_values['bonjour']
    end
    
    @host
  end
  alias_method :host_without_bonjour_validation, :host
  alias_method :host, :host_with_bonjour_validation
 
  def init
    if super_init
      @environment = DEVELOPMENT
      
      @new_app = true
      @dirty = @valid = @revertable = false
      @host, @path, @aliases, @user_defined_data = '', '', '', ''
      @vhostname = '*:80'
      @bonjour = false
      @valid_bonjour_host = false
      
      set_original_values!
      self
    end
  end
  
  def initWithFile(file)
    if init
      @new_app = false
      @valid = false
      load_data_from_vhost_file(file)
      set_original_values!
      self
    end
  end
  
  def initWithPath(path)
    if init
      mark_dirty!
      
      @path = path
      set_default_host_from_path(path)
      
      @valid = true
      set_original_values!
      self
    end
  end
  
  def application_type
    @application_type ||= check_application_type
  end
  
  def new_app?; @new_app; end
  def dirty?; @dirty; end
  def valid?; @valid; end
  def revertable?; @revertable; end
  
  def apply(save_config = nil)
    unless @valid
      p "Not applying changes to invalid Rails application: #{@path}"
      return false
    end
    
    p "Applying changes to Rails application: #{@path}"
    (@new_app ? start : restart) unless save_config == false
    # todo: check if it went ok before assuming so.
    @new_app = self.dirty = self.valid = false
 
    true
  end
  
  def start
    p "Starting Rails application: #{@path}"
    save_config!
  end
  
  def restart(sender = nil)
    p "Restarting Rails application: #{@path}"
    if @host != @original_values['host'] || @aliases != @original_values['aliases']
      execute('/usr/bin/ruby', CONFIG_UNINSTALLER, [@original_values].to_yaml)
    end
    save_config! if @dirty
    
    tmp_dir = File.join(@path, 'tmp')
    FileUtils.mkdir(tmp_dir) unless File.exist?(tmp_dir)
    Kernel.system("/usr/bin/touch '#{File.join(tmp_dir, 'restart.txt')}'")
  end
  
  def revert(sender = nil)
    @original_values.each do |key, value|
      send "#{key}=", value
    end
    self.valid = self.dirty = self.revertable = false
  end
  
  def reload!
    return if new_app? || (@mtime && @mtime >= File.mtime(config_path))
    load_data_from_vhost_file
    mark_dirty! if values_changed_after_load?
    set_original_values!
    self.valid = true
  end
  
  def save_config!
    p "Saving configuration: #{config_path}"
    execute '/usr/bin/ruby', CONFIG_INSTALLER, [to_hash].to_yaml
    set_original_values!
  end
  
  def config_path
    File.join(PassengerPaneConfig::PASSENGER_APPS_DIR, "#{@original_values['host'] || @host}.#{PassengerPaneConfig::PASSENGER_APPS_EXTENSION}")
  end
  
  def rbSetValue_forKey(value, key)
    super
    self.revertable = true
    mark_dirty!
    @custom_environment = nil if key == 'environment'
    set_default_host_from_path(@path) if key == 'path' && (@host.nil? || @host.empty?) && (!@path.nil? && !@path.empty?)
    self.valid = (!@host.nil? && !@host.empty? && !@path.nil? && !@path.empty?)
  end
  
  def mark_dirty!
    self.dirty = true
    PrefPanePassenger.sharedInstance.applicationMarkedDirty self
  end
  
  def to_hash
    @user_defined_data = " <directory \"#{File.join(@path.to_s, 'public')}\">\n Order allow,deny\n Allow from all\n </directory>"
    {
      'app_type' => application_type,
      'config_path' => config_path,
      'host' => @host.to_s,
      'aliases' => @aliases.to_s,
      'path' => @path.to_s,
      'environment' => (@environment.nil? ? @custom_environment : (@environment == DEVELOPMENT ? 'development' : 'production')),
      'vhostname' => @vhostname,
'bonjour' => @bonjour,
      'user_defined_data' => @user_defined_data
    }
  end
  
  private
  
  def check_application_type
    env_file = File.join(@path, 'config', 'environment.rb')
    (File.exist?(env_file) and File.read(env_file) =~ /Rails::Initializer/) ? RAILS : RACK
  end
  
  def load_data_from_vhost_file(file = config_path)
    @mtime = File.mtime(file)
    data = File.read(file).strip
    
    data.gsub!(/\n\s*ServerName\s+(.+)/, '')
    self.host = $1
    
    data.gsub!(/\n\s*ServerAlias\s+(.+)/, '')
    self.aliases = $1 || ''
    
    data.gsub!(/\n\s*DocumentRoot\s+"(.+)\/public"/, '')
    self.path = $1
    
    data.gsub!(/\n\s*(Rails|Rack)Env\s+(\w+)/, '')
    if %w{ development production }.include?($2)
      self.environment = ($2 == 'development' ? DEVELOPMENT : PRODUCTION)
    else
      self.environment = nil
      @custom_environment = $2
    end
    
    data.gsub!(/<VirtualHost\s(.+?)>/, '')
    self.vhostname = $1
    
data.gsub!(/(# publish on bonjour)/, '')
self.bonjour = ($1) ? true : false
 
    data.gsub!(/\s*<\/VirtualHost>\n*/, '').gsub!(/^\n*/, '')
    @user_defined_data = data
  end
  
  def values_changed_after_load?
    @original_values.any? do |key, value|
      # user_defined_data and aliases can be empty
      if %{ user_defined_data aliases }.include?(key) && (value.nil? || value.empty?)
        false
      else
        send(key) != value
      end
    end
  end
  
  def set_original_values!
    @original_values = {
      'host' => @host,
      'aliases' => @aliases,
      'path' => @path,
      'environment' => @custom_environment || @environment,
      'bonjour' => @bonjour,
      'user_defined_data' => @user_defined_data
    }
  end
  
  def set_default_host_from_path(path)
    self.host = "#{File.basename(path).downcase.gsub('_','-')}.local"
  end
end