// Copyright (C) 2010 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.gerrit.common; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.gerrit.common.data.ContributorAgreement; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; import com.google.gerrit.extensions.events.LifecycleListener; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.lifecycle.LifecycleModule; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.config.AnonymousCowardName; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.data.ApprovalAttribute; import com.google.gerrit.server.events.ChangeAbandonedEvent; import com.google.gerrit.server.events.ChangeEvent; import com.google.gerrit.server.events.ChangeMergedEvent; import com.google.gerrit.server.events.ChangeRestoredEvent; import com.google.gerrit.server.events.CommentAddedEvent; import com.google.gerrit.server.events.DraftPublishedEvent; import com.google.gerrit.server.events.EventFactory; import com.google.gerrit.server.events.MergeFailedEvent; import com.google.gerrit.server.events.PatchSetCreatedEvent; import com.google.gerrit.server.events.RefUpdatedEvent; import com.google.gerrit.server.events.ReviewerAddedEvent; import com.google.gerrit.server.events.TopicChangedEvent; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectState; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Singleton; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** Spawns local executables when a hook action occurs. */ @Singleton public class ChangeHookRunner implements ChangeHooks, LifecycleListener { /** A logger for this class. */ private static final Logger log = LoggerFactory.getLogger(ChangeHookRunner.class); public static class Module extends LifecycleModule { @Override protected void configure() { bind(ChangeHookRunner.class); bind(ChangeHooks.class).to(ChangeHookRunner.class); listener().to(ChangeHookRunner.class); } } private static class ChangeListenerHolder { final ChangeListener listener; final IdentifiedUser user; ChangeListenerHolder(ChangeListener l, IdentifiedUser u) { listener = l; user = u; } } /** Container class used to hold the return code and output of script hook execution */ public static class HookResult { private int exitValue = -1; private String output; private String executionError; private HookResult(int exitValue, String output) { this.exitValue = exitValue; this.output = output; } private HookResult(String output, String executionError) { this.output = output; this.executionError = executionError; } public int getExitValue() { return exitValue; } public void setExitValue(int exitValue) { this.exitValue = exitValue; } public String getOutput() { return output; } public String toString() { StringBuilder sb = new StringBuilder(); if (output != null && output.length() != 0) { sb.append(output); if (executionError != null) { sb.append(" - "); } } if (executionError != null ) { sb.append(executionError); } return sb.toString(); } } /** Listeners to receive changes as they happen (limited by visibility * of holder's user). */ private final Map<ChangeListener, ChangeListenerHolder> listeners = new ConcurrentHashMap<>(); /** Listeners to receive all changes as they happen. */ private final DynamicSet<ChangeListener> unrestrictedListeners; /** Filename of the new patchset hook. */ private final File patchsetCreatedHook; /** Filename of the draft published hook. */ private final File draftPublishedHook; /** Filename of the new comments hook. */ private final File commentAddedHook; /** Filename of the change merged hook. */ private final File changeMergedHook; /** Filename of the merge failed hook. */ private final File mergeFailedHook; /** Filename of the change abandoned hook. */ private final File changeAbandonedHook; /** Filename of the change restored hook. */ private final File changeRestoredHook; /** Filename of the ref updated hook. */ private final File refUpdatedHook; /** Filename of the reviewer added hook. */ private final File reviewerAddedHook; /** Filename of the topic changed hook. */ private final File topicChangedHook; /** Filename of the cla signed hook. */ private final File claSignedHook; /** Filename of the update hook. */ private final File refUpdateHook; private final String anonymousCowardName; /** Repository Manager. */ private final GitRepositoryManager repoManager; /** Queue of hooks that need to run. */ private final WorkQueue.Executor hookQueue; private final ProjectCache projectCache; private final AccountCache accountCache; private final EventFactory eventFactory; private final SitePaths sitePaths; /** Thread pool used to monitor sync hooks */ private final ExecutorService syncHookThreadPool; /** Timeout value for synchronous hooks */ private final int syncHookTimeout; /** * Create a new ChangeHookRunner. * * @param queue Queue to use when processing hooks. * @param repoManager The repository manager. * @param config Config file to use. * @param sitePath The sitepath of this gerrit install. * @param projectCache the project cache instance for the server. */ @Inject public ChangeHookRunner(final WorkQueue queue, final GitRepositoryManager repoManager, final @GerritServerConfig Config config, final @AnonymousCowardName String anonymousCowardName, final SitePaths sitePath, final ProjectCache projectCache, final AccountCache accountCache, final EventFactory eventFactory, final SitePaths sitePaths, final DynamicSet<ChangeListener> unrestrictedListeners) { this.anonymousCowardName = anonymousCowardName; this.repoManager = repoManager; this.hookQueue = queue.createQueue(1, "hook"); this.projectCache = projectCache; this.accountCache = accountCache; this.eventFactory = eventFactory; this.sitePaths = sitePath; this.unrestrictedListeners = unrestrictedListeners; final File hooksPath = sitePath.resolve(getValue(config, "hooks", "path", sitePath.hooks_dir.getAbsolutePath())); patchsetCreatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "patchsetCreatedHook", "patchset-created")).getPath()); draftPublishedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "draftPublishedHook", "draft-published")).getPath()); commentAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "commentAddedHook", "comment-added")).getPath()); changeMergedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeMergedHook", "change-merged")).getPath()); mergeFailedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "mergeFailed", "merge-failed")).getPath()); changeAbandonedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeAbandonedHook", "change-abandoned")).getPath()); changeRestoredHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeRestoredHook", "change-restored")).getPath()); refUpdatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdatedHook", "ref-updated")).getPath()); reviewerAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "reviewerAddedHook", "reviewer-added")).getPath()); topicChangedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "topicChangedHook", "topic-changed")).getPath()); claSignedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "claSignedHook", "cla-signed")).getPath()); refUpdateHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdateHook", "ref-update")).getPath()); syncHookTimeout = config.getInt("hooks", "syncHookTimeout", 30); syncHookThreadPool = Executors.newCachedThreadPool( new ThreadFactoryBuilder() .setNameFormat("SyncHook-%d") .build()); } public void addChangeListener(ChangeListener listener, IdentifiedUser user) { listeners.put(listener, new ChangeListenerHolder(listener, user)); } public void removeChangeListener(ChangeListener listener) { listeners.remove(listener); } /** * Helper Method for getting values from the config. * * @param config Config file to get value from. * @param section Section to look in. * @param setting Setting to get. * @param fallback Fallback value. * @return Setting value if found, else fallback. */ private String getValue(final Config config, final String section, final String setting, final String fallback) { final String result = config.getString(section, null, setting); return (result == null) ? fallback : result; } /** * Get the Repository for the given project name, or null on error. * * @param name Project to get repo for, * @return Repository or null. */ private Repository openRepository(final Project.NameKey name) { try { return repoManager.openRepository(name); } catch (IOException err) { log.warn("Cannot open repository " + name.get(), err); return null; } } private void addArg(List<String> args, String name, String value) { if (value != null) { args.add(name); args.add(value); } } /** * Fire the update hook * */ public HookResult doRefUpdateHook(final Project project, final String refname, final Account uploader, final ObjectId oldId, final ObjectId newId) { final List<String> args = new ArrayList<>(); addArg(args, "--project", project.getName()); addArg(args, "--refname", refname); addArg(args, "--uploader", getDisplayName(uploader)); addArg(args, "--oldrev", oldId.getName()); addArg(args, "--newrev", newId.getName()); HookResult hookResult; try { hookResult = runSyncHook(project.getNameKey(), refUpdateHook, args); } catch (TimeoutException e) { hookResult = new HookResult(-1, "Synchronous hook timed out"); } return hookResult; } /** * Fire the Patchset Created Hook. * * @param change The change itself. * @param patchSet The Patchset that was created. * @throws OrmException */ public void doPatchsetCreatedHook(final Change change, final PatchSet patchSet, final ReviewDb db) throws OrmException { final PatchSetCreatedEvent event = new PatchSetCreatedEvent(); final AccountState uploader = accountCache.get(patchSet.getUploader()); final AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.uploader = eventFactory.asAccountAttribute(uploader.getAccount()); fireEvent(change, event, db); final List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--is-draft", String.valueOf(patchSet.isDraft())); addArg(args, "--kind", String.valueOf(event.patchSet.kind)); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--uploader", getDisplayName(uploader.getAccount())); addArg(args, "--commit", event.patchSet.revision); addArg(args, "--patchset", event.patchSet.number); runHook(change.getProject(), patchsetCreatedHook, args); } public void doDraftPublishedHook(final Change change, final PatchSet patchSet, final ReviewDb db) throws OrmException { final DraftPublishedEvent event = new DraftPublishedEvent(); final AccountState uploader = accountCache.get(patchSet.getUploader()); final AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.uploader = eventFactory.asAccountAttribute(uploader.getAccount()); fireEvent(change, event, db); final List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--uploader", getDisplayName(uploader.getAccount())); addArg(args, "--commit", event.patchSet.revision); addArg(args, "--patchset", event.patchSet.number); runHook(change.getProject(), draftPublishedHook, args); } public void doCommentAddedHook(final Change change, final Account account, final PatchSet patchSet, final String comment, final Map<String, Short> approvals, final ReviewDb db) throws OrmException { final CommentAddedEvent event = new CommentAddedEvent(); final AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.author = eventFactory.asAccountAttribute(account); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.comment = comment; LabelTypes labelTypes = projectCache.get(change.getProject()).getLabelTypes(); if (approvals.size() > 0) { event.approvals = new ApprovalAttribute[approvals.size()]; int i = 0; for (Map.Entry<String, Short> approval : approvals.entrySet()) { event.approvals[i++] = getApprovalAttribute(labelTypes, approval); } } fireEvent(change, event, db); final List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--is-draft", patchSet.isDraft() ? "true" : "false"); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--author", getDisplayName(account)); addArg(args, "--commit", event.patchSet.revision); addArg(args, "--comment", comment == null ? "" : comment); for (Map.Entry<String, Short> approval : approvals.entrySet()) { LabelType lt = labelTypes.byLabel(approval.getKey()); if (lt != null) { addArg(args, "--" + lt.getName(), Short.toString(approval.getValue())); } } runHook(change.getProject(), commentAddedHook, args); } public void doChangeMergedHook(final Change change, final Account account, final PatchSet patchSet, final ReviewDb db) throws OrmException { final ChangeMergedEvent event = new ChangeMergedEvent(); final AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.submitter = eventFactory.asAccountAttribute(account); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); fireEvent(change, event, db); final List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--submitter", getDisplayName(account)); addArg(args, "--commit", event.patchSet.revision); runHook(change.getProject(), changeMergedHook, args); } public void doMergeFailedHook(final Change change, final Account account, final PatchSet patchSet, final String reason, final ReviewDb db) throws OrmException { final MergeFailedEvent event = new MergeFailedEvent(); final AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.submitter = eventFactory.asAccountAttribute(account); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.reason = reason; fireEvent(change, event, db); final List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--submitter", getDisplayName(account)); addArg(args, "--commit", event.patchSet.revision); addArg(args, "--reason", reason == null ? "" : reason); runHook(change.getProject(), mergeFailedHook, args); } public void doChangeAbandonedHook(final Change change, final Account account, final PatchSet patchSet, final String reason, final ReviewDb db) throws OrmException { final ChangeAbandonedEvent event = new ChangeAbandonedEvent(); final AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.abandoner = eventFactory.asAccountAttribute(account); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.reason = reason; fireEvent(change, event, db); final List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--abandoner", getDisplayName(account)); addArg(args, "--commit", event.patchSet.revision); addArg(args, "--reason", reason == null ? "" : reason); runHook(change.getProject(), changeAbandonedHook, args); } public void doChangeRestoredHook(final Change change, final Account account, final PatchSet patchSet, final String reason, final ReviewDb db) throws OrmException { final ChangeRestoredEvent event = new ChangeRestoredEvent(); final AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.restorer = eventFactory.asAccountAttribute(account); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.reason = reason; fireEvent(change, event, db); final List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--topic", event.change.topic); addArg(args, "--restorer", getDisplayName(account)); addArg(args, "--commit", event.patchSet.revision); addArg(args, "--reason", reason == null ? "" : reason); runHook(change.getProject(), changeRestoredHook, args); } public void doRefUpdatedHook(final Branch.NameKey refName, final RefUpdate refUpdate, final Account account) { doRefUpdatedHook(refName, refUpdate.getOldObjectId(), refUpdate.getNewObjectId(), account); } public void doRefUpdatedHook(final Branch.NameKey refName, final ObjectId oldId, final ObjectId newId, final Account account) { final RefUpdatedEvent event = new RefUpdatedEvent(); if (account != null) { event.submitter = eventFactory.asAccountAttribute(account); } event.refUpdate = eventFactory.asRefUpdateAttribute(oldId, newId, refName); fireEvent(refName, event); final List<String> args = new ArrayList<>(); addArg(args, "--oldrev", event.refUpdate.oldRev); addArg(args, "--newrev", event.refUpdate.newRev); addArg(args, "--refname", event.refUpdate.refName); addArg(args, "--project", event.refUpdate.project); if (account != null) { addArg(args, "--submitter", getDisplayName(account)); } runHook(refName.getParentKey(), refUpdatedHook, args); } public void doReviewerAddedHook(final Change change, final Account account, final PatchSet patchSet, final ReviewDb db) throws OrmException { final ReviewerAddedEvent event = new ReviewerAddedEvent(); final AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.patchSet = eventFactory.asPatchSetAttribute(patchSet); event.reviewer = eventFactory.asAccountAttribute(account); fireEvent(change, event, db); final List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-url", event.change.url); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--reviewer", getDisplayName(account)); runHook(change.getProject(), reviewerAddedHook, args); } public void doTopicChangedHook(final Change change, final Account account, final String oldTopic, final ReviewDb db) throws OrmException { final TopicChangedEvent event = new TopicChangedEvent(); final AccountState owner = accountCache.get(change.getOwner()); event.change = eventFactory.asChangeAttribute(change); event.changer = eventFactory.asAccountAttribute(account); event.oldTopic = oldTopic; fireEvent(change, event, db); final List<String> args = new ArrayList<>(); addArg(args, "--change", event.change.id); addArg(args, "--change-owner", getDisplayName(owner.getAccount())); addArg(args, "--project", event.change.project); addArg(args, "--branch", event.change.branch); addArg(args, "--changer", getDisplayName(account)); addArg(args, "--old-topic", oldTopic); addArg(args, "--new-topic", event.change.topic); runHook(change.getProject(), topicChangedHook, args); } public void doClaSignupHook(Account account, ContributorAgreement cla) { if (account != null) { final List<String> args = new ArrayList<>(); addArg(args, "--submitter", getDisplayName(account)); addArg(args, "--user-id", account.getId().toString()); addArg(args, "--cla-name", cla.getName()); runHook(claSignedHook, args); } } @Override public void postEvent(final Change change, final ChangeEvent event, final ReviewDb db) throws OrmException { fireEvent(change, event, db); } @Override public void postEvent(final Branch.NameKey branchName, final ChangeEvent event) { fireEvent(branchName, event); } private void fireEventForUnrestrictedListeners(final ChangeEvent event) { for (ChangeListener listener : unrestrictedListeners) { listener.onChangeEvent(event); } } private void fireEvent(final Change change, final ChangeEvent event, final ReviewDb db) throws OrmException { for (ChangeListenerHolder holder : listeners.values()) { if (isVisibleTo(change, holder.user, db)) { holder.listener.onChangeEvent(event); } } fireEventForUnrestrictedListeners( event ); } private void fireEvent(Branch.NameKey branchName, final ChangeEvent event) { for (ChangeListenerHolder holder : listeners.values()) { if (isVisibleTo(branchName, holder.user)) { holder.listener.onChangeEvent(event); } } fireEventForUnrestrictedListeners( event ); } private boolean isVisibleTo(Change change, IdentifiedUser user, ReviewDb db) throws OrmException { final ProjectState pe = projectCache.get(change.getProject()); if (pe == null) { return false; } final ProjectControl pc = pe.controlFor(user); return pc.controlFor(change).isVisible(db); } private boolean isVisibleTo(Branch.NameKey branchName, IdentifiedUser user) { final ProjectState pe = projectCache.get(branchName.getParentKey()); if (pe == null) { return false; } final ProjectControl pc = pe.controlFor(user); return pc.controlForRef(branchName).isVisible(); } /** * Create an ApprovalAttribute for the given approval suitable for serialization to JSON. * @param approval * @return object suitable for serialization to JSON */ private ApprovalAttribute getApprovalAttribute(LabelTypes labelTypes, Entry<String, Short> approval) { ApprovalAttribute a = new ApprovalAttribute(); a.type = approval.getKey(); LabelType lt = labelTypes.byLabel(approval.getKey()); if (lt != null) { a.description = lt.getName(); } a.value = Short.toString(approval.getValue()); return a; } /** * Get the display name for the given account. * * @param account Account to get name for. * @return Name for this account. */ private String getDisplayName(final Account account) { if (account != null) { String result = (account.getFullName() == null) ? anonymousCowardName : account.getFullName(); if (account.getPreferredEmail() != null) { result += " (" + account.getPreferredEmail() + ")"; } return result; } return anonymousCowardName; } /** * Run a hook. * * @param project used to open repository to run the hook for. * @param hook the hook to execute. * @param args Arguments to use to run the hook. */ private synchronized void runHook(Project.NameKey project, File hook, List<String> args) { if (project != null && hook.exists()) { hookQueue.execute(new AsyncHookTask(project, hook, args)); } } private synchronized void runHook(File hook, List<String> args) { if (hook.exists()) { hookQueue.execute(new AsyncHookTask(null, hook, args)); } } private HookResult runSyncHook(Project.NameKey project, File hook, List<String> args) throws TimeoutException { if (!hook.exists()) { return null; } SyncHookTask syncHook = new SyncHookTask(project, hook, args); FutureTask<HookResult> task = new FutureTask<>(syncHook); syncHookThreadPool.execute(task); String message; try { return task.get(syncHookTimeout, TimeUnit.SECONDS); } catch (TimeoutException e) { message = "Synchronous hook timed out " + hook.getAbsolutePath(); log.error(message); } catch (Exception e) { message = "Error running hook " + hook.getAbsolutePath(); log.error(message, e); } task.cancel(true); syncHook.cancel(); return new HookResult(syncHook.getOutput(), message); } @Override public void start() { } @Override public void stop() { syncHookThreadPool.shutdown(); boolean isTerminated; do { try { isTerminated = syncHookThreadPool.awaitTermination(10, TimeUnit.SECONDS); } catch (InterruptedException ie) { isTerminated = false; } } while (!isTerminated); } private class HookTask { private final Project.NameKey project; private final File hook; private final List<String> args; private StringWriter output; private Process ps; protected HookTask(Project.NameKey project, File hook, List<String> args) { this.project = project; this.hook = hook; this.args = args; } public String getOutput() { return output != null ? output.toString() : null; } protected HookResult runHook() { Repository repo = null; HookResult result = null; try { final List<String> argv = new ArrayList<>(1 + args.size()); argv.add(hook.getAbsolutePath()); argv.addAll(args); final ProcessBuilder pb = new ProcessBuilder(argv); pb.redirectErrorStream(true); if (project != null) { repo = openRepository(project); } final Map<String, String> env = pb.environment(); env.put("GERRIT_SITE", sitePaths.site_path.getAbsolutePath()); if (repo != null) { pb.directory(repo.getDirectory()); env.put("GIT_DIR", repo.getDirectory().getAbsolutePath()); } ps = pb.start(); ps.getOutputStream().close(); InputStream is = ps.getInputStream(); String output = null; try { output = readOutput(is); } finally { try { is.close(); } catch (IOException closeErr) { } ps.waitFor(); result = new HookResult(ps.exitValue(), output); } } catch (InterruptedException iex) { // InterruptedExeception - timeout or cancel } catch (Throwable err) { log.error("Error running hook " + hook.getAbsolutePath(), err); } finally { if (repo != null) { repo.close(); } } if (result != null) { final int exitValue = result.getExitValue(); if (exitValue == 0) { log.debug("hook[" + getName() + "] exitValue:" + exitValue); } else { log.info("hook[" + getName() + "] exitValue:" + exitValue); } BufferedReader br = new BufferedReader(new StringReader(result.getOutput())); try { String line; while ((line = br.readLine()) != null) { log.info("hook[" + getName() + "] output: " + line); } } catch(IOException iox) { log.error("Error writing hook output", iox); } } return result; } private String readOutput(InputStream is) throws IOException { output = new StringWriter(); InputStreamReader input = new InputStreamReader(is); char[] buffer = new char[4096]; int n; while ((n = input.read(buffer)) != -1) { output.write(buffer, 0, n); } return output.toString(); } protected String getName() { return hook.getName(); } @Override public String toString() { return "hook " + hook.getName(); } public void cancel() { ps.destroy(); } } /** Callable type used to run synchronous hooks */ private final class SyncHookTask extends HookTask implements Callable<HookResult> { private SyncHookTask(Project.NameKey project, File hook, List<String> args) { super(project, hook, args); } @Override public HookResult call() throws Exception { return super.runHook(); } } /** Runnable type used to run asynchronous hooks */ private final class AsyncHookTask extends HookTask implements Runnable { private AsyncHookTask(Project.NameKey project, File hook, List<String> args) { super(project, hook, args); } @Override public void run() { super.runHook(); } } }