diff options
25 files changed, 402 insertions, 19 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 50f1f65e4e8..e911d7e5b89 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -68,6 +68,13 @@ stages: - //@gitlab-org/gitlab-ee - //@gitlab/gitlab-ee +# Skip all jobs except the ones that begin with 'docs/'. +# Used for commits including ONLY documentation changes. +# https://docs.gitlab.com/ce/development/writing_documentation.html#testing +.except-docs: &except-docs + except: + - /^docs\/.*/ + .rspec-knapsack: &rspec-knapsack stage: test <<: *dedicated-runner @@ -91,11 +98,13 @@ stages: .rspec-knapsack-pg: &rspec-knapsack-pg <<: *rspec-knapsack <<: *use-pg + <<: *except-docs .rspec-knapsack-mysql: &rspec-knapsack-mysql <<: *rspec-knapsack <<: *use-mysql <<: *only-master-and-ee-or-mysql + <<: *except-docs .spinach-knapsack: &spinach-knapsack stage: test @@ -120,16 +129,19 @@ stages: .spinach-knapsack-pg: &spinach-knapsack-pg <<: *spinach-knapsack <<: *use-pg + <<: *except-docs .spinach-knapsack-mysql: &spinach-knapsack-mysql <<: *spinach-knapsack <<: *use-mysql <<: *only-master-and-ee-or-mysql + <<: *except-docs # Prepare and merge knapsack tests knapsack: <<: *knapsack-state <<: *dedicated-runner + <<: *except-docs stage: prepare script: - mkdir -p knapsack/${CI_PROJECT_NAME}/ @@ -156,6 +168,7 @@ update-knapsack: setup-test-env: <<: *use-pg <<: *dedicated-runner + <<: *except-docs stage: prepare script: - node --version @@ -243,6 +256,7 @@ spinach mysql 9 10: *spinach-knapsack-mysql .exec: &exec <<: *ruby-static-analysis <<: *dedicated-runner + <<: *except-docs stage: test script: - bundle exec $CI_JOB_NAME @@ -250,6 +264,7 @@ spinach mysql 9 10: *spinach-knapsack-mysql rubocop: <<: *ruby-static-analysis <<: *dedicated-runner + <<: *except-docs stage: test script: - bundle exec "rubocop --require rubocop-rspec" @@ -266,6 +281,7 @@ rake downtime_check: - master - tags - /^[\d-]+-stable(-ee)?$/ + - /^docs\/*/ rake ee_compat_check: <<: *exec @@ -296,10 +312,12 @@ rake ee_compat_check: rake pg db:migrate:reset: <<: *db-migrate-reset <<: *use-pg + <<: *except-docs rake mysql db:migrate:reset: <<: *db-migrate-reset <<: *use-mysql + <<: *except-docs .db-rollback: &db-rollback stage: test @@ -311,10 +329,12 @@ rake mysql db:migrate:reset: rake pg db:rollback: <<: *db-rollback <<: *use-pg + <<: *except-docs rake mysql db:rollback: <<: *db-rollback <<: *use-mysql + <<: *except-docs .db-seed_fu: &db-seed_fu stage: test @@ -336,14 +356,17 @@ rake mysql db:rollback: rake pg db:seed_fu: <<: *db-seed_fu <<: *use-pg + <<: *except-docs rake mysql db:seed_fu: <<: *db-seed_fu <<: *use-mysql + <<: *except-docs rake gitlab:assets:compile: stage: test <<: *dedicated-runner + <<: *except-docs dependencies: [] variables: NODE_ENV: "production" @@ -367,6 +390,7 @@ rake karma: stage: test <<: *use-pg <<: *dedicated-runner + <<: *except-docs variables: BABEL_ENV: "coverage" script: @@ -447,6 +471,7 @@ coverage: stage: post-test services: [] <<: *dedicated-runner + <<: *except-docs variables: SETUP_DB: "false" USE_BUNDLE_INSTALL: "true" @@ -462,6 +487,7 @@ coverage: lint:javascript: <<: *dedicated-runner + <<: *except-docs stage: test before_script: [] script: @@ -469,6 +495,7 @@ lint:javascript: lint:javascript:report: <<: *dedicated-runner + <<: *except-docs stage: post-test before_script: [] script: diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 1d0ba9ea182..267577d47e4 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.4.0 +0.4.1 diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index a8c0937569c..be2e6c7f193 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -38,6 +38,7 @@ module ServiceParams :new_issue_url, :notify, :notify_only_broken_pipelines, + :notify_only_default_branch, :password, :priority, :project_key, diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb index fb2a4837735..1ff08cce8cb 100644 --- a/app/controllers/projects/settings/integrations_controller.rb +++ b/app/controllers/projects/settings/integrations_controller.rb @@ -5,7 +5,7 @@ module Projects before_action :authorize_admin_project! layout "project_settings" - + def show @hooks = @project.hooks @hook = ProjectHook.new diff --git a/app/controllers/unicorn_test_controller.rb b/app/controllers/unicorn_test_controller.rb new file mode 100644 index 00000000000..b7a1a046be0 --- /dev/null +++ b/app/controllers/unicorn_test_controller.rb @@ -0,0 +1,12 @@ +if Rails.env.test? + class UnicornTestController < ActionController::Base + def pid + render plain: Process.pid.to_s + end + + def kill + Process.kill(params[:signal], Process.pid) + render plain: 'Bye!' + end + end +end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 5f5c76d3722..960111ca045 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -10,11 +10,12 @@ module EventsHelper 'deleted' => 'icon_trash_o' }.freeze - def link_to_author(event) + def link_to_author(event, self_added: false) author = event.author if author - link_to author.name, user_path(author.username), title: author.name + name = self_added ? 'You' : author.name + link_to name, user_path(author.username), title: name else event.author_name end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 4f5adf623f2..f19e2f9db9c 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -13,13 +13,13 @@ module TodosHelper def todo_action_name(todo) case todo.action - when Todo::ASSIGNED then 'assigned you' - when Todo::MENTIONED then 'mentioned you on' + when Todo::ASSIGNED then todo.self_added? ? 'assigned' : 'assigned you' + when Todo::MENTIONED then "mentioned #{todo_action_subject(todo)} on" when Todo::BUILD_FAILED then 'The build failed for' when Todo::MARKED then 'added a todo for' - when Todo::APPROVAL_REQUIRED then 'set you as an approver for' + when Todo::APPROVAL_REQUIRED then "set #{todo_action_subject(todo)} as an approver for" when Todo::UNMERGEABLE then 'Could not merge' - when Todo::DIRECTLY_ADDRESSED then 'directly addressed you on' + when Todo::DIRECTLY_ADDRESSED then "directly addressed #{todo_action_subject(todo)} on" end end @@ -148,6 +148,10 @@ module TodosHelper private + def todo_action_subject(todo) + todo.self_added? ? 'yourself' : 'you' + end + def show_todo_state?(todo) (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state) end diff --git a/app/models/member.rb b/app/models/member.rb index 97fba501759..7228e82e978 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -154,6 +154,11 @@ class Member < ActiveRecord::Base def add_users(source, users, access_level, current_user: nil, expires_at: nil) return [] unless users.present? + # Collect all user ids into separate array + # so we can use single sql query to get user objects + user_ids = users.select { |user| user =~ /\A\d+\Z/ } + users = users - user_ids + User.where(id: user_ids) + self.transaction do users.map do |user| add_user( diff --git a/app/models/todo.rb b/app/models/todo.rb index da3fa7277c2..b011001b235 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -84,6 +84,10 @@ class Todo < ActiveRecord::Base action == BUILD_FAILED end + def assigned? + action == ASSIGNED + end + def action_name ACTION_NAMES[action] end @@ -117,6 +121,14 @@ class Todo < ActiveRecord::Base end end + def self_added? + author == user + end + + def self_assigned? + assigned? && self_added? + end + private def keep_around_commit diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index d0c12aa57ae..38fd053ae65 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -9,7 +9,7 @@ .title-item.author-name - if todo.author - = link_to_author(todo) + = link_to_author(todo, self_added: todo.self_added?) - else (removed) @@ -22,6 +22,10 @@ - else (removed) + - if todo.self_assigned? + .title-item.action-name + to yourself + .title-item · diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index 774d20fb5ba..5e8a2a0f5d8 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -64,7 +64,7 @@ %span.remaining-days= remaining_days - if !project || can?(current_user, :read_issue, project) - .block + .block.issues .sidebar-collapsed-icon %strong = icon('hashtag', 'aria-hidden': 'true') @@ -85,7 +85,7 @@ Closed: = milestone.issues_visible_to_user(current_user).closed.count - .block + .block.merge-requests .sidebar-collapsed-icon %strong = icon('exclamation', 'aria-hidden': 'true') diff --git a/changelogs/unreleased/28020-improve-todo-list-when-comes-from-yourself.yml b/changelogs/unreleased/28020-improve-todo-list-when-comes-from-yourself.yml new file mode 100644 index 00000000000..14aecc35bd2 --- /dev/null +++ b/changelogs/unreleased/28020-improve-todo-list-when-comes-from-yourself.yml @@ -0,0 +1,4 @@ +--- +title: Improve text on todo list when the todo action comes from yourself +merge_request: 10594 +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/pages-0-4-1.yml b/changelogs/unreleased/pages-0-4-1.yml new file mode 100644 index 00000000000..fbc78a36cae --- /dev/null +++ b/changelogs/unreleased/pages-0-4-1.yml @@ -0,0 +1,4 @@ +--- +title: Use GitLab Pages v0.4.1 +merge_request: +author: diff --git a/changelogs/unreleased/zj-accept-default-branch-param.yml b/changelogs/unreleased/zj-accept-default-branch-param.yml new file mode 100644 index 00000000000..8f6fa8a6386 --- /dev/null +++ b/changelogs/unreleased/zj-accept-default-branch-param.yml @@ -0,0 +1,4 @@ +--- +title: Ensure the chat notifications service properly saves the "Notify only default branch" setting +merge_request: 10959 +author: diff --git a/config/routes.rb b/config/routes.rb index 1da226a3b57..2584981bb04 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -99,5 +99,7 @@ Rails.application.routes.draw do end end + draw :test if Rails.env.test? + get '*unmatched_route', to: 'application#route_not_found' end diff --git a/config/routes/test.rb b/config/routes/test.rb new file mode 100644 index 00000000000..ac477cdbbbc --- /dev/null +++ b/config/routes/test.rb @@ -0,0 +1,2 @@ +get '/unicorn_test/pid' => 'unicorn_test#pid' +post '/unicorn_test/kill' => 'unicorn_test#kill' diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 166a10293c3..2814c18e0b6 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -70,3 +70,27 @@ All the docs follow the same [styleguide](doc_styleguide.md). ### Markdown Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future. + +## Testing + +We try to treat documentation as code, thus have implemented some testing. +Currently, the following tests are in place: + +1. `docs:check:links`: Check that all internal (relative) links work correctly +1. `docs:check:apilint`: Check that the API docs follow some conventions + +If your contribution contains **only** documentation changes, you can speed up +the CI process by prepending to the name of your branch: `docs/`. For example, +a valid name would be `docs/update-api-issues` and it will run only the docs +tests. If the name is `docs-update-api-issues`, the whole test suite will run +(including docs). + +--- + +When you submit a merge request to GitLab Community Edition (CE), there is an +additional job called `rake ee_compat_check` that runs against Enterprise +Edition (EE) and checks if your changes can apply cleanly to the EE codebase. +If that job fails, read the instructions in the job log for what to do next. +Contributors do not need to submit their changes to EE, GitLab Inc. employees +on the other hand need to make sure that their changes apply cleanly to both +CE and EE. diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake index 2301ec9b228..99b3168d9eb 100644 --- a/lib/tasks/brakeman.rake +++ b/lib/tasks/brakeman.rake @@ -2,7 +2,7 @@ desc 'Security check via brakeman' task :brakeman do # We get 0 warnings at level 'w3' but we would like to reach 'w2'. Merge # requests are welcome! - if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb -w3 -z)) + if system(*%w(brakeman --no-progress --skip-files lib/backup/repository.rb,app/controllers/unicorn_test_controller.rb -w3 -z)) puts 'Security check succeed' else puts 'Security check failed' diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb new file mode 100644 index 00000000000..d321bfcea9d --- /dev/null +++ b/spec/controllers/oauth/authorizations_controller_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Oauth::AuthorizationsController do + let(:user) { create(:user) } + + let(:doorkeeper) do + Doorkeeper::Application.create( + name: "MyApp", + redirect_uri: 'http://example.com', + scopes: "") + end + + let(:params) do + { + response_type: "code", + client_id: doorkeeper.uid, + redirect_uri: doorkeeper.redirect_uri, + state: 'state' + } + end + + before do + sign_in(user) + end + + describe 'GET #new' do + context 'without valid params' do + it 'returns 200 code and renders error view' do + get :new + + expect(response).to have_http_status(200) + expect(response).to render_template('doorkeeper/authorizations/error') + end + end + + context 'with valid params' do + it 'returns 200 code and renders view' do + get :new, params + + expect(response).to have_http_status(200) + expect(response).to render_template('doorkeeper/authorizations/new') + end + + it 'deletes session.user_return_to and redirects when skip authorization' do + request.session['user_return_to'] = 'http://example.com' + allow(controller).to receive(:skip_authorization?).and_return(true) + + get :new, params + + expect(request.session['user_return_to']).to be_nil + expect(response).to have_http_status(302) + end + end + end +end diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb index 5e19907eef9..b4fc0edbde8 100644 --- a/spec/features/projects/milestones/milestone_spec.rb +++ b/spec/features/projects/milestones/milestone_spec.rb @@ -78,11 +78,10 @@ feature 'Project milestone', :feature do it 'shows the total MR and issue counts' do find('.milestone-sidebar .block', match: :first) - blocks = all('.milestone-sidebar .block') aggregate_failures 'MR and issue blocks' do - expect(blocks[3]).to have_content 1 - expect(blocks[4]).to have_content 0 + expect(find('.milestone-sidebar .block.issues')).to have_content 1 + expect(find('.milestone-sidebar .block.merge-requests')).to have_content 0 end end end diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb index cecb98641a6..f32e70c2c3f 100644 --- a/spec/features/todos/todos_filtering_spec.rb +++ b/spec/features/todos/todos_filtering_spec.rb @@ -45,8 +45,8 @@ describe 'Dashboard > User filters todos', feature: true, js: true do wait_for_ajax - expect(find('.todos-list')).to have_content user_1.name - expect(find('.todos-list')).not_to have_content user_2.name + expect(find('.todos-list')).to have_content 'merge request' + expect(find('.todos-list')).not_to have_content 'issue' end it "shows only authors of existing todos" do diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 50c207fb9cb..be5b3af417f 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -99,6 +99,83 @@ describe 'Dashboard Todos', feature: true do end end + context 'User created todos for themself' do + before do + login_as(user) + end + + context 'issue assigned todo' do + before do + create(:todo, :assigned, user: user, project: project, target: issue, author: user) + visit dashboard_todos_path + end + + it 'shows issue assigned to yourself message' do + page.within('.js-todos-all') do + expect(page).to have_content("You assigned issue #{issue.to_reference(full: true)} to yourself") + end + end + end + + context 'marked todo' do + before do + create(:todo, :marked, user: user, project: project, target: issue, author: user) + visit dashboard_todos_path + end + + it 'shows you added a todo message' do + page.within('.js-todos-all') do + expect(page).to have_content("You added a todo for issue #{issue.to_reference(full: true)}") + expect(page).not_to have_content('to yourself') + end + end + end + + context 'mentioned todo' do + before do + create(:todo, :mentioned, user: user, project: project, target: issue, author: user) + visit dashboard_todos_path + end + + it 'shows you mentioned yourself message' do + page.within('.js-todos-all') do + expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference(full: true)}") + expect(page).not_to have_content('to yourself') + end + end + end + + context 'directly_addressed todo' do + before do + create(:todo, :directly_addressed, user: user, project: project, target: issue, author: user) + visit dashboard_todos_path + end + + it 'shows you directly addressed yourself message' do + page.within('.js-todos-all') do + expect(page).to have_content("You directly addressed yourself on issue #{issue.to_reference(full: true)}") + expect(page).not_to have_content('to yourself') + end + end + end + + context 'approval todo' do + let(:merge_request) { create(:merge_request) } + + before do + create(:todo, :approval_required, user: user, project: project, target: merge_request, author: user) + visit dashboard_todos_path + end + + it 'shows you set yourself as an approver message' do + page.within('.js-todos-all') do + expect(page).to have_content("You set yourself as an approver for merge request #{merge_request.to_reference(full: true)}") + expect(page).not_to have_content('to yourself') + end + end + end + end + context 'User has done todos', js: true do before do create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author) diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index b0f3657d3b5..ccc3deac199 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -390,13 +390,15 @@ describe Member, models: true do %w[project group].each do |source_type| context "when source is a #{source_type}" do let!(:source) { create(source_type, :public, :access_requestable) } - let!(:user) { create(:user) } let!(:admin) { create(:admin) } + let(:user1) { create(:user) } + let(:user2) { create(:user) } it 'returns a <Source>Member objects' do - members = described_class.add_users(source, [user], :master) + members = described_class.add_users(source, [user1, user2], :master) expect(members).to be_a Array + expect(members.size).to eq(2) expect(members.first).to be_a "#{source_type.classify}Member".constantize expect(members.first).to be_persisted end diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index 581305ad39f..3f80e1ac534 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -125,4 +125,50 @@ describe Todo, models: true do expect(subject.target_reference).to eq issue.to_reference(full: true) end end + + describe '#self_added?' do + let(:user_1) { build(:user) } + + before do + subject.user = user_1 + end + + it 'is true when the user is the author' do + subject.author = user_1 + + expect(subject).to be_self_added + end + + it 'is false when the user is not the author' do + subject.author = build(:user) + + expect(subject).not_to be_self_added + end + end + + describe '#self_assigned?' do + let(:user_1) { build(:user) } + + before do + subject.user = user_1 + subject.author = user_1 + subject.action = Todo::ASSIGNED + end + + it 'is true when todo is ASSIGNED and self_added' do + expect(subject).to be_self_assigned + end + + it 'is false when the todo is not ASSIGNED' do + subject.action = Todo::MENTIONED + + expect(subject).not_to be_self_assigned + end + + it 'is false when todo is not self_added' do + subject.author = build(:user) + + expect(subject).not_to be_self_assigned + end + end end diff --git a/spec/unicorn/unicorn_spec.rb b/spec/unicorn/unicorn_spec.rb new file mode 100644 index 00000000000..8518c047a47 --- /dev/null +++ b/spec/unicorn/unicorn_spec.rb @@ -0,0 +1,98 @@ +require 'fileutils' + +require 'excon' + +require 'spec_helper' + +describe 'Unicorn' do + before(:all) do + config_lines = File.read('config/unicorn.rb.example').split("\n") + + # Remove these because they make setup harder. + config_lines = config_lines.reject do |line| + %w[ + working_directory + worker_processes + listen + pid + stderr_path + stdout_path + ].any? { |prefix| line.start_with?(prefix) } + end + + config_lines << "working_directory '#{Rails.root}'" + + # We want to have exactly 1 worker process because that makes it + # predictable which process will handle our requests. + config_lines << 'worker_processes 1' + + @socket_path = File.join(Dir.pwd, 'tmp/tests/unicorn.socket') + config_lines << "listen '#{@socket_path}'" + + ready_file = 'tmp/tests/unicorn-worker-ready' + FileUtils.rm_f(ready_file) + after_fork_index = config_lines.index { |l| l.start_with?('after_fork') } + config_lines.insert(after_fork_index + 1, "File.write('#{ready_file}', Process.pid)") + + config_path = 'tmp/tests/unicorn.rb' + File.write(config_path, config_lines.join("\n") + "\n") + + cmd = %W[unicorn -E test -c #{config_path} #{Rails.root.join('config.ru')}] + @unicorn_master_pid = spawn(*cmd) + wait_unicorn_boot!(@unicorn_master_pid, ready_file) + WebMock.allow_net_connect! + end + + %w[SIGQUIT SIGTERM SIGKILL].each do |signal| + it "has a worker that self-terminates on signal #{signal}" do + response = Excon.get('unix:///unicorn_test/pid', socket: @socket_path) + expect(response.status).to eq(200) + + worker_pid = response.body.to_i + expect(worker_pid).to be > 0 + + begin + Excon.post('unix:///unicorn_test/kill', socket: @socket_path, body: "signal=#{signal}") + rescue Excon::Error::Socket + # The connection may be closed abruptly + end + + expect(pid_gone?(worker_pid)).to eq(true) + end + end + + after(:all) do + WebMock.disable_net_connect!(allow_localhost: true) + Process.kill('TERM', @unicorn_master_pid) + end + + def wait_unicorn_boot!(master_pid, ready_file) + # Unicorn should boot in under 60 seconds so 120 seconds seems like a good timeout. + timeout = 120 + timeout.times do + return if File.exist?(ready_file) + pid = Process.waitpid(master_pid, Process::WNOHANG) + raise "unicorn failed to boot: #{$?}" unless pid.nil? + + sleep 1 + end + + raise "unicorn boot timed out after #{timeout} seconds" + end + + def pid_gone?(pid) + # Worker termination should take less than a second. That makes 10 + # seconds a generous timeout. + 10.times do + begin + Process.kill(0, pid) + rescue Errno::ESRCH + return true + end + + sleep 1 + end + + false + end +end |