├── .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 | [](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