├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Changelog.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── foreman └── foreman-runner ├── data ├── example │ ├── .profile.d │ │ └── foo.sh │ ├── Procfile │ ├── Procfile.without_colon │ ├── error │ ├── log │ │ └── neverdie.log │ ├── spawnee │ ├── spawner │ ├── ticker │ └── utf8 └── export │ ├── bluepill │ └── master.pill.erb │ ├── daemon │ ├── master.conf.erb │ ├── process.conf.erb │ └── process_master.conf.erb │ ├── launchd │ └── launchd.plist.erb │ ├── runit │ ├── log │ │ └── run.erb │ └── run.erb │ ├── supervisord │ └── app.conf.erb │ ├── systemd │ ├── master.target.erb │ └── process.service.erb │ └── upstart │ ├── master.conf.erb │ ├── process.conf.erb │ └── process_master.conf.erb ├── dist └── gem.rake ├── foreman.gemspec ├── lib ├── foreman.rb └── foreman │ ├── cli.rb │ ├── distribution.rb │ ├── engine.rb │ ├── engine │ └── cli.rb │ ├── env.rb │ ├── export.rb │ ├── export │ ├── base.rb │ ├── bluepill.rb │ ├── daemon.rb │ ├── inittab.rb │ ├── launchd.rb │ ├── runit.rb │ ├── supervisord.rb │ ├── systemd.rb │ └── upstart.rb │ ├── helpers.rb │ ├── process.rb │ ├── procfile.rb │ ├── vendor │ └── thor │ │ └── lib │ │ ├── thor.rb │ │ └── thor │ │ ├── actions.rb │ │ ├── actions │ │ ├── create_file.rb │ │ ├── create_link.rb │ │ ├── directory.rb │ │ ├── empty_directory.rb │ │ ├── file_manipulation.rb │ │ └── inject_into_file.rb │ │ ├── base.rb │ │ ├── command.rb │ │ ├── core_ext │ │ ├── hash_with_indifferent_access.rb │ │ ├── io_binary_read.rb │ │ └── ordered_hash.rb │ │ ├── error.rb │ │ ├── group.rb │ │ ├── invocation.rb │ │ ├── line_editor.rb │ │ ├── line_editor │ │ ├── basic.rb │ │ └── readline.rb │ │ ├── parser.rb │ │ ├── parser │ │ ├── argument.rb │ │ ├── arguments.rb │ │ ├── option.rb │ │ └── options.rb │ │ ├── rake_compat.rb │ │ ├── runner.rb │ │ ├── shell.rb │ │ ├── shell │ │ ├── basic.rb │ │ ├── color.rb │ │ └── html.rb │ │ ├── util.rb │ │ └── version.rb │ └── version.rb ├── man ├── foreman.1 └── foreman.1.ronn ├── pkg └── .gitignore ├── spec ├── foreman │ ├── cli_spec.rb │ ├── engine_spec.rb │ ├── export │ │ ├── base_spec.rb │ │ ├── bluepill_spec.rb │ │ ├── daemon_spec.rb │ │ ├── inittab_spec.rb │ │ ├── launchd_spec.rb │ │ ├── runit_spec.rb │ │ ├── supervisord_spec.rb │ │ ├── systemd_spec.rb │ │ └── upstart_spec.rb │ ├── export_spec.rb │ ├── helpers_spec.rb │ ├── process_spec.rb │ └── procfile_spec.rb ├── foreman_spec.rb ├── helper_spec.rb ├── resources │ ├── .env │ ├── Procfile │ ├── Procfile.bad │ ├── bin │ │ ├── echo │ │ ├── env │ │ ├── test │ │ └── utf8 │ └── export │ │ ├── bluepill │ │ ├── app-concurrency.pill │ │ └── app.pill │ │ ├── daemon │ │ ├── app-alpha-1.conf │ │ ├── app-alpha-2.conf │ │ ├── app-alpha.conf │ │ ├── app-bravo-1.conf │ │ ├── app-bravo.conf │ │ └── app.conf │ │ ├── inittab │ │ ├── inittab.concurrency │ │ └── inittab.default │ │ ├── launchd │ │ ├── launchd-a.default │ │ ├── launchd-b.default │ │ └── launchd-c.default │ │ ├── runit │ │ ├── app-alpha-1 │ │ │ ├── log │ │ │ │ └── run │ │ │ └── run │ │ ├── app-alpha-2 │ │ │ ├── log │ │ │ │ └── run │ │ │ └── run │ │ └── app-bravo-1 │ │ │ ├── log │ │ │ └── run │ │ │ └── run │ │ ├── supervisord │ │ ├── app-alpha-1.conf │ │ └── app-alpha-2.conf │ │ ├── systemd │ │ ├── app-alpha.1.service │ │ ├── app-alpha.2.service │ │ ├── app-alpha.target │ │ ├── app-bravo.1.service │ │ ├── app-bravo.target │ │ └── app.target │ │ └── upstart │ │ ├── app-alpha-1.conf │ │ ├── app-alpha-2.conf │ │ ├── app-alpha.conf │ │ ├── app-bravo-1.conf │ │ ├── app-bravo.conf │ │ └── app.conf └── spec_helper.rb └── tasks ├── dist.rake ├── release.rake ├── rspec.rake └── vendor.rake /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | - push 4 | - pull_request 5 | permissions: 6 | contents: read 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | ruby: 14 | - "2.3" 15 | - "2.4" 16 | - "2.5" 17 | - "2.6" 18 | - "2.7" 19 | - "3.0" 20 | - "3.1" 21 | - "3.2" 22 | env: 23 | BUNDLE_WITHOUT: development 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | - name: Set up Ruby 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 32 | rubygems: latest # runs 'gem update --system' 33 | - name: Run test 34 | run: bundle exec rake spec 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /.env 3 | /coverage 4 | /man/*.html 5 | /man/*.markdown 6 | /pkg/ 7 | /vendor 8 | Gemfile.lock 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'thor', '0.19.4', :require => false 6 | 7 | group :test do 8 | gem 'rake' 9 | gem 'fakefs' 10 | gem 'rspec', '~> 3.5' 11 | gem "simplecov", :require => false 12 | gem 'timecop' 13 | end 14 | 15 | group :development do 16 | gem 'aws-s3' 17 | gem 'ronn-ng' 18 | gem 'yard', '~> 0.9.11' 19 | gem 'automatiek' 20 | end 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 David Dollar 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foreman 2 | 3 | [![CI](https://github.com/ddollar/foreman/actions/workflows/ci.yml/badge.svg)](https://github.com/ddollar/foreman/actions/workflows/ci.yml) 4 | 5 | Manage Procfile-based applications 6 | 7 | ## Installation 8 | 9 | $ gem install foreman 10 | 11 | Ruby users should take care _not_ to install foreman in their project's `Gemfile`. See this [wiki article](https://github.com/ddollar/foreman/wiki/Don't-Bundle-Foreman) for more details. 12 | 13 | ## Getting Started 14 | 15 | - http://blog.daviddollar.org/2011/05/06/introducing-foreman.html 16 | 17 | ## Supported Ruby versions 18 | 19 | See [ci.yml](.github/workflows/ci.yml) for a list of Ruby versions against which Foreman is tested. 20 | 21 | ## Documentation 22 | 23 | - [man page](http://ddollar.github.io/foreman/) 24 | - [wiki](https://github.com/ddollar/foreman/wiki) 25 | - [changelog](https://github.com/ddollar/foreman/blob/main/Changelog.md) 26 | 27 | ## Ports 28 | 29 | - [forego](https://github.com/ddollar/forego) - Go 30 | - [node-foreman](https://github.com/strongloop/node-foreman) - Node.js 31 | - [gaffer](https://github.com/jingweno/gaffer) - Java/JVM 32 | - [goreman](https://github.com/mattn/goreman) - Go 33 | - [honcho](https://github.com/nickstenning/honcho) - python 34 | - [proclet](https://github.com/kazeburo/Proclet) - Perl 35 | - [shoreman](https://github.com/chrismytton/shoreman) - shell 36 | - [crank](https://github.com/arktisklada/crank) - Crystal 37 | - [houseman](https://github.com/fujimura/houseman) - Haskell 38 | - [spm](https://github.com/bytegust/spm) - Go 39 | 40 | ## Authors 41 | 42 | #### Created and maintained by 43 | 44 | David Dollar 45 | 46 | #### Patches contributed by 47 | 48 | [Contributor List](https://github.com/ddollar/foreman/contributors) 49 | 50 | ## License 51 | 52 | Foreman is licensed under the MIT license. 53 | 54 | See LICENSE for the full license text. 55 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path("../lib", __FILE__) 2 | require "foreman" 3 | 4 | require "bundler/setup" 5 | 6 | Dir[File.expand_path("../tasks/*.rake", __FILE__)].each do |task| 7 | load task 8 | end 9 | -------------------------------------------------------------------------------- /bin/foreman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.expand_path("../../lib", __FILE__) 4 | 5 | require "foreman/cli" 6 | 7 | Foreman::CLI.start 8 | -------------------------------------------------------------------------------- /bin/foreman-runner: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | #/ Usage: foreman-runner [-d ] [-p] [...] 4 | #/ 5 | #/ Run a command with exec, optionally changing directory first 6 | 7 | set -e 8 | 9 | error() { 10 | echo $@ >&2 11 | exit 1 12 | } 13 | 14 | usage() { 15 | cat $0 | grep '^#/' | cut -c4- 16 | exit 17 | } 18 | 19 | read_profile="" 20 | 21 | while getopts ":hd:p" OPT; do 22 | case $OPT in 23 | d) cd "$OPTARG" ;; 24 | p) read_profile="1" ;; 25 | h) usage ;; 26 | \?) error "invalid option: -$OPTARG" ;; 27 | :) error "option -$OPTARG requires an argument" ;; 28 | esac 29 | done 30 | 31 | shift $((OPTIND-1)) 32 | 33 | [ -z "$1" ] && usage 34 | 35 | if [ "$read_profile" = "1" ]; then 36 | if [ -f .profile ]; then 37 | . ./.profile 38 | fi 39 | fi 40 | 41 | exec "$@" 42 | -------------------------------------------------------------------------------- /data/example/.profile.d/foo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export FOO=bar 3 | -------------------------------------------------------------------------------- /data/example/Procfile: -------------------------------------------------------------------------------- 1 | ticker: ruby ./ticker $PORT 2 | error: ruby ./error 3 | utf8: ruby ./utf8 4 | spawner: ./spawner 5 | -------------------------------------------------------------------------------- /data/example/Procfile.without_colon: -------------------------------------------------------------------------------- 1 | ticker ./ticker $PORT 2 | error ./error -------------------------------------------------------------------------------- /data/example/error: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $stdout.sync = true 4 | 5 | puts "will error in 10s" 6 | sleep 5 7 | raise "Dying" 8 | -------------------------------------------------------------------------------- /data/example/log/neverdie.log: -------------------------------------------------------------------------------- 1 | tick 2 | tick 3 | ./never_die:6:in `sleep': Interrupt 4 | from ./never_die:6 5 | -------------------------------------------------------------------------------- /data/example/spawnee: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | NAME="$1" 4 | 5 | sigterm() { 6 | echo "$NAME: got sigterm" 7 | } 8 | 9 | #trap sigterm SIGTERM 10 | 11 | while true; do 12 | echo "$NAME: ping $$" 13 | sleep 1 14 | done 15 | -------------------------------------------------------------------------------- /data/example/spawner: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ./spawnee A & 4 | ./spawnee B & 5 | ./spawnee C & 6 | 7 | wait 8 | -------------------------------------------------------------------------------- /data/example/ticker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $stdout.sync = true 4 | 5 | %w( SIGINT SIGTERM ).each do |signal| 6 | trap(signal) do 7 | puts "received #{signal} but i'm ignoring it!" 8 | end 9 | end 10 | 11 | while true 12 | puts "tick: #{ARGV.inspect} -- FOO:#{ENV["FOO"]}" 13 | sleep 1 14 | end 15 | -------------------------------------------------------------------------------- /data/example/utf8: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: BINARY 3 | 4 | $stdout.sync = true 5 | 6 | while true 7 | puts "\u65e5\u672c\u8a9e\u6587\u5b57\u5217" 8 | puts "\u0915\u0932\u094d\u0907\u0928\u0643\u0637\u0628\u041a\u0430\u043b\u0438\u043d\u0430" 9 | puts "\xff\x03" 10 | sleep 1 11 | end 12 | -------------------------------------------------------------------------------- /data/export/bluepill/master.pill.erb: -------------------------------------------------------------------------------- 1 | Bluepill.application("<%= app %>", :foreground => false, :log_file => "/var/log/bluepill.log") do |app| 2 | 3 | app.uid = "<%= user %>" 4 | app.gid = "<%= user %>" 5 | 6 | <% engine.each_process do |name, process| %> 7 | <% 1.upto(engine.formation[name]) do |num| %> 8 | <% port = engine.port_for(process, num) %> 9 | app.process("<%= name %>-<%= num %>") do |process| 10 | process.start_command = %Q{<%= process.command %>} 11 | 12 | process.working_dir = "<%= engine.root %>" 13 | process.daemonize = true 14 | process.environment = <%= engine.env.merge("PORT" => port.to_s).inspect %> 15 | process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] 16 | process.stop_grace_time = 45.seconds 17 | 18 | process.stdout = process.stderr = "<%= log %>/<%= app %>-<%= name %>-<%= num %>.log" 19 | 20 | process.monitor_children do |children| 21 | children.stop_command "kill {{PID}}" 22 | end 23 | 24 | process.group = "<%= app %>-<%= name %>" 25 | end 26 | <% end %> 27 | <% end %> 28 | end 29 | -------------------------------------------------------------------------------- /data/export/daemon/master.conf.erb: -------------------------------------------------------------------------------- 1 | pre-start script 2 | 3 | bash << "EOF" 4 | mkdir -p <%= log %> 5 | chown -R <%= user %> <%= log %> 6 | mkdir -p <%= run %> 7 | chown -R <%= user %> <%= run %> 8 | EOF 9 | 10 | end script 11 | 12 | start on runlevel [2345] 13 | 14 | stop on runlevel [016] 15 | -------------------------------------------------------------------------------- /data/export/daemon/process.conf.erb: -------------------------------------------------------------------------------- 1 | start on starting <%= app %>-<%= name.gsub('_', '-') %> 2 | stop on stopping <%= app %>-<%= name.gsub('_', '-') %> 3 | respawn 4 | 5 | env PORT=<%= port %><% engine.env.each_pair do |var, env| %> 6 | env <%= var %>=<%= env %><% end %> 7 | 8 | exec start-stop-daemon --start --chuid <%= user %> --chdir <%= engine.root %> --make-pidfile --pidfile <%= run %>/<%= app %>-<%= name %>-<%= num %>.pid --exec <%= executable %><%= arguments %> >> <%= log %>/<%= app %>-<%= name %>-<%= num %>.log 2>&1 9 | -------------------------------------------------------------------------------- /data/export/daemon/process_master.conf.erb: -------------------------------------------------------------------------------- 1 | start on starting <%= app %> 2 | stop on stopping <%= app %> 3 | -------------------------------------------------------------------------------- /data/export/launchd/launchd.plist.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | <%= "#{app}-#{name}-#{num}" %> 7 | EnvironmentVariables 8 | 9 | <%- engine.env.merge("PORT" => port).each_pair do |var,env| -%> 10 | <%= var %> 11 | <%= env %> 12 | <%- end -%> 13 | 14 | ProgramArguments 15 | 16 | <%- command_args.each do |command| -%> 17 | <%= command %> 18 | <%- end -%> 19 | 20 | KeepAlive 21 | 22 | RunAtLoad 23 | 24 | StandardOutPath 25 | <%= log %>/<%= app %>-<%= name %>-<%=num%>.log 26 | StandardErrorPath 27 | <%= log %>/<%= app %>-<%= name %>-<%=num%>.log 28 | UserName 29 | <%= user %> 30 | WorkingDirectory 31 | <%= engine.root %> 32 | 33 | 34 | -------------------------------------------------------------------------------- /data/export/runit/log/run.erb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | LOG=<%= log %>/<%= name %>-<%= num %> 5 | 6 | test -d "$LOG" || mkdir -p -m 2750 "$LOG" && chown <%= user %> "$LOG" 7 | exec chpst -u <%= user %> svlogd "$LOG" 8 | -------------------------------------------------------------------------------- /data/export/runit/run.erb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd <%= engine.root %> 3 | exec 2>&1 4 | exec chpst -u <%= user %> -e <%= File.join(location, "#{process_directory}/env") %> <%= process.command %> 5 | -------------------------------------------------------------------------------- /data/export/supervisord/app.conf.erb: -------------------------------------------------------------------------------- 1 | <% 2 | app_names = [] 3 | engine.each_process do |name, process| 4 | 1.upto(engine.formation[name]) do |num| 5 | port = engine.port_for(process, num) 6 | full_name = "#{app}-#{name}-#{num}" 7 | environment = engine.env.merge("PORT" => port.to_s).map do |key, value| 8 | value = shell_quote(value) 9 | value = value.gsub('\=', '=') 10 | value = value.gsub('\&', '&') 11 | value = value.gsub('\?', '?') 12 | "#{key}=\"#{value}\"" 13 | end 14 | app_names << full_name 15 | -%> 16 | [program:<%= full_name %>] 17 | command=<%= process.command %> 18 | autostart=true 19 | autorestart=true 20 | stdout_logfile=<%= log %>/<%= name %>-<%= num %>.log 21 | stderr_logfile=<%= log %>/<%= name %>-<%= num %>.error.log 22 | user=<%= user %> 23 | directory=<%= engine.root %> 24 | environment=<%= environment.join(',') %> 25 | 26 | <% 27 | end 28 | end 29 | -%> 30 | [group:<%= app %>] 31 | programs=<%= app_names.join(',') %> 32 | -------------------------------------------------------------------------------- /data/export/systemd/master.target.erb: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Wants=<%= service_names.join(' ') %> 3 | 4 | [Install] 5 | WantedBy=multi-user.target 6 | -------------------------------------------------------------------------------- /data/export/systemd/process.service.erb: -------------------------------------------------------------------------------- 1 | [Unit] 2 | PartOf=<%= app %>.target 3 | StopWhenUnneeded=yes 4 | 5 | [Service] 6 | User=<%= user %> 7 | WorkingDirectory=<%= engine.root %> 8 | Environment=PORT=<%= port %> 9 | Environment=PS=<%= process_name %> 10 | <% engine.env.each_pair do |var,env| -%> 11 | Environment="<%= var %>=<%= env %>" 12 | <% end -%> 13 | ExecStart=/bin/bash -lc 'exec -a "<%= app %>-<%= process_name %>" <%= process.command %>' 14 | Restart=always 15 | RestartSec=14s 16 | StandardInput=null 17 | StandardOutput=syslog 18 | StandardError=syslog 19 | SyslogIdentifier=%n 20 | KillMode=mixed 21 | TimeoutStopSec=<%= engine.options[:timeout] %> 22 | -------------------------------------------------------------------------------- /data/export/upstart/master.conf.erb: -------------------------------------------------------------------------------- 1 | start on runlevel [2345] 2 | stop on runlevel [!2345] 3 | -------------------------------------------------------------------------------- /data/export/upstart/process.conf.erb: -------------------------------------------------------------------------------- 1 | start on starting <%= app %>-<%= name %> 2 | stop on stopping <%= app %>-<%= name %> 3 | respawn 4 | 5 | env PORT=<%= port %> 6 | <% engine.env.each do |name,value| -%> 7 | <% next if name.upcase == "PORT" -%> 8 | env <%= name %>='<%= value.gsub(/'/, "'\"'\"'") %>' 9 | <% end -%> 10 | 11 | setuid <%= user %> 12 | 13 | chdir <%= engine.root %> 14 | 15 | exec <%= process.command %> 16 | -------------------------------------------------------------------------------- /data/export/upstart/process_master.conf.erb: -------------------------------------------------------------------------------- 1 | start on starting <%= app %> 2 | stop on stopping <%= app %> 3 | -------------------------------------------------------------------------------- /dist/gem.rake: -------------------------------------------------------------------------------- 1 | file pkg("foreman-#{version}.gem") => distribution_files do |t| 2 | sh "gem build foreman.gemspec" 3 | sh "mv foreman-#{version}.gem #{t.name}" 4 | end 5 | 6 | task "gem:build" => pkg("foreman-#{version}.gem") 7 | 8 | task "gem:clean" do 9 | clean pkg("foreman-#{version}.gem") 10 | end 11 | 12 | task "gem:release" => "gem:build" do |t| 13 | sh "gem push #{pkg("foreman-#{version}.gem")} || echo 'error'" 14 | end 15 | -------------------------------------------------------------------------------- /foreman.gemspec: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path("../lib", __FILE__) 2 | require "foreman/version" 3 | 4 | Gem::Specification.new do |gem| 5 | gem.name = "foreman" 6 | gem.license = "MIT" 7 | gem.version = Foreman::VERSION 8 | 9 | gem.author = "David Dollar" 10 | gem.email = "ddollar@gmail.com" 11 | gem.homepage = "https://github.com/ddollar/foreman" 12 | gem.summary = "Process manager for applications with multiple components" 13 | 14 | gem.description = gem.summary 15 | 16 | gem.executables = "foreman" 17 | gem.files = Dir["**/*"].select { |d| d =~ %r{^(README|bin/|data/|ext/|lib/|spec/|test/)} } 18 | gem.files << "man/foreman.1" 19 | end 20 | -------------------------------------------------------------------------------- /lib/foreman.rb: -------------------------------------------------------------------------------- 1 | require "foreman/version" 2 | 3 | module Foreman 4 | 5 | def self.runner 6 | File.expand_path("../../bin/foreman-runner", __FILE__) 7 | end 8 | 9 | def self.ruby_18? 10 | defined?(RUBY_VERSION) and RUBY_VERSION =~ /^1\.8\.\d+/ 11 | end 12 | 13 | def self.windows? 14 | defined?(RUBY_PLATFORM) and RUBY_PLATFORM =~ /(win|w)32$/ 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /lib/foreman/cli.rb: -------------------------------------------------------------------------------- 1 | require "foreman" 2 | require "foreman/helpers" 3 | require "foreman/engine" 4 | require "foreman/engine/cli" 5 | require "foreman/export" 6 | require "foreman/version" 7 | require "shellwords" 8 | require "yaml" 9 | require "foreman/vendor/thor/lib/thor" 10 | 11 | class Foreman::CLI < Foreman::Thor 12 | 13 | include Foreman::Helpers 14 | 15 | map ["-v", "--version"] => :version 16 | 17 | class_option :procfile, :type => :string, :aliases => "-f", :desc => "Default: Procfile" 18 | class_option :root, :type => :string, :aliases => "-d", :desc => "Default: Procfile directory" 19 | 20 | desc "start [PROCESS]", "Start the application (or a specific PROCESS)" 21 | 22 | method_option :color, :type => :boolean, :aliases => "-c", :desc => "Force color to be enabled" 23 | method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env" 24 | method_option :formation, :type => :string, :aliases => "-m", :banner => '"alpha=5,bar=3"', :desc => 'Specify what processes will run and how many. Default: "all=1"' 25 | method_option :port, :type => :numeric, :aliases => "-p" 26 | method_option :timeout, :type => :numeric, :aliases => "-t", :desc => "Specify the amount of time (in seconds) processes have to shutdown gracefully before receiving a SIGKILL, defaults to 5." 27 | method_option :timestamp, :type => :boolean, :default => true, :desc => "Include timestamp in output" 28 | 29 | class << self 30 | # Hackery. Take the run method away from Thor so that we can redefine it. 31 | def is_thor_reserved_word?(word, type) 32 | return false if word == "run" 33 | super 34 | end 35 | end 36 | 37 | def start(process=nil) 38 | check_procfile! 39 | load_environment! 40 | engine.load_procfile(procfile) 41 | engine.options[:formation] = "#{process}=1" if process 42 | engine.start 43 | rescue Foreman::Procfile::EmptyFileError 44 | error "no processes defined" 45 | end 46 | 47 | desc "export FORMAT LOCATION", "Export the application to another process management format" 48 | 49 | method_option :app, :type => :string, :aliases => "-a" 50 | method_option :log, :type => :string, :aliases => "-l" 51 | method_option :run, :type => :string, :aliases => "-r", :desc => "Specify the pid file directory, defaults to /var/run/" 52 | method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env" 53 | method_option :port, :type => :numeric, :aliases => "-p" 54 | method_option :user, :type => :string, :aliases => "-u" 55 | method_option :template, :type => :string, :aliases => "-t" 56 | method_option :formation, :type => :string, :aliases => "-m", :banner => '"alpha=5,bar=3"', :desc => 'Specify what processes will run and how many. Default: "all=1"' 57 | method_option :timeout, :type => :numeric, :aliases => "-t", :desc => "Specify the amount of time (in seconds) processes have to shutdown gracefully before receiving a SIGKILL, defaults to 5." 58 | 59 | def export(format, location=nil) 60 | check_procfile! 61 | load_environment! 62 | engine.load_procfile(procfile) 63 | formatter = Foreman::Export.formatter(format) 64 | formatter.new(location, engine, options).export 65 | rescue Foreman::Export::Exception, Foreman::Procfile::EmptyFileError => ex 66 | error ex.message 67 | end 68 | 69 | desc "check", "Validate your application's Procfile" 70 | 71 | def check 72 | check_procfile! 73 | engine.load_procfile(procfile) 74 | puts "valid procfile detected (#{engine.process_names.join(', ')})" 75 | rescue Foreman::Procfile::EmptyFileError 76 | error "no processes defined" 77 | end 78 | 79 | desc "run COMMAND [ARGS...]", "Run a command using your application's environment" 80 | 81 | method_option :env, :type => :string, :aliases => "-e", :desc => "Specify an environment file to load, defaults to .env" 82 | stop_on_unknown_option! :run 83 | 84 | def run(*args) 85 | load_environment! 86 | 87 | if File.file?(procfile) 88 | engine.load_procfile(procfile) 89 | end 90 | 91 | pid = fork do 92 | begin 93 | engine.env.each { |k,v| ENV[k] = v } 94 | if args.size == 1 && process = engine.process(args.first) 95 | process.exec(:env => engine.env) 96 | else 97 | exec args.shelljoin 98 | end 99 | rescue Errno::EACCES 100 | error "not executable: #{args.first}" 101 | rescue Errno::ENOENT 102 | error "command not found: #{args.first}" 103 | end 104 | end 105 | trap("INT") do 106 | Process.kill(:INT, pid) 107 | end 108 | Process.wait(pid) 109 | exit $?.exitstatus || 0 110 | rescue Interrupt 111 | rescue Foreman::Procfile::EmptyFileError 112 | error "no processes defined" 113 | end 114 | 115 | desc "version", "Display Foreman gem version" 116 | 117 | def version 118 | puts Foreman::VERSION 119 | end 120 | 121 | no_tasks do 122 | def engine 123 | @engine ||= begin 124 | engine_class = Foreman::Engine::CLI 125 | engine = engine_class.new(options) 126 | engine 127 | end 128 | end 129 | end 130 | 131 | private ###################################################################### 132 | 133 | def error(message) 134 | puts "ERROR: #{message}" 135 | exit 1 136 | end 137 | 138 | def check_procfile! 139 | error("#{procfile} does not exist.") unless File.file?(procfile) 140 | end 141 | 142 | def load_environment! 143 | if options[:env] 144 | options[:env].split(",").each do |file| 145 | engine.load_env file 146 | end 147 | else 148 | default_env = File.join(engine.root, ".env") 149 | engine.load_env default_env if File.file?(default_env) 150 | end 151 | end 152 | 153 | def procfile 154 | case 155 | when options[:procfile] then options[:procfile] 156 | when options[:root] then File.expand_path(File.join(options[:root], "Procfile")) 157 | else "Procfile" 158 | end 159 | end 160 | 161 | def options 162 | original_options = super 163 | return original_options unless File.file?(".foreman") 164 | defaults = ::YAML::load_file(".foreman") || {} 165 | Foreman::Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options)) 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /lib/foreman/distribution.rb: -------------------------------------------------------------------------------- 1 | module Foreman 2 | module Distribution 3 | def self.files 4 | Dir[File.expand_path("../../../{bin,data,lib}/**/*", __FILE__)].select do |file| 5 | File.file?(file) 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/foreman/engine/cli.rb: -------------------------------------------------------------------------------- 1 | require "foreman/engine" 2 | 3 | class Foreman::Engine::CLI < Foreman::Engine 4 | 5 | module Color 6 | 7 | ANSI = { 8 | :reset => 0, 9 | :black => 30, 10 | :red => 31, 11 | :green => 32, 12 | :yellow => 33, 13 | :blue => 34, 14 | :magenta => 35, 15 | :cyan => 36, 16 | :white => 37, 17 | :bright_black => 30, 18 | :bright_red => 31, 19 | :bright_green => 32, 20 | :bright_yellow => 33, 21 | :bright_blue => 34, 22 | :bright_magenta => 35, 23 | :bright_cyan => 36, 24 | :bright_white => 37, 25 | } 26 | 27 | def self.enable(io, force=false) 28 | io.extend(self) 29 | @@color_force = force 30 | end 31 | 32 | def color? 33 | return true if @@color_force 34 | return false if Foreman.windows? 35 | return false unless self.respond_to?(:isatty) 36 | self.isatty && ENV["TERM"] 37 | end 38 | 39 | def color(name) 40 | return "" unless color? 41 | return "" unless ansi = ANSI[name.to_sym] 42 | "\e[#{ansi}m" 43 | end 44 | 45 | end 46 | 47 | FOREMAN_COLORS = %w( cyan yellow green magenta red blue bright_cyan bright_yellow 48 | bright_green bright_magenta bright_red bright_blue ) 49 | 50 | def startup 51 | @colors = map_colors 52 | proctitle "foreman: main" unless Foreman.windows? 53 | Color.enable($stdout, options[:color]) 54 | end 55 | 56 | def output(name, data) 57 | data.to_s.lines.map(&:chomp).each do |message| 58 | output = "" 59 | output += $stdout.color(@colors[name.split(".").first].to_sym) 60 | output += "#{Time.now.strftime("%H:%M:%S")} " if options[:timestamp] 61 | output += "#{pad_process_name(name)} | " 62 | output += $stdout.color(:reset) 63 | output += message 64 | $stdout.puts output 65 | $stdout.flush 66 | end 67 | rescue Errno::EPIPE 68 | terminate_gracefully 69 | end 70 | 71 | def shutdown 72 | end 73 | 74 | private 75 | 76 | def name_padding 77 | @name_padding ||= begin 78 | index_padding = @names.values.map { |n| formation[n] }.max.to_s.length + 1 79 | name_padding = @names.values.map { |n| n.length + index_padding }.sort.last 80 | [ 6, name_padding.to_i ].max 81 | end 82 | end 83 | 84 | def pad_process_name(name) 85 | name.ljust(name_padding, " ") 86 | end 87 | 88 | def map_colors 89 | colors = Hash.new("white") 90 | @names.values.each_with_index do |name, index| 91 | colors[name] = FOREMAN_COLORS[index % FOREMAN_COLORS.length] 92 | end 93 | colors["system"] = "bright_white" 94 | colors 95 | end 96 | 97 | def proctitle(title) 98 | $0 = title 99 | end 100 | 101 | def termtitle(title) 102 | printf("\033]0;#{title}\007") unless Foreman.windows? 103 | end 104 | 105 | end 106 | -------------------------------------------------------------------------------- /lib/foreman/env.rb: -------------------------------------------------------------------------------- 1 | require "foreman" 2 | 3 | class Foreman::Env 4 | 5 | attr_reader :entries 6 | 7 | def initialize(filename) 8 | @entries = File.read(filename).gsub("\r\n","\n").split("\n").inject({}) do |ax, line| 9 | if line =~ /\A([A-Za-z_0-9]+)=(.*)\z/ 10 | key = $1 11 | case val = $2 12 | # Remove single quotes 13 | when /\A'(.*)'\z/ then ax[key] = $1 14 | # Remove double quotes and unescape string preserving newline characters 15 | when /\A"(.*)"\z/ then ax[key] = $1.gsub('\n', "\n").gsub(/\\(.)/, '\1') 16 | else ax[key] = val 17 | end 18 | end 19 | ax 20 | end 21 | end 22 | 23 | def entries 24 | @entries.each do |key, value| 25 | yield key, value 26 | end 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /lib/foreman/export.rb: -------------------------------------------------------------------------------- 1 | require "foreman" 2 | require "foreman/helpers" 3 | require "pathname" 4 | 5 | module Foreman::Export 6 | extend Foreman::Helpers 7 | 8 | class Exception < ::Exception; end 9 | 10 | def self.formatter(format) 11 | begin 12 | require "foreman/export/#{ format.tr('-', '_') }" 13 | classy_format = classify(format) 14 | formatter = constantize("Foreman::Export::#{ classy_format }") 15 | rescue NameError => ex 16 | error "Unknown export format: #{format} (no class Foreman::Export::#{ classy_format })." 17 | rescue LoadError => ex 18 | error "Unknown export format: #{format} (unable to load file 'foreman/export/#{ format.tr('-', '_') }')." 19 | end 20 | end 21 | 22 | def self.error(message) 23 | raise Foreman::Export::Exception.new(message) 24 | end 25 | 26 | end 27 | 28 | require "foreman/export/base" 29 | require "foreman/export/inittab" 30 | require "foreman/export/upstart" 31 | require "foreman/export/daemon" 32 | require "foreman/export/bluepill" 33 | require "foreman/export/runit" 34 | require "foreman/export/supervisord" 35 | require "foreman/export/launchd" 36 | require "foreman/export/systemd" 37 | -------------------------------------------------------------------------------- /lib/foreman/export/base.rb: -------------------------------------------------------------------------------- 1 | require "foreman/export" 2 | require "ostruct" 3 | require "pathname" 4 | require "shellwords" 5 | 6 | class Foreman::Export::Base 7 | 8 | attr_reader :location 9 | attr_reader :engine 10 | attr_reader :options 11 | attr_reader :formation 12 | 13 | # deprecated 14 | attr_reader :port 15 | 16 | def initialize(location, engine, options={}) 17 | @location = location 18 | @engine = engine 19 | @options = options.dup 20 | @formation = engine.formation 21 | 22 | # deprecated 23 | def port 24 | Foreman::Export::Base.warn_deprecation! 25 | engine.base_port 26 | end 27 | 28 | # deprecated 29 | def template 30 | Foreman::Export::Base.warn_deprecation! 31 | options[:template] 32 | end 33 | 34 | # deprecated 35 | def @engine.procfile 36 | Foreman::Export::Base.warn_deprecation! 37 | @processes.map do |process| 38 | OpenStruct.new( 39 | :name => @names[process], 40 | :process => process 41 | ) 42 | end 43 | end 44 | end 45 | 46 | def export 47 | error("Must specify a location") unless location 48 | FileUtils.mkdir_p(location) rescue error("Could not create: #{location}") 49 | chown user, log 50 | chown user, run 51 | end 52 | 53 | def app 54 | options[:app] || "app" 55 | end 56 | 57 | def log 58 | options[:log] || "/var/log/#{app}" 59 | end 60 | 61 | def run 62 | options[:run] || "/var/run/#{app}" 63 | end 64 | 65 | def user 66 | options[:user] || app 67 | end 68 | 69 | private ###################################################################### 70 | 71 | def self.warn_deprecation! 72 | @@deprecation_warned ||= false 73 | return if @@deprecation_warned 74 | puts "WARNING: Using deprecated exporter interface. Please update your exporter" 75 | puts "the interface shown in the upstart exporter:" 76 | puts 77 | puts "https://github.com/ddollar/foreman/blob/main/lib/foreman/export/upstart.rb" 78 | puts "https://github.com/ddollar/foreman/blob/main/data/export/upstart/process.conf.erb" 79 | puts 80 | @@deprecation_warned = true 81 | end 82 | 83 | def chown user, dir 84 | FileUtils.chown user, nil, dir 85 | rescue 86 | error("Could not chown #{dir} to #{user}") unless File.writable?(dir) || ! File.exist?(dir) 87 | end 88 | 89 | def error(message) 90 | raise Foreman::Export::Exception.new(message) 91 | end 92 | 93 | def say(message) 94 | puts "[foreman export] %s" % message 95 | end 96 | 97 | def clean(filename) 98 | return unless File.exist?(filename) 99 | say "cleaning up: #{filename}" 100 | FileUtils.rm(filename) 101 | end 102 | 103 | def clean_dir(dirname) 104 | return unless File.exist?(dirname) 105 | say "cleaning up directory: #{dirname}" 106 | FileUtils.rm_r(dirname) 107 | end 108 | 109 | def shell_quote(value) 110 | Shellwords.escape(value) 111 | end 112 | 113 | # deprecated 114 | def old_export_template(exporter, file, template_root) 115 | if template_root && File.exist?(file_path = File.join(template_root, file)) 116 | File.read(file_path) 117 | elsif File.exist?(file_path = File.expand_path(File.join("~/.foreman/templates", file))) 118 | File.read(file_path) 119 | else 120 | File.read(File.expand_path("../../../../data/export/#{exporter}/#{file}", __FILE__)) 121 | end 122 | end 123 | 124 | def export_template(name, file=nil, template_root=nil) 125 | if file && template_root 126 | old_export_template name, file, template_root 127 | else 128 | name_without_first = name.split("/")[1..-1].join("/") 129 | matchers = [] 130 | matchers << File.join(options[:template], name_without_first) if options[:template] 131 | matchers << File.expand_path("~/.foreman/templates/#{name}") 132 | matchers << File.expand_path("../../../../data/export/#{name}", __FILE__) 133 | File.read(matchers.detect { |m| File.exist?(m) }) 134 | end 135 | end 136 | 137 | def write_template(name, target, binding) 138 | compiled = if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+ 139 | ERB.new(export_template(name), trim_mode: '-').result(binding) 140 | else 141 | ERB.new(export_template(name), nil, '-').result(binding) 142 | end 143 | write_file target, compiled 144 | end 145 | 146 | def chmod(mode, file) 147 | say "setting #{file} to mode #{mode}" 148 | FileUtils.chmod mode, File.join(location, file) 149 | end 150 | 151 | def create_directory(dir) 152 | say "creating: #{dir}" 153 | FileUtils.mkdir_p(File.join(location, dir)) 154 | end 155 | 156 | def create_symlink(link, target) 157 | say "symlinking: #{link} -> #{target}" 158 | FileUtils.symlink(target, File.join(location, link)) 159 | end 160 | 161 | def write_file(filename, contents) 162 | say "writing: #{filename}" 163 | 164 | filename = File.join(location, filename) unless Pathname.new(filename).absolute? 165 | 166 | File.open(filename, "w") do |file| 167 | file.puts contents 168 | end 169 | end 170 | 171 | end 172 | -------------------------------------------------------------------------------- /lib/foreman/export/bluepill.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | require "foreman/export" 3 | 4 | class Foreman::Export::Bluepill < Foreman::Export::Base 5 | 6 | def export 7 | super 8 | clean "#{location}/#{app}.pill" 9 | write_template "bluepill/master.pill.erb", "#{app}.pill", binding 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/foreman/export/daemon.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | require "foreman/export" 3 | 4 | class Foreman::Export::Daemon < Foreman::Export::Base 5 | 6 | def export 7 | super 8 | 9 | (Dir["#{location}/#{app}-*.conf"] << "#{location}/#{app}.conf").each do |file| 10 | clean file 11 | end 12 | 13 | write_template "daemon/master.conf.erb", "#{app}.conf", binding 14 | 15 | engine.each_process do |name, process| 16 | next if engine.formation[name] < 1 17 | write_template "daemon/process_master.conf.erb", "#{app}-#{name}.conf", binding 18 | 19 | 1.upto(engine.formation[name]) do |num| 20 | port = engine.port_for(process, num) 21 | arguments = process.command.split(" ") 22 | executable = arguments.slice!(0) 23 | arguments = arguments.size > 0 ? " -- #{arguments.join(' ')}" : "" 24 | write_template "daemon/process.conf.erb", "#{app}-#{name}-#{num}.conf", binding 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/foreman/export/inittab.rb: -------------------------------------------------------------------------------- 1 | require "foreman/export" 2 | 3 | class Foreman::Export::Inittab < Foreman::Export::Base 4 | 5 | def export 6 | error("Must specify a location") unless location 7 | 8 | inittab = [] 9 | inittab << "# ----- foreman #{app} processes -----" 10 | 11 | index = 1 12 | engine.each_process do |name, process| 13 | 1.upto(engine.formation[name]) do |num| 14 | id = app.slice(0, 2).upcase + sprintf("%02d", index) 15 | port = engine.port_for(process, num) 16 | 17 | commands = [] 18 | commands << "cd #{engine.root}" 19 | commands << "export PORT=#{port}" 20 | engine.env.each_pair do |var, env| 21 | commands << "export #{var.upcase}=#{shell_quote(env)}" 22 | end 23 | commands << "#{process.command} >> #{log}/#{name}-#{num}.log 2>&1" 24 | 25 | inittab << "#{id}:4:respawn:/bin/su - #{user} -c '#{commands.join(";")}'" 26 | index += 1 27 | end 28 | end 29 | 30 | inittab << "# ----- end foreman #{app} processes -----" 31 | 32 | inittab = inittab.join("\n") + "\n" 33 | 34 | if location == "-" 35 | puts inittab 36 | else 37 | say "writing: #{location}" 38 | File.open(location, "w") { |file| file.puts inittab } 39 | end 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /lib/foreman/export/launchd.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | require "foreman/export" 3 | 4 | class Foreman::Export::Launchd < Foreman::Export::Base 5 | 6 | def export 7 | super 8 | engine.each_process do |name, process| 9 | 1.upto(engine.formation[name]) do |num| 10 | port = engine.port_for(process, num) 11 | command_args = process.command.split(/\s+/).map{|arg| 12 | case arg 13 | when "$PORT" then port 14 | else arg 15 | end 16 | } 17 | write_template "launchd/launchd.plist.erb", "#{app}-#{name}-#{num}.plist", binding 18 | end 19 | end 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /lib/foreman/export/runit.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | require "foreman/export" 3 | 4 | class Foreman::Export::Runit < Foreman::Export::Base 5 | 6 | ENV_VARIABLE_REGEX = /([a-zA-Z_]+[a-zA-Z0-9_]*)=(\S+)/ 7 | 8 | def export 9 | super 10 | 11 | engine.each_process do |name, process| 12 | 1.upto(engine.formation[name]) do |num| 13 | process_directory = "#{app}-#{name}-#{num}" 14 | 15 | create_directory process_directory 16 | create_directory "#{process_directory}/env" 17 | create_directory "#{process_directory}/log" 18 | 19 | write_template "runit/run.erb", "#{process_directory}/run", binding 20 | chmod 0755, "#{process_directory}/run" 21 | 22 | port = engine.port_for(process, num) 23 | engine.env.merge("PORT" => port.to_s).each do |key, value| 24 | write_file "#{process_directory}/env/#{key}", value 25 | end 26 | 27 | write_template "runit/log/run.erb", "#{process_directory}/log/run", binding 28 | chmod 0755, "#{process_directory}/log/run" 29 | end 30 | end 31 | 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /lib/foreman/export/supervisord.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | require "foreman/export" 3 | 4 | class Foreman::Export::Supervisord < Foreman::Export::Base 5 | 6 | def export 7 | super 8 | 9 | Dir["#{location}/#{app}.conf"].each do |file| 10 | clean file 11 | end 12 | 13 | write_template "supervisord/app.conf.erb", "#{app}.conf", binding 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /lib/foreman/export/systemd.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | require "foreman/export" 3 | 4 | class Foreman::Export::Systemd < Foreman::Export::Base 5 | 6 | def export 7 | super 8 | 9 | Dir["#{location}/#{app}*.target"] 10 | .concat(Dir["#{location}/#{app}*.service"]) 11 | .concat(Dir["#{location}/#{app}*.target.wants/#{app}*.service"]) 12 | .each do |file| 13 | clean file 14 | end 15 | 16 | Dir["#{location}/#{app}*.target.wants"].each do |file| 17 | clean_dir file 18 | end 19 | 20 | service_names = [] 21 | 22 | engine.each_process do |name, process| 23 | 1.upto(engine.formation[name]) do |num| 24 | port = engine.port_for(process, num) 25 | process_name = "#{name}.#{num}" 26 | service_filename = "#{app}-#{process_name}.service" 27 | write_template "systemd/process.service.erb", service_filename, binding 28 | service_names << service_filename 29 | end 30 | end 31 | 32 | write_template "systemd/master.target.erb", "#{app}.target", binding 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/foreman/export/upstart.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | require "foreman/export" 3 | 4 | class Foreman::Export::Upstart < Foreman::Export::Base 5 | 6 | def export 7 | super 8 | 9 | master_file = "#{app}.conf" 10 | 11 | clean File.join(location, master_file) 12 | write_template master_template, master_file, binding 13 | 14 | engine.each_process do |name, process| 15 | process_master_file = "#{app}-#{name}.conf" 16 | process_file = "#{app}-#{name}-%s.conf" 17 | 18 | Dir[ 19 | File.join(location, process_master_file), 20 | File.join(location, process_file % "*") 21 | ].each { |f| clean(f) } 22 | 23 | next if engine.formation[name] < 1 24 | write_template process_master_template, process_master_file, binding 25 | 26 | 1.upto(engine.formation[name]) do |num| 27 | port = engine.port_for(process, num) 28 | write_template process_template, process_file % num, binding 29 | end 30 | end 31 | end 32 | 33 | private 34 | 35 | def master_template 36 | "upstart/master.conf.erb" 37 | end 38 | 39 | def process_master_template 40 | "upstart/process_master.conf.erb" 41 | end 42 | 43 | def process_template 44 | "upstart/process.conf.erb" 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/foreman/helpers.rb: -------------------------------------------------------------------------------- 1 | module Foreman::Helpers 2 | # Copied whole sale from, https://github.com/defunkt/resque/ 3 | 4 | # Given a word with dashes, returns a camel cased version of it. 5 | # 6 | # classify('job-name') # => 'JobName' 7 | def classify(dashed_word) 8 | dashed_word.split('-').each { |part| part[0] = part[0].chr.upcase }.join 9 | end # Tries to find a constant with the name specified in the argument string: 10 | 11 | # 12 | # constantize("Module") # => Module 13 | # constantize("Test::Unit") # => Test::Unit 14 | # 15 | # The name is assumed to be the one of a top-level constant, no matter 16 | # whether it starts with "::" or not. No lexical context is taken into 17 | # account: 18 | # 19 | # C = 'outside' 20 | # module M 21 | # C = 'inside' 22 | # C # => 'inside' 23 | # constantize("C") # => 'outside', same as ::C 24 | # end 25 | # 26 | # NameError is raised when the constant is unknown. 27 | def constantize(camel_cased_word) 28 | camel_cased_word = camel_cased_word.to_s 29 | 30 | names = camel_cased_word.split('::') 31 | names.shift if names.empty? || names.first.empty? 32 | 33 | constant = Object 34 | names.each do |name| 35 | args = Module.method(:const_get).arity != 1 ? [false] : [] 36 | 37 | if constant.const_defined?(name, *args) 38 | constant = constant.const_get(name) 39 | else 40 | constant = constant.const_missing(name) 41 | end 42 | end 43 | constant 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/foreman/process.rb: -------------------------------------------------------------------------------- 1 | require "foreman" 2 | require "shellwords" 3 | 4 | class Foreman::Process 5 | 6 | attr_reader :command 7 | attr_reader :env 8 | 9 | # Create a Process 10 | # 11 | # @param [String] command The command to run 12 | # @param [Hash] options 13 | # 14 | # @option options [String] :cwd (./) Change to this working directory before executing the process 15 | # @option options [Hash] :env ({}) Environment variables to set for this process 16 | # 17 | def initialize(command, options={}) 18 | @command = command 19 | @options = options.dup 20 | 21 | @options[:env] ||= {} 22 | end 23 | 24 | # Get environment-expanded command for a +Process+ 25 | # 26 | # @param [Hash] custom_env ({}) Environment variables to merge with defaults 27 | # 28 | # @return [String] The expanded command 29 | # 30 | def expanded_command(custom_env={}) 31 | env = @options[:env].merge(custom_env) 32 | expanded_command = command.dup 33 | env.each do |key, val| 34 | expanded_command.gsub!("$#{key}", val) 35 | end 36 | expanded_command 37 | end 38 | 39 | # Run a +Process+ 40 | # 41 | # @param [Hash] options 42 | # 43 | # @option options :env ({}) Environment variables to set for this execution 44 | # @option options :output ($stdout) The output stream 45 | # 46 | # @returns [Fixnum] pid The +pid+ of the process 47 | # 48 | def run(options={}) 49 | env = @options[:env].merge(options[:env] || {}) 50 | output = options[:output] || $stdout 51 | runner = "#{Foreman.runner}".shellescape 52 | 53 | Dir.chdir(cwd) do 54 | Process.spawn env, expanded_command(env), :out => output, :err => output 55 | end 56 | end 57 | 58 | # Exec a +Process+ 59 | # 60 | # @param [Hash] options 61 | # 62 | # @option options :env ({}) Environment variables to set for this execution 63 | # 64 | # @return Does not return 65 | def exec(options={}) 66 | env = @options[:env].merge(options[:env] || {}) 67 | env.each { |k, v| ENV[k] = v } 68 | Dir.chdir(cwd) 69 | Kernel.exec expanded_command(env) 70 | end 71 | 72 | # Returns the working directory for this +Process+ 73 | # 74 | # @returns [String] 75 | # 76 | def cwd 77 | File.expand_path(@options[:cwd] || ".") 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /lib/foreman/procfile.rb: -------------------------------------------------------------------------------- 1 | require "foreman" 2 | 3 | # Reads and writes Procfiles 4 | # 5 | # A valid Procfile entry is captured by this regex: 6 | # 7 | # /^([A-Za-z0-9_-]+):\s*(.+)$/ 8 | # 9 | # All other lines are ignored. 10 | # 11 | class Foreman::Procfile 12 | 13 | EmptyFileError = Class.new(StandardError) 14 | 15 | # Initialize a Procfile 16 | # 17 | # @param [String] filename (nil) An optional filename to read from 18 | # 19 | def initialize(filename=nil) 20 | @entries = [] 21 | load(filename) if filename 22 | end 23 | 24 | # Yield each +Procfile+ entry in order 25 | # 26 | def entries 27 | @entries.each do |(name, command)| 28 | yield name, command 29 | end 30 | end 31 | 32 | # Retrieve a +Procfile+ command by name 33 | # 34 | # @param [String] name The name of the Procfile entry to retrieve 35 | # 36 | def [](name) 37 | if entry = @entries.detect { |n,c| name == n } 38 | entry.last 39 | end 40 | end 41 | 42 | # Create a +Procfile+ entry 43 | # 44 | # @param [String] name The name of the +Procfile+ entry to create 45 | # @param [String] command The command of the +Procfile+ entry to create 46 | # 47 | def []=(name, command) 48 | delete name 49 | @entries << [name, command] 50 | end 51 | 52 | # Remove a +Procfile+ entry 53 | # 54 | # @param [String] name The name of the +Procfile+ entry to remove 55 | # 56 | def delete(name) 57 | @entries.reject! { |n,c| name == n } 58 | end 59 | 60 | # Load a Procfile from a file 61 | # 62 | # @param [String] filename The filename of the +Procfile+ to load 63 | # 64 | def load(filename) 65 | parse_data = parse(filename) 66 | 67 | raise EmptyFileError if parse_data.empty? 68 | 69 | @entries.replace parse_data 70 | end 71 | 72 | # Save a Procfile to a file 73 | # 74 | # @param [String] filename Save the +Procfile+ to this file 75 | # 76 | def save(filename) 77 | File.open(filename, 'w') do |file| 78 | file.puts self.to_s 79 | end 80 | end 81 | 82 | # Get the +Procfile+ as a +String+ 83 | # 84 | def to_s 85 | @entries.map do |name, command| 86 | [ name, command ].join(": ") 87 | end.join("\n") 88 | end 89 | 90 | private 91 | 92 | def parse(filename) 93 | File.read(filename).gsub("\r\n","\n").split("\n").map do |line| 94 | if line =~ /^([A-Za-z0-9_-]+):\s*(.+)$/ 95 | [$1, $2] 96 | end 97 | end.compact 98 | end 99 | 100 | end 101 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/actions/create_file.rb: -------------------------------------------------------------------------------- 1 | require "foreman/vendor/thor/lib/thor/actions/empty_directory" 2 | 3 | class Foreman::Thor 4 | module Actions 5 | # Create a new file relative to the destination root with the given data, 6 | # which is the return value of a block or a data string. 7 | # 8 | # ==== Parameters 9 | # destination:: the relative path to the destination root. 10 | # data:: the data to append to the file. 11 | # config:: give :verbose => false to not log the status. 12 | # 13 | # ==== Examples 14 | # 15 | # create_file "lib/fun_party.rb" do 16 | # hostname = ask("What is the virtual hostname I should use?") 17 | # "vhost.name = #{hostname}" 18 | # end 19 | # 20 | # create_file "config/apache.conf", "your apache config" 21 | # 22 | def create_file(destination, *args, &block) 23 | config = args.last.is_a?(Hash) ? args.pop : {} 24 | data = args.first 25 | action CreateFile.new(self, destination, block || data.to_s, config) 26 | end 27 | alias_method :add_file, :create_file 28 | 29 | # CreateFile is a subset of Template, which instead of rendering a file with 30 | # ERB, it gets the content from the user. 31 | # 32 | class CreateFile < EmptyDirectory #:nodoc: 33 | attr_reader :data 34 | 35 | def initialize(base, destination, data, config = {}) 36 | @data = data 37 | super(base, destination, config) 38 | end 39 | 40 | # Checks if the content of the file at the destination is identical to the rendered result. 41 | # 42 | # ==== Returns 43 | # Boolean:: true if it is identical, false otherwise. 44 | # 45 | def identical? 46 | exists? && File.binread(destination) == render 47 | end 48 | 49 | # Holds the content to be added to the file. 50 | # 51 | def render 52 | @render ||= if data.is_a?(Proc) 53 | data.call 54 | else 55 | data 56 | end 57 | end 58 | 59 | def invoke! 60 | invoke_with_conflict_check do 61 | FileUtils.mkdir_p(File.dirname(destination)) 62 | File.open(destination, "wb") { |f| f.write render } 63 | end 64 | given_destination 65 | end 66 | 67 | protected 68 | 69 | # Now on conflict we check if the file is identical or not. 70 | # 71 | def on_conflict_behavior(&block) 72 | if identical? 73 | say_status :identical, :blue 74 | else 75 | options = base.options.merge(config) 76 | force_or_skip_or_conflict(options[:force], options[:skip], &block) 77 | end 78 | end 79 | 80 | # If force is true, run the action, otherwise check if it's not being 81 | # skipped. If both are false, show the file_collision menu, if the menu 82 | # returns true, force it, otherwise skip. 83 | # 84 | def force_or_skip_or_conflict(force, skip, &block) 85 | if force 86 | say_status :force, :yellow 87 | yield unless pretend? 88 | elsif skip 89 | say_status :skip, :yellow 90 | else 91 | say_status :conflict, :red 92 | force_or_skip_or_conflict(force_on_collision?, true, &block) 93 | end 94 | end 95 | 96 | # Shows the file collision menu to the user and gets the result. 97 | # 98 | def force_on_collision? 99 | base.shell.file_collision(destination) { render } 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/actions/create_link.rb: -------------------------------------------------------------------------------- 1 | require "foreman/vendor/thor/lib/thor/actions/create_file" 2 | 3 | class Foreman::Thor 4 | module Actions 5 | # Create a new file relative to the destination root from the given source. 6 | # 7 | # ==== Parameters 8 | # destination:: the relative path to the destination root. 9 | # source:: the relative path to the source root. 10 | # config:: give :verbose => false to not log the status. 11 | # :: give :symbolic => false for hard link. 12 | # 13 | # ==== Examples 14 | # 15 | # create_link "config/apache.conf", "/etc/apache.conf" 16 | # 17 | def create_link(destination, *args) 18 | config = args.last.is_a?(Hash) ? args.pop : {} 19 | source = args.first 20 | action CreateLink.new(self, destination, source, config) 21 | end 22 | alias_method :add_link, :create_link 23 | 24 | # CreateLink is a subset of CreateFile, which instead of taking a block of 25 | # data, just takes a source string from the user. 26 | # 27 | class CreateLink < CreateFile #:nodoc: 28 | attr_reader :data 29 | 30 | # Checks if the content of the file at the destination is identical to the rendered result. 31 | # 32 | # ==== Returns 33 | # Boolean:: true if it is identical, false otherwise. 34 | # 35 | def identical? 36 | exists? && File.identical?(render, destination) 37 | end 38 | 39 | def invoke! 40 | invoke_with_conflict_check do 41 | FileUtils.mkdir_p(File.dirname(destination)) 42 | # Create a symlink by default 43 | config[:symbolic] = true if config[:symbolic].nil? 44 | File.unlink(destination) if exists? 45 | if config[:symbolic] 46 | File.symlink(render, destination) 47 | else 48 | File.link(render, destination) 49 | end 50 | end 51 | given_destination 52 | end 53 | 54 | def exists? 55 | super || File.symlink?(destination) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/actions/directory.rb: -------------------------------------------------------------------------------- 1 | require "foreman/vendor/thor/lib/thor/actions/empty_directory" 2 | 3 | class Foreman::Thor 4 | module Actions 5 | # Copies recursively the files from source directory to root directory. 6 | # If any of the files finishes with .tt, it's considered to be a template 7 | # and is placed in the destination without the extension .tt. If any 8 | # empty directory is found, it's copied and all .empty_directory files are 9 | # ignored. If any file name is wrapped within % signs, the text within 10 | # the % signs will be executed as a method and replaced with the returned 11 | # value. Let's suppose a doc directory with the following files: 12 | # 13 | # doc/ 14 | # components/.empty_directory 15 | # README 16 | # rdoc.rb.tt 17 | # %app_name%.rb 18 | # 19 | # When invoked as: 20 | # 21 | # directory "doc" 22 | # 23 | # It will create a doc directory in the destination with the following 24 | # files (assuming that the `app_name` method returns the value "blog"): 25 | # 26 | # doc/ 27 | # components/ 28 | # README 29 | # rdoc.rb 30 | # blog.rb 31 | # 32 | # Encoded path note: Since Foreman::Thor internals use Object#respond_to? to check if it can 33 | # expand %something%, this `something` should be a public method in the class calling 34 | # #directory. If a method is private, Foreman::Thor stack raises PrivateMethodEncodedError. 35 | # 36 | # ==== Parameters 37 | # source:: the relative path to the source root. 38 | # destination:: the relative path to the destination root. 39 | # config:: give :verbose => false to not log the status. 40 | # If :recursive => false, does not look for paths recursively. 41 | # If :mode => :preserve, preserve the file mode from the source. 42 | # If :exclude_pattern => /regexp/, prevents copying files that match that regexp. 43 | # 44 | # ==== Examples 45 | # 46 | # directory "doc" 47 | # directory "doc", "docs", :recursive => false 48 | # 49 | def directory(source, *args, &block) 50 | config = args.last.is_a?(Hash) ? args.pop : {} 51 | destination = args.first || source 52 | action Directory.new(self, source, destination || source, config, &block) 53 | end 54 | 55 | class Directory < EmptyDirectory #:nodoc: 56 | attr_reader :source 57 | 58 | def initialize(base, source, destination = nil, config = {}, &block) 59 | @source = File.expand_path(base.find_in_source_paths(source.to_s)) 60 | @block = block 61 | super(base, destination, {:recursive => true}.merge(config)) 62 | end 63 | 64 | def invoke! 65 | base.empty_directory given_destination, config 66 | execute! 67 | end 68 | 69 | def revoke! 70 | execute! 71 | end 72 | 73 | protected 74 | 75 | def execute! 76 | lookup = Util.escape_globs(source) 77 | lookup = config[:recursive] ? File.join(lookup, "**") : lookup 78 | lookup = file_level_lookup(lookup) 79 | 80 | files(lookup).sort.each do |file_source| 81 | next if File.directory?(file_source) 82 | next if config[:exclude_pattern] && file_source.match(config[:exclude_pattern]) 83 | file_destination = File.join(given_destination, file_source.gsub(source, ".")) 84 | file_destination.gsub!("/./", "/") 85 | 86 | case file_source 87 | when /\.empty_directory$/ 88 | dirname = File.dirname(file_destination).gsub(%r{/\.$}, "") 89 | next if dirname == given_destination 90 | base.empty_directory(dirname, config) 91 | when /#{TEMPLATE_EXTNAME}$/ 92 | base.template(file_source, file_destination[0..-4], config, &@block) 93 | else 94 | base.copy_file(file_source, file_destination, config, &@block) 95 | end 96 | end 97 | end 98 | 99 | if RUBY_VERSION < "2.0" 100 | def file_level_lookup(previous_lookup) 101 | File.join(previous_lookup, "{*,.[a-z]*}") 102 | end 103 | 104 | def files(lookup) 105 | Dir[lookup] 106 | end 107 | else 108 | def file_level_lookup(previous_lookup) 109 | File.join(previous_lookup, "*") 110 | end 111 | 112 | def files(lookup) 113 | Dir.glob(lookup, File::FNM_DOTMATCH) 114 | end 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/actions/empty_directory.rb: -------------------------------------------------------------------------------- 1 | class Foreman::Thor 2 | module Actions 3 | # Creates an empty directory. 4 | # 5 | # ==== Parameters 6 | # destination:: the relative path to the destination root. 7 | # config:: give :verbose => false to not log the status. 8 | # 9 | # ==== Examples 10 | # 11 | # empty_directory "doc" 12 | # 13 | def empty_directory(destination, config = {}) 14 | action EmptyDirectory.new(self, destination, config) 15 | end 16 | 17 | # Class which holds create directory logic. This is the base class for 18 | # other actions like create_file and directory. 19 | # 20 | # This implementation is based in Templater actions, created by Jonas Nicklas 21 | # and Michael S. Klishin under MIT LICENSE. 22 | # 23 | class EmptyDirectory #:nodoc: 24 | attr_reader :base, :destination, :given_destination, :relative_destination, :config 25 | 26 | # Initializes given the source and destination. 27 | # 28 | # ==== Parameters 29 | # base:: A Foreman::Thor::Base instance 30 | # source:: Relative path to the source of this file 31 | # destination:: Relative path to the destination of this file 32 | # config:: give :verbose => false to not log the status. 33 | # 34 | def initialize(base, destination, config = {}) 35 | @base = base 36 | @config = {:verbose => true}.merge(config) 37 | self.destination = destination 38 | end 39 | 40 | # Checks if the destination file already exists. 41 | # 42 | # ==== Returns 43 | # Boolean:: true if the file exists, false otherwise. 44 | # 45 | def exists? 46 | ::File.exist?(destination) 47 | end 48 | 49 | def invoke! 50 | invoke_with_conflict_check do 51 | ::FileUtils.mkdir_p(destination) 52 | end 53 | end 54 | 55 | def revoke! 56 | say_status :remove, :red 57 | ::FileUtils.rm_rf(destination) if !pretend? && exists? 58 | given_destination 59 | end 60 | 61 | protected 62 | 63 | # Shortcut for pretend. 64 | # 65 | def pretend? 66 | base.options[:pretend] 67 | end 68 | 69 | # Sets the absolute destination value from a relative destination value. 70 | # It also stores the given and relative destination. Let's suppose our 71 | # script is being executed on "dest", it sets the destination root to 72 | # "dest". The destination, given_destination and relative_destination 73 | # are related in the following way: 74 | # 75 | # inside "bar" do 76 | # empty_directory "baz" 77 | # end 78 | # 79 | # destination #=> dest/bar/baz 80 | # relative_destination #=> bar/baz 81 | # given_destination #=> baz 82 | # 83 | def destination=(destination) 84 | return unless destination 85 | @given_destination = convert_encoded_instructions(destination.to_s) 86 | @destination = ::File.expand_path(@given_destination, base.destination_root) 87 | @relative_destination = base.relative_to_original_destination_root(@destination) 88 | end 89 | 90 | # Filenames in the encoded form are converted. If you have a file: 91 | # 92 | # %file_name%.rb 93 | # 94 | # It calls #file_name from the base and replaces %-string with the 95 | # return value (should be String) of #file_name: 96 | # 97 | # user.rb 98 | # 99 | # The method referenced can be either public or private. 100 | # 101 | def convert_encoded_instructions(filename) 102 | filename.gsub(/%(.*?)%/) do |initial_string| 103 | method = $1.strip 104 | base.respond_to?(method, true) ? base.send(method) : initial_string 105 | end 106 | end 107 | 108 | # Receives a hash of options and just execute the block if some 109 | # conditions are met. 110 | # 111 | def invoke_with_conflict_check(&block) 112 | if exists? 113 | on_conflict_behavior(&block) 114 | else 115 | say_status :create, :green 116 | yield unless pretend? 117 | end 118 | 119 | destination 120 | end 121 | 122 | # What to do when the destination file already exists. 123 | # 124 | def on_conflict_behavior 125 | say_status :exist, :blue 126 | end 127 | 128 | # Shortcut to say_status shell method. 129 | # 130 | def say_status(status, color) 131 | base.shell.say_status status, relative_destination, color if config[:verbose] 132 | end 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/actions/inject_into_file.rb: -------------------------------------------------------------------------------- 1 | require "foreman/vendor/thor/lib/thor/actions/empty_directory" 2 | 3 | class Foreman::Thor 4 | module Actions 5 | # Injects the given content into a file. Different from gsub_file, this 6 | # method is reversible. 7 | # 8 | # ==== Parameters 9 | # destination:: Relative path to the destination root 10 | # data:: Data to add to the file. Can be given as a block. 11 | # config:: give :verbose => false to not log the status and the flag 12 | # for injection (:after or :before) or :force => true for 13 | # insert two or more times the same content. 14 | # 15 | # ==== Examples 16 | # 17 | # insert_into_file "config/environment.rb", "config.gem :thor", :after => "Rails::Initializer.run do |config|\n" 18 | # 19 | # insert_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do 20 | # gems = ask "Which gems would you like to add?" 21 | # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n") 22 | # end 23 | # 24 | def insert_into_file(destination, *args, &block) 25 | data = block_given? ? block : args.shift 26 | config = args.shift 27 | action InjectIntoFile.new(self, destination, data, config) 28 | end 29 | alias_method :inject_into_file, :insert_into_file 30 | 31 | class InjectIntoFile < EmptyDirectory #:nodoc: 32 | attr_reader :replacement, :flag, :behavior 33 | 34 | def initialize(base, destination, data, config) 35 | super(base, destination, {:verbose => true}.merge(config)) 36 | 37 | @behavior, @flag = if @config.key?(:after) 38 | [:after, @config.delete(:after)] 39 | else 40 | [:before, @config.delete(:before)] 41 | end 42 | 43 | @replacement = data.is_a?(Proc) ? data.call : data 44 | @flag = Regexp.escape(@flag) unless @flag.is_a?(Regexp) 45 | end 46 | 47 | def invoke! 48 | say_status :invoke 49 | 50 | content = if @behavior == :after 51 | '\0' + replacement 52 | else 53 | replacement + '\0' 54 | end 55 | 56 | replace!(/#{flag}/, content, config[:force]) 57 | end 58 | 59 | def revoke! 60 | say_status :revoke 61 | 62 | regexp = if @behavior == :after 63 | content = '\1\2' 64 | /(#{flag})(.*)(#{Regexp.escape(replacement)})/m 65 | else 66 | content = '\2\3' 67 | /(#{Regexp.escape(replacement)})(.*)(#{flag})/m 68 | end 69 | 70 | replace!(regexp, content, true) 71 | end 72 | 73 | protected 74 | 75 | def say_status(behavior) 76 | status = if behavior == :invoke 77 | if flag == /\A/ 78 | :prepend 79 | elsif flag == /\z/ 80 | :append 81 | else 82 | :insert 83 | end 84 | else 85 | :subtract 86 | end 87 | 88 | super(status, config[:verbose]) 89 | end 90 | 91 | # Adds the content to the file. 92 | # 93 | def replace!(regexp, string, force) 94 | return if base.options[:pretend] 95 | content = File.binread(destination) 96 | if force || !content.include?(replacement) 97 | content.gsub!(regexp, string) 98 | File.open(destination, "wb") { |file| file.write(content) } 99 | end 100 | end 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/command.rb: -------------------------------------------------------------------------------- 1 | class Foreman::Thor 2 | class Command < Struct.new(:name, :description, :long_description, :usage, :options, :disable_class_options) 3 | FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/ 4 | 5 | def initialize(name, description, long_description, usage, options = nil, disable_class_options = false) 6 | super(name.to_s, description, long_description, usage, options || {}, disable_class_options) 7 | end 8 | 9 | def initialize_copy(other) #:nodoc: 10 | super(other) 11 | self.options = other.options.dup if other.options 12 | end 13 | 14 | def hidden? 15 | false 16 | end 17 | 18 | # By default, a command invokes a method in the thor class. You can change this 19 | # implementation to create custom commands. 20 | def run(instance, args = []) 21 | arity = nil 22 | 23 | if private_method?(instance) 24 | instance.class.handle_no_command_error(name) 25 | elsif public_method?(instance) 26 | arity = instance.method(name).arity 27 | instance.__send__(name, *args) 28 | elsif local_method?(instance, :method_missing) 29 | instance.__send__(:method_missing, name.to_sym, *args) 30 | else 31 | instance.class.handle_no_command_error(name) 32 | end 33 | rescue ArgumentError => e 34 | handle_argument_error?(instance, e, caller) ? instance.class.handle_argument_error(self, e, args, arity) : (raise e) 35 | rescue NoMethodError => e 36 | handle_no_method_error?(instance, e, caller) ? instance.class.handle_no_command_error(name) : (raise e) 37 | end 38 | 39 | # Returns the formatted usage by injecting given required arguments 40 | # and required options into the given usage. 41 | def formatted_usage(klass, namespace = true, subcommand = false) 42 | if namespace 43 | namespace = klass.namespace 44 | formatted = "#{namespace.gsub(/^(default)/, '')}:" 45 | end 46 | formatted = "#{klass.namespace.split(':').last} " if subcommand 47 | 48 | formatted ||= "" 49 | 50 | # Add usage with required arguments 51 | formatted << if klass && !klass.arguments.empty? 52 | usage.to_s.gsub(/^#{name}/) do |match| 53 | match << " " << klass.arguments.map(&:usage).compact.join(" ") 54 | end 55 | else 56 | usage.to_s 57 | end 58 | 59 | # Add required options 60 | formatted << " #{required_options}" 61 | 62 | # Strip and go! 63 | formatted.strip 64 | end 65 | 66 | protected 67 | 68 | def not_debugging?(instance) 69 | !(instance.class.respond_to?(:debugging) && instance.class.debugging) 70 | end 71 | 72 | def required_options 73 | @required_options ||= options.map { |_, o| o.usage if o.required? }.compact.sort.join(" ") 74 | end 75 | 76 | # Given a target, checks if this class name is a public method. 77 | def public_method?(instance) #:nodoc: 78 | !(instance.public_methods & [name.to_s, name.to_sym]).empty? 79 | end 80 | 81 | def private_method?(instance) 82 | !(instance.private_methods & [name.to_s, name.to_sym]).empty? 83 | end 84 | 85 | def local_method?(instance, name) 86 | methods = instance.public_methods(false) + instance.private_methods(false) + instance.protected_methods(false) 87 | !(methods & [name.to_s, name.to_sym]).empty? 88 | end 89 | 90 | def sans_backtrace(backtrace, caller) #:nodoc: 91 | saned = backtrace.reject { |frame| frame =~ FILE_REGEXP || (frame =~ /\.java:/ && RUBY_PLATFORM =~ /java/) || (frame =~ %r{^kernel/} && RUBY_ENGINE =~ /rbx/) } 92 | saned - caller 93 | end 94 | 95 | def handle_argument_error?(instance, error, caller) 96 | not_debugging?(instance) && (error.message =~ /wrong number of arguments/ || error.message =~ /given \d*, expected \d*/) && begin 97 | saned = sans_backtrace(error.backtrace, caller) 98 | # Ruby 1.9 always include the called method in the backtrace 99 | saned.empty? || (saned.size == 1 && RUBY_VERSION >= "1.9") 100 | end 101 | end 102 | 103 | def handle_no_method_error?(instance, error, caller) 104 | not_debugging?(instance) && 105 | error.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/ 106 | end 107 | end 108 | Task = Command 109 | 110 | # A command that is hidden in help messages but still invocable. 111 | class HiddenCommand < Command 112 | def hidden? 113 | true 114 | end 115 | end 116 | HiddenTask = HiddenCommand 117 | 118 | # A dynamic command that handles method missing scenarios. 119 | class DynamicCommand < Command 120 | def initialize(name, options = nil) 121 | super(name.to_s, "A dynamically-generated command", name.to_s, name.to_s, options) 122 | end 123 | 124 | def run(instance, args = []) 125 | if (instance.methods & [name.to_s, name.to_sym]).empty? 126 | super 127 | else 128 | instance.class.handle_no_command_error(name) 129 | end 130 | end 131 | end 132 | DynamicTask = DynamicCommand 133 | end 134 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb: -------------------------------------------------------------------------------- 1 | class Foreman::Thor 2 | module CoreExt #:nodoc: 3 | # A hash with indifferent access and magic predicates. 4 | # 5 | # hash = Foreman::Thor::CoreExt::HashWithIndifferentAccess.new 'foo' => 'bar', 'baz' => 'bee', 'force' => true 6 | # 7 | # hash[:foo] #=> 'bar' 8 | # hash['foo'] #=> 'bar' 9 | # hash.foo? #=> true 10 | # 11 | class HashWithIndifferentAccess < ::Hash #:nodoc: 12 | def initialize(hash = {}) 13 | super() 14 | hash.each do |key, value| 15 | self[convert_key(key)] = value 16 | end 17 | end 18 | 19 | def [](key) 20 | super(convert_key(key)) 21 | end 22 | 23 | def []=(key, value) 24 | super(convert_key(key), value) 25 | end 26 | 27 | def delete(key) 28 | super(convert_key(key)) 29 | end 30 | 31 | def fetch(key, *args) 32 | super(convert_key(key), *args) 33 | end 34 | 35 | def key?(key) 36 | super(convert_key(key)) 37 | end 38 | 39 | def values_at(*indices) 40 | indices.map { |key| self[convert_key(key)] } 41 | end 42 | 43 | def merge(other) 44 | dup.merge!(other) 45 | end 46 | 47 | def merge!(other) 48 | other.each do |key, value| 49 | self[convert_key(key)] = value 50 | end 51 | self 52 | end 53 | 54 | # Convert to a Hash with String keys. 55 | def to_hash 56 | Hash.new(default).merge!(self) 57 | end 58 | 59 | protected 60 | 61 | def convert_key(key) 62 | key.is_a?(Symbol) ? key.to_s : key 63 | end 64 | 65 | # Magic predicates. For instance: 66 | # 67 | # options.force? # => !!options['force'] 68 | # options.shebang # => "/usr/lib/local/ruby" 69 | # options.test_framework?(:rspec) # => options[:test_framework] == :rspec 70 | # 71 | def method_missing(method, *args) 72 | method = method.to_s 73 | if method =~ /^(\w+)\?$/ 74 | if args.empty? 75 | !!self[$1] 76 | else 77 | self[$1] == args.first 78 | end 79 | else 80 | self[method] 81 | end 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/core_ext/io_binary_read.rb: -------------------------------------------------------------------------------- 1 | class IO #:nodoc: 2 | class << self 3 | unless method_defined? :binread 4 | def binread(file, *args) 5 | raise ArgumentError, "wrong number of arguments (#{1 + args.size} for 1..3)" unless args.size < 3 6 | File.open(file, "rb") do |f| 7 | f.read(*args) 8 | end 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/core_ext/ordered_hash.rb: -------------------------------------------------------------------------------- 1 | class Foreman::Thor 2 | module CoreExt 3 | class OrderedHash < ::Hash 4 | if RUBY_VERSION < "1.9" 5 | def initialize(*args, &block) 6 | super 7 | @keys = [] 8 | end 9 | 10 | def initialize_copy(other) 11 | super 12 | # make a deep copy of keys 13 | @keys = other.keys 14 | end 15 | 16 | def []=(key, value) 17 | @keys << key unless key?(key) 18 | super 19 | end 20 | 21 | def delete(key) 22 | if key? key 23 | index = @keys.index(key) 24 | @keys.delete_at index 25 | end 26 | super 27 | end 28 | 29 | def delete_if 30 | super 31 | sync_keys! 32 | self 33 | end 34 | 35 | alias_method :reject!, :delete_if 36 | 37 | def reject(&block) 38 | dup.reject!(&block) 39 | end 40 | 41 | def keys 42 | @keys.dup 43 | end 44 | 45 | def values 46 | @keys.map { |key| self[key] } 47 | end 48 | 49 | def to_hash 50 | self 51 | end 52 | 53 | def to_a 54 | @keys.map { |key| [key, self[key]] } 55 | end 56 | 57 | def each_key 58 | return to_enum(:each_key) unless block_given? 59 | @keys.each { |key| yield(key) } 60 | self 61 | end 62 | 63 | def each_value 64 | return to_enum(:each_value) unless block_given? 65 | @keys.each { |key| yield(self[key]) } 66 | self 67 | end 68 | 69 | def each 70 | return to_enum(:each) unless block_given? 71 | @keys.each { |key| yield([key, self[key]]) } 72 | self 73 | end 74 | 75 | def each_pair 76 | return to_enum(:each_pair) unless block_given? 77 | @keys.each { |key| yield(key, self[key]) } 78 | self 79 | end 80 | 81 | alias_method :select, :find_all 82 | 83 | def clear 84 | super 85 | @keys.clear 86 | self 87 | end 88 | 89 | def shift 90 | k = @keys.first 91 | v = delete(k) 92 | [k, v] 93 | end 94 | 95 | def merge!(other_hash) 96 | if block_given? 97 | other_hash.each { |k, v| self[k] = key?(k) ? yield(k, self[k], v) : v } 98 | else 99 | other_hash.each { |k, v| self[k] = v } 100 | end 101 | self 102 | end 103 | 104 | alias_method :update, :merge! 105 | 106 | def merge(other_hash, &block) 107 | dup.merge!(other_hash, &block) 108 | end 109 | 110 | # When replacing with another hash, the initial order of our keys must come from the other hash -ordered or not. 111 | def replace(other) 112 | super 113 | @keys = other.keys 114 | self 115 | end 116 | 117 | def inspect 118 | "#<#{self.class} #{super}>" 119 | end 120 | 121 | private 122 | 123 | def sync_keys! 124 | @keys.delete_if { |k| !key?(k) } 125 | end 126 | end 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/error.rb: -------------------------------------------------------------------------------- 1 | class Foreman::Thor 2 | # Foreman::Thor::Error is raised when it's caused by wrong usage of thor classes. Those 3 | # errors have their backtrace suppressed and are nicely shown to the user. 4 | # 5 | # Errors that are caused by the developer, like declaring a method which 6 | # overwrites a thor keyword, SHOULD NOT raise a Foreman::Thor::Error. This way, we 7 | # ensure that developer errors are shown with full backtrace. 8 | class Error < StandardError 9 | end 10 | 11 | # Raised when a command was not found. 12 | class UndefinedCommandError < Error 13 | end 14 | UndefinedTaskError = UndefinedCommandError 15 | 16 | class AmbiguousCommandError < Error 17 | end 18 | AmbiguousTaskError = AmbiguousCommandError 19 | 20 | # Raised when a command was found, but not invoked properly. 21 | class InvocationError < Error 22 | end 23 | 24 | class UnknownArgumentError < Error 25 | end 26 | 27 | class RequiredArgumentMissingError < InvocationError 28 | end 29 | 30 | class MalformattedArgumentError < InvocationError 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/invocation.rb: -------------------------------------------------------------------------------- 1 | class Foreman::Thor 2 | module Invocation 3 | def self.included(base) #:nodoc: 4 | base.extend ClassMethods 5 | end 6 | 7 | module ClassMethods 8 | # This method is responsible for receiving a name and find the proper 9 | # class and command for it. The key is an optional parameter which is 10 | # available only in class methods invocations (i.e. in Foreman::Thor::Group). 11 | def prepare_for_invocation(key, name) #:nodoc: 12 | case name 13 | when Symbol, String 14 | Foreman::Thor::Util.find_class_and_command_by_namespace(name.to_s, !key) 15 | else 16 | name 17 | end 18 | end 19 | end 20 | 21 | # Make initializer aware of invocations and the initialization args. 22 | def initialize(args = [], options = {}, config = {}, &block) #:nodoc: 23 | @_invocations = config[:invocations] || Hash.new { |h, k| h[k] = [] } 24 | @_initializer = [args, options, config] 25 | super 26 | end 27 | 28 | # Make the current command chain accessible with in a Foreman::Thor-(sub)command 29 | def current_command_chain 30 | @_invocations.values.flatten.map(&:to_sym) 31 | end 32 | 33 | # Receives a name and invokes it. The name can be a string (either "command" or 34 | # "namespace:command"), a Foreman::Thor::Command, a Class or a Foreman::Thor instance. If the 35 | # command cannot be guessed by name, it can also be supplied as second argument. 36 | # 37 | # You can also supply the arguments, options and configuration values for 38 | # the command to be invoked, if none is given, the same values used to 39 | # initialize the invoker are used to initialize the invoked. 40 | # 41 | # When no name is given, it will invoke the default command of the current class. 42 | # 43 | # ==== Examples 44 | # 45 | # class A < Foreman::Thor 46 | # def foo 47 | # invoke :bar 48 | # invoke "b:hello", ["Erik"] 49 | # end 50 | # 51 | # def bar 52 | # invoke "b:hello", ["Erik"] 53 | # end 54 | # end 55 | # 56 | # class B < Foreman::Thor 57 | # def hello(name) 58 | # puts "hello #{name}" 59 | # end 60 | # end 61 | # 62 | # You can notice that the method "foo" above invokes two commands: "bar", 63 | # which belongs to the same class and "hello" which belongs to the class B. 64 | # 65 | # By using an invocation system you ensure that a command is invoked only once. 66 | # In the example above, invoking "foo" will invoke "b:hello" just once, even 67 | # if it's invoked later by "bar" method. 68 | # 69 | # When class A invokes class B, all arguments used on A initialization are 70 | # supplied to B. This allows lazy parse of options. Let's suppose you have 71 | # some rspec commands: 72 | # 73 | # class Rspec < Foreman::Thor::Group 74 | # class_option :mock_framework, :type => :string, :default => :rr 75 | # 76 | # def invoke_mock_framework 77 | # invoke "rspec:#{options[:mock_framework]}" 78 | # end 79 | # end 80 | # 81 | # As you noticed, it invokes the given mock framework, which might have its 82 | # own options: 83 | # 84 | # class Rspec::RR < Foreman::Thor::Group 85 | # class_option :style, :type => :string, :default => :mock 86 | # end 87 | # 88 | # Since it's not rspec concern to parse mock framework options, when RR 89 | # is invoked all options are parsed again, so RR can extract only the options 90 | # that it's going to use. 91 | # 92 | # If you want Rspec::RR to be initialized with its own set of options, you 93 | # have to do that explicitly: 94 | # 95 | # invoke "rspec:rr", [], :style => :foo 96 | # 97 | # Besides giving an instance, you can also give a class to invoke: 98 | # 99 | # invoke Rspec::RR, [], :style => :foo 100 | # 101 | def invoke(name = nil, *args) 102 | if name.nil? 103 | warn "[Foreman::Thor] Calling invoke() without argument is deprecated. Please use invoke_all instead.\n#{caller.join("\n")}" 104 | return invoke_all 105 | end 106 | 107 | args.unshift(nil) if args.first.is_a?(Array) || args.first.nil? 108 | command, args, opts, config = args 109 | 110 | klass, command = _retrieve_class_and_command(name, command) 111 | raise "Missing Foreman::Thor class for invoke #{name}" unless klass 112 | raise "Expected Foreman::Thor class, got #{klass}" unless klass <= Foreman::Thor::Base 113 | 114 | args, opts, config = _parse_initialization_options(args, opts, config) 115 | klass.send(:dispatch, command, args, opts, config) do |instance| 116 | instance.parent_options = options 117 | end 118 | end 119 | 120 | # Invoke the given command if the given args. 121 | def invoke_command(command, *args) #:nodoc: 122 | current = @_invocations[self.class] 123 | 124 | unless current.include?(command.name) 125 | current << command.name 126 | command.run(self, *args) 127 | end 128 | end 129 | alias_method :invoke_task, :invoke_command 130 | 131 | # Invoke all commands for the current instance. 132 | def invoke_all #:nodoc: 133 | self.class.all_commands.map { |_, command| invoke_command(command) } 134 | end 135 | 136 | # Invokes using shell padding. 137 | def invoke_with_padding(*args) 138 | with_padding { invoke(*args) } 139 | end 140 | 141 | protected 142 | 143 | # Configuration values that are shared between invocations. 144 | def _shared_configuration #:nodoc: 145 | {:invocations => @_invocations} 146 | end 147 | 148 | # This method simply retrieves the class and command to be invoked. 149 | # If the name is nil or the given name is a command in the current class, 150 | # use the given name and return self as class. Otherwise, call 151 | # prepare_for_invocation in the current class. 152 | def _retrieve_class_and_command(name, sent_command = nil) #:nodoc: 153 | if name.nil? 154 | [self.class, nil] 155 | elsif self.class.all_commands[name.to_s] 156 | [self.class, name.to_s] 157 | else 158 | klass, command = self.class.prepare_for_invocation(nil, name) 159 | [klass, command || sent_command] 160 | end 161 | end 162 | alias_method :_retrieve_class_and_task, :_retrieve_class_and_command 163 | 164 | # Initialize klass using values stored in the @_initializer. 165 | def _parse_initialization_options(args, opts, config) #:nodoc: 166 | stored_args, stored_opts, stored_config = @_initializer 167 | 168 | args ||= stored_args.dup 169 | opts ||= stored_opts.dup 170 | 171 | config ||= {} 172 | config = stored_config.merge(_shared_configuration).merge!(config) 173 | 174 | [args, opts, config] 175 | end 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/line_editor.rb: -------------------------------------------------------------------------------- 1 | require "foreman/vendor/thor/lib/thor/line_editor/basic" 2 | require "foreman/vendor/thor/lib/thor/line_editor/readline" 3 | 4 | class Foreman::Thor 5 | module LineEditor 6 | def self.readline(prompt, options = {}) 7 | best_available.new(prompt, options).readline 8 | end 9 | 10 | def self.best_available 11 | [ 12 | Foreman::Thor::LineEditor::Readline, 13 | Foreman::Thor::LineEditor::Basic 14 | ].detect(&:available?) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/line_editor/basic.rb: -------------------------------------------------------------------------------- 1 | class Foreman::Thor 2 | module LineEditor 3 | class Basic 4 | attr_reader :prompt, :options 5 | 6 | def self.available? 7 | true 8 | end 9 | 10 | def initialize(prompt, options) 11 | @prompt = prompt 12 | @options = options 13 | end 14 | 15 | def readline 16 | $stdout.print(prompt) 17 | get_input 18 | end 19 | 20 | private 21 | 22 | def get_input 23 | if echo? 24 | $stdin.gets 25 | else 26 | $stdin.noecho(&:gets) 27 | end 28 | end 29 | 30 | def echo? 31 | options.fetch(:echo, true) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/line_editor/readline.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require "readline" 3 | rescue LoadError 4 | end 5 | 6 | class Foreman::Thor 7 | module LineEditor 8 | class Readline < Basic 9 | def self.available? 10 | Object.const_defined?(:Readline) 11 | end 12 | 13 | def readline 14 | if echo? 15 | ::Readline.completion_append_character = nil 16 | # Ruby 1.8.7 does not allow Readline.completion_proc= to receive nil. 17 | if complete = completion_proc 18 | ::Readline.completion_proc = complete 19 | end 20 | ::Readline.readline(prompt, add_to_history?) 21 | else 22 | super 23 | end 24 | end 25 | 26 | private 27 | 28 | def add_to_history? 29 | options.fetch(:add_to_history, true) 30 | end 31 | 32 | def completion_proc 33 | if use_path_completion? 34 | proc { |text| PathCompletion.new(text).matches } 35 | elsif completion_options.any? 36 | proc do |text| 37 | completion_options.select { |option| option.start_with?(text) } 38 | end 39 | end 40 | end 41 | 42 | def completion_options 43 | options.fetch(:limited_to, []) 44 | end 45 | 46 | def use_path_completion? 47 | options.fetch(:path, false) 48 | end 49 | 50 | class PathCompletion 51 | attr_reader :text 52 | private :text 53 | 54 | def initialize(text) 55 | @text = text 56 | end 57 | 58 | def matches 59 | relative_matches 60 | end 61 | 62 | private 63 | 64 | def relative_matches 65 | absolute_matches.map { |path| path.sub(base_path, "") } 66 | end 67 | 68 | def absolute_matches 69 | Dir[glob_pattern].map do |path| 70 | if File.directory?(path) 71 | "#{path}/" 72 | else 73 | path 74 | end 75 | end 76 | end 77 | 78 | def glob_pattern 79 | "#{base_path}#{text}*" 80 | end 81 | 82 | def base_path 83 | "#{Dir.pwd}/" 84 | end 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/parser.rb: -------------------------------------------------------------------------------- 1 | require "foreman/vendor/thor/lib/thor/parser/argument" 2 | require "foreman/vendor/thor/lib/thor/parser/arguments" 3 | require "foreman/vendor/thor/lib/thor/parser/option" 4 | require "foreman/vendor/thor/lib/thor/parser/options" 5 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/parser/argument.rb: -------------------------------------------------------------------------------- 1 | class Foreman::Thor 2 | class Argument #:nodoc: 3 | VALID_TYPES = [:numeric, :hash, :array, :string] 4 | 5 | attr_reader :name, :description, :enum, :required, :type, :default, :banner 6 | alias_method :human_name, :name 7 | 8 | def initialize(name, options = {}) 9 | class_name = self.class.name.split("::").last 10 | 11 | type = options[:type] 12 | 13 | raise ArgumentError, "#{class_name} name can't be nil." if name.nil? 14 | raise ArgumentError, "Type :#{type} is not valid for #{class_name.downcase}s." if type && !valid_type?(type) 15 | 16 | @name = name.to_s 17 | @description = options[:desc] 18 | @required = options.key?(:required) ? options[:required] : true 19 | @type = (type || :string).to_sym 20 | @default = options[:default] 21 | @banner = options[:banner] || default_banner 22 | @enum = options[:enum] 23 | 24 | validate! # Trigger specific validations 25 | end 26 | 27 | def usage 28 | required? ? banner : "[#{banner}]" 29 | end 30 | 31 | def required? 32 | required 33 | end 34 | 35 | def show_default? 36 | case default 37 | when Array, String, Hash 38 | !default.empty? 39 | else 40 | default 41 | end 42 | end 43 | 44 | protected 45 | 46 | def validate! 47 | raise ArgumentError, "An argument cannot be required and have default value." if required? && !default.nil? 48 | raise ArgumentError, "An argument cannot have an enum other than an array." if @enum && !@enum.is_a?(Array) 49 | end 50 | 51 | def valid_type?(type) 52 | self.class::VALID_TYPES.include?(type.to_sym) 53 | end 54 | 55 | def default_banner 56 | case type 57 | when :boolean 58 | nil 59 | when :string, :default 60 | human_name.upcase 61 | when :numeric 62 | "N" 63 | when :hash 64 | "key:value" 65 | when :array 66 | "one two three" 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/parser/arguments.rb: -------------------------------------------------------------------------------- 1 | class Foreman::Thor 2 | class Arguments #:nodoc: # rubocop:disable ClassLength 3 | NUMERIC = /[-+]?(\d*\.\d+|\d+)/ 4 | 5 | # Receives an array of args and returns two arrays, one with arguments 6 | # and one with switches. 7 | # 8 | def self.split(args) 9 | arguments = [] 10 | 11 | args.each do |item| 12 | break if item =~ /^-/ 13 | arguments << item 14 | end 15 | 16 | [arguments, args[Range.new(arguments.size, -1)]] 17 | end 18 | 19 | def self.parse(*args) 20 | to_parse = args.pop 21 | new(*args).parse(to_parse) 22 | end 23 | 24 | # Takes an array of Foreman::Thor::Argument objects. 25 | # 26 | def initialize(arguments = []) 27 | @assigns = {} 28 | @non_assigned_required = [] 29 | @switches = arguments 30 | 31 | arguments.each do |argument| 32 | if !argument.default.nil? 33 | @assigns[argument.human_name] = argument.default 34 | elsif argument.required? 35 | @non_assigned_required << argument 36 | end 37 | end 38 | end 39 | 40 | def parse(args) 41 | @pile = args.dup 42 | 43 | @switches.each do |argument| 44 | break unless peek 45 | @non_assigned_required.delete(argument) 46 | @assigns[argument.human_name] = send(:"parse_#{argument.type}", argument.human_name) 47 | end 48 | 49 | check_requirement! 50 | @assigns 51 | end 52 | 53 | def remaining 54 | @pile 55 | end 56 | 57 | private 58 | 59 | def no_or_skip?(arg) 60 | arg =~ /^--(no|skip)-([-\w]+)$/ 61 | $2 62 | end 63 | 64 | def last? 65 | @pile.empty? 66 | end 67 | 68 | def peek 69 | @pile.first 70 | end 71 | 72 | def shift 73 | @pile.shift 74 | end 75 | 76 | def unshift(arg) 77 | if arg.is_a?(Array) 78 | @pile = arg + @pile 79 | else 80 | @pile.unshift(arg) 81 | end 82 | end 83 | 84 | def current_is_value? 85 | peek && peek.to_s !~ /^-/ 86 | end 87 | 88 | # Runs through the argument array getting strings that contains ":" and 89 | # mark it as a hash: 90 | # 91 | # [ "name:string", "age:integer" ] 92 | # 93 | # Becomes: 94 | # 95 | # { "name" => "string", "age" => "integer" } 96 | # 97 | def parse_hash(name) 98 | return shift if peek.is_a?(Hash) 99 | hash = {} 100 | 101 | while current_is_value? && peek.include?(":") 102 | key, value = shift.split(":", 2) 103 | raise MalformattedArgumentError, "You can't specify '#{key}' more than once in option '#{name}'; got #{key}:#{hash[key]} and #{key}:#{value}" if hash.include? key 104 | hash[key] = value 105 | end 106 | hash 107 | end 108 | 109 | # Runs through the argument array getting all strings until no string is 110 | # found or a switch is found. 111 | # 112 | # ["a", "b", "c"] 113 | # 114 | # And returns it as an array: 115 | # 116 | # ["a", "b", "c"] 117 | # 118 | def parse_array(name) 119 | return shift if peek.is_a?(Array) 120 | array = [] 121 | array << shift while current_is_value? 122 | array 123 | end 124 | 125 | # Check if the peek is numeric format and return a Float or Integer. 126 | # Check if the peek is included in enum if enum is provided. 127 | # Otherwise raises an error. 128 | # 129 | def parse_numeric(name) 130 | return shift if peek.is_a?(Numeric) 131 | 132 | unless peek =~ NUMERIC && $& == peek 133 | raise MalformattedArgumentError, "Expected numeric value for '#{name}'; got #{peek.inspect}" 134 | end 135 | 136 | value = $&.index(".") ? shift.to_f : shift.to_i 137 | if @switches.is_a?(Hash) && switch = @switches[name] 138 | if switch.enum && !switch.enum.include?(value) 139 | raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}" 140 | end 141 | end 142 | value 143 | end 144 | 145 | # Parse string: 146 | # for --string-arg, just return the current value in the pile 147 | # for --no-string-arg, nil 148 | # Check if the peek is included in enum if enum is provided. Otherwise raises an error. 149 | # 150 | def parse_string(name) 151 | if no_or_skip?(name) 152 | nil 153 | else 154 | value = shift 155 | if @switches.is_a?(Hash) && switch = @switches[name] 156 | if switch.enum && !switch.enum.include?(value) 157 | raise MalformattedArgumentError, "Expected '#{name}' to be one of #{switch.enum.join(', ')}; got #{value}" 158 | end 159 | end 160 | value 161 | end 162 | end 163 | 164 | # Raises an error if @non_assigned_required array is not empty. 165 | # 166 | def check_requirement! 167 | return if @non_assigned_required.empty? 168 | names = @non_assigned_required.map do |o| 169 | o.respond_to?(:switch_name) ? o.switch_name : o.human_name 170 | end.join("', '") 171 | class_name = self.class.name.split("::").last.downcase 172 | raise RequiredArgumentMissingError, "No value provided for required #{class_name} '#{names}'" 173 | end 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/parser/option.rb: -------------------------------------------------------------------------------- 1 | class Foreman::Thor 2 | class Option < Argument #:nodoc: 3 | attr_reader :aliases, :group, :lazy_default, :hide 4 | 5 | VALID_TYPES = [:boolean, :numeric, :hash, :array, :string] 6 | 7 | def initialize(name, options = {}) 8 | options[:required] = false unless options.key?(:required) 9 | super 10 | @lazy_default = options[:lazy_default] 11 | @group = options[:group].to_s.capitalize if options[:group] 12 | @aliases = Array(options[:aliases]) 13 | @hide = options[:hide] 14 | end 15 | 16 | # This parse quick options given as method_options. It makes several 17 | # assumptions, but you can be more specific using the option method. 18 | # 19 | # parse :foo => "bar" 20 | # #=> Option foo with default value bar 21 | # 22 | # parse [:foo, :baz] => "bar" 23 | # #=> Option foo with default value bar and alias :baz 24 | # 25 | # parse :foo => :required 26 | # #=> Required option foo without default value 27 | # 28 | # parse :foo => 2 29 | # #=> Option foo with default value 2 and type numeric 30 | # 31 | # parse :foo => :numeric 32 | # #=> Option foo without default value and type numeric 33 | # 34 | # parse :foo => true 35 | # #=> Option foo with default value true and type boolean 36 | # 37 | # The valid types are :boolean, :numeric, :hash, :array and :string. If none 38 | # is given a default type is assumed. This default type accepts arguments as 39 | # string (--foo=value) or booleans (just --foo). 40 | # 41 | # By default all options are optional, unless :required is given. 42 | # 43 | def self.parse(key, value) 44 | if key.is_a?(Array) 45 | name, *aliases = key 46 | else 47 | name = key 48 | aliases = [] 49 | end 50 | 51 | name = name.to_s 52 | default = value 53 | 54 | type = case value 55 | when Symbol 56 | default = nil 57 | if VALID_TYPES.include?(value) 58 | value 59 | elsif required = (value == :required) # rubocop:disable AssignmentInCondition 60 | :string 61 | end 62 | when TrueClass, FalseClass 63 | :boolean 64 | when Numeric 65 | :numeric 66 | when Hash, Array, String 67 | value.class.name.downcase.to_sym 68 | end 69 | 70 | new(name.to_s, :required => required, :type => type, :default => default, :aliases => aliases) 71 | end 72 | 73 | def switch_name 74 | @switch_name ||= dasherized? ? name : dasherize(name) 75 | end 76 | 77 | def human_name 78 | @human_name ||= dasherized? ? undasherize(name) : name 79 | end 80 | 81 | def usage(padding = 0) 82 | sample = if banner && !banner.to_s.empty? 83 | "#{switch_name}=#{banner}" 84 | else 85 | switch_name 86 | end 87 | 88 | sample = "[#{sample}]" unless required? 89 | 90 | if boolean? 91 | sample << ", [#{dasherize('no-' + human_name)}]" unless (name == "force") || name.start_with?("no-") 92 | end 93 | 94 | if aliases.empty? 95 | (" " * padding) << sample 96 | else 97 | "#{aliases.join(', ')}, #{sample}" 98 | end 99 | end 100 | 101 | VALID_TYPES.each do |type| 102 | class_eval <<-RUBY, __FILE__, __LINE__ + 1 103 | def #{type}? 104 | self.type == #{type.inspect} 105 | end 106 | RUBY 107 | end 108 | 109 | protected 110 | 111 | def validate! 112 | raise ArgumentError, "An option cannot be boolean and required." if boolean? && required? 113 | validate_default_type! 114 | end 115 | 116 | def validate_default_type! 117 | default_type = case @default 118 | when nil 119 | return 120 | when TrueClass, FalseClass 121 | required? ? :string : :boolean 122 | when Numeric 123 | :numeric 124 | when Symbol 125 | :string 126 | when Hash, Array, String 127 | @default.class.name.downcase.to_sym 128 | end 129 | 130 | # TODO: This should raise an ArgumentError in a future version of Foreman::Thor 131 | warn "Expected #{@type} default value for '#{switch_name}'; got #{@default.inspect} (#{default_type})" unless default_type == @type 132 | end 133 | 134 | def dasherized? 135 | name.index("-") == 0 136 | end 137 | 138 | def undasherize(str) 139 | str.sub(/^-{1,2}/, "") 140 | end 141 | 142 | def dasherize(str) 143 | (str.length > 1 ? "--" : "-") + str.tr("_", "-") 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/parser/options.rb: -------------------------------------------------------------------------------- 1 | class Foreman::Thor 2 | class Options < Arguments #:nodoc: # rubocop:disable ClassLength 3 | LONG_RE = /^(--\w+(?:-\w+)*)$/ 4 | SHORT_RE = /^(-[a-z])$/i 5 | EQ_RE = /^(--\w+(?:-\w+)*|-[a-z])=(.*)$/i 6 | SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args 7 | SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i 8 | OPTS_END = "--".freeze 9 | 10 | # Receives a hash and makes it switches. 11 | def self.to_switches(options) 12 | options.map do |key, value| 13 | case value 14 | when true 15 | "--#{key}" 16 | when Array 17 | "--#{key} #{value.map(&:inspect).join(' ')}" 18 | when Hash 19 | "--#{key} #{value.map { |k, v| "#{k}:#{v}" }.join(' ')}" 20 | when nil, false 21 | "" 22 | else 23 | "--#{key} #{value.inspect}" 24 | end 25 | end.join(" ") 26 | end 27 | 28 | # Takes a hash of Foreman::Thor::Option and a hash with defaults. 29 | # 30 | # If +stop_on_unknown+ is true, #parse will stop as soon as it encounters 31 | # an unknown option or a regular argument. 32 | def initialize(hash_options = {}, defaults = {}, stop_on_unknown = false) 33 | @stop_on_unknown = stop_on_unknown 34 | options = hash_options.values 35 | super(options) 36 | 37 | # Add defaults 38 | defaults.each do |key, value| 39 | @assigns[key.to_s] = value 40 | @non_assigned_required.delete(hash_options[key]) 41 | end 42 | 43 | @shorts = {} 44 | @switches = {} 45 | @extra = [] 46 | 47 | options.each do |option| 48 | @switches[option.switch_name] = option 49 | 50 | option.aliases.each do |short| 51 | name = short.to_s.sub(/^(?!\-)/, "-") 52 | @shorts[name] ||= option.switch_name 53 | end 54 | end 55 | end 56 | 57 | def remaining 58 | @extra 59 | end 60 | 61 | def peek 62 | return super unless @parsing_options 63 | 64 | result = super 65 | if result == OPTS_END 66 | shift 67 | @parsing_options = false 68 | super 69 | else 70 | result 71 | end 72 | end 73 | 74 | def parse(args) # rubocop:disable MethodLength 75 | @pile = args.dup 76 | @parsing_options = true 77 | 78 | while peek 79 | if parsing_options? 80 | match, is_switch = current_is_switch? 81 | shifted = shift 82 | 83 | if is_switch 84 | case shifted 85 | when SHORT_SQ_RE 86 | unshift($1.split("").map { |f| "-#{f}" }) 87 | next 88 | when EQ_RE, SHORT_NUM 89 | unshift($2) 90 | switch = $1 91 | when LONG_RE, SHORT_RE 92 | switch = $1 93 | end 94 | 95 | switch = normalize_switch(switch) 96 | option = switch_option(switch) 97 | @assigns[option.human_name] = parse_peek(switch, option) 98 | elsif @stop_on_unknown 99 | @parsing_options = false 100 | @extra << shifted 101 | @extra << shift while peek 102 | break 103 | elsif match 104 | @extra << shifted 105 | @extra << shift while peek && peek !~ /^-/ 106 | else 107 | @extra << shifted 108 | end 109 | else 110 | @extra << shift 111 | end 112 | end 113 | 114 | check_requirement! 115 | 116 | assigns = Foreman::Thor::CoreExt::HashWithIndifferentAccess.new(@assigns) 117 | assigns.freeze 118 | assigns 119 | end 120 | 121 | def check_unknown! 122 | # an unknown option starts with - or -- and has no more --'s afterward. 123 | unknown = @extra.select { |str| str =~ /^--?(?:(?!--).)*$/ } 124 | raise UnknownArgumentError, "Unknown switches '#{unknown.join(', ')}'" unless unknown.empty? 125 | end 126 | 127 | protected 128 | 129 | # Check if the current value in peek is a registered switch. 130 | # 131 | # Two booleans are returned. The first is true if the current value 132 | # starts with a hyphen; the second is true if it is a registered switch. 133 | def current_is_switch? 134 | case peek 135 | when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM 136 | [true, switch?($1)] 137 | when SHORT_SQ_RE 138 | [true, $1.split("").any? { |f| switch?("-#{f}") }] 139 | else 140 | [false, false] 141 | end 142 | end 143 | 144 | def current_is_switch_formatted? 145 | case peek 146 | when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM, SHORT_SQ_RE 147 | true 148 | else 149 | false 150 | end 151 | end 152 | 153 | def current_is_value? 154 | peek && (!parsing_options? || super) 155 | end 156 | 157 | def switch?(arg) 158 | switch_option(normalize_switch(arg)) 159 | end 160 | 161 | def switch_option(arg) 162 | if match = no_or_skip?(arg) # rubocop:disable AssignmentInCondition 163 | @switches[arg] || @switches["--#{match}"] 164 | else 165 | @switches[arg] 166 | end 167 | end 168 | 169 | # Check if the given argument is actually a shortcut. 170 | # 171 | def normalize_switch(arg) 172 | (@shorts[arg] || arg).tr("_", "-") 173 | end 174 | 175 | def parsing_options? 176 | peek 177 | @parsing_options 178 | end 179 | 180 | # Parse boolean values which can be given as --foo=true, --foo or --no-foo. 181 | # 182 | def parse_boolean(switch) 183 | if current_is_value? 184 | if ["true", "TRUE", "t", "T", true].include?(peek) 185 | shift 186 | true 187 | elsif ["false", "FALSE", "f", "F", false].include?(peek) 188 | shift 189 | false 190 | else 191 | true 192 | end 193 | else 194 | @switches.key?(switch) || !no_or_skip?(switch) 195 | end 196 | end 197 | 198 | # Parse the value at the peek analyzing if it requires an input or not. 199 | # 200 | def parse_peek(switch, option) 201 | if parsing_options? && (current_is_switch_formatted? || last?) 202 | if option.boolean? 203 | # No problem for boolean types 204 | elsif no_or_skip?(switch) 205 | return nil # User set value to nil 206 | elsif option.string? && !option.required? 207 | # Return the default if there is one, else the human name 208 | return option.lazy_default || option.default || option.human_name 209 | elsif option.lazy_default 210 | return option.lazy_default 211 | else 212 | raise MalformattedArgumentError, "No value provided for option '#{switch}'" 213 | end 214 | end 215 | 216 | @non_assigned_required.delete(option) 217 | send(:"parse_#{option.type}", switch) 218 | end 219 | end 220 | end 221 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/rake_compat.rb: -------------------------------------------------------------------------------- 1 | require "rake" 2 | require "rake/dsl_definition" 3 | 4 | class Foreman::Thor 5 | # Adds a compatibility layer to your Foreman::Thor classes which allows you to use 6 | # rake package tasks. For example, to use rspec rake tasks, one can do: 7 | # 8 | # require 'foreman/vendor/thor/lib/thor/rake_compat' 9 | # require 'rspec/core/rake_task' 10 | # 11 | # class Default < Foreman::Thor 12 | # include Foreman::Thor::RakeCompat 13 | # 14 | # RSpec::Core::RakeTask.new(:spec) do |t| 15 | # t.spec_opts = ['--options', './.rspec'] 16 | # t.spec_files = FileList['spec/**/*_spec.rb'] 17 | # end 18 | # end 19 | # 20 | module RakeCompat 21 | include Rake::DSL if defined?(Rake::DSL) 22 | 23 | def self.rake_classes 24 | @rake_classes ||= [] 25 | end 26 | 27 | def self.included(base) 28 | # Hack. Make rakefile point to invoker, so rdoc task is generated properly. 29 | rakefile = File.basename(caller[0].match(/(.*):\d+/)[1]) 30 | Rake.application.instance_variable_set(:@rakefile, rakefile) 31 | rake_classes << base 32 | end 33 | end 34 | end 35 | 36 | # override task on (main), for compatibility with Rake 0.9 37 | instance_eval do 38 | alias rake_namespace namespace 39 | 40 | def task(*) 41 | task = super 42 | 43 | if klass = Foreman::Thor::RakeCompat.rake_classes.last # rubocop:disable AssignmentInCondition 44 | non_namespaced_name = task.name.split(":").last 45 | 46 | description = non_namespaced_name 47 | description << task.arg_names.map { |n| n.to_s.upcase }.join(" ") 48 | description.strip! 49 | 50 | klass.desc description, Rake.application.last_description || non_namespaced_name 51 | Rake.application.last_description = nil 52 | klass.send :define_method, non_namespaced_name do |*args| 53 | Rake::Task[task.name.to_sym].invoke(*args) 54 | end 55 | end 56 | 57 | task 58 | end 59 | 60 | def namespace(name) 61 | if klass = Foreman::Thor::RakeCompat.rake_classes.last # rubocop:disable AssignmentInCondition 62 | const_name = Foreman::Thor::Util.camel_case(name.to_s).to_sym 63 | klass.const_set(const_name, Class.new(Foreman::Thor)) 64 | new_klass = klass.const_get(const_name) 65 | Foreman::Thor::RakeCompat.rake_classes << new_klass 66 | end 67 | 68 | super 69 | Foreman::Thor::RakeCompat.rake_classes.pop 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/shell.rb: -------------------------------------------------------------------------------- 1 | require "rbconfig" 2 | 3 | class Foreman::Thor 4 | module Base 5 | class << self 6 | attr_writer :shell 7 | 8 | # Returns the shell used in all Foreman::Thor classes. If you are in a Unix platform 9 | # it will use a colored log, otherwise it will use a basic one without color. 10 | # 11 | def shell 12 | @shell ||= if ENV["THOR_SHELL"] && !ENV["THOR_SHELL"].empty? 13 | Foreman::Thor::Shell.const_get(ENV["THOR_SHELL"]) 14 | elsif RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ && !ENV["ANSICON"] 15 | Foreman::Thor::Shell::Basic 16 | else 17 | Foreman::Thor::Shell::Color 18 | end 19 | end 20 | end 21 | end 22 | 23 | module Shell 24 | SHELL_DELEGATED_METHODS = [:ask, :error, :set_color, :yes?, :no?, :say, :say_status, :print_in_columns, :print_table, :print_wrapped, :file_collision, :terminal_width] 25 | attr_writer :shell 26 | 27 | autoload :Basic, "foreman/vendor/thor/lib/thor/shell/basic" 28 | autoload :Color, "foreman/vendor/thor/lib/thor/shell/color" 29 | autoload :HTML, "foreman/vendor/thor/lib/thor/shell/html" 30 | 31 | # Add shell to initialize config values. 32 | # 33 | # ==== Configuration 34 | # shell:: An instance of the shell to be used. 35 | # 36 | # ==== Examples 37 | # 38 | # class MyScript < Foreman::Thor 39 | # argument :first, :type => :numeric 40 | # end 41 | # 42 | # MyScript.new [1.0], { :foo => :bar }, :shell => Foreman::Thor::Shell::Basic.new 43 | # 44 | def initialize(args = [], options = {}, config = {}) 45 | super 46 | self.shell = config[:shell] 47 | shell.base ||= self if shell.respond_to?(:base) 48 | end 49 | 50 | # Holds the shell for the given Foreman::Thor instance. If no shell is given, 51 | # it gets a default shell from Foreman::Thor::Base.shell. 52 | def shell 53 | @shell ||= Foreman::Thor::Base.shell.new 54 | end 55 | 56 | # Common methods that are delegated to the shell. 57 | SHELL_DELEGATED_METHODS.each do |method| 58 | module_eval <<-METHOD, __FILE__, __LINE__ 59 | def #{method}(*args,&block) 60 | shell.#{method}(*args,&block) 61 | end 62 | METHOD 63 | end 64 | 65 | # Yields the given block with padding. 66 | def with_padding 67 | shell.padding += 1 68 | yield 69 | ensure 70 | shell.padding -= 1 71 | end 72 | 73 | protected 74 | 75 | # Allow shell to be shared between invocations. 76 | # 77 | def _shared_configuration #:nodoc: 78 | super.merge!(:shell => shell) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/shell/color.rb: -------------------------------------------------------------------------------- 1 | require "foreman/vendor/thor/lib/thor/shell/basic" 2 | 3 | class Foreman::Thor 4 | module Shell 5 | # Inherit from Foreman::Thor::Shell::Basic and add set_color behavior. Check 6 | # Foreman::Thor::Shell::Basic to see all available methods. 7 | # 8 | class Color < Basic 9 | # Embed in a String to clear all previous ANSI sequences. 10 | CLEAR = "\e[0m" 11 | # The start of an ANSI bold sequence. 12 | BOLD = "\e[1m" 13 | 14 | # Set the terminal's foreground ANSI color to black. 15 | BLACK = "\e[30m" 16 | # Set the terminal's foreground ANSI color to red. 17 | RED = "\e[31m" 18 | # Set the terminal's foreground ANSI color to green. 19 | GREEN = "\e[32m" 20 | # Set the terminal's foreground ANSI color to yellow. 21 | YELLOW = "\e[33m" 22 | # Set the terminal's foreground ANSI color to blue. 23 | BLUE = "\e[34m" 24 | # Set the terminal's foreground ANSI color to magenta. 25 | MAGENTA = "\e[35m" 26 | # Set the terminal's foreground ANSI color to cyan. 27 | CYAN = "\e[36m" 28 | # Set the terminal's foreground ANSI color to white. 29 | WHITE = "\e[37m" 30 | 31 | # Set the terminal's background ANSI color to black. 32 | ON_BLACK = "\e[40m" 33 | # Set the terminal's background ANSI color to red. 34 | ON_RED = "\e[41m" 35 | # Set the terminal's background ANSI color to green. 36 | ON_GREEN = "\e[42m" 37 | # Set the terminal's background ANSI color to yellow. 38 | ON_YELLOW = "\e[43m" 39 | # Set the terminal's background ANSI color to blue. 40 | ON_BLUE = "\e[44m" 41 | # Set the terminal's background ANSI color to magenta. 42 | ON_MAGENTA = "\e[45m" 43 | # Set the terminal's background ANSI color to cyan. 44 | ON_CYAN = "\e[46m" 45 | # Set the terminal's background ANSI color to white. 46 | ON_WHITE = "\e[47m" 47 | 48 | # Set color by using a string or one of the defined constants. If a third 49 | # option is set to true, it also adds bold to the string. This is based 50 | # on Highline implementation and it automatically appends CLEAR to the end 51 | # of the returned String. 52 | # 53 | # Pass foreground, background and bold options to this method as 54 | # symbols. 55 | # 56 | # Example: 57 | # 58 | # set_color "Hi!", :red, :on_white, :bold 59 | # 60 | # The available colors are: 61 | # 62 | # :bold 63 | # :black 64 | # :red 65 | # :green 66 | # :yellow 67 | # :blue 68 | # :magenta 69 | # :cyan 70 | # :white 71 | # :on_black 72 | # :on_red 73 | # :on_green 74 | # :on_yellow 75 | # :on_blue 76 | # :on_magenta 77 | # :on_cyan 78 | # :on_white 79 | def set_color(string, *colors) 80 | if colors.compact.empty? || !can_display_colors? 81 | string 82 | elsif colors.all? { |color| color.is_a?(Symbol) || color.is_a?(String) } 83 | ansi_colors = colors.map { |color| lookup_color(color) } 84 | "#{ansi_colors.join}#{string}#{CLEAR}" 85 | else 86 | # The old API was `set_color(color, bold=boolean)`. We 87 | # continue to support the old API because you should never 88 | # break old APIs unnecessarily :P 89 | foreground, bold = colors 90 | foreground = self.class.const_get(foreground.to_s.upcase) if foreground.is_a?(Symbol) 91 | 92 | bold = bold ? BOLD : "" 93 | "#{bold}#{foreground}#{string}#{CLEAR}" 94 | end 95 | end 96 | 97 | protected 98 | 99 | def can_display_colors? 100 | stdout.tty? 101 | end 102 | 103 | # Overwrite show_diff to show diff with colors if Diff::LCS is 104 | # available. 105 | # 106 | def show_diff(destination, content) #:nodoc: 107 | if diff_lcs_loaded? && ENV["THOR_DIFF"].nil? && ENV["RAILS_DIFF"].nil? 108 | actual = File.binread(destination).to_s.split("\n") 109 | content = content.to_s.split("\n") 110 | 111 | Diff::LCS.sdiff(actual, content).each do |diff| 112 | output_diff_line(diff) 113 | end 114 | else 115 | super 116 | end 117 | end 118 | 119 | def output_diff_line(diff) #:nodoc: 120 | case diff.action 121 | when "-" 122 | say "- #{diff.old_element.chomp}", :red, true 123 | when "+" 124 | say "+ #{diff.new_element.chomp}", :green, true 125 | when "!" 126 | say "- #{diff.old_element.chomp}", :red, true 127 | say "+ #{diff.new_element.chomp}", :green, true 128 | else 129 | say " #{diff.old_element.chomp}", nil, true 130 | end 131 | end 132 | 133 | # Check if Diff::LCS is loaded. If it is, use it to create pretty output 134 | # for diff. 135 | # 136 | def diff_lcs_loaded? #:nodoc: 137 | return true if defined?(Diff::LCS) 138 | return @diff_lcs_loaded unless @diff_lcs_loaded.nil? 139 | 140 | @diff_lcs_loaded = begin 141 | require "diff/lcs" 142 | true 143 | rescue LoadError 144 | false 145 | end 146 | end 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/shell/html.rb: -------------------------------------------------------------------------------- 1 | require "foreman/vendor/thor/lib/thor/shell/basic" 2 | 3 | class Foreman::Thor 4 | module Shell 5 | # Inherit from Foreman::Thor::Shell::Basic and add set_color behavior. Check 6 | # Foreman::Thor::Shell::Basic to see all available methods. 7 | # 8 | class HTML < Basic 9 | # The start of an HTML bold sequence. 10 | BOLD = "font-weight: bold" 11 | 12 | # Set the terminal's foreground HTML color to black. 13 | BLACK = "color: black" 14 | # Set the terminal's foreground HTML color to red. 15 | RED = "color: red" 16 | # Set the terminal's foreground HTML color to green. 17 | GREEN = "color: green" 18 | # Set the terminal's foreground HTML color to yellow. 19 | YELLOW = "color: yellow" 20 | # Set the terminal's foreground HTML color to blue. 21 | BLUE = "color: blue" 22 | # Set the terminal's foreground HTML color to magenta. 23 | MAGENTA = "color: magenta" 24 | # Set the terminal's foreground HTML color to cyan. 25 | CYAN = "color: cyan" 26 | # Set the terminal's foreground HTML color to white. 27 | WHITE = "color: white" 28 | 29 | # Set the terminal's background HTML color to black. 30 | ON_BLACK = "background-color: black" 31 | # Set the terminal's background HTML color to red. 32 | ON_RED = "background-color: red" 33 | # Set the terminal's background HTML color to green. 34 | ON_GREEN = "background-color: green" 35 | # Set the terminal's background HTML color to yellow. 36 | ON_YELLOW = "background-color: yellow" 37 | # Set the terminal's background HTML color to blue. 38 | ON_BLUE = "background-color: blue" 39 | # Set the terminal's background HTML color to magenta. 40 | ON_MAGENTA = "background-color: magenta" 41 | # Set the terminal's background HTML color to cyan. 42 | ON_CYAN = "background-color: cyan" 43 | # Set the terminal's background HTML color to white. 44 | ON_WHITE = "background-color: white" 45 | 46 | # Set color by using a string or one of the defined constants. If a third 47 | # option is set to true, it also adds bold to the string. This is based 48 | # on Highline implementation and it automatically appends CLEAR to the end 49 | # of the returned String. 50 | # 51 | def set_color(string, *colors) 52 | if colors.all? { |color| color.is_a?(Symbol) || color.is_a?(String) } 53 | html_colors = colors.map { |color| lookup_color(color) } 54 | "#{string}" 55 | else 56 | color, bold = colors 57 | html_color = self.class.const_get(color.to_s.upcase) if color.is_a?(Symbol) 58 | styles = [html_color] 59 | styles << BOLD if bold 60 | "#{string}" 61 | end 62 | end 63 | 64 | # Ask something to the user and receives a response. 65 | # 66 | # ==== Example 67 | # ask("What is your name?") 68 | # 69 | # TODO: Implement #ask for Foreman::Thor::Shell::HTML 70 | def ask(statement, color = nil) 71 | raise NotImplementedError, "Implement #ask for Foreman::Thor::Shell::HTML" 72 | end 73 | 74 | protected 75 | 76 | def can_display_colors? 77 | true 78 | end 79 | 80 | # Overwrite show_diff to show diff with colors if Diff::LCS is 81 | # available. 82 | # 83 | def show_diff(destination, content) #:nodoc: 84 | if diff_lcs_loaded? && ENV["THOR_DIFF"].nil? && ENV["RAILS_DIFF"].nil? 85 | actual = File.binread(destination).to_s.split("\n") 86 | content = content.to_s.split("\n") 87 | 88 | Diff::LCS.sdiff(actual, content).each do |diff| 89 | output_diff_line(diff) 90 | end 91 | else 92 | super 93 | end 94 | end 95 | 96 | def output_diff_line(diff) #:nodoc: 97 | case diff.action 98 | when "-" 99 | say "- #{diff.old_element.chomp}", :red, true 100 | when "+" 101 | say "+ #{diff.new_element.chomp}", :green, true 102 | when "!" 103 | say "- #{diff.old_element.chomp}", :red, true 104 | say "+ #{diff.new_element.chomp}", :green, true 105 | else 106 | say " #{diff.old_element.chomp}", nil, true 107 | end 108 | end 109 | 110 | # Check if Diff::LCS is loaded. If it is, use it to create pretty output 111 | # for diff. 112 | # 113 | def diff_lcs_loaded? #:nodoc: 114 | return true if defined?(Diff::LCS) 115 | return @diff_lcs_loaded unless @diff_lcs_loaded.nil? 116 | 117 | @diff_lcs_loaded = begin 118 | require "diff/lcs" 119 | true 120 | rescue LoadError 121 | false 122 | end 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/util.rb: -------------------------------------------------------------------------------- 1 | require "rbconfig" 2 | 3 | class Foreman::Thor 4 | module Sandbox #:nodoc: 5 | end 6 | 7 | # This module holds several utilities: 8 | # 9 | # 1) Methods to convert thor namespaces to constants and vice-versa. 10 | # 11 | # Foreman::Thor::Util.namespace_from_thor_class(Foo::Bar::Baz) #=> "foo:bar:baz" 12 | # 13 | # 2) Loading thor files and sandboxing: 14 | # 15 | # Foreman::Thor::Util.load_thorfile("~/.thor/foo") 16 | # 17 | module Util 18 | class << self 19 | # Receives a namespace and search for it in the Foreman::Thor::Base subclasses. 20 | # 21 | # ==== Parameters 22 | # namespace:: The namespace to search for. 23 | # 24 | def find_by_namespace(namespace) 25 | namespace = "default#{namespace}" if namespace.empty? || namespace =~ /^:/ 26 | Foreman::Thor::Base.subclasses.detect { |klass| klass.namespace == namespace } 27 | end 28 | 29 | # Receives a constant and converts it to a Foreman::Thor namespace. Since Foreman::Thor 30 | # commands can be added to a sandbox, this method is also responsable for 31 | # removing the sandbox namespace. 32 | # 33 | # This method should not be used in general because it's used to deal with 34 | # older versions of Foreman::Thor. On current versions, if you need to get the 35 | # namespace from a class, just call namespace on it. 36 | # 37 | # ==== Parameters 38 | # constant:: The constant to be converted to the thor path. 39 | # 40 | # ==== Returns 41 | # String:: If we receive Foo::Bar::Baz it returns "foo:bar:baz" 42 | # 43 | def namespace_from_thor_class(constant) 44 | constant = constant.to_s.gsub(/^Foreman::Thor::Sandbox::/, "") 45 | constant = snake_case(constant).squeeze(":") 46 | constant 47 | end 48 | 49 | # Given the contents, evaluate it inside the sandbox and returns the 50 | # namespaces defined in the sandbox. 51 | # 52 | # ==== Parameters 53 | # contents 54 | # 55 | # ==== Returns 56 | # Array[Object] 57 | # 58 | def namespaces_in_content(contents, file = __FILE__) 59 | old_constants = Foreman::Thor::Base.subclasses.dup 60 | Foreman::Thor::Base.subclasses.clear 61 | 62 | load_thorfile(file, contents) 63 | 64 | new_constants = Foreman::Thor::Base.subclasses.dup 65 | Foreman::Thor::Base.subclasses.replace(old_constants) 66 | 67 | new_constants.map!(&:namespace) 68 | new_constants.compact! 69 | new_constants 70 | end 71 | 72 | # Returns the thor classes declared inside the given class. 73 | # 74 | def thor_classes_in(klass) 75 | stringfied_constants = klass.constants.map(&:to_s) 76 | Foreman::Thor::Base.subclasses.select do |subclass| 77 | next unless subclass.name 78 | stringfied_constants.include?(subclass.name.gsub("#{klass.name}::", "")) 79 | end 80 | end 81 | 82 | # Receives a string and convert it to snake case. SnakeCase returns snake_case. 83 | # 84 | # ==== Parameters 85 | # String 86 | # 87 | # ==== Returns 88 | # String 89 | # 90 | def snake_case(str) 91 | return str.downcase if str =~ /^[A-Z_]+$/ 92 | str.gsub(/\B[A-Z]/, '_\&').squeeze("_") =~ /_*(.*)/ 93 | $+.downcase 94 | end 95 | 96 | # Receives a string and convert it to camel case. camel_case returns CamelCase. 97 | # 98 | # ==== Parameters 99 | # String 100 | # 101 | # ==== Returns 102 | # String 103 | # 104 | def camel_case(str) 105 | return str if str !~ /_/ && str =~ /[A-Z]+.*/ 106 | str.split("_").map(&:capitalize).join 107 | end 108 | 109 | # Receives a namespace and tries to retrieve a Foreman::Thor or Foreman::Thor::Group class 110 | # from it. It first searches for a class using the all the given namespace, 111 | # if it's not found, removes the highest entry and searches for the class 112 | # again. If found, returns the highest entry as the class name. 113 | # 114 | # ==== Examples 115 | # 116 | # class Foo::Bar < Foreman::Thor 117 | # def baz 118 | # end 119 | # end 120 | # 121 | # class Baz::Foo < Foreman::Thor::Group 122 | # end 123 | # 124 | # Foreman::Thor::Util.namespace_to_thor_class("foo:bar") #=> Foo::Bar, nil # will invoke default command 125 | # Foreman::Thor::Util.namespace_to_thor_class("baz:foo") #=> Baz::Foo, nil 126 | # Foreman::Thor::Util.namespace_to_thor_class("foo:bar:baz") #=> Foo::Bar, "baz" 127 | # 128 | # ==== Parameters 129 | # namespace 130 | # 131 | def find_class_and_command_by_namespace(namespace, fallback = true) 132 | if namespace.include?(":") # look for a namespaced command 133 | pieces = namespace.split(":") 134 | command = pieces.pop 135 | klass = Foreman::Thor::Util.find_by_namespace(pieces.join(":")) 136 | end 137 | unless klass # look for a Foreman::Thor::Group with the right name 138 | klass = Foreman::Thor::Util.find_by_namespace(namespace) 139 | command = nil 140 | end 141 | if !klass && fallback # try a command in the default namespace 142 | command = namespace 143 | klass = Foreman::Thor::Util.find_by_namespace("") 144 | end 145 | [klass, command] 146 | end 147 | alias_method :find_class_and_task_by_namespace, :find_class_and_command_by_namespace 148 | 149 | # Receives a path and load the thor file in the path. The file is evaluated 150 | # inside the sandbox to avoid namespacing conflicts. 151 | # 152 | def load_thorfile(path, content = nil, debug = false) 153 | content ||= File.binread(path) 154 | 155 | begin 156 | Foreman::Thor::Sandbox.class_eval(content, path) 157 | rescue StandardError => e 158 | $stderr.puts("WARNING: unable to load thorfile #{path.inspect}: #{e.message}") 159 | if debug 160 | $stderr.puts(*e.backtrace) 161 | else 162 | $stderr.puts(e.backtrace.first) 163 | end 164 | end 165 | end 166 | 167 | def user_home 168 | @@user_home ||= if ENV["HOME"] 169 | ENV["HOME"] 170 | elsif ENV["USERPROFILE"] 171 | ENV["USERPROFILE"] 172 | elsif ENV["HOMEDRIVE"] && ENV["HOMEPATH"] 173 | File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"]) 174 | elsif ENV["APPDATA"] 175 | ENV["APPDATA"] 176 | else 177 | begin 178 | File.expand_path("~") 179 | rescue 180 | if File::ALT_SEPARATOR 181 | "C:/" 182 | else 183 | "/" 184 | end 185 | end 186 | end 187 | end 188 | 189 | # Returns the root where thor files are located, depending on the OS. 190 | # 191 | def thor_root 192 | File.join(user_home, ".thor").tr('\\', "/") 193 | end 194 | 195 | # Returns the files in the thor root. On Windows thor_root will be something 196 | # like this: 197 | # 198 | # C:\Documents and Settings\james\.thor 199 | # 200 | # If we don't #gsub the \ character, Dir.glob will fail. 201 | # 202 | def thor_root_glob 203 | files = Dir["#{escape_globs(thor_root)}/*"] 204 | 205 | files.map! do |file| 206 | File.directory?(file) ? File.join(file, "main.thor") : file 207 | end 208 | end 209 | 210 | # Where to look for Foreman::Thor files. 211 | # 212 | def globs_for(path) 213 | path = escape_globs(path) 214 | ["#{path}/Foreman::Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"] 215 | end 216 | 217 | # Return the path to the ruby interpreter taking into account multiple 218 | # installations and windows extensions. 219 | # 220 | def ruby_command 221 | @ruby_command ||= begin 222 | ruby_name = RbConfig::CONFIG["ruby_install_name"] 223 | ruby = File.join(RbConfig::CONFIG["bindir"], ruby_name) 224 | ruby << RbConfig::CONFIG["EXEEXT"] 225 | 226 | # avoid using different name than ruby (on platforms supporting links) 227 | if ruby_name != "ruby" && File.respond_to?(:readlink) 228 | begin 229 | alternate_ruby = File.join(RbConfig::CONFIG["bindir"], "ruby") 230 | alternate_ruby << RbConfig::CONFIG["EXEEXT"] 231 | 232 | # ruby is a symlink 233 | if File.symlink? alternate_ruby 234 | linked_ruby = File.readlink alternate_ruby 235 | 236 | # symlink points to 'ruby_install_name' 237 | ruby = alternate_ruby if linked_ruby == ruby_name || linked_ruby == ruby 238 | end 239 | rescue NotImplementedError # rubocop:disable HandleExceptions 240 | # just ignore on windows 241 | end 242 | end 243 | 244 | # escape string in case path to ruby executable contain spaces. 245 | ruby.sub!(/.*\s.*/m, '"\&"') 246 | ruby 247 | end 248 | end 249 | 250 | # Returns a string that has had any glob characters escaped. 251 | # The glob characters are `* ? { } [ ]`. 252 | # 253 | # ==== Examples 254 | # 255 | # Foreman::Thor::Util.escape_globs('[apps]') # => '\[apps\]' 256 | # 257 | # ==== Parameters 258 | # String 259 | # 260 | # ==== Returns 261 | # String 262 | # 263 | def escape_globs(path) 264 | path.to_s.gsub(/[*?{}\[\]]/, '\\\\\\&') 265 | end 266 | end 267 | end 268 | end 269 | -------------------------------------------------------------------------------- /lib/foreman/vendor/thor/lib/thor/version.rb: -------------------------------------------------------------------------------- 1 | class Foreman::Thor 2 | VERSION = "0.19.4" 3 | end 4 | -------------------------------------------------------------------------------- /lib/foreman/version.rb: -------------------------------------------------------------------------------- 1 | module Foreman 2 | 3 | VERSION = "0.88.1" 4 | 5 | end 6 | -------------------------------------------------------------------------------- /man/foreman.1: -------------------------------------------------------------------------------- 1 | .\" generated with Ronn-NG/v0.10.1 2 | .\" http://github.com/apjanke/ronn-ng/tree/0.10.1 3 | .TH "FOREMAN" "1" "April 2024" "Foreman 0.88.1" "Foreman Manual" 4 | .SH "NAME" 5 | \fBforeman\fR \- manage Procfile\-based applications 6 | .SH "SYNOPSIS" 7 | \fBforeman start [process]\fR 8 | .br 9 | \fBforeman run \fR 10 | .br 11 | \fBforeman export [location]\fR 12 | .SH "DESCRIPTION" 13 | Foreman is a manager for Procfile\-based applications\. Its aim is to abstract away the details of the Procfile format, and allow you to either run your application directly or export it to some other process management format\. 14 | .SH "RUNNING" 15 | \fBforeman start\fR is used to run your application directly from the command line\. 16 | .P 17 | If no additional parameters are passed, foreman will run one instance of each type of process defined in your Procfile\. 18 | .P 19 | If a parameter is passed, foreman will run one instance of the specified application type\. 20 | .P 21 | The following options control how the application is run: 22 | .TP 23 | \fB\-m\fR, \fB\-\-formation\fR 24 | Specify the number of each process type to run\. The value passed in should be in the format \fBprocess=num,process=num\fR 25 | .TP 26 | \fB\-e\fR, \fB\-\-env\fR 27 | Specify one or more \.env files to load 28 | .TP 29 | \fB\-f\fR, \fB\-\-procfile\fR 30 | Specify an alternate Procfile to load, implies \fB\-d\fR at the Procfile root\. 31 | .TP 32 | \fB\-p\fR, \fB\-\-port\fR 33 | Specify which port to use as the base for this application\. Should be a multiple of 1000\. 34 | .TP 35 | \fB\-t\fR, \fB\-\-timeout\fR 36 | Specify the amount of time (in seconds) processes have to shutdown gracefully before receiving a SIGKILL, defaults to 5\. 37 | .P 38 | \fBforeman run\fR is used to run one\-off commands using the same environment as your defined processes\. 39 | .SH "EXPORTING" 40 | \fBforeman export\fR is used to export your application to another process management format\. 41 | .P 42 | A location to export can be passed as an argument\. This argument may be either required or optional depending on the export format\. 43 | .P 44 | The following options control how the application is run: 45 | .TP 46 | \fB\-a\fR, \fB\-\-app\fR 47 | Use this name rather than the application's root directory name as the name of the application when exporting\. 48 | .TP 49 | \fB\-m\fR, \fB\-\-formation\fR 50 | Specify the number of each process type to run\. The value passed in should be in the format \fBprocess=num,process=num\fR 51 | .TP 52 | \fB\-l\fR, \fB\-\-log\fR 53 | Specify the directory to place process logs in\. 54 | .TP 55 | \fB\-p\fR, \fB\-\-port\fR 56 | Specify which port to use as the base for this application\. Should be a multiple of 1000\. 57 | .TP 58 | \fB\-t\fR, \fB\-\-template\fR 59 | Specify an alternate template to use for creating export files\. See \fIhttps://github\.com/ddollar/foreman/tree/master/data/export\fR for examples\. 60 | .TP 61 | \fB\-u\fR, \fB\-\-user\fR 62 | Specify the user the application should be run as\. Defaults to the app name 63 | .SH "GLOBAL OPTIONS" 64 | These options control all modes of foreman's operation\. 65 | .TP 66 | \fB\-d\fR, \fB\-\-root\fR 67 | Specify an alternate application root\. This defaults to the directory containing the Procfile\. 68 | .TP 69 | \fB\-e\fR, \fB\-\-env\fR 70 | Specify an alternate environment file\. You can specify more than one file by using: \fB\-\-env file1,file2\fR\. 71 | .TP 72 | \fB\-f\fR, \fB\-\-procfile\fR 73 | Specify an alternate location for the application's Procfile\. This file's containing directory will be assumed to be the root directory of the application\. 74 | .SH "EXPORT FORMATS" 75 | foreman currently supports the following output formats: 76 | .IP "\(bu" 4 77 | bluepill 78 | .IP "\(bu" 4 79 | inittab 80 | .IP "\(bu" 4 81 | launchd 82 | .IP "\(bu" 4 83 | runit 84 | .IP "\(bu" 4 85 | supervisord 86 | .IP "\(bu" 4 87 | systemd 88 | .IP "\(bu" 4 89 | upstart 90 | .IP "" 0 91 | .SH "INITTAB EXPORT" 92 | Will export a chunk of inittab\-compatible configuration: 93 | .IP "" 4 94 | .nf 95 | # \-\-\-\-\- foreman example processes \-\-\-\-\- 96 | EX01:4:respawn:/bin/su \- example \-c 'PORT=5000 bundle exec thin start >> /var/log/web\-1\.log 2>&1' 97 | EX02:4:respawn:/bin/su \- example \-c 'PORT=5100 bundle exec rake jobs:work >> /var/log/job\-1\.log 2>&1' 98 | # \-\-\-\-\- end foreman example processes \-\-\-\-\- 99 | .fi 100 | .IP "" 0 101 | .SH "SYSTEMD EXPORT" 102 | Will create a series of systemd scripts in the location you specify\. Scripts will be structured to make the following commands valid: 103 | .P 104 | \fBsystemctl start appname\.target\fR 105 | .P 106 | \fBsystemctl stop appname\-processname\.target\fR 107 | .P 108 | \fBsystemctl restart appname\-processname\-3\.service\fR 109 | .SH "UPSTART EXPORT" 110 | Will create a series of upstart scripts in the location you specify\. Scripts will be structured to make the following commands valid: 111 | .P 112 | \fBstart appname\fR 113 | .P 114 | \fBstop appname\-processname\fR 115 | .P 116 | \fBrestart appname\-processname\-3\fR 117 | .SH "PROCFILE" 118 | A Procfile should contain both a name for the process and the command used to run it\. 119 | .IP "" 4 120 | .nf 121 | web: bundle exec thin start 122 | job: bundle exec rake jobs:work 123 | .fi 124 | .IP "" 0 125 | .P 126 | A process name may contain letters, numbers and the underscore character\. You can validate your Procfile format using the \fBcheck\fR command: 127 | .IP "" 4 128 | .nf 129 | $ foreman check 130 | .fi 131 | .IP "" 0 132 | .P 133 | The special environment variables \fB$PORT\fR and \fB$PS\fR are available within the Procfile\. \fB$PORT\fR is the port selected for that process\. \fB$PS\fR is the name of the process for the line\. 134 | .P 135 | The \fB$PORT\fR value starts as the base port as specified by \fB\-p\fR, then increments by 100 for each new process line\. Multiple instances of the same process are assigned \fB$PORT\fR values that increment by 1\. 136 | .SH "ENVIRONMENT" 137 | If a \fB\.env\fR file exists in the current directory, the default environment will be read from it\. This file should contain key/value pairs, separated by \fB=\fR, with one key/value pair per line\. 138 | .IP "" 4 139 | .nf 140 | FOO=bar 141 | BAZ=qux 142 | .fi 143 | .IP "" 0 144 | .SH "DEFAULT OPTIONS" 145 | If a \fB\.foreman\fR file exists in the current directory, default options will be read from it\. This file should be in YAML format with the long option name as keys\. Example: 146 | .IP "" 4 147 | .nf 148 | formation: alpha=0,bravo=1 149 | port: 15000 150 | .fi 151 | .IP "" 0 152 | .SH "EXAMPLES" 153 | Start one instance of each process type, interleave the output on stdout: 154 | .IP "" 4 155 | .nf 156 | $ foreman start 157 | .fi 158 | .IP "" 0 159 | .P 160 | Export the application in upstart format: 161 | .IP "" 4 162 | .nf 163 | $ foreman export upstart /etc/init 164 | .fi 165 | .IP "" 0 166 | .P 167 | Run one process type from the application defined in a specific Procfile: 168 | .IP "" 4 169 | .nf 170 | $ foreman start alpha \-f ~/myapp/Procfile 171 | .fi 172 | .IP "" 0 173 | .P 174 | Start all processes except the one named worker: 175 | .IP "" 4 176 | .nf 177 | $ foreman start \-m all=1,worker=0 178 | .fi 179 | .IP "" 0 180 | .SH "COPYRIGHT" 181 | Foreman is Copyright (C) 2010 David Dollar \fIhttp://daviddollar\.org\fR 182 | -------------------------------------------------------------------------------- /man/foreman.1.ronn: -------------------------------------------------------------------------------- 1 | foreman(1) -- manage Procfile-based applications 2 | ================================================ 3 | 4 | ## SYNOPSIS 5 | 6 | `foreman start [process]`
7 | `foreman run `
8 | `foreman export [location]` 9 | 10 | ## DESCRIPTION 11 | 12 | Foreman is a manager for Procfile-based applications. Its aim is to 13 | abstract away the details of the Procfile format, and allow you to either run 14 | your application directly or export it to some other process management 15 | format. 16 | 17 | ## RUNNING 18 | 19 | `foreman start` is used to run your application directly from the command line. 20 | 21 | If no additional parameters are passed, foreman will run one instance of each 22 | type of process defined in your Procfile. 23 | 24 | If a parameter is passed, foreman will run one instance of the specified 25 | application type. 26 | 27 | The following options control how the application is run: 28 | 29 | * `-m`, `--formation`: 30 | Specify the number of each process type to run. The value passed in 31 | should be in the format `process=num,process=num` 32 | 33 | * `-e`, `--env`: 34 | Specify one or more .env files to load 35 | 36 | * `-f`, `--procfile`: 37 | Specify an alternate Procfile to load, implies `-d` at the Procfile root. 38 | 39 | * `-p`, `--port`: 40 | Specify which port to use as the base for this application. Should be 41 | a multiple of 1000. 42 | 43 | * `-t`, `--timeout`: 44 | Specify the amount of time (in seconds) processes have to shutdown 45 | gracefully before receiving a SIGKILL, defaults to 5. 46 | 47 | `foreman run` is used to run one-off commands using the same environment 48 | as your defined processes. 49 | 50 | ## EXPORTING 51 | 52 | `foreman export` is used to export your application to another process 53 | management format. 54 | 55 | A location to export can be passed as an argument. This argument may be 56 | either required or optional depending on the export format. 57 | 58 | The following options control how the application is run: 59 | 60 | * `-a`, `--app`: 61 | Use this name rather than the application's root directory name as the 62 | name of the application when exporting. 63 | 64 | * `-m`, `--formation`: 65 | Specify the number of each process type to run. The value passed in 66 | should be in the format `process=num,process=num` 67 | 68 | * `-l`, `--log`: 69 | Specify the directory to place process logs in. 70 | 71 | * `-p`, `--port`: 72 | Specify which port to use as the base for this application. Should be 73 | a multiple of 1000. 74 | 75 | * `-t`, `--template`: 76 | Specify an alternate template to use for creating export files. 77 | See for examples. 78 | 79 | * `-u`, `--user`: 80 | Specify the user the application should be run as. Defaults to the 81 | app name 82 | 83 | ## GLOBAL OPTIONS 84 | 85 | These options control all modes of foreman's operation. 86 | 87 | * `-d`, `--root`: 88 | Specify an alternate application root. This defaults to the directory 89 | containing the Procfile. 90 | 91 | * `-e`, `--env`: 92 | Specify an alternate environment file. You can specify more than one 93 | file by using: `--env file1,file2`. 94 | 95 | * `-f`, `--procfile`: 96 | Specify an alternate location for the application's Procfile. This file's 97 | containing directory will be assumed to be the root directory of the 98 | application. 99 | 100 | ## EXPORT FORMATS 101 | 102 | foreman currently supports the following output formats: 103 | 104 | * bluepill 105 | 106 | * inittab 107 | 108 | * launchd 109 | 110 | * runit 111 | 112 | * supervisord 113 | 114 | * systemd 115 | 116 | * upstart 117 | 118 | ## INITTAB EXPORT 119 | 120 | Will export a chunk of inittab-compatible configuration: 121 | 122 | # ----- foreman example processes ----- 123 | EX01:4:respawn:/bin/su - example -c 'PORT=5000 bundle exec thin start >> /var/log/web-1.log 2>&1' 124 | EX02:4:respawn:/bin/su - example -c 'PORT=5100 bundle exec rake jobs:work >> /var/log/job-1.log 2>&1' 125 | # ----- end foreman example processes ----- 126 | 127 | ## SYSTEMD EXPORT 128 | 129 | Will create a series of systemd scripts in the location you specify. Scripts 130 | will be structured to make the following commands valid: 131 | 132 | `systemctl start appname.target` 133 | 134 | `systemctl stop appname-processname.target` 135 | 136 | `systemctl restart appname-processname-3.service` 137 | 138 | ## UPSTART EXPORT 139 | 140 | Will create a series of upstart scripts in the location you specify. Scripts 141 | will be structured to make the following commands valid: 142 | 143 | `start appname` 144 | 145 | `stop appname-processname` 146 | 147 | `restart appname-processname-3` 148 | 149 | ## PROCFILE 150 | 151 | A Procfile should contain both a name for the process and the command used 152 | to run it. 153 | 154 | web: bundle exec thin start 155 | job: bundle exec rake jobs:work 156 | 157 | A process name may contain letters, numbers and the underscore character. 158 | You can validate your Procfile format using the `check` command: 159 | 160 | $ foreman check 161 | 162 | The special environment variables `$PORT` and `$PS` are available within the 163 | Procfile. `$PORT` is the port selected for that process. `$PS` is the name of 164 | the process for the line. 165 | 166 | The `$PORT` value starts as the base port as specified by `-p`, then increments 167 | by 100 for each new process line. Multiple instances of the same process are 168 | assigned `$PORT` values that increment by 1. 169 | 170 | ## ENVIRONMENT 171 | 172 | If a `.env` file exists in the current directory, the default environment will 173 | be read from it. This file should contain key/value pairs, separated by `=`, with 174 | one key/value pair per line. 175 | 176 | FOO=bar 177 | BAZ=qux 178 | 179 | ## DEFAULT OPTIONS 180 | 181 | If a `.foreman` file exists in the current directory, default options will 182 | be read from it. This file should be in YAML format with the long option 183 | name as keys. Example: 184 | 185 | formation: alpha=0,bravo=1 186 | port: 15000 187 | 188 | ## EXAMPLES 189 | 190 | Start one instance of each process type, interleave the output on stdout: 191 | 192 | $ foreman start 193 | 194 | Export the application in upstart format: 195 | 196 | $ foreman export upstart /etc/init 197 | 198 | Run one process type from the application defined in a specific Procfile: 199 | 200 | $ foreman start alpha -f ~/myapp/Procfile 201 | 202 | Start all processes except the one named worker: 203 | 204 | $ foreman start -m all=1,worker=0 205 | 206 | ## COPYRIGHT 207 | 208 | Foreman is Copyright (C) 2010 David Dollar 209 | -------------------------------------------------------------------------------- /pkg/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ddollar/foreman/3a262714030a83a59388a48ffdb2e93cdac12388/pkg/.gitignore -------------------------------------------------------------------------------- /spec/foreman/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/cli" 3 | 4 | describe "Foreman::CLI", :fakefs do 5 | subject { Foreman::CLI.new } 6 | 7 | describe ".foreman" do 8 | before { File.open(".foreman", "w") { |f| f.puts "formation: alpha=2" } } 9 | 10 | it "provides default options" do 11 | expect(subject.send(:options)["formation"]).to eq("alpha=2") 12 | end 13 | 14 | it "is overridden by options at the cli" do 15 | subject = Foreman::CLI.new([], :formation => "alpha=3") 16 | expect(subject.send(:options)["formation"]).to eq("alpha=3") 17 | end 18 | end 19 | 20 | describe "start" do 21 | describe "when a Procfile doesnt exist", :fakefs do 22 | it "displays an error" do 23 | mock_error(subject, "Procfile does not exist.") do 24 | expect_any_instance_of(Foreman::Engine).to_not receive(:start) 25 | subject.start 26 | end 27 | end 28 | end 29 | 30 | describe "with a valid Procfile" do 31 | it "can run a single command" do 32 | without_fakefs do 33 | output = foreman("start env -f #{resource_path("Procfile")}") 34 | expect(output).to match(/env.1/) 35 | expect(output).not_to match(/test.1/) 36 | end 37 | end 38 | 39 | it "can run all commands" do 40 | without_fakefs do 41 | output = foreman("start -f #{resource_path("Procfile")} -e #{resource_path(".env")}") 42 | expect(output).to match(/echo.1 \| echoing/) 43 | expect(output).to match(/env.1 \| bar/) 44 | expect(output).to match(/test.1 \| testing/) 45 | end 46 | end 47 | 48 | it "sets PS variable with the process name" do 49 | without_fakefs do 50 | output = foreman("start -f #{resource_path("Procfile")}") 51 | expect(output).to match(/ps.1 \| PS env var is ps.1/) 52 | end 53 | end 54 | 55 | it "fails if process fails" do 56 | output = `bundle exec foreman start -f #{resource_path "Procfile.bad"} && echo success` 57 | expect(output).not_to include 'success' 58 | end 59 | end 60 | end 61 | 62 | describe "check" do 63 | it "with a valid Procfile displays the jobs" do 64 | write_procfile 65 | expect(foreman("check")).to eq("valid procfile detected (alpha, bravo, foo_bar, foo-bar)\n") 66 | end 67 | 68 | it "with a blank Procfile displays an error" do 69 | FileUtils.touch "Procfile" 70 | expect(foreman("check")).to eq("ERROR: no processes defined\n") 71 | end 72 | 73 | it "without a Procfile displays an error" do 74 | expect(foreman("check")).to eq("ERROR: Procfile does not exist.\n") 75 | end 76 | end 77 | 78 | describe "run" do 79 | it "can run a command" do 80 | expect(forked_foreman("run -f #{resource_path("Procfile")} echo 1")).to eq("1\n") 81 | end 82 | 83 | it "doesn't parse options for the command" do 84 | expect(forked_foreman("run -f #{resource_path("Procfile")} grep -e FOO #{resource_path(".env")}")).to eq("FOO=bar\n") 85 | end 86 | 87 | it "includes the environment" do 88 | expect(forked_foreman("run -f #{resource_path("Procfile")} -e #{resource_path(".env")} #{resource_path("bin/env FOO")}")).to eq("bar\n") 89 | end 90 | 91 | it "can run a command from the Procfile" do 92 | expect(forked_foreman("run -f #{resource_path("Procfile")} test")).to eq("testing\n") 93 | end 94 | 95 | it "exits with the same exit code as the command" do 96 | expect(fork_and_get_exitstatus("run -f #{resource_path("Procfile")} echo 1")).to eq(0) 97 | expect(fork_and_get_exitstatus("run -f #{resource_path("Procfile")} date 'invalid_date'")).to eq(1) 98 | end 99 | end 100 | 101 | describe "version" do 102 | it "displays gem version" do 103 | expect(foreman("version").chomp).to eq(Foreman::VERSION) 104 | end 105 | 106 | it "displays gem version on shortcut command" do 107 | expect(foreman("-v").chomp).to eq(Foreman::VERSION) 108 | end 109 | end 110 | 111 | end 112 | -------------------------------------------------------------------------------- /spec/foreman/engine_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/engine" 3 | 4 | class Foreman::Engine::Tester < Foreman::Engine 5 | attr_reader :buffer 6 | 7 | def startup 8 | @buffer = "" 9 | end 10 | 11 | def output(name, data) 12 | @buffer += "#{name}: #{data}" 13 | end 14 | 15 | def shutdown 16 | end 17 | end 18 | 19 | describe "Foreman::Engine", :fakefs do 20 | subject do 21 | write_procfile "Procfile" 22 | Foreman::Engine::Tester.new.load_procfile("Procfile") 23 | end 24 | 25 | describe "initialize" do 26 | describe "with a Procfile" do 27 | before { write_procfile } 28 | 29 | it "reads the processes" do 30 | expect(subject.process("alpha").command).to eq("./alpha") 31 | expect(subject.process("bravo").command).to eq("./bravo") 32 | end 33 | end 34 | end 35 | 36 | describe "start" do 37 | it "forks the processes" do 38 | expect(subject.process("alpha")).to receive(:run) 39 | expect(subject.process("bravo")).to receive(:run) 40 | expect(subject).to receive(:watch_for_output) 41 | expect(subject).to receive(:wait_for_shutdown_or_child_termination) 42 | subject.start 43 | end 44 | 45 | it "handles concurrency" do 46 | subject.options[:formation] = "alpha=2" 47 | expect(subject.process("alpha")).to receive(:run).twice 48 | expect(subject.process("bravo")).to_not receive(:run) 49 | expect(subject).to receive(:watch_for_output) 50 | expect(subject).to receive(:wait_for_shutdown_or_child_termination) 51 | subject.start 52 | end 53 | end 54 | 55 | describe "directories" do 56 | it "has the directory default relative to the Procfile" do 57 | write_procfile "/some/app/Procfile" 58 | engine = Foreman::Engine.new.load_procfile("/some/app/Procfile") 59 | expect(engine.root).to eq("/some/app") 60 | end 61 | end 62 | 63 | describe "environment" do 64 | it "should read env files" do 65 | write_file("/tmp/env") { |f| f.puts("FOO=baz") } 66 | subject.load_env("/tmp/env") 67 | expect(subject.env["FOO"]).to eq("baz") 68 | end 69 | 70 | it "should read more than one if specified" do 71 | write_file("/tmp/env1") { |f| f.puts("FOO=bar") } 72 | write_file("/tmp/env2") { |f| f.puts("BAZ=qux") } 73 | subject.load_env "/tmp/env1" 74 | subject.load_env "/tmp/env2" 75 | expect(subject.env["FOO"]).to eq("bar") 76 | expect(subject.env["BAZ"]).to eq("qux") 77 | end 78 | 79 | it "should handle quoted values" do 80 | write_file("/tmp/env") do |f| 81 | f.puts 'FOO=bar' 82 | f.puts 'BAZ="qux"' 83 | f.puts "FRED='barney'" 84 | f.puts 'OTHER="escaped\"quote"' 85 | f.puts 'URL="http://example.com/api?foo=bar&baz=1"' 86 | end 87 | subject.load_env "/tmp/env" 88 | expect(subject.env["FOO"]).to eq("bar") 89 | expect(subject.env["BAZ"]).to eq("qux") 90 | expect(subject.env["FRED"]).to eq("barney") 91 | expect(subject.env["OTHER"]).to eq('escaped"quote') 92 | expect(subject.env["URL"]).to eq("http://example.com/api?foo=bar&baz=1") 93 | end 94 | 95 | it "should handle multiline strings" do 96 | write_file("/tmp/env") do |f| 97 | f.puts 'FOO="bar\nbaz"' 98 | end 99 | subject.load_env "/tmp/env" 100 | expect(subject.env["FOO"]).to eq("bar\nbaz") 101 | end 102 | 103 | it "should fail if specified and doesnt exist" do 104 | expect { subject.load_env "/tmp/env" }.to raise_error(Errno::ENOENT) 105 | end 106 | 107 | it "should set port from .env if specified" do 108 | write_file("/tmp/env") { |f| f.puts("PORT=9000") } 109 | subject.load_env "/tmp/env" 110 | expect(subject.send(:base_port)).to eq(9000) 111 | end 112 | end 113 | 114 | end 115 | -------------------------------------------------------------------------------- /spec/foreman/export/base_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/engine" 3 | require "foreman/export" 4 | 5 | describe "Foreman::Export::Base", :fakefs do 6 | let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } 7 | let(:location) { "/tmp/init" } 8 | let(:engine) { Foreman::Engine.new().load_procfile(procfile) } 9 | let(:subject) { Foreman::Export::Base.new(location, engine) } 10 | 11 | it "has a say method for displaying info" do 12 | expect(subject).to receive(:puts).with("[foreman export] foo") 13 | subject.send(:say, "foo") 14 | end 15 | 16 | it "raises errors as a Foreman::Export::Exception" do 17 | expect { subject.send(:error, "foo") }.to raise_error(Foreman::Export::Exception, "foo") 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/foreman/export/bluepill_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/engine" 3 | require "foreman/export/bluepill" 4 | require "tmpdir" 5 | 6 | describe Foreman::Export::Bluepill, :fakefs do 7 | let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } 8 | let(:formation) { nil } 9 | let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) } 10 | let(:options) { Hash.new } 11 | let(:bluepill) { Foreman::Export::Bluepill.new("/tmp/init", engine, options) } 12 | 13 | before(:each) { load_export_templates_into_fakefs("bluepill") } 14 | before(:each) { allow(bluepill).to receive(:say) } 15 | 16 | it "exports to the filesystem" do 17 | bluepill.export 18 | expect(normalize_space(File.read("/tmp/init/app.pill"))).to eq(normalize_space(example_export_file("bluepill/app.pill"))) 19 | end 20 | 21 | it "cleans up if exporting into an existing dir" do 22 | expect(FileUtils).to receive(:rm).with("/tmp/init/app.pill") 23 | 24 | bluepill.export 25 | bluepill.export 26 | end 27 | 28 | context "with a process formation" do 29 | let(:formation) { "alpha=2" } 30 | 31 | it "exports to the filesystem with concurrency" do 32 | bluepill.export 33 | expect(normalize_space(File.read("/tmp/init/app.pill"))).to eq(normalize_space(example_export_file("bluepill/app-concurrency.pill"))) 34 | end 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /spec/foreman/export/daemon_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/engine" 3 | require "foreman/export/daemon" 4 | require "tmpdir" 5 | 6 | describe Foreman::Export::Daemon, :fakefs do 7 | let(:procfile) { write_procfile("/tmp/app/Procfile") } 8 | let(:formation) { nil } 9 | let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) } 10 | let(:options) { Hash.new } 11 | let(:daemon) { Foreman::Export::Daemon.new("/tmp/init", engine, options) } 12 | 13 | before(:each) { load_export_templates_into_fakefs("daemon") } 14 | before(:each) { allow(daemon).to receive(:say) } 15 | 16 | it "exports to the filesystem" do 17 | daemon.export 18 | 19 | expect(File.read("/tmp/init/app.conf")).to eq(example_export_file("daemon/app.conf")) 20 | expect(File.read("/tmp/init/app-alpha.conf")).to eq(example_export_file("daemon/app-alpha.conf")) 21 | expect(File.read("/tmp/init/app-alpha-1.conf")).to eq(example_export_file("daemon/app-alpha-1.conf")) 22 | expect(File.read("/tmp/init/app-bravo.conf")).to eq(example_export_file("daemon/app-bravo.conf")) 23 | expect(File.read("/tmp/init/app-bravo-1.conf")).to eq(example_export_file("daemon/app-bravo-1.conf")) 24 | end 25 | 26 | it "cleans up if exporting into an existing dir" do 27 | expect(FileUtils).to receive(:rm).with("/tmp/init/app.conf") 28 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-alpha.conf") 29 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-alpha-1.conf") 30 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-bravo.conf") 31 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-bravo-1.conf") 32 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo-bar.conf") 33 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo-bar-1.conf") 34 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo_bar.conf") 35 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo_bar-1.conf") 36 | 37 | daemon.export 38 | daemon.export 39 | end 40 | 41 | it "does not delete exported files for similarly named applications" do 42 | FileUtils.mkdir_p "/tmp/init" 43 | 44 | ["app2", "app2-alpha", "app2-alpha-1"].each do |name| 45 | path = "/tmp/init/#{name}.conf" 46 | FileUtils.touch(path) 47 | expect(FileUtils).to_not receive(:rm).with(path) 48 | end 49 | 50 | daemon.export 51 | end 52 | 53 | context "with a formation" do 54 | let(:formation) { "alpha=2" } 55 | 56 | it "exports to the filesystem with concurrency" do 57 | daemon.export 58 | 59 | expect(File.read("/tmp/init/app.conf")).to eq(example_export_file("daemon/app.conf")) 60 | expect(File.read("/tmp/init/app-alpha.conf")).to eq(example_export_file("daemon/app-alpha.conf")) 61 | expect(File.read("/tmp/init/app-alpha-1.conf")).to eq(example_export_file("daemon/app-alpha-1.conf")) 62 | expect(File.read("/tmp/init/app-alpha-2.conf")).to eq(example_export_file("daemon/app-alpha-2.conf")) 63 | expect(File.exist?("/tmp/init/app-bravo-1.conf")).to eq(false) 64 | end 65 | end 66 | 67 | context "with alternate templates" do 68 | let(:template) { "/tmp/alternate" } 69 | let(:options) { { :app => "app", :template => template } } 70 | 71 | before do 72 | FileUtils.mkdir_p template 73 | File.open("#{template}/master.conf.erb", "w") { |f| f.puts "alternate_template" } 74 | end 75 | 76 | it "can export with alternate template files" do 77 | daemon.export 78 | expect(File.read("/tmp/init/app.conf")).to eq("alternate_template\n") 79 | end 80 | end 81 | 82 | context "with alternate templates from home dir" do 83 | 84 | before do 85 | FileUtils.mkdir_p File.expand_path("~/.foreman/templates/daemon") 86 | File.open(File.expand_path("~/.foreman/templates/daemon/master.conf.erb"), "w") do |file| 87 | file.puts "default_alternate_template" 88 | end 89 | end 90 | 91 | it "can export with alternate template files" do 92 | daemon.export 93 | expect(File.read("/tmp/init/app.conf")).to eq("default_alternate_template\n") 94 | end 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /spec/foreman/export/inittab_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/engine" 3 | require "foreman/export/inittab" 4 | require "tmpdir" 5 | 6 | describe Foreman::Export::Inittab, :fakefs do 7 | let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } 8 | let(:location) { "/tmp/inittab" } 9 | let(:formation) { nil } 10 | let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) } 11 | let(:options) { Hash.new } 12 | let(:inittab) { Foreman::Export::Inittab.new(location, engine, options) } 13 | 14 | before(:each) { load_export_templates_into_fakefs("inittab") } 15 | before(:each) { allow(inittab).to receive(:say) } 16 | 17 | it "exports to the filesystem" do 18 | inittab.export 19 | expect(File.read("/tmp/inittab")).to eq(example_export_file("inittab/inittab.default")) 20 | end 21 | 22 | context "to stdout" do 23 | let(:location) { "-" } 24 | 25 | it "exports to stdout" do 26 | expect(inittab).to receive(:puts).with(example_export_file("inittab/inittab.default")) 27 | inittab.export 28 | end 29 | end 30 | 31 | context "with concurrency" do 32 | let(:formation) { "alpha=2" } 33 | 34 | it "exports to the filesystem with concurrency" do 35 | inittab.export 36 | expect(File.read("/tmp/inittab")).to eq(example_export_file("inittab/inittab.concurrency")) 37 | end 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /spec/foreman/export/launchd_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/engine" 3 | require "foreman/export/launchd" 4 | require "tmpdir" 5 | 6 | describe Foreman::Export::Launchd, :fakefs do 7 | let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } 8 | let(:options) { Hash.new } 9 | let(:engine) { Foreman::Engine.new().load_procfile(procfile) } 10 | let(:launchd) { Foreman::Export::Launchd.new("/tmp/init", engine, options) } 11 | 12 | before(:each) { load_export_templates_into_fakefs("launchd") } 13 | before(:each) { allow(launchd).to receive(:say) } 14 | 15 | it "exports to the filesystem" do 16 | launchd.export 17 | expect(File.read("/tmp/init/app-alpha-1.plist")).to eq(example_export_file("launchd/launchd-a.default")) 18 | expect(File.read("/tmp/init/app-bravo-1.plist")).to eq(example_export_file("launchd/launchd-b.default")) 19 | end 20 | 21 | context "with multiple command arguments" do 22 | let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile", "charlie") } 23 | 24 | it "splits each command argument" do 25 | launchd.export 26 | expect(File.read("/tmp/init/app-alpha-1.plist")).to eq(example_export_file("launchd/launchd-c.default")) 27 | end 28 | 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /spec/foreman/export/runit_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/engine" 3 | require "foreman/export/runit" 4 | require "tmpdir" 5 | 6 | describe Foreman::Export::Runit, :fakefs do 7 | let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile", 'bar=baz') } 8 | let(:engine) { Foreman::Engine.new(:formation => "alpha=2,bravo=1").load_procfile(procfile) } 9 | let(:options) { Hash.new } 10 | let(:runit) { Foreman::Export::Runit.new('/tmp/init', engine, options) } 11 | 12 | before(:each) { load_export_templates_into_fakefs("runit") } 13 | before(:each) { allow(runit).to receive(:say) } 14 | before(:each) { allow(FakeFS::FileUtils).to receive(:chmod) } 15 | 16 | it "exports to the filesystem" do 17 | engine.env["BAR"] = "baz" 18 | runit.export 19 | 20 | expect(File.read("/tmp/init/app-alpha-1/run")).to eq(example_export_file('runit/app-alpha-1/run')) 21 | expect(File.read("/tmp/init/app-alpha-1/log/run")).to eq(example_export_file('runit/app-alpha-1/log/run')) 22 | expect(File.read("/tmp/init/app-alpha-1/env/PORT")).to eq("5000\n") 23 | expect(File.read("/tmp/init/app-alpha-1/env/BAR")).to eq("baz\n") 24 | expect(File.read("/tmp/init/app-alpha-2/run")).to eq(example_export_file('runit/app-alpha-2/run')) 25 | expect(File.read("/tmp/init/app-alpha-2/log/run")).to eq(example_export_file('runit/app-alpha-2/log/run')) 26 | expect(File.read("/tmp/init/app-alpha-2/env/PORT")).to eq("5001\n") 27 | expect(File.read("/tmp/init/app-alpha-2/env/BAR")).to eq("baz\n") 28 | expect(File.read("/tmp/init/app-bravo-1/run")).to eq(example_export_file('runit/app-bravo-1/run')) 29 | expect(File.read("/tmp/init/app-bravo-1/log/run")).to eq(example_export_file('runit/app-bravo-1/log/run')) 30 | expect(File.read("/tmp/init/app-bravo-1/env/PORT")).to eq("5100\n") 31 | end 32 | 33 | it "creates a full path to the export directory" do 34 | expect { runit.export }.to_not raise_error 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/foreman/export/supervisord_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/engine" 3 | require "foreman/export/supervisord" 4 | require "tmpdir" 5 | 6 | describe Foreman::Export::Supervisord, :fakefs do 7 | let(:procfile) { FileUtils.mkdir_p("/tmp/app"); write_procfile("/tmp/app/Procfile") } 8 | let(:formation) { nil } 9 | let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) } 10 | let(:options) { Hash.new } 11 | let(:supervisord) { Foreman::Export::Supervisord.new("/tmp/init", engine, options) } 12 | 13 | before(:each) { load_export_templates_into_fakefs("supervisord") } 14 | before(:each) { allow(supervisord).to receive(:say) } 15 | 16 | it "exports to the filesystem" do 17 | write_env(".env", "FOO"=>"bar", "URL"=>"http://example.com/api?foo=bar&baz=1") 18 | supervisord.engine.load_env('.env') 19 | supervisord.export 20 | expect(File.read("/tmp/init/app.conf")).to eq(example_export_file("supervisord/app-alpha-1.conf")) 21 | end 22 | 23 | it "cleans up if exporting into an existing dir" do 24 | expect(FileUtils).to receive(:rm).with("/tmp/init/app.conf") 25 | supervisord.export 26 | supervisord.export 27 | end 28 | 29 | context "with concurrency" do 30 | let(:formation) { "alpha=2" } 31 | 32 | it "exports to the filesystem with concurrency" do 33 | supervisord.export 34 | expect(File.read("/tmp/init/app.conf")).to eq(example_export_file("supervisord/app-alpha-2.conf")) 35 | end 36 | end 37 | 38 | end 39 | -------------------------------------------------------------------------------- /spec/foreman/export/systemd_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/engine" 3 | require "foreman/export/systemd" 4 | require "tmpdir" 5 | 6 | describe Foreman::Export::Systemd, :fakefs, :aggregate_failures do 7 | let(:procfile) { write_procfile("/tmp/app/Procfile") } 8 | let(:formation) { nil } 9 | let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) } 10 | let(:options) { Hash.new } 11 | let(:systemd) { Foreman::Export::Systemd.new("/tmp/init", engine, options) } 12 | 13 | before(:each) { load_export_templates_into_fakefs("systemd") } 14 | before(:each) { allow(systemd).to receive(:say) } 15 | 16 | it "exports to the filesystem" do 17 | systemd.export 18 | 19 | expect(File.read("/tmp/init/app.target")).to eq(example_export_file("systemd/app.target")) 20 | expect(File.read("/tmp/init/app-alpha.1.service")).to eq(example_export_file("systemd/app-alpha.1.service")) 21 | expect(File.read("/tmp/init/app-bravo.1.service")).to eq(example_export_file("systemd/app-bravo.1.service")) 22 | end 23 | 24 | context "when systemd export was run using the previous version of systemd export" do 25 | before do 26 | write_file("/tmp/init/app.target") 27 | 28 | write_file("/tmp/init/app-alpha@.service") 29 | write_file("/tmp/init/app-alpha.target") 30 | write_file("/tmp/init/app-alpha.target.wants/app-alpha@5000.service") 31 | 32 | write_file("/tmp/init/app-bravo.target") 33 | write_file("/tmp/init/app-bravo@.service") 34 | write_file("/tmp/init/app-bravo.target.wants/app-bravo@5100.service") 35 | 36 | write_file("/tmp/init/app-foo_bar.target") 37 | write_file("/tmp/init/app-foo_bar@.service") 38 | write_file("/tmp/init/app-foo_bar.target.wants/app-foo_bar@5200.service") 39 | 40 | write_file("/tmp/init/app-foo-bar.target") 41 | write_file("/tmp/init/app-foo-bar@.service") 42 | write_file("/tmp/init/app-foo-bar.target.wants/app-foo-bar@5300.service") 43 | end 44 | 45 | it "cleans up service files created by systemd export" do 46 | expect(FileUtils).to receive(:rm).with("/tmp/init/app.target") 47 | 48 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-alpha@.service") 49 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-alpha.target") 50 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-alpha.target.wants/app-alpha@5000.service") 51 | expect(FileUtils).to receive(:rm_r).with("/tmp/init/app-alpha.target.wants") 52 | 53 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-bravo.target") 54 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-bravo@.service") 55 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-bravo.target.wants/app-bravo@5100.service") 56 | expect(FileUtils).to receive(:rm_r).with("/tmp/init/app-bravo.target.wants") 57 | 58 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo_bar.target") 59 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo_bar@.service") 60 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo_bar.target.wants/app-foo_bar@5200.service") 61 | expect(FileUtils).to receive(:rm_r).with("/tmp/init/app-foo_bar.target.wants") 62 | 63 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo-bar.target") 64 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo-bar@.service") 65 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo-bar.target.wants/app-foo-bar@5300.service") 66 | expect(FileUtils).to receive(:rm_r).with("/tmp/init/app-foo-bar.target.wants") 67 | 68 | systemd.export 69 | end 70 | end 71 | 72 | context "when systemd export was run using the current version of systemd export" do 73 | before do 74 | systemd.export 75 | end 76 | 77 | it "cleans up service files created by systemd export" do 78 | expect(FileUtils).to receive(:rm).with("/tmp/init/app.target") 79 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-alpha.1.service") 80 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-bravo.1.service") 81 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo_bar.1.service") 82 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo-bar.1.service") 83 | 84 | systemd.export 85 | end 86 | end 87 | 88 | it "includes environment variables" do 89 | engine.env['KEY'] = 'some "value"' 90 | systemd.export 91 | expect(File.read("/tmp/init/app-alpha.1.service")).to match(/KEY=some "value"/) 92 | end 93 | 94 | it "includes ExecStart line" do 95 | engine.env['KEY'] = 'some "value"' 96 | systemd.export 97 | expect(File.read("/tmp/init/app-alpha.1.service")).to match(/^ExecStart=/) 98 | end 99 | 100 | context "with a custom formation specified" do 101 | let(:formation) { "alpha=2" } 102 | 103 | it "exports only those services that are specified in the formation" do 104 | systemd.export 105 | 106 | expect(File.read("/tmp/init/app.target")).to include("Wants=app-alpha.1.service app-alpha.2.service\n") 107 | expect(File.read("/tmp/init/app-alpha.1.service")).to eq(example_export_file("systemd/app-alpha.1.service")) 108 | expect(File.read("/tmp/init/app-alpha.2.service")).to eq(example_export_file("systemd/app-alpha.2.service")) 109 | expect(File.exist?("/tmp/init/app-bravo.1.service")).to be_falsey 110 | end 111 | end 112 | 113 | context "with alternate template directory specified" do 114 | let(:template) { "/tmp/alternate" } 115 | let(:options) { { :app => "app", :template => template } } 116 | 117 | before do 118 | FileUtils.mkdir_p template 119 | File.open("#{template}/master.target.erb", "w") { |f| f.puts "alternate_template" } 120 | end 121 | 122 | it "uses template files found in the alternate directory" do 123 | systemd.export 124 | expect(File.read("/tmp/init/app.target")).to eq("alternate_template\n") 125 | end 126 | 127 | context "with alternate templates in the user home directory" do 128 | before do 129 | FileUtils.mkdir_p File.expand_path("~/.foreman/templates/systemd") 130 | File.open(File.expand_path("~/.foreman/templates/systemd/master.target.erb"), "w") do |file| 131 | file.puts "home_dir_template" 132 | end 133 | end 134 | 135 | it "uses template files found in the alternate directory" do 136 | systemd.export 137 | expect(File.read("/tmp/init/app.target")).to eq("alternate_template\n") 138 | end 139 | end 140 | end 141 | 142 | context "with alternate templates in the user home directory" do 143 | before do 144 | FileUtils.mkdir_p File.expand_path("~/.foreman/templates/systemd") 145 | File.open(File.expand_path("~/.foreman/templates/systemd/master.target.erb"), "w") do |file| 146 | file.puts "home_dir_template" 147 | end 148 | end 149 | 150 | it "uses template files found in the user home directory" do 151 | systemd.export 152 | expect(File.read("/tmp/init/app.target")).to eq("home_dir_template\n") 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /spec/foreman/export/upstart_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/engine" 3 | require "foreman/export/upstart" 4 | require "tmpdir" 5 | 6 | describe Foreman::Export::Upstart, :fakefs do 7 | let(:procfile) { write_procfile("/tmp/app/Procfile") } 8 | let(:formation) { nil } 9 | let(:engine) { Foreman::Engine.new(:formation => formation).load_procfile(procfile) } 10 | let(:options) { Hash.new } 11 | let(:upstart) { Foreman::Export::Upstart.new("/tmp/init", engine, options) } 12 | 13 | before(:each) { load_export_templates_into_fakefs("upstart") } 14 | before(:each) { allow(upstart).to receive(:say) } 15 | 16 | it "exports to the filesystem" do 17 | upstart.export 18 | 19 | expect(File.read("/tmp/init/app.conf")).to eq(example_export_file("upstart/app.conf")) 20 | expect(File.read("/tmp/init/app-alpha.conf")).to eq(example_export_file("upstart/app-alpha.conf")) 21 | expect(File.read("/tmp/init/app-alpha-1.conf")).to eq(example_export_file("upstart/app-alpha-1.conf")) 22 | expect(File.read("/tmp/init/app-bravo.conf")).to eq(example_export_file("upstart/app-bravo.conf")) 23 | expect(File.read("/tmp/init/app-bravo-1.conf")).to eq(example_export_file("upstart/app-bravo-1.conf")) 24 | end 25 | 26 | it "cleans up if exporting into an existing dir" do 27 | expect(FileUtils).to receive(:rm).with("/tmp/init/app.conf") 28 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-alpha.conf") 29 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-alpha-1.conf") 30 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-bravo.conf") 31 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-bravo-1.conf") 32 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo-bar.conf") 33 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo-bar-1.conf") 34 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo_bar.conf") 35 | expect(FileUtils).to receive(:rm).with("/tmp/init/app-foo_bar-1.conf") 36 | 37 | upstart.export 38 | upstart.export 39 | end 40 | 41 | it "does not delete exported files for similarly named applications" do 42 | FileUtils.mkdir_p "/tmp/init" 43 | 44 | ["app2", "app2-alpha", "app2-alpha-1"].each do |name| 45 | path = "/tmp/init/#{name}.conf" 46 | FileUtils.touch(path) 47 | expect(FileUtils).to_not receive(:rm).with(path) 48 | end 49 | 50 | upstart.export 51 | end 52 | 53 | it 'does not delete exported files for app which share name prefix' do 54 | FileUtils.mkdir_p "/tmp/init" 55 | 56 | ["app-worker", "app-worker-worker", "app-worker-worker-1"].each do |name| 57 | path = "/tmp/init/#{name}.conf" 58 | FileUtils.touch(path) 59 | expect(FileUtils).to_not receive(:rm).with(path) 60 | end 61 | 62 | upstart.export 63 | expect(File.exist?('/tmp/init/app.conf')).to be true 64 | expect(File.exist?('/tmp/init/app-worker.conf')).to be true 65 | end 66 | 67 | it "quotes and escapes environment variables" do 68 | engine.env['KEY'] = 'd"\|d' 69 | upstart.export 70 | expect("foobarfoo").to include "bar" 71 | expect(File.read("/tmp/init/app-alpha-1.conf")).to match(/KEY='d"\\\|d'/) 72 | end 73 | 74 | context "with a formation" do 75 | let(:formation) { "alpha=2" } 76 | 77 | it "exports to the filesystem with concurrency" do 78 | upstart.export 79 | 80 | expect(File.read("/tmp/init/app.conf")).to eq(example_export_file("upstart/app.conf")) 81 | expect(File.read("/tmp/init/app-alpha.conf")).to eq(example_export_file("upstart/app-alpha.conf")) 82 | expect(File.read("/tmp/init/app-alpha-1.conf")).to eq(example_export_file("upstart/app-alpha-1.conf")) 83 | expect(File.read("/tmp/init/app-alpha-2.conf")).to eq(example_export_file("upstart/app-alpha-2.conf")) 84 | expect(File.exist?("/tmp/init/app-bravo-1.conf")).to eq(false) 85 | end 86 | end 87 | 88 | context "with alternate templates" do 89 | let(:template) { "/tmp/alternate" } 90 | let(:options) { { :app => "app", :template => template } } 91 | 92 | before do 93 | FileUtils.mkdir_p template 94 | File.open("#{template}/master.conf.erb", "w") { |f| f.puts "alternate_template" } 95 | end 96 | 97 | it "can export with alternate template files" do 98 | upstart.export 99 | expect(File.read("/tmp/init/app.conf")).to eq("alternate_template\n") 100 | end 101 | end 102 | 103 | context "with alternate templates from home dir" do 104 | 105 | before do 106 | FileUtils.mkdir_p File.expand_path("~/.foreman/templates/upstart") 107 | File.open(File.expand_path("~/.foreman/templates/upstart/master.conf.erb"), "w") do |file| 108 | file.puts "default_alternate_template" 109 | end 110 | end 111 | 112 | it "can export with alternate template files" do 113 | upstart.export 114 | expect(File.read("/tmp/init/app.conf")).to eq("default_alternate_template\n") 115 | end 116 | end 117 | 118 | end 119 | -------------------------------------------------------------------------------- /spec/foreman/export_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/export" 3 | 4 | describe "Foreman::Export" do 5 | subject { Foreman::Export } 6 | 7 | describe "with a formatter that doesn't declare the appropriate class" do 8 | it "prints an error" do 9 | expect(subject).to receive(:require).with("foreman/export/invalidformatter") 10 | mock_export_error("Unknown export format: invalidformatter (no class Foreman::Export::Invalidformatter).") do 11 | subject.formatter("invalidformatter") 12 | end 13 | end 14 | end 15 | 16 | describe "with an invalid formatter" do 17 | 18 | it "prints an error" do 19 | mock_export_error("Unknown export format: invalidformatter (unable to load file 'foreman/export/invalidformatter').") do 20 | subject.formatter("invalidformatter") 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/foreman/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman/helpers" 3 | 4 | describe "Foreman::Helpers" do 5 | before do 6 | module Foo 7 | class Bar; end 8 | end 9 | end 10 | 11 | after do 12 | Object.send(:remove_const, :Foo) 13 | end 14 | 15 | subject { o = Object.new; o.extend(Foreman::Helpers); o } 16 | 17 | it "should classify words" do 18 | expect(subject.classify("foo")).to eq("Foo") 19 | expect(subject.classify("foo-bar")).to eq("FooBar") 20 | end 21 | 22 | it "should constantize words" do 23 | expect(subject.constantize("Object")).to eq(Object) 24 | expect(subject.constantize("Foo::Bar")).to eq(Foo::Bar) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/foreman/process_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'foreman/process' 3 | require 'ostruct' 4 | require 'timeout' 5 | require 'tmpdir' 6 | 7 | describe Foreman::Process do 8 | 9 | def run(process, options={}) 10 | rd, wr = IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY") 11 | process.run(options.merge(:output => wr)) 12 | rd.gets 13 | end 14 | 15 | describe "#run" do 16 | 17 | it "runs the process" do 18 | process = Foreman::Process.new(resource_path("bin/test")) 19 | expect(run(process)).to eq("testing\n") 20 | end 21 | 22 | it "can set environment" do 23 | process = Foreman::Process.new(resource_path("bin/env FOO"), :env => { "FOO" => "bar" }) 24 | expect(run(process)).to eq("bar\n") 25 | end 26 | 27 | it "can set per-run environment" do 28 | process = Foreman::Process.new(resource_path("bin/env FOO")) 29 | expect(run(process, :env => { "FOO" => "bar "})).to eq("bar\n") 30 | end 31 | 32 | it "can handle env vars in the command" do 33 | process = Foreman::Process.new(resource_path("bin/echo $FOO"), :env => { "FOO" => "bar" }) 34 | expect(run(process)).to eq("bar\n") 35 | end 36 | 37 | it "can handle per-run env vars in the command" do 38 | process = Foreman::Process.new(resource_path("bin/echo $FOO")) 39 | expect(run(process, :env => { "FOO" => "bar" })).to eq("bar\n") 40 | end 41 | 42 | it "should output utf8 properly" do 43 | process = Foreman::Process.new(resource_path("bin/utf8")) 44 | expect(run(process)).to eq(Foreman.ruby_18? ? "\xFF\x03\n" : "\xFF\x03\n".force_encoding('binary')) 45 | end 46 | 47 | it "can expand env in the command" do 48 | process = Foreman::Process.new("command $FOO $BAR", :env => { "FOO" => "bar" }) 49 | expect(process.expanded_command).to eq("command bar $BAR") 50 | end 51 | 52 | it "can expand extra env in the command" do 53 | process = Foreman::Process.new("command $FOO $BAR", :env => { "FOO" => "bar" }) 54 | expect(process.expanded_command("BAR" => "qux")).to eq("command bar qux") 55 | end 56 | 57 | it "can execute" do 58 | expect(Kernel).to receive(:exec).with("bin/command") 59 | process = Foreman::Process.new("bin/command") 60 | process.exec 61 | end 62 | 63 | it "can execute with env" do 64 | expect(Kernel).to receive(:exec).with("bin/command bar") 65 | process = Foreman::Process.new("bin/command $FOO") 66 | process.exec(:env => { "FOO" => "bar" }) 67 | end 68 | 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /spec/foreman/procfile_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'foreman/procfile' 3 | require 'pathname' 4 | require 'tmpdir' 5 | 6 | describe Foreman::Procfile, :fakefs do 7 | subject { Foreman::Procfile.new } 8 | 9 | it "can load from a file" do 10 | write_procfile 11 | subject.load "Procfile" 12 | expect(subject["alpha"]).to eq("./alpha") 13 | expect(subject["bravo"]).to eq("./bravo") 14 | end 15 | 16 | it "loads a passed-in Procfile" do 17 | write_procfile 18 | procfile = Foreman::Procfile.new("Procfile") 19 | expect(procfile["alpha"]).to eq("./alpha") 20 | expect(procfile["bravo"]).to eq("./bravo") 21 | expect(procfile["foo-bar"]).to eq("./foo-bar") 22 | expect(procfile["foo_bar"]).to eq("./foo_bar") 23 | end 24 | 25 | it "raises an error if Procfile is empty" do 26 | write_file "Procfile" do |procfile| 27 | procfile.puts 28 | end 29 | 30 | expect { Foreman::Procfile.new("Procfile") }.to raise_error described_class::EmptyFileError 31 | end 32 | 33 | it 'only creates Procfile entries for lines matching regex' do 34 | write_procfile 35 | procfile = Foreman::Procfile.new("Procfile") 36 | keys = procfile.instance_variable_get(:@entries).map(&:first) 37 | expect(keys).to match_array(%w[alpha bravo foo-bar foo_bar]) 38 | end 39 | 40 | it "returns nil when attempting to retrieve an non-existing entry" do 41 | write_procfile 42 | procfile = Foreman::Procfile.new("Procfile") 43 | expect(procfile["unicorn"]).to eq(nil) 44 | end 45 | 46 | it "can have a process appended to it" do 47 | subject["charlie"] = "./charlie" 48 | expect(subject["charlie"]).to eq("./charlie") 49 | end 50 | 51 | it "can write to a string" do 52 | subject["foo"] = "./foo" 53 | subject["bar"] = "./bar" 54 | expect(subject.to_s).to eq("foo: ./foo\nbar: ./bar") 55 | end 56 | 57 | it "can write to a file" do 58 | subject["foo"] = "./foo" 59 | subject["bar"] = "./bar" 60 | Dir.mkdir('/tmp') 61 | subject.save "/tmp/proc" 62 | expect(File.read("/tmp/proc")).to eq("foo: ./foo\nbar: ./bar\n") 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /spec/foreman_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "foreman" 3 | 4 | describe Foreman do 5 | 6 | describe "VERSION" do 7 | subject { Foreman::VERSION } 8 | it { is_expected.to be_a String } 9 | end 10 | 11 | describe "runner" do 12 | it "should exist" do 13 | expect(File.exist?(Foreman.runner)).to eq(true) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/helper_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "spec helpers" do 4 | describe "#preserving_env" do 5 | after { ENV.delete "FOO" } 6 | 7 | it "should remove added environment vars" do 8 | old = ENV["FOO"] 9 | preserving_env { ENV["FOO"] = "baz" } 10 | expect(ENV["FOO"]).to eq(old) 11 | end 12 | 13 | it "should reset modified environment vars" do 14 | ENV["FOO"] = "bar" 15 | preserving_env { ENV["FOO"] = "baz"} 16 | expect(ENV["FOO"]).to eq("bar") 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/resources/.env: -------------------------------------------------------------------------------- 1 | FOO=bar 2 | -------------------------------------------------------------------------------- /spec/resources/Procfile: -------------------------------------------------------------------------------- 1 | echo: bin/echo echoing 2 | env: bin/env FOO 3 | test: bin/test 4 | utf8: bin/utf8 5 | ps: bin/echo PS env var is $PS 6 | -------------------------------------------------------------------------------- /spec/resources/Procfile.bad: -------------------------------------------------------------------------------- 1 | good: sleep 1 2 | bad: false 3 | -------------------------------------------------------------------------------- /spec/resources/bin/echo: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo $* 3 | -------------------------------------------------------------------------------- /spec/resources/bin/env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo ${!1} 3 | -------------------------------------------------------------------------------- /spec/resources/bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "testing" 3 | -------------------------------------------------------------------------------- /spec/resources/bin/utf8: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | puts "\xff\x03" 3 | -------------------------------------------------------------------------------- /spec/resources/export/bluepill/app-concurrency.pill: -------------------------------------------------------------------------------- 1 | Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepill.log") do |app| 2 | 3 | app.uid = "app" 4 | app.gid = "app" 5 | 6 | 7 | 8 | 9 | app.process("alpha-1") do |process| 10 | process.start_command = %Q{./alpha} 11 | 12 | process.working_dir = "/tmp/app" 13 | process.daemonize = true 14 | process.environment = {"PORT"=>"5000"} 15 | process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] 16 | process.stop_grace_time = 45.seconds 17 | 18 | process.stdout = process.stderr = "/var/log/app/app-alpha-1.log" 19 | 20 | process.monitor_children do |children| 21 | children.stop_command "kill {{PID}}" 22 | end 23 | 24 | process.group = "app-alpha" 25 | end 26 | 27 | 28 | app.process("alpha-2") do |process| 29 | process.start_command = %Q{./alpha} 30 | 31 | process.working_dir = "/tmp/app" 32 | process.daemonize = true 33 | process.environment = {"PORT"=>"5001"} 34 | process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] 35 | process.stop_grace_time = 45.seconds 36 | 37 | process.stdout = process.stderr = "/var/log/app/app-alpha-2.log" 38 | 39 | process.monitor_children do |children| 40 | children.stop_command "kill {{PID}}" 41 | end 42 | 43 | process.group = "app-alpha" 44 | end 45 | 46 | 47 | 48 | 49 | end 50 | -------------------------------------------------------------------------------- /spec/resources/export/bluepill/app.pill: -------------------------------------------------------------------------------- 1 | Bluepill.application("app", :foreground => false, :log_file => "/var/log/bluepill.log") do |app| 2 | 3 | app.uid = "app" 4 | app.gid = "app" 5 | 6 | 7 | 8 | 9 | app.process("alpha-1") do |process| 10 | process.start_command = %Q{./alpha} 11 | 12 | process.working_dir = "/tmp/app" 13 | process.daemonize = true 14 | process.environment = {"PORT"=>"5000"} 15 | process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] 16 | process.stop_grace_time = 45.seconds 17 | 18 | process.stdout = process.stderr = "/var/log/app/app-alpha-1.log" 19 | 20 | process.monitor_children do |children| 21 | children.stop_command "kill {{PID}}" 22 | end 23 | 24 | process.group = "app-alpha" 25 | end 26 | 27 | app.process("bravo-1") do |process| 28 | process.start_command = %Q{./bravo} 29 | 30 | process.working_dir = "/tmp/app" 31 | process.daemonize = true 32 | process.environment = {"PORT"=>"5100"} 33 | process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] 34 | process.stop_grace_time = 45.seconds 35 | 36 | process.stdout = process.stderr = "/var/log/app/app-bravo-1.log" 37 | 38 | process.monitor_children do |children| 39 | children.stop_command "kill {{PID}}" 40 | end 41 | 42 | process.group = "app-bravo" 43 | end 44 | 45 | app.process("foo_bar-1") do |process| 46 | process.start_command = %Q{./foo_bar} 47 | 48 | process.working_dir = "/tmp/app" 49 | process.daemonize = true 50 | process.environment = {"PORT"=>"5200"} 51 | process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] 52 | process.stop_grace_time = 45.seconds 53 | 54 | process.stdout = process.stderr = "/var/log/app/app-foo_bar-1.log" 55 | 56 | process.monitor_children do |children| 57 | children.stop_command "kill {{PID}}" 58 | end 59 | 60 | process.group = "app-foo_bar" 61 | end 62 | 63 | app.process("foo-bar-1") do |process| 64 | process.start_command = %Q{./foo-bar} 65 | 66 | process.working_dir = "/tmp/app" 67 | process.daemonize = true 68 | process.environment = {"PORT"=>"5300"} 69 | process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill] 70 | process.stop_grace_time = 45.seconds 71 | 72 | process.stdout = process.stderr = "/var/log/app/app-foo-bar-1.log" 73 | 74 | process.monitor_children do |children| 75 | 76 | children.stop_command "kill {{PID}}" 77 | end 78 | 79 | process.group = "app-foo-bar" 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/resources/export/daemon/app-alpha-1.conf: -------------------------------------------------------------------------------- 1 | start on starting app-alpha 2 | stop on stopping app-alpha 3 | respawn 4 | 5 | env PORT=5000 6 | 7 | exec start-stop-daemon --start --chuid app --chdir /tmp/app --make-pidfile --pidfile /var/run/app/app-alpha-1.pid --exec ./alpha >> /var/log/app/app-alpha-1.log 2>&1 8 | -------------------------------------------------------------------------------- /spec/resources/export/daemon/app-alpha-2.conf: -------------------------------------------------------------------------------- 1 | start on starting app-alpha 2 | stop on stopping app-alpha 3 | respawn 4 | 5 | env PORT=5001 6 | 7 | exec start-stop-daemon --start --chuid app --chdir /tmp/app --make-pidfile --pidfile /var/run/app/app-alpha-2.pid --exec ./alpha >> /var/log/app/app-alpha-2.log 2>&1 8 | -------------------------------------------------------------------------------- /spec/resources/export/daemon/app-alpha.conf: -------------------------------------------------------------------------------- 1 | start on starting app 2 | stop on stopping app 3 | -------------------------------------------------------------------------------- /spec/resources/export/daemon/app-bravo-1.conf: -------------------------------------------------------------------------------- 1 | start on starting app-bravo 2 | stop on stopping app-bravo 3 | respawn 4 | 5 | env PORT=5100 6 | 7 | exec start-stop-daemon --start --chuid app --chdir /tmp/app --make-pidfile --pidfile /var/run/app/app-bravo-1.pid --exec ./bravo >> /var/log/app/app-bravo-1.log 2>&1 8 | -------------------------------------------------------------------------------- /spec/resources/export/daemon/app-bravo.conf: -------------------------------------------------------------------------------- 1 | start on starting app 2 | stop on stopping app 3 | -------------------------------------------------------------------------------- /spec/resources/export/daemon/app.conf: -------------------------------------------------------------------------------- 1 | pre-start script 2 | 3 | bash << "EOF" 4 | mkdir -p /var/log/app 5 | chown -R app /var/log/app 6 | mkdir -p /var/run/app 7 | chown -R app /var/run/app 8 | EOF 9 | 10 | end script 11 | 12 | start on runlevel [2345] 13 | 14 | stop on runlevel [016] 15 | -------------------------------------------------------------------------------- /spec/resources/export/inittab/inittab.concurrency: -------------------------------------------------------------------------------- 1 | # ----- foreman app processes ----- 2 | AP01:4:respawn:/bin/su - app -c 'cd /tmp/app;export PORT=5000;./alpha >> /var/log/app/alpha-1.log 2>&1' 3 | AP02:4:respawn:/bin/su - app -c 'cd /tmp/app;export PORT=5001;./alpha >> /var/log/app/alpha-2.log 2>&1' 4 | # ----- end foreman app processes ----- 5 | -------------------------------------------------------------------------------- /spec/resources/export/inittab/inittab.default: -------------------------------------------------------------------------------- 1 | # ----- foreman app processes ----- 2 | AP01:4:respawn:/bin/su - app -c 'cd /tmp/app;export PORT=5000;./alpha >> /var/log/app/alpha-1.log 2>&1' 3 | AP02:4:respawn:/bin/su - app -c 'cd /tmp/app;export PORT=5100;./bravo >> /var/log/app/bravo-1.log 2>&1' 4 | AP03:4:respawn:/bin/su - app -c 'cd /tmp/app;export PORT=5200;./foo_bar >> /var/log/app/foo_bar-1.log 2>&1' 5 | AP04:4:respawn:/bin/su - app -c 'cd /tmp/app;export PORT=5300;./foo-bar >> /var/log/app/foo-bar-1.log 2>&1' 6 | # ----- end foreman app processes ----- 7 | -------------------------------------------------------------------------------- /spec/resources/export/launchd/launchd-a.default: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | app-alpha-1 7 | EnvironmentVariables 8 | 9 | PORT 10 | 5000 11 | 12 | ProgramArguments 13 | 14 | ./alpha 15 | 16 | KeepAlive 17 | 18 | RunAtLoad 19 | 20 | StandardOutPath 21 | /var/log/app/app-alpha-1.log 22 | StandardErrorPath 23 | /var/log/app/app-alpha-1.log 24 | UserName 25 | app 26 | WorkingDirectory 27 | /tmp/app 28 | 29 | 30 | -------------------------------------------------------------------------------- /spec/resources/export/launchd/launchd-b.default: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | app-bravo-1 7 | EnvironmentVariables 8 | 9 | PORT 10 | 5100 11 | 12 | ProgramArguments 13 | 14 | ./bravo 15 | 16 | KeepAlive 17 | 18 | RunAtLoad 19 | 20 | StandardOutPath 21 | /var/log/app/app-bravo-1.log 22 | StandardErrorPath 23 | /var/log/app/app-bravo-1.log 24 | UserName 25 | app 26 | WorkingDirectory 27 | /tmp/app 28 | 29 | 30 | -------------------------------------------------------------------------------- /spec/resources/export/launchd/launchd-c.default: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | app-alpha-1 7 | EnvironmentVariables 8 | 9 | PORT 10 | 5000 11 | 12 | ProgramArguments 13 | 14 | ./alpha 15 | charlie 16 | 17 | KeepAlive 18 | 19 | RunAtLoad 20 | 21 | StandardOutPath 22 | /var/log/app/app-alpha-1.log 23 | StandardErrorPath 24 | /var/log/app/app-alpha-1.log 25 | UserName 26 | app 27 | WorkingDirectory 28 | /tmp/app 29 | 30 | 31 | -------------------------------------------------------------------------------- /spec/resources/export/runit/app-alpha-1/log/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | LOG=/var/log/app/alpha-1 5 | 6 | test -d "$LOG" || mkdir -p -m 2750 "$LOG" && chown app "$LOG" 7 | exec chpst -u app svlogd "$LOG" 8 | -------------------------------------------------------------------------------- /spec/resources/export/runit/app-alpha-1/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /tmp/app 3 | exec 2>&1 4 | exec chpst -u app -e /tmp/init/app-alpha-1/env ./alpha bar=baz 5 | -------------------------------------------------------------------------------- /spec/resources/export/runit/app-alpha-2/log/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | LOG=/var/log/app/alpha-2 5 | 6 | test -d "$LOG" || mkdir -p -m 2750 "$LOG" && chown app "$LOG" 7 | exec chpst -u app svlogd "$LOG" 8 | -------------------------------------------------------------------------------- /spec/resources/export/runit/app-alpha-2/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /tmp/app 3 | exec 2>&1 4 | exec chpst -u app -e /tmp/init/app-alpha-2/env ./alpha bar=baz 5 | -------------------------------------------------------------------------------- /spec/resources/export/runit/app-bravo-1/log/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | LOG=/var/log/app/bravo-1 5 | 6 | test -d "$LOG" || mkdir -p -m 2750 "$LOG" && chown app "$LOG" 7 | exec chpst -u app svlogd "$LOG" 8 | -------------------------------------------------------------------------------- /spec/resources/export/runit/app-bravo-1/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /tmp/app 3 | exec 2>&1 4 | exec chpst -u app -e /tmp/init/app-bravo-1/env ./bravo 5 | -------------------------------------------------------------------------------- /spec/resources/export/supervisord/app-alpha-1.conf: -------------------------------------------------------------------------------- 1 | [program:app-alpha-1] 2 | command=./alpha 3 | autostart=true 4 | autorestart=true 5 | stdout_logfile=/var/log/app/alpha-1.log 6 | stderr_logfile=/var/log/app/alpha-1.error.log 7 | user=app 8 | directory=/tmp/app 9 | environment=FOO="bar",URL="http://example.com/api?foo=bar&baz=1",PORT="5000" 10 | 11 | [program:app-bravo-1] 12 | command=./bravo 13 | autostart=true 14 | autorestart=true 15 | stdout_logfile=/var/log/app/bravo-1.log 16 | stderr_logfile=/var/log/app/bravo-1.error.log 17 | user=app 18 | directory=/tmp/app 19 | environment=FOO="bar",URL="http://example.com/api?foo=bar&baz=1",PORT="5100" 20 | 21 | [program:app-foo_bar-1] 22 | command=./foo_bar 23 | autostart=true 24 | autorestart=true 25 | stdout_logfile=/var/log/app/foo_bar-1.log 26 | stderr_logfile=/var/log/app/foo_bar-1.error.log 27 | user=app 28 | directory=/tmp/app 29 | environment=FOO="bar",URL="http://example.com/api?foo=bar&baz=1",PORT="5200" 30 | 31 | [program:app-foo-bar-1] 32 | command=./foo-bar 33 | autostart=true 34 | autorestart=true 35 | stdout_logfile=/var/log/app/foo-bar-1.log 36 | stderr_logfile=/var/log/app/foo-bar-1.error.log 37 | user=app 38 | directory=/tmp/app 39 | environment=FOO="bar",URL="http://example.com/api?foo=bar&baz=1",PORT="5300" 40 | 41 | [group:app] 42 | programs=app-alpha-1,app-bravo-1,app-foo_bar-1,app-foo-bar-1 43 | -------------------------------------------------------------------------------- /spec/resources/export/supervisord/app-alpha-2.conf: -------------------------------------------------------------------------------- 1 | [program:app-alpha-1] 2 | command=./alpha 3 | autostart=true 4 | autorestart=true 5 | stdout_logfile=/var/log/app/alpha-1.log 6 | stderr_logfile=/var/log/app/alpha-1.error.log 7 | user=app 8 | directory=/tmp/app 9 | environment=PORT="5000" 10 | 11 | [program:app-alpha-2] 12 | command=./alpha 13 | autostart=true 14 | autorestart=true 15 | stdout_logfile=/var/log/app/alpha-2.log 16 | stderr_logfile=/var/log/app/alpha-2.error.log 17 | user=app 18 | directory=/tmp/app 19 | environment=PORT="5001" 20 | 21 | [group:app] 22 | programs=app-alpha-1,app-alpha-2 23 | -------------------------------------------------------------------------------- /spec/resources/export/systemd/app-alpha.1.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | PartOf=app.target 3 | StopWhenUnneeded=yes 4 | 5 | [Service] 6 | User=app 7 | WorkingDirectory=/tmp/app 8 | Environment=PORT=5000 9 | Environment=PS=alpha.1 10 | ExecStart=/bin/bash -lc 'exec -a "app-alpha.1" ./alpha' 11 | Restart=always 12 | RestartSec=14s 13 | StandardInput=null 14 | StandardOutput=syslog 15 | StandardError=syslog 16 | SyslogIdentifier=%n 17 | KillMode=mixed 18 | TimeoutStopSec=5 19 | -------------------------------------------------------------------------------- /spec/resources/export/systemd/app-alpha.2.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | PartOf=app.target 3 | StopWhenUnneeded=yes 4 | 5 | [Service] 6 | User=app 7 | WorkingDirectory=/tmp/app 8 | Environment=PORT=5001 9 | Environment=PS=alpha.2 10 | ExecStart=/bin/bash -lc 'exec -a "app-alpha.2" ./alpha' 11 | Restart=always 12 | RestartSec=14s 13 | StandardInput=null 14 | StandardOutput=syslog 15 | StandardError=syslog 16 | SyslogIdentifier=%n 17 | KillMode=mixed 18 | TimeoutStopSec=5 19 | -------------------------------------------------------------------------------- /spec/resources/export/systemd/app-alpha.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | PartOf=app.target 3 | -------------------------------------------------------------------------------- /spec/resources/export/systemd/app-bravo.1.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | PartOf=app.target 3 | StopWhenUnneeded=yes 4 | 5 | [Service] 6 | User=app 7 | WorkingDirectory=/tmp/app 8 | Environment=PORT=5100 9 | Environment=PS=bravo.1 10 | ExecStart=/bin/bash -lc 'exec -a "app-bravo.1" ./bravo' 11 | Restart=always 12 | RestartSec=14s 13 | StandardInput=null 14 | StandardOutput=syslog 15 | StandardError=syslog 16 | SyslogIdentifier=%n 17 | KillMode=mixed 18 | TimeoutStopSec=5 19 | -------------------------------------------------------------------------------- /spec/resources/export/systemd/app-bravo.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | PartOf=app.target 3 | -------------------------------------------------------------------------------- /spec/resources/export/systemd/app.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Wants=app-alpha.1.service app-bravo.1.service app-foo_bar.1.service app-foo-bar.1.service 3 | 4 | [Install] 5 | WantedBy=multi-user.target 6 | -------------------------------------------------------------------------------- /spec/resources/export/upstart/app-alpha-1.conf: -------------------------------------------------------------------------------- 1 | start on starting app-alpha 2 | stop on stopping app-alpha 3 | respawn 4 | 5 | env PORT=5000 6 | 7 | setuid app 8 | 9 | chdir /tmp/app 10 | 11 | exec ./alpha 12 | -------------------------------------------------------------------------------- /spec/resources/export/upstart/app-alpha-2.conf: -------------------------------------------------------------------------------- 1 | start on starting app-alpha 2 | stop on stopping app-alpha 3 | respawn 4 | 5 | env PORT=5001 6 | 7 | setuid app 8 | 9 | chdir /tmp/app 10 | 11 | exec ./alpha 12 | -------------------------------------------------------------------------------- /spec/resources/export/upstart/app-alpha.conf: -------------------------------------------------------------------------------- 1 | start on starting app 2 | stop on stopping app 3 | -------------------------------------------------------------------------------- /spec/resources/export/upstart/app-bravo-1.conf: -------------------------------------------------------------------------------- 1 | start on starting app-bravo 2 | stop on stopping app-bravo 3 | respawn 4 | 5 | env PORT=5100 6 | 7 | setuid app 8 | 9 | chdir /tmp/app 10 | 11 | exec ./bravo 12 | -------------------------------------------------------------------------------- /spec/resources/export/upstart/app-bravo.conf: -------------------------------------------------------------------------------- 1 | start on starting app 2 | stop on stopping app 3 | -------------------------------------------------------------------------------- /spec/resources/export/upstart/app.conf: -------------------------------------------------------------------------------- 1 | start on runlevel [2345] 2 | stop on runlevel [!2345] 3 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | SimpleCov.start do 3 | add_filter "/spec/" 4 | end 5 | 6 | require "rspec" 7 | require "timecop" 8 | require "pp" 9 | require "fakefs/safe" 10 | require "fakefs/spec_helpers" 11 | 12 | $:.unshift File.expand_path("../../lib", __FILE__) 13 | 14 | def mock_export_error(message) 15 | expect { yield }.to raise_error(Foreman::Export::Exception, message) 16 | end 17 | 18 | def mock_error(subject, message) 19 | mock_exit do 20 | expect(subject).to receive(:puts).with("ERROR: #{message}") 21 | yield 22 | end 23 | end 24 | 25 | def make_pipe 26 | IO.method(:pipe).arity.zero? ? IO.pipe : IO.pipe("BINARY") 27 | end 28 | 29 | def foreman(args) 30 | capture_stdout do 31 | begin 32 | Foreman::CLI.start(args.split(" ")) 33 | rescue SystemExit 34 | end 35 | end 36 | end 37 | 38 | def forked_foreman(args) 39 | rd, wr = make_pipe 40 | Process.spawn("bundle exec bin/foreman #{args}", :out => wr, :err => wr) 41 | wr.close 42 | rd.read 43 | end 44 | 45 | def fork_and_capture(&blk) 46 | rd, wr = make_pipe 47 | pid = fork do 48 | rd.close 49 | wr.sync = true 50 | $stdout.reopen wr 51 | $stderr.reopen wr 52 | blk.call 53 | $stdout.flush 54 | $stdout.close 55 | end 56 | wr.close 57 | Process.wait pid 58 | buffer = "" 59 | until rd.eof? 60 | buffer += rd.gets 61 | end 62 | end 63 | 64 | def fork_and_get_exitstatus(args) 65 | pid = Process.spawn("bundle exec bin/foreman #{args}", :out => "/dev/null", :err => "/dev/null") 66 | Process.wait(pid) 67 | $?.exitstatus 68 | end 69 | 70 | def mock_exit(&block) 71 | expect { block.call }.to raise_error(SystemExit) 72 | end 73 | 74 | def write_foreman_config(app) 75 | File.open("/etc/foreman/#{app}.conf", "w") do |file| 76 | file.puts %{#{app}_processes="alpha bravo"} 77 | file.puts %{#{app}_alpha="1"} 78 | file.puts %{#{app}_bravo="2"} 79 | end 80 | end 81 | 82 | def write_procfile(procfile="Procfile", alpha_env="") 83 | FileUtils.mkdir_p(File.dirname(procfile)) 84 | File.open(procfile, "w") do |file| 85 | file.puts "alpha: ./alpha" + " #{alpha_env}".rstrip 86 | file.puts "\n" 87 | file.puts "bravo:\t./bravo" 88 | file.puts "foo_bar:\t./foo_bar" 89 | file.puts "foo-bar:\t./foo-bar" 90 | file.puts "# baz:\t./baz" 91 | end 92 | File.expand_path(procfile) 93 | end 94 | 95 | def write_file(file) 96 | FileUtils.mkdir_p(File.dirname(file)) 97 | File.open(file, 'w') do |f| 98 | yield(f) if block_given? 99 | end 100 | end 101 | 102 | def write_env(env=".env", options={"FOO"=>"bar"}) 103 | File.open(env, "w") do |file| 104 | options.each do |key, val| 105 | file.puts "#{key}=#{val}" 106 | end 107 | end 108 | end 109 | 110 | def without_fakefs 111 | FakeFS.deactivate! 112 | ret = yield 113 | FakeFS.activate! 114 | ret 115 | end 116 | 117 | def load_export_templates_into_fakefs(type) 118 | without_fakefs do 119 | Dir[File.expand_path("../../data/export/#{type}/**/*", __FILE__)].inject({}) do |hash, file| 120 | next(hash) if File.directory?(file) 121 | hash.update(file => File.read(file)) 122 | end 123 | end.each do |filename, contents| 124 | FileUtils.mkdir_p File.dirname(filename) 125 | File.open(filename, "w") do |f| 126 | f.puts contents 127 | end 128 | end 129 | end 130 | 131 | def resource_path(filename) 132 | File.expand_path("../resources/#{filename}", __FILE__) 133 | end 134 | 135 | def example_export_file(filename) 136 | FakeFS.deactivate! 137 | data = File.read(File.expand_path(resource_path("export/#{filename}"), __FILE__)) 138 | FakeFS.activate! 139 | data 140 | end 141 | 142 | def preserving_env 143 | old_env = ENV.to_hash 144 | begin 145 | yield 146 | ensure 147 | ENV.clear 148 | ENV.update(old_env) 149 | end 150 | end 151 | 152 | def normalize_space(s) 153 | s.gsub(/\n[\n\s]*/, "\n") 154 | end 155 | 156 | def capture_stdout 157 | old_stdout = $stdout.dup 158 | rd, wr = make_pipe 159 | $stdout = wr 160 | yield 161 | wr.close 162 | rd.read 163 | ensure 164 | $stdout = old_stdout 165 | end 166 | 167 | RSpec.configure do |config| 168 | config.color = true 169 | config.order = 'rand' 170 | config.include FakeFS::SpecHelpers, :fakefs 171 | config.before(:each) do 172 | FileUtils.mkdir_p('/tmp') 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /tasks/dist.rake: -------------------------------------------------------------------------------- 1 | require "erb" 2 | require "fileutils" 3 | require "tmpdir" 4 | 5 | def assemble(source, target, perms=0644) 6 | FileUtils.mkdir_p(File.dirname(target)) 7 | File.open(target, "w") do |f| 8 | f.puts ERB.new(File.read(source)).result(binding) 9 | end 10 | File.chmod(perms, target) 11 | end 12 | 13 | def assemble_distribution(target_dir=Dir.pwd) 14 | distribution_files.each do |source| 15 | target = source.gsub(/^#{project_root}/, target_dir) 16 | FileUtils.mkdir_p(File.dirname(target)) 17 | FileUtils.cp(source, target) 18 | end 19 | end 20 | 21 | GEM_BLACKLIST = %w( bundler foreman ) 22 | 23 | def assemble_gems(target_dir=Dir.pwd) 24 | lines = %x{ cd #{project_root} && bundle show }.strip.split("\n") 25 | raise "error running bundler" unless $?.success? 26 | 27 | %x{ env BUNDLE_WITHOUT="development:test" bundle show }.split("\n").each do |line| 28 | if line =~ /^ \* (.*?) \((.*?)\)/ 29 | next if GEM_BLACKLIST.include?($1) 30 | puts "vendoring: #{$1}-#{$2}" 31 | gem_dir = %x{ bundle show #{$1} }.strip 32 | FileUtils.mkdir_p "#{target_dir}/vendor/gems" 33 | %x{ cp -R "#{gem_dir}" "#{target_dir}/vendor/gems" } 34 | end 35 | end.compact 36 | end 37 | 38 | def beta? 39 | Foreman::VERSION.to_s =~ /pre/ 40 | end 41 | 42 | def clean(file) 43 | rm file if File.exist?(file) 44 | end 45 | 46 | def distribution_files(type=nil) 47 | require "foreman/distribution" 48 | base_files = Foreman::Distribution.files 49 | type_files = type ? 50 | Dir[File.expand_path("../../dist/resources/#{type}/**/*", __FILE__)] : [] 51 | base_files.concat(type_files) 52 | end 53 | 54 | def mkchdir(dir) 55 | FileUtils.mkdir_p(dir) 56 | Dir.chdir(dir) do |dir| 57 | yield(File.expand_path(dir)) 58 | end 59 | end 60 | 61 | def pkg(filename) 62 | File.expand_path("../../pkg/#{filename}", __FILE__) 63 | end 64 | 65 | def project_root 66 | File.expand_path("../..", __FILE__) 67 | end 68 | 69 | def resource(name) 70 | File.expand_path("../../dist/resources/#{name}", __FILE__) 71 | end 72 | 73 | def s3_connect 74 | return if @s3_connected 75 | 76 | require "aws/s3" 77 | 78 | unless ENV["FOREMAN_RELEASE_ACCESS"] && ENV["FOREMAN_RELEASE_SECRET"] 79 | puts "please set FOREMAN_RELEASE_ACCESS and FOREMAN_RELEASE_SECRET in your environment" 80 | exit 1 81 | end 82 | 83 | AWS::S3::Base.establish_connection!( 84 | :access_key_id => ENV["FOREMAN_RELEASE_ACCESS"], 85 | :secret_access_key => ENV["FOREMAN_RELEASE_SECRET"] 86 | ) 87 | 88 | @s3_connected = true 89 | end 90 | 91 | def store(package_file, filename, bucket="assets.foreman.io") 92 | s3_connect 93 | puts "storing: #{filename}" 94 | AWS::S3::S3Object.store(filename, File.open(package_file), bucket, :access => :public_read) 95 | end 96 | 97 | def tempdir 98 | Dir.mktmpdir do |dir| 99 | Dir.chdir(dir) do 100 | yield(dir) 101 | end 102 | end 103 | end 104 | 105 | def version 106 | require "foreman/version" 107 | Foreman::VERSION 108 | end 109 | 110 | Dir[File.expand_path("../../dist/**/*.rake", __FILE__)].each do |rake| 111 | import rake 112 | end 113 | -------------------------------------------------------------------------------- /tasks/release.rake: -------------------------------------------------------------------------------- 1 | require "time" 2 | 3 | desc "Build the manual" 4 | task :man do 5 | ENV['RONN_MANUAL'] = "Foreman Manual" 6 | ENV['RONN_ORGANIZATION'] = "Foreman #{Foreman::VERSION}" 7 | sh "ronn -w -s toc -r5 --markdown man/*.ronn" 8 | end 9 | 10 | desc "Commit the manual to git" 11 | task "man:commit" => :man do 12 | sh "git add README.md" 13 | sh "git commit -am 'update docs' || echo 'nothing to commit'" 14 | sh "git push" 15 | end 16 | 17 | desc "Generate the Github docs" 18 | task :pages => "man:commit" do 19 | sh %{ 20 | cp man/foreman.1.html /tmp/foreman.1.html 21 | git checkout gh-pages 22 | rm ./index.html 23 | cp /tmp/foreman.1.html ./index.html 24 | git add -u index.html 25 | git commit -m "saving man page to github docs" 26 | git push origin -f gh-pages 27 | git checkout main 28 | } 29 | end 30 | 31 | def latest_release 32 | latest = File.read("Changelog.md").split("\n").first.split(" ")[1] 33 | end 34 | 35 | def newer_release 36 | tags = %x{ git tag --contains v#{latest_release} | grep -v pre }.split("\n").sort_by do |tag| 37 | Gem::Version.new(tag[1..-1]) 38 | end 39 | tags[1] 40 | end 41 | 42 | desc "Generate a Changelog" 43 | task :changelog do 44 | while release = newer_release 45 | entry = %x{ git show --format="%cd" #{release} | head -n 1 } 46 | puts entry 47 | date = Time.parse(entry.chomp).strftime("%Y-%m-%d") 48 | 49 | message = "## #{release[1..-1]} (#{date})\n\n" 50 | message += %x{ git log --format="* %s [%an]" v#{latest_release}..#{release} | grep -v "Merge pull request" | grep -v "* #{release[1..-1]}" | grep -v "* update docs" } 51 | 52 | changelog = File.read("Changelog.md") 53 | changelog = message + "\n" + changelog 54 | 55 | puts release 56 | 57 | File.open("Changelog.md", "w") do |file| 58 | file.print changelog 59 | end 60 | end 61 | end 62 | 63 | desc "Cut a release" 64 | task :release do 65 | Rake::Task["changelog"].invoke 66 | Rake::Task["pages"].invoke 67 | Rake::Task["gem:release"].invoke 68 | end 69 | -------------------------------------------------------------------------------- /tasks/rspec.rake: -------------------------------------------------------------------------------- 1 | require "rspec/core/rake_task" 2 | 3 | task :default => :spec 4 | 5 | desc "Run all specs" 6 | RSpec::Core::RakeTask.new(:spec) do |t| 7 | t.pattern = 'spec/**/*_spec.rb' 8 | end 9 | -------------------------------------------------------------------------------- /tasks/vendor.rake: -------------------------------------------------------------------------------- 1 | begin 2 | require "automatiek" 3 | 4 | desc "Vendor a specific version of thor" 5 | Automatiek::RakeTask.new("thor") do |lib| 6 | lib.download = { github: "https://github.com/erikhuda/thor" } 7 | lib.namespace = "Thor" 8 | lib.prefix = "Foreman" 9 | lib.vendor_lib = "lib/foreman/vendor/thor" 10 | end 11 | rescue LoadError 12 | end 13 | 14 | --------------------------------------------------------------------------------