diff --git a/.gitignore b/.gitignore index 5f45163..1811cfd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ work .classpath .settings .project -.DS_Store \ No newline at end of file +.DS_Store + +# Coverity +/cov-int diff --git a/pom.xml b/pom.xml index 8e2dc80..c09274b 100644 --- a/pom.xml +++ b/pom.xml @@ -120,7 +120,13 @@ commons-codec 1.4 - + + com.google.code.findbugs + jsr305 + 2.0.1 + jar + + diff --git a/src/main/java/hudson/plugins/perforce/PerforceSCM.java b/src/main/java/hudson/plugins/perforce/PerforceSCM.java index 55f1c32..200bf97 100644 --- a/src/main/java/hudson/plugins/perforce/PerforceSCM.java +++ b/src/main/java/hudson/plugins/perforce/PerforceSCM.java @@ -16,7 +16,6 @@ import hudson.Extension; import hudson.FilePath; import hudson.Util; -import hudson.FilePath.FileCallable; import hudson.Launcher; import static hudson.Util.fixNull; import hudson.matrix.MatrixBuild; @@ -28,7 +27,6 @@ import hudson.plugins.perforce.config.WorkspaceCleanupConfig; import hudson.plugins.perforce.utils.MacroStringHelper; import hudson.plugins.perforce.utils.ParameterSubstitutionException; -import hudson.remoting.VirtualChannel; import hudson.scm.ChangeLogParser; import hudson.scm.PollingResult; import hudson.scm.SCM; @@ -36,7 +34,6 @@ import hudson.scm.SCMRevisionState; import hudson.slaves.EnvironmentVariablesNodeProperty; import hudson.slaves.NodeProperty; -import hudson.tasks.BuildTrigger; import hudson.tasks.Messages; import hudson.util.FormValidation; import hudson.util.LogTaskListener; @@ -56,7 +53,6 @@ import java.io.PrintWriter; import java.io.Serializable; import java.io.StringWriter; -import java.net.InetAddress; import java.util.*; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; @@ -64,6 +60,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; /** * Extends {@link SCM} to provide integration with Perforce SCM repositories. @@ -450,29 +448,23 @@ public static PerforceSCMDescriptor getInstance() { * @param node the value of node * @exception ParameterSubstitutionException */ - protected Depot getDepot(Launcher launcher, FilePath workspace, AbstractProject project, AbstractBuild build, Node node) - throws ParameterSubstitutionException - { - + @Nonnull + protected Depot getDepot(@Nonnull Launcher launcher, @Nonnull FilePath workspace, + @CheckForNull AbstractProject project, + @CheckForNull AbstractBuild build, @CheckForNull Node node) + throws ParameterSubstitutionException, InterruptedException { HudsonP4ExecutorFactory p4Factory = new HudsonP4ExecutorFactory(launcher,workspace); Depot depot = new Depot(p4Factory); - if (build != null) { - depot.setClient(MacroStringHelper.substituteParameters(p4Client, build, null)); - depot.setUser(MacroStringHelper.substituteParameters(p4User, build, null)); - depot.setPort(MacroStringHelper.substituteParameters(p4Port, build, null)); + depot.setClient(MacroStringHelper.substituteParameters(p4Client, this, build, project, node, null)); + depot.setUser(MacroStringHelper.substituteParameters(p4User, this, build, project, node, null)); + depot.setPort(MacroStringHelper.substituteParameters(p4Port, this, build, project, node, null)); + + if (build != null) { // We can retrieve all parameters from the build's environment depot.setPassword(getDecryptedP4Passwd(build)); - } else if (project != null) { - depot.setClient(MacroStringHelper.substituteParameters(p4Client, getDefaultSubstitutions(project))); - depot.setUser(MacroStringHelper.substituteParameters(p4User, getDefaultSubstitutions(project))); - depot.setPort(MacroStringHelper.substituteParameters(p4Port, getDefaultSubstitutions(project))); - depot.setPassword(getDecryptedP4Passwd(project)); - } else { - depot.setClient(p4Client); - depot.setUser(p4User); - depot.setPort(p4Port); - depot.setPassword(getDecryptedP4Passwd()); + } else { // project can be null + depot.setPassword(project != null ? getDecryptedP4Passwd(project, node) : getDecryptedP4Passwd()); } if (p4Ticket != null && !p4Ticket.equals("")) @@ -513,8 +505,7 @@ protected Depot getDepot(Launcher launcher, FilePath workspace, AbstractProject return depot; } - - + /** * Override of SCM.buildEnvVars() in order to setup the last change we have * sync'd to as a Hudson @@ -524,32 +515,29 @@ protected Depot getDepot(Launcher launcher, FilePath workspace, AbstractProject * @param env */ @Override - public void buildEnvVars(AbstractBuild build, Map env) { + public void buildEnvVars(@Nonnull AbstractBuild build, @Nonnull Map env) { super.buildEnvVars(build, env); try { - env.put("P4PORT", MacroStringHelper.substituteParameters(p4Port, build, env)); - env.put("P4USER", MacroStringHelper.substituteParameters(p4User, build, env)); + env.put("P4PORT", MacroStringHelper.substituteParameters(p4Port, this, build, env)); + env.put("P4USER", MacroStringHelper.substituteParameters(p4User, this, build, env)); + + // if we want to allow p4 commands in script steps this helps + if (isExposeP4Passwd()) { + PerforcePasswordEncryptor encryptor = new PerforcePasswordEncryptor(); + env.put("P4PASSWD", encryptor.decryptString(p4Passwd)); + } + // this may help when tickets are used since we are + // not storing the ticket on the client during login + if (p4Ticket != null) { + env.put("P4TICKET", p4Ticket); + } + + env.put("P4CLIENT", getConcurrentClientName(build.getWorkspace(), getEffectiveClientName(build, env))); } catch (ParameterSubstitutionException ex) { - LOGGER.log(MacroStringHelper.SUBSTITUTION_ERROR_LEVEL, "Can't substitute P4USER or P4PORT", ex); - //TODO: exit? - } - - // if we want to allow p4 commands in script steps this helps - if (isExposeP4Passwd()) { - PerforcePasswordEncryptor encryptor = new PerforcePasswordEncryptor(); - env.put("P4PASSWD", encryptor.decryptString(p4Passwd)); - } - // this may help when tickets are used since we are - // not storing the ticket on the client during login - if (p4Ticket != null) { - env.put("P4TICKET", p4Ticket); - } - - try { - env.put("P4CLIENT", getConcurrentClientName(build.getWorkspace(), getEffectiveClientName(build, env))); - } catch(ParameterSubstitutionException ex) { - LOGGER.log(MacroStringHelper.SUBSTITUTION_ERROR_LEVEL, "Can't substitute P4CLIENT",ex); + LOGGER.log(MacroStringHelper.SUBSTITUTION_ERROR_LEVEL, "Cannot build environent variables due to unresolved macros", ex); //TODO: exit? + } catch (InterruptedException ex) { + LOGGER.log(MacroStringHelper.SUBSTITUTION_ERROR_LEVEL, "Cannot build environment vars. The method has been interrupted"); } PerforceTagAction pta = build.getAction(PerforceTagAction.class); @@ -574,14 +562,17 @@ public void buildEnvVars(AbstractBuild build, Map env) { * @param tool the p4 tool installation name * @return path to p4 tool path or an empty string if none is found */ - public String getP4Executable(String tool) { + @Nonnull + public String getP4Executable(@CheckForNull String tool) { PerforceToolInstallation toolInstallation = getP4Tool(tool); if (toolInstallation == null) return "p4"; return toolInstallation.getP4Exe(); } - public String getP4Executable(String tool, Node node, TaskListener listener) { + @Nonnull + public String getP4Executable(@CheckForNull String tool, + @Nonnull Node node, @Nonnull TaskListener listener) { PerforceToolInstallation toolInstallation = getP4Tool(tool); if (toolInstallation == null) return "p4"; @@ -602,7 +593,8 @@ public String getP4Executable(String tool, Node node, TaskListener listener) { * @param tool the p4 tool installation name * @return path to p4 tool installation or null */ - public PerforceToolInstallation getP4Tool(String tool) { + @CheckForNull + public PerforceToolInstallation getP4Tool(@CheckForNull String tool) { PerforceToolInstallation[] installations = ((hudson.plugins.perforce.PerforceToolInstallation.DescriptorImpl)Hudson.getInstance(). getDescriptorByType(PerforceToolInstallation.DescriptorImpl.class)).getInstallations(); for (PerforceToolInstallation i : installations) { @@ -658,64 +650,38 @@ public Object readResolve() { return this; } - private Hashtable getDefaultSubstitutions(AbstractProject project) { - Hashtable subst = new Hashtable(); - subst.put("JOB_NAME", MacroStringHelper.getSafeJobName(project)); - for (NodeProperty nodeProperty: Hudson.getInstance().getGlobalNodeProperties()) { - if (nodeProperty instanceof EnvironmentVariablesNodeProperty) { - subst.putAll(((EnvironmentVariablesNodeProperty)nodeProperty).getEnvVars()); - } - } - ParametersDefinitionProperty pdp = (ParametersDefinitionProperty) project.getProperty(hudson.model.ParametersDefinitionProperty.class); - if (pdp != null) { - for (ParameterDefinition pd : pdp.getParameterDefinitions()) { - try { - ParameterValue defaultValue = pd.getDefaultParameterValue(); - if (defaultValue != null) { - String name = defaultValue.getName(); - String value = defaultValue.createVariableResolver(null).resolve(name); - subst.put(name, value); - } - } catch (Exception e) { - } - } - } - subst.put("P4USER", MacroStringHelper.substituteParametersNoCheck(p4User, subst)); - return subst; - } - - private String getEffectiveProjectPath(AbstractBuild build, AbstractProject project, PrintStream log, Depot depot) - throws PerforceException, ParameterSubstitutionException { - String projectPath; - if (useClientSpec) { - projectPath = getEffectiveProjectPathFromFile(build, project, log, depot); - } else if (build != null) { - projectPath = MacroStringHelper.substituteParameters(this.projectPath, build, null); - } else { - projectPath = MacroStringHelper.substituteParameters(this.projectPath, getDefaultSubstitutions(project)); - } - return projectPath; - } - - private String getEffectiveProjectPathFromFile(AbstractBuild build, AbstractProject project, PrintStream log, Depot depot) throws PerforceException, ParameterSubstitutionException { - String clientSpec; - if (build != null) { - clientSpec = MacroStringHelper.substituteParameters(this.clientSpec, build, null); - } else { - clientSpec = MacroStringHelper.substituteParametersNoCheck(this.clientSpec, getDefaultSubstitutions(project)); - } - log.println("Read ClientSpec from: " + clientSpec); - com.tek42.perforce.parse.File f = depot.getFile(clientSpec); - String projectPath = f.read(); - if (build != null) { - projectPath = MacroStringHelper.substituteParameters(projectPath, build, null); - } else { - projectPath = MacroStringHelper.substituteParametersNoCheck(projectPath, getDefaultSubstitutions(project)); - } - return projectPath; - } - - private int getLastBuildChangeset(AbstractProject project) { + @Nonnull + private String getEffectiveProjectPath( + @CheckForNull AbstractBuild build, + @Nonnull AbstractProject project, + @CheckForNull Node node, + @Nonnull PrintStream log, + @Nonnull Depot depot) + throws PerforceException, ParameterSubstitutionException, InterruptedException { + String effectiveProjectPath = useClientSpec + ? getEffectiveProjectPathFromFile(build, project, node, log, depot) + : MacroStringHelper.substituteParameters(this.projectPath, this, build, project, node, null); + return effectiveProjectPath; + } + + @Nonnull + private String getEffectiveProjectPathFromFile( + @CheckForNull AbstractBuild build, + @CheckForNull AbstractProject project, + @CheckForNull Node node, + @Nonnull PrintStream log, @Nonnull Depot depot) + throws PerforceException, ParameterSubstitutionException, InterruptedException { + String effectiveClientSpec = + MacroStringHelper.substituteParameters(this.clientSpec, this, build, project, node, null); + log.println("Read ClientSpec from: " + effectiveClientSpec); + com.tek42.perforce.parse.File f = depot.getFile(effectiveClientSpec); + String effectiveProjectPath = + MacroStringHelper.substituteParameters(f.read(), this, build, project, node, null); + + return effectiveProjectPath; + } + + private int getLastBuildChangeset(@Nonnull AbstractProject project) { Run lastBuild = project.getLastBuild(); return getLastChange(lastBuild); } @@ -730,11 +696,14 @@ private int getLastBuildChangeset(AbstractProject project) { * @throws IOException * @throws InterruptedException */ - private String getLocalPathName(FilePath path, boolean isUnix) throws IOException, InterruptedException { + @Nonnull + private String getLocalPathName(@Nonnull FilePath path, boolean isUnix) + throws IOException, InterruptedException { return processPathName(path.getRemote(), isUnix); } - public static String processPathName(String path, boolean isUnix) { + @Nonnull + public static String processPathName(@Nonnull String path, boolean isUnix) { String pathName = path; pathName = pathName.replaceAll("/\\./", "/"); pathName = pathName.replaceAll("\\\\\\.\\\\", "\\\\"); @@ -752,7 +721,8 @@ public static String processPathName(String path, boolean isUnix) { return pathName; } - private static void retrieveUserInformation(Depot depot, List changes) throws PerforceException { + private static void retrieveUserInformation(@Nonnull Depot depot, + @Nonnull List changes) throws PerforceException { // uniqify in order to reduce number of calls to P4. HashSet users = new HashSet(); for (Changelist change : changes) { @@ -813,12 +783,10 @@ public static boolean isFileInView(String filename, String projectPath, boolean private static class WipeWorkspaceExcludeFilter implements FileFilter, Serializable { - private List excluded = new ArrayList(); + private final List excluded = new ArrayList(); public WipeWorkspaceExcludeFilter(String... args) { - for (String arg : args) { - excluded.add(arg); - } + excluded.addAll(Arrays.asList(args)); } public void exclude(String arg) { @@ -835,7 +803,8 @@ public boolean accept(File arg0) { } } - private static boolean overrideWithBooleanParameter(String paramName, AbstractBuild build, boolean dflt) { + private static boolean overrideWithBooleanParameter(String paramName, + @Nonnull AbstractBuild build, boolean dflt) { if (build.getBuildVariables() != null) { Object param = build.getBuildVariables().get(paramName); if (param != null) { @@ -856,13 +825,13 @@ public boolean checkout(AbstractBuild build, Launcher launcher, PrintStream log = listener.getLogger(); changelogFilename = changelogFile.getAbsolutePath(); // HACK: Force build env vars to initialize - MacroStringHelper.substituteParameters("", build, null); + MacroStringHelper.substituteParameters("", this, build, null); // Use local variables so that substitutions are not saved - String p4Label = MacroStringHelper.substituteParameters(this.p4Label, build, null); - String viewMask = MacroStringHelper.substituteParameters(this.viewMask, build, null); + String p4Label = MacroStringHelper.substituteParameters(this.p4Label, this, build, null); + String viewMask = MacroStringHelper.substituteParameters(this.viewMask, this, build, null); Depot depot = getDepot(launcher,workspace, build.getProject(), build, build.getBuiltOn()); - String p4Stream = MacroStringHelper.substituteParameters(this.p4Stream, build, null); + String p4Stream = MacroStringHelper.substituteParameters(this.p4Stream, this, build, null); // Pull from optional named parameters boolean wipeBeforeBuild = overrideWithBooleanParameter( @@ -892,10 +861,10 @@ public boolean checkout(AbstractBuild build, Launcher launcher, try { // keep projectPath local so any modifications for slaves don't get saved - String projectPath; - projectPath = getEffectiveProjectPath(build, build.getProject(), log, depot); + String effectiveProjectPath= getEffectiveProjectPath(build, + build.getProject(), build.getBuiltOn(), log, depot); - Workspace p4workspace = getPerforceWorkspace(build.getProject(), projectPath, depot, build.getBuiltOn(), build, launcher, workspace, listener, false); + Workspace p4workspace = getPerforceWorkspace(build.getProject(), effectiveProjectPath, depot, build.getBuiltOn(), build, launcher, workspace, listener, false); boolean dirtyWorkspace = p4workspace.isDirty(); saveWorkspaceIfDirty(depot, p4workspace, log); @@ -904,7 +873,7 @@ public boolean checkout(AbstractBuild build, Launcher launcher, String p4config; WipeWorkspaceExcludeFilter wipeFilter; try { - p4config = MacroStringHelper.substituteParameters("${P4CONFIG}", build, null); + p4config = MacroStringHelper.substituteParameters("${P4CONFIG}", this, build, null); wipeFilter = new WipeWorkspaceExcludeFilter(".p4config",p4config); } catch (ParameterSubstitutionException ex) { wipeFilter = new WipeWorkspaceExcludeFilter(); @@ -953,12 +922,12 @@ public boolean checkout(AbstractBuild build, Launcher launcher, String p4Client = getConcurrentClientName(workspace, getEffectiveClientName(build, null)); p4workspace = depot.getWorkspaces().getWorkspace(p4Client, p4Stream); } - projectPath = p4workspace.getTrimmedViewsAsString(); + effectiveProjectPath = p4workspace.getTrimmedViewsAsString(); } // If we're not managing the view, populate the projectPath with the current view from perforce // This is both for convenience, and so the labeling mechanism can operate correctly if (!updateView) { - projectPath = p4workspace.getTrimmedViewsAsString(); + effectiveProjectPath = p4workspace.getTrimmedViewsAsString(); } String p4WorkspacePath = "//" + p4workspace.getName() + "/..."; @@ -993,7 +962,7 @@ public boolean checkout(AbstractBuild build, Launcher launcher, if (p4Counter != null && !updateCounterValue) { //use a counter String counterName; - counterName = MacroStringHelper.substituteParameters(this.p4Counter, build, null); + counterName = MacroStringHelper.substituteParameters(this.p4Counter, this, build, null); Counter counter = depot.getCounters().getCounter(counterName); newestChange = counter.getValue(); } else { @@ -1025,7 +994,7 @@ public boolean checkout(AbstractBuild build, Launcher launcher, for (int i = workspaceChanges.size()-1; i >= 0; --i) { int changeNumber = workspaceChanges.get(i); Changelist changelist = depot.getChanges().getChangelist(changeNumber, fileLimit); - if (!isChangelistExcluded(changelist, build.getProject(), p4workspace.getViewsAsString(), log)) { + if (!isChangelistExcluded(changelist, build.getProject(), build.getBuiltOn(), p4workspace.getViewsAsString(), log)) { newestChange = changeNumber; break; } @@ -1036,7 +1005,7 @@ public boolean checkout(AbstractBuild build, Launcher launcher, } if (build instanceof MatrixRun) { - newestChange = getOrSetMatrixChangeSet(build, depot, newestChange, projectPath, log); + newestChange = getOrSetMatrixChangeSet(build, depot, newestChange, effectiveProjectPath, log); } if (lastChange <= 0) { @@ -1132,8 +1101,8 @@ public boolean checkout(AbstractBuild build, Launcher launcher, } // If we aren't managing the client views, update the current ones // with those from perforce, and save them if they have changed. - if (!this.updateView && !projectPath.equals(this.projectPath)) { - this.projectPath = projectPath; + if (!this.updateView && !effectiveProjectPath.equals(this.projectPath)) { + this.projectPath = effectiveProjectPath; doSaveProject = true; } if (doSaveProject) { @@ -1143,14 +1112,14 @@ public boolean checkout(AbstractBuild build, Launcher launcher, // Add tagging action that enables the user to create a label // for this build. build.addAction(new PerforceTagAction( - build, depot, newestChange, projectPath, MacroStringHelper.substituteParameters(p4User, build, null))); + build, depot, newestChange, effectiveProjectPath, MacroStringHelper.substituteParameters(p4User, this, build, null))); build.addAction(new PerforceSCMRevisionState(newestChange)); if (p4Counter != null && updateCounterValue) { // Set or create a counter to mark this change Counter counter = new Counter(); - String counterName = MacroStringHelper.substituteParameters(this.p4Counter, build, null); + String counterName = MacroStringHelper.substituteParameters(this.p4Counter, this, build, null); counter.setName(counterName); counter.setValue(newestChange); log.println("Updating counter " + counterName + " to " + newestChange); @@ -1191,10 +1160,13 @@ public boolean checkout(AbstractBuild build, Launcher launcher, } - private synchronized int getOrSetMatrixChangeSet(AbstractBuild build, Depot depot, int newestChange, String projectPath, PrintStream log) - throws ParameterSubstitutionException + private synchronized int getOrSetMatrixChangeSet( + @Nonnull AbstractBuild build, + @Nonnull Depot depot, int newestChange, String projectPath, + @Nonnull PrintStream log) + throws ParameterSubstitutionException, InterruptedException { - int lastChange = 0; + int matrixLastChange = 0; // special consideration for matrix builds if (build instanceof MatrixRun) { log.println("This is a matrix run, trying to use change number from parent/siblings..."); @@ -1204,16 +1176,17 @@ private synchronized int getOrSetMatrixChangeSet(AbstractBuild build, Depot depo if (parentChange > 0) { // use existing changeset from parent log.println("Latest change from parent is: "+Integer.toString(parentChange)); - lastChange = parentChange; + matrixLastChange = parentChange; } else { // no changeset on parent, set it for other // matrixruns to use log.println("No change number has been set by parent/siblings. Using latest."); - parentBuild.addAction(new PerforceTagAction(build, depot, newestChange, projectPath, MacroStringHelper.substituteParameters(p4User,build,null))); + parentBuild.addAction(new PerforceTagAction(build, depot, newestChange, projectPath, + MacroStringHelper.substituteParameters(p4User, this, build, null))); } } } - return lastChange; + return matrixLastChange; } /** @@ -1223,7 +1196,7 @@ private synchronized int getOrSetMatrixChangeSet(AbstractBuild build, Depot depo * @param p4workspace the workspace * @return a string of path(s), e.g. //mymodule1/... //mymodule2/... */ - private String getChangesPaths(Workspace p4workspace) { + private String getChangesPaths(@Nonnull Workspace p4workspace) { return PerforceSCMHelper.computePathFromViews(p4workspace.getViews()); } @@ -1291,7 +1264,6 @@ protected PollingResult compareRemoteRevisionWith(AbstractProject project, return PollingResult.BUILD_NOW; } - Hashtable subst = getDefaultSubstitutions(project); try { Node buildNode = getPollingNode(project); Depot depot; @@ -1303,11 +1275,11 @@ protected PollingResult compareRemoteRevisionWith(AbstractProject project, logger.println("Using node: " + buildNode.getDisplayName()); } - Workspace p4workspace = getPerforceWorkspace(project, getEffectiveProjectPath(null, project, logger, depot), depot, buildNode, null, launcher, workspace, listener, true); + Workspace p4workspace = getPerforceWorkspace(project, getEffectiveProjectPath(null, project, buildNode, logger, depot), depot, buildNode, null, launcher, workspace, listener, true); saveWorkspaceIfDirty(depot, p4workspace, logger); int lastChangeNumber = baseline.getRevision(); - SCMRevisionState repositoryState = getCurrentDepotRevisionState(p4workspace, project, depot, logger, lastChangeNumber); + SCMRevisionState repositoryState = getCurrentDepotRevisionState(p4workspace, project, buildNode, depot, logger, lastChangeNumber); PollingResult.Change change; if (repositoryState.equals(baseline)) { @@ -1325,13 +1297,13 @@ protected PollingResult compareRemoteRevisionWith(AbstractProject project, } } - private Node getPollingNode(AbstractProject project) { + @CheckForNull + private Node getPollingNode(@Nonnull AbstractProject project) { Node buildNode = project.getLastBuiltOn(); if (pollOnlyOnMaster) { buildNode = null; } else { // try to get an active node that the project is configured to use - buildNode = project.getLastBuiltOn(); if (!isNodeOnline(buildNode)) { buildNode = null; } @@ -1345,7 +1317,8 @@ private Node getPollingNode(AbstractProject project) { return buildNode; } - private Node getOnlineConfiguredNode(AbstractProject project) { + @CheckForNull + private Node getOnlineConfiguredNode(@Nonnull AbstractProject project) { Node buildNode = null; for (Node node : Hudson.getInstance().getNodes()) { hudson.model.Label l = project.getAssignedLabel(); @@ -1364,11 +1337,14 @@ private Node getOnlineConfiguredNode(AbstractProject project) { return buildNode; } - private boolean isNodeOnline(Node node) { + private boolean isNodeOnline(@CheckForNull Node node) { return node != null && node.toComputer() != null && node.toComputer().isOnline(); } - private SCMRevisionState getCurrentDepotRevisionState(Workspace p4workspace, AbstractProject project, Depot depot, + private SCMRevisionState getCurrentDepotRevisionState( + @Nonnull Workspace p4workspace, + @CheckForNull AbstractProject project, + @CheckForNull Node node, @Nonnull Depot depot, PrintStream logger, int lastChangeNumber) throws IOException, InterruptedException, PerforceException { int highestSelectedChangeNumber; @@ -1391,18 +1367,20 @@ private SCMRevisionState getCurrentDepotRevisionState(Workspace p4workspace, Abs // by this workspace). Integer newestChange; - String p4Label = MacroStringHelper.substituteParametersNoCheck(this.p4Label, getDefaultSubstitutions(project)); - if (p4Label != null && !p4Label.trim().isEmpty()) { + String effectiveP4Label = MacroStringHelper.substituteParameters( + this.p4Label, this, project, node, null); + if (effectiveP4Label != null && !effectiveP4Label.trim().isEmpty()) { //In case where we are using a rolling label. String root = "//" + p4workspace.getName() + "/..."; - newestChange = depot.getChanges().getHighestLabelChangeNumber(p4workspace, p4Label.trim(), root); + newestChange = depot.getChanges().getHighestLabelChangeNumber(p4workspace, effectiveP4Label.trim(), root); } else { Counter counter = depot.getCounters().getCounter("change"); newestChange = counter.getValue(); } if (useViewMaskForPolling && useViewMask) { - changeNumbers = depot.getChanges().getChangeNumbersInRange(p4workspace, lastChangeNumber+1, newestChange, MacroStringHelper.substituteParametersNoCheck(viewMask, getDefaultSubstitutions(project)), false); + changeNumbers = depot.getChanges().getChangeNumbersInRange(p4workspace, lastChangeNumber+1, newestChange, + MacroStringHelper.substituteParameters(viewMask, this, project, node, null), false); } else { String root = "//" + p4workspace.getName() + "/..."; changeNumbers = depot.getChanges().getChangeNumbersInRange(p4workspace, lastChangeNumber+1, newestChange, root, false); @@ -1427,7 +1405,8 @@ private SCMRevisionState getCurrentDepotRevisionState(Workspace p4workspace, Abs } else { for (int changeNumber : changeNumbers) { - if (isChangelistExcluded(depot.getChanges().getChangelist(changeNumber, fileLimit), project, p4workspace.getViewsAsString(), logger)) { + if (isChangelistExcluded(depot.getChanges().getChangelist(changeNumber, fileLimit), + project, node, p4workspace.getViewsAsString(), logger)) { logger.println("Changelist "+changeNumber+" is composed of file(s) and/or user(s) that are excluded."); } else { return new PerforceSCMRevisionState(changeNumber); @@ -1444,13 +1423,17 @@ private SCMRevisionState getCurrentDepotRevisionState(Workspace p4workspace, Abs * @param changelist the p4 changelist * @return True if changelist only contains user(s) and/or file(s) that are denoted to be excluded */ - private boolean isChangelistExcluded(Changelist changelist, AbstractProject project, String view, PrintStream logger) { + private boolean isChangelistExcluded(Changelist changelist, + AbstractProject project, Node node, String view, PrintStream logger) + throws ParameterSubstitutionException, InterruptedException + { if (changelist == null) { return false; } if (excludedUsers != null && !excludedUsers.trim().equals("")) { - List users = Arrays.asList(MacroStringHelper.substituteParametersNoCheck(excludedUsers,getDefaultSubstitutions(project)).split("\n")); + List users = Arrays.asList( + MacroStringHelper.substituteParameters(excludedUsers,this, project, node, null).split("\n")); if (users.contains(changelist.getUser())) { logger.println("Excluded User [" + changelist.getUser() + "] found in changelist."); @@ -1472,7 +1455,8 @@ private boolean isChangelistExcluded(Changelist changelist, AbstractProject proj } if (excludedFiles != null && !excludedFiles.trim().equals("")) { - List files = Arrays.asList(MacroStringHelper.substituteParametersNoCheck(excludedFiles,getDefaultSubstitutions(project)).split("\n")); + List files = Arrays.asList( + MacroStringHelper.substituteParameters(excludedFiles, this, project, node, null).split("\n")); StringBuffer buff = null; if (files.size() > 0 && changelist.getFiles().size() > 0) { @@ -1496,7 +1480,8 @@ private boolean isChangelistExcluded(Changelist changelist, AbstractProject proj return false; } - private static boolean doesFilenameMatchAnyP4Pattern(String filename, List patternStrings, boolean caseSensitive) { + private static boolean doesFilenameMatchAnyP4Pattern(String filename, + @Nonnull List patternStrings, boolean caseSensitive) { for (String patternString : patternStrings) { if (patternString.trim().equals("")) continue; if (doesFilenameMatchP4Pattern(filename, patternString, caseSensitive)) { @@ -1506,7 +1491,8 @@ private static boolean doesFilenameMatchAnyP4Pattern(String filename, List 0) return firstChange; @@ -1550,7 +1536,7 @@ public int getLastChange(Run build) { return getLastChangeNoFirstChange(build); } - private static int getLastChangeNoFirstChange(Run build) { + private static int getLastChangeNoFirstChange(@CheckForNull Run build) { // If we can't find a PerforceTagAction, we will default to 0. @@ -1562,7 +1548,8 @@ private static int getLastChangeNoFirstChange(Run build) { return action.getChangeNumber(); } - private static PerforceTagAction getMostRecentTagAction(Run build) { + @CheckForNull + private static PerforceTagAction getMostRecentTagAction(@CheckForNull Run build) { if (build == null) return null; @@ -1590,42 +1577,39 @@ private Workspace getPerforceWorkspace(AbstractProject project, String projectPa // make sure each slave has a unique client name by adding it's // hostname to the end of the client spec - String p4Client; - if (build != null) { - p4Client = getEffectiveClientName(build, null); - } else { - p4Client = getDefaultEffectiveClientName(project, buildNode, workspace); - } + String effectiveP4Client = build != null + ? getEffectiveClientName(build, null) + : getDefaultEffectiveClientName(project, buildNode, workspace); // If we are running concurrent builds, the Jenkins workspace path is different // for each concurrent build. Append Perforce workspace name with Jenkins // workspace identifier suffix. - p4Client = getConcurrentClientName(workspace, p4Client); + effectiveP4Client = getConcurrentClientName(workspace, effectiveP4Client); if (!nodeIsRemote(buildNode)) { log.print("Using master perforce client: "); - log.println(p4Client); + log.println(effectiveP4Client); } else if (dontRenameClient) { log.print("Using shared perforce client: "); - log.println(p4Client); + log.println(effectiveP4Client); } else { - log.println("Using remote perforce client: " + p4Client); + log.println("Using remote perforce client: " + effectiveP4Client); } - depot.setClient(p4Client); - String p4Stream = (build == null ? MacroStringHelper.substituteParameters(this.p4Stream, getDefaultSubstitutions(project)) : MacroStringHelper.substituteParameters(this.p4Stream, build, null)); + depot.setClient(effectiveP4Client); + String effectiveP4Stream = MacroStringHelper.substituteParameters(this.p4Stream, this, build, project, buildNode, null); // Get the clientspec (workspace) from perforce - Workspace p4workspace = depot.getWorkspaces().getWorkspace(p4Client, p4Stream); + Workspace p4workspace = depot.getWorkspaces().getWorkspace(effectiveP4Client, effectiveP4Stream); assert p4workspace != null; boolean creatingNewWorkspace = p4workspace.isNew(); // If the client workspace doesn't exist, and we're not managing the clients, // Then terminate the build with an error if (!createWorkspace && creatingNewWorkspace) { - log.println("*** Perforce client workspace '" + p4Client +"' doesn't exist."); + log.println("*** Perforce client workspace '" + effectiveP4Client +"' doesn't exist."); log.println("*** Please create it, or allow Jenkins to manage clients on it's own."); log.println("*** If the client name mentioned above is not what you expected, "); log.println("*** check your 'Client name format for slaves' advanced config option."); @@ -1635,7 +1619,7 @@ else if (dontRenameClient) { // Ensure that the clientspec (workspace) name is set correctly // TODO Examine why this would be necessary. - p4workspace.setName(p4Client); + p4workspace.setName(effectiveP4Client); // Set the workspace options according to the configuration if (projectOptions != null) @@ -1676,7 +1660,7 @@ else if (localPath.trim().equals("")) if (updateView || creatingNewWorkspace) { // Switch to another stream view if necessary if (useStreamDepot) { - p4workspace.setStream(p4Stream); + p4workspace.setStream(effectiveP4Stream); } // If necessary, rewrite the views field in the clientspec. Also, clear the stream. // TODO If dontRenameClient==false, and updateView==false, user @@ -1685,9 +1669,9 @@ else if (localPath.trim().equals("")) else { p4workspace.setStream(""); if (useClientSpec) { - projectPath = getEffectiveProjectPathFromFile(build, project, log, depot); + projectPath = getEffectiveProjectPathFromFile(build, project, buildNode, log, depot); } - List mappingPairs = parseProjectPath(projectPath, p4Client, log); + List mappingPairs = parseProjectPath(projectPath, effectiveP4Client, log); if (!equalsProjectPath(mappingPairs, p4workspace.getViews())) { log.println("Changing P4 Client View from:\n" + p4workspace.getViewsAsString()); log.println("Changing P4 Client View to: "); @@ -1709,60 +1693,51 @@ else if (localPath.trim().equals("")) return p4workspace; } - private String getEffectiveClientName(AbstractBuild build, Map env) throws ParameterSubstitutionException { + private String getEffectiveClientName(AbstractBuild build, Map env) + throws ParameterSubstitutionException, InterruptedException { Node buildNode = build.getBuiltOn(); FilePath workspace = build.getWorkspace(); - String p4Client = this.p4Client; - p4Client = MacroStringHelper.substituteParameters(p4Client, build, env); + String effectiveP4Client = this.p4Client; try { - p4Client = getEffectiveClientName(p4Client, buildNode); + effectiveP4Client = getEffectiveClientName(effectiveP4Client, build.getProject(), buildNode); } catch (Exception e) { new StreamTaskListener(System.out).getLogger().println( "Could not get effective client name: " + e.getMessage()); } - return p4Client; + effectiveP4Client = MacroStringHelper.substituteParameters(effectiveP4Client, this, build, env); + return effectiveP4Client; } - private String getDefaultEffectiveClientName(AbstractProject project, Node buildNode, FilePath workspace) + //TODO: Workspace is unused! + private String getDefaultEffectiveClientName( + @CheckForNull AbstractProject project, + @CheckForNull Node buildNode, + FilePath workspace) throws IOException, InterruptedException { - String basename = MacroStringHelper.substituteParametersNoCheck(this.p4Client, getDefaultSubstitutions(project)); - return getEffectiveClientName(basename, buildNode); + String basename = getEffectiveClientName(this.p4Client, project, buildNode); + return MacroStringHelper.substituteParameters(basename, this, project, buildNode, null); } - private String getEffectiveClientName(String basename, Node buildNode) + private String getEffectiveClientName( + @Nonnull String basename, + @CheckForNull AbstractProject project, + @CheckForNull Node buildNode) throws IOException, InterruptedException { - String p4Client = basename; - - if (nodeIsRemote(buildNode) && !getSlaveClientNameFormat().equals("")) { - String host=null; - - Computer c = buildNode.toComputer(); - if (c != null) - host = c.getHostName(); - - if (host == null) { - LOGGER.log(Level.WARNING,"Could not get hostname for slave " + buildNode.getDisplayName()); - host = "UNKNOWNHOST"; - } - - if (host.contains(".")) { - host = String.valueOf(host.subSequence(0, host.indexOf('.'))); - } - // use hashcode of the nodename to get a unique, slave-specific client name - String hash = String.valueOf(buildNode.getNodeName().hashCode()); - - Map substitutions = new Hashtable(); - substitutions.put("nodename", buildNode.getNodeName()); - substitutions.put("hostname", host); - substitutions.put("hash", hash); - substitutions.put("basename", basename); + String effectiveP4Client = basename; - p4Client = MacroStringHelper.substituteParametersNoCheck(getSlaveClientNameFormat(), substitutions); + //TODO: Seems that local node should be handled as well + if (buildNode!=null && nodeIsRemote(buildNode) && !getSlaveClientNameFormat().equals("")) { + + Map additionalSubstitutions = new TreeMap(); + //TODO: Get rid of the outdated variable + additionalSubstitutions.put("basename", basename); + effectiveP4Client = MacroStringHelper.substituteParameters ( + getSlaveClientNameFormat(), this, project, buildNode, additionalSubstitutions); } // eliminate spaces, just in case - p4Client = p4Client.replaceAll(" ", "_"); - return p4Client; + effectiveP4Client = effectiveP4Client.replaceAll(" ", "_"); + return effectiveP4Client; } public String getSlaveClientNameFormat() { @@ -2038,6 +2013,11 @@ public FormValidation doValidatePerforceLogin(StaplerRequest req) { /** * Checks to see if the specified workspace is valid. + * The method also checks forbidden variables in the client name. + * (see + * Perforce Plugin Wiki page) + * to get the clarification of forbidden variables. + * An improper usage of the variable may corrupt Perforce workspaces in project builds. */ public FormValidation doValidateP4Client(StaplerRequest req) { String workspace = Util.fixEmptyAndTrim(req.getParameter("client")); @@ -2050,6 +2030,14 @@ public FormValidation doValidateP4Client(StaplerRequest req) { return FormValidation.error("Client name doesn't meet global pattern: "+getP4ClientPattern()); } + // Check forbidden variables + for (String variableName : P4CLIENT_FORBIDDEN_VARIABLES) { + if (MacroStringHelper.containsVariable(workspace, variableName)) { + return FormValidation.error(hudson.plugins.perforce.Messages. + PerforceSCM_doValidateP4Client_forbiddenVariableError(variableName)); + } + } + // Then, check depot Depot depot = getDepotFromRequest(req); if (depot == null) { @@ -2389,7 +2377,10 @@ public void onRenamed(Item item, String oldName, String newName) { Pattern.compile("^\\s*([+-]?//\\S+?/\\S+)\\s+\"//\\S+?(/[^\"]+)\"\\s*$"); private static final Pattern QUOTED_DEPOT_AND_WORKSPACE = Pattern.compile("^\\s*\"([+-]?//\\S+?/[^\"]+)\"\\s+//\\S+?(/\\S+)$\\s*"); - + private static final String[] P4CLIENT_FORBIDDEN_VARIABLES= + {"EXECUTOR_NUMBER"}; + + /** * Parses the projectPath into a list of pairs of strings representing the depot and client * paths. Even items are depot and odd items are client. @@ -2583,12 +2574,26 @@ public String getDecryptedP4Passwd() { return encryptor.decryptString(p4Passwd); } - public String getDecryptedP4Passwd(AbstractBuild build) throws ParameterSubstitutionException { - return MacroStringHelper.substituteParameters(getDecryptedP4Passwd(), build, null); + public String getDecryptedP4Passwd(AbstractBuild build) + throws ParameterSubstitutionException, InterruptedException { + return MacroStringHelper.substituteParameters(getDecryptedP4Passwd(), this, build, null); } - public String getDecryptedP4Passwd(AbstractProject project) { - return MacroStringHelper.substituteParametersNoCheck(getDecryptedP4Passwd(), getDefaultSubstitutions(project)); + /** + * @deprecated Use {@link #getDecryptedP4Passwd(hudson.model.AbstractProject, hudson.model.Node)} instead. + */ + public String getDecryptedP4Passwd(AbstractProject project) throws InterruptedException { + try { + return getDecryptedP4Passwd(project, null); + } catch (ParameterSubstitutionException ex) { + throw new IllegalArgumentException("Cannot substitute all variables in P4Passwd"); + } + } + + public String getDecryptedP4Passwd(@CheckForNull AbstractProject project, @CheckForNull Node node) + throws ParameterSubstitutionException, InterruptedException { + return MacroStringHelper.substituteParameters(getDecryptedP4Passwd(), + this, project, node, null); } /** @@ -3148,9 +3153,11 @@ public boolean processWorkspaceBeforeDeletion(AbstractProject project, File try { Depot depot = getDepot(launcher, workspace, project, null, node); + final String effectiveProjectPath = MacroStringHelper.substituteParameters( + projectPath, this, project, node, null); Workspace p4workspace = getPerforceWorkspace( project, - MacroStringHelper.substituteParametersNoCheck(projectPath,getDefaultSubstitutions(project)), + effectiveProjectPath, depot, node, null, @@ -3173,20 +3180,20 @@ public boolean processWorkspaceBeforeDeletion(AbstractProject project, File return false; } - public boolean isSlaveClientNameStatic() { + public boolean isSlaveClientNameStatic() throws ParameterSubstitutionException { Map testSub1 = new Hashtable(); testSub1.put("hostname", "HOSTNAME1"); testSub1.put("nodename", "NODENAME1"); testSub1.put("hash", "HASH1"); testSub1.put("basename", this.p4Client); - String result1 = MacroStringHelper.substituteParametersNoCheck(getSlaveClientNameFormat(), testSub1); + String result1 = MacroStringHelper.substituteParameters(getSlaveClientNameFormat(), testSub1); Map testSub2 = new Hashtable(); testSub2.put("hostname", "HOSTNAME2"); testSub2.put("nodename", "NODENAME2"); testSub2.put("hash", "HASH2"); testSub2.put("basename", this.p4Client); - String result2 = MacroStringHelper.substituteParametersNoCheck(getSlaveClientNameFormat(), testSub2); + String result2 = MacroStringHelper.substituteParameters(getSlaveClientNameFormat(), testSub2); return result1.equals(result2); } diff --git a/src/main/java/hudson/plugins/perforce/PerforceTagAction.java b/src/main/java/hudson/plugins/perforce/PerforceTagAction.java index a90348a..2293c89 100644 --- a/src/main/java/hudson/plugins/perforce/PerforceTagAction.java +++ b/src/main/java/hudson/plugins/perforce/PerforceTagAction.java @@ -19,6 +19,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.List; +import javax.annotation.CheckForNull; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; @@ -144,6 +145,14 @@ public String isInvalidTag(String tag) { } return null; } + + @CheckForNull + public PerforceSCM getSCM() { + if(this.getBuild().getProject().getScm() instanceof PerforceSCM){ + return (PerforceSCM)this.getBuild().getProject().getScm(); + } + return null; + } /** * Checks if the value is a valid Perforce tag (label) name. @@ -158,7 +167,7 @@ public synchronized FormValidation doCheckTag(@QueryParameter String value) { /** * Invoked to actually tag the workspace. */ - public synchronized void doSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + public synchronized void doSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, InterruptedException { getACL().checkPermission(getPermission()); String tag = req.getParameter("name"); @@ -170,16 +179,16 @@ public synchronized void doSubmit(StaplerRequest req, StaplerResponse rsp) throw rsp.sendRedirect("."); } - public void tagBuild(String tagname, String description, String owner) throws IOException { + public void tagBuild(String tagname, String description, String owner) throws IOException, InterruptedException { Label label = new Label(); label.setName(tagname); label.setDescription(description); label.setRevision(new Integer(changeNumber).toString()); if(owner!=null && !owner.equals("")) label.setOwner(owner); - if(this.getBuild().getProject().getScm() instanceof PerforceSCM){ - PerforceSCM scm = (PerforceSCM)this.getBuild().getProject().getScm(); - depot.setPassword(scm.getDecryptedP4Passwd(this.getBuild().getProject())); + PerforceSCM scm = getSCM(); + if(scm != null){ + depot.setPassword(scm.getDecryptedP4Passwd(this.getBuild().getProject(), this.getBuild().getBuiltOn())); } //Only take the depot paths and add them to the view. diff --git a/src/main/java/hudson/plugins/perforce/PerforceTagNotifier.java b/src/main/java/hudson/plugins/perforce/PerforceTagNotifier.java index 03fc60d..a342f00 100644 --- a/src/main/java/hudson/plugins/perforce/PerforceTagNotifier.java +++ b/src/main/java/hudson/plugins/perforce/PerforceTagNotifier.java @@ -110,14 +110,20 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListene return false; } } + + PerforceSCM scm = tagAction.getSCM(); + if (scm == null) { + listener.getLogger().println("Cannot retrieve the Perforce SCM"); + return false; + } listener.getLogger().println("Labelling Build in Perforce using " + rawLabelName); String labelName, labelDesc, labelOwner; try { - labelName = MacroStringHelper.substituteParameters(rawLabelName, build, null); - labelDesc = MacroStringHelper.substituteParameters(rawLabelDesc, build, null); - labelOwner = MacroStringHelper.substituteParameters(rawLabelOwner, build, null); + labelName = MacroStringHelper.substituteParameters(rawLabelName, scm, build, null); + labelDesc = MacroStringHelper.substituteParameters(rawLabelDesc, scm, build, null); + labelOwner = MacroStringHelper.substituteParameters(rawLabelOwner, scm, build, null); } catch (ParameterSubstitutionException ex) { listener.getLogger().println("Parameter substitution error in label items. "+ex.getMessage()); return false; diff --git a/src/main/java/hudson/plugins/perforce/utils/MacroStringHelper.java b/src/main/java/hudson/plugins/perforce/utils/MacroStringHelper.java index 6b6ccfe..fdf7090 100644 --- a/src/main/java/hudson/plugins/perforce/utils/MacroStringHelper.java +++ b/src/main/java/hudson/plugins/perforce/utils/MacroStringHelper.java @@ -25,22 +25,32 @@ package hudson.plugins.perforce.utils; import hudson.EnvVars; +import hudson.matrix.Axis; +import hudson.matrix.MatrixConfiguration; +import hudson.matrix.MatrixProject; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Hudson; +import hudson.model.Node; +import hudson.model.ParameterDefinition; +import hudson.model.ParameterValue; +import hudson.model.ParametersDefinitionProperty; import hudson.model.TaskListener; import hudson.plugins.perforce.PerforceSCM; import hudson.slaves.EnvironmentVariablesNodeProperty; import hudson.slaves.NodeProperty; import java.io.IOException; -import java.util.Hashtable; +import java.util.HashMap; import java.util.Map; +import java.util.Map.Entry; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; /** - * Provides validation of macro strings after parameter substitution. + * The class aggregates all variables substitution methods within the plugin. * @author Mike Wille * @author Brian Westrich * @author Victor Szoltysek @@ -49,17 +59,71 @@ * @since 1.3.25 */ public class MacroStringHelper { + public static final Level SUBSTITUTION_ERROR_LEVEL = Level.WARNING; + + /** + * Substitute parameters and validate contents of the resulting string. + * This is a generic method, which invokes routines for {@link AbstractBuild} if it is not null. + * Otherwise, the default handlers for {@link AbstractProject} and {@link Node} will be used. + * @param string Input string to be substituted + * @param instance Instance of {@link PerforceSCM} + * @param build A build to be substituted + * @param project A project + * @param node A node to be substituted + * @param env Additional environment variables. + * @return Substituted string. May be null if the input string is null + * @throws ParameterSubstitutionException Format error (unresolved variable, etc.) + */ + public static String substituteParameters( + @CheckForNull String string, + @Nonnull PerforceSCM instance, + @CheckForNull AbstractBuild build, + @CheckForNull AbstractProject project, + @CheckForNull Node node, + @CheckForNull Map env) + throws ParameterSubstitutionException, InterruptedException { + + return build != null + ? substituteParameters(string, instance, build, env) + : substituteParameters(string, instance, project, node, env); + } + + /** + * Substitute parameters and validate contents of the resulting string + * @param string Input string to be substituted + * @param instance Instance of {@link PerforceSCM} + * @param project A project + * @param node A node to be substituted + * @param env Additional environment variables. + * @return Substituted string. May be null if the input string is null + * @throws ParameterSubstitutionException Format error (unresolved variable, etc.) + */ + public static String substituteParameters( + @CheckForNull String string, + @Nonnull PerforceSCM instance, + @CheckForNull AbstractProject project, + @CheckForNull Node node, + @CheckForNull Map env) + throws ParameterSubstitutionException, InterruptedException { + if (string == null) return null; + String result = substituteParametersNoCheck(string, instance, project, node, env); + checkString(result); + return result; + } + /** * Substitute parameters and validate contents of the resulting string * @param string Input string * @param subst Variables Map - * @return Substituted string + * @return Substituted string. May be null if the input string is null * @throws ParameterSubstitutionException Format error (unresolved variable, etc.) */ - public static String substituteParameters(String string, Map subst) - throws ParameterSubstitutionException - { + public static String substituteParameters( + @CheckForNull String string, + @Nonnull Map subst) + throws ParameterSubstitutionException { + if (string == null) return null; String result = substituteParametersNoCheck(string, subst); checkString(result); return result; @@ -67,36 +131,38 @@ public static String substituteParameters(String string, Map sub /** * Substitute parameters and validate contents of the resulting string - * @param string Input string - * @param build Related build - * @param env Additional variables to be substituted. Used as a workaround for build environment - * @return Substituted string + * @param string Input string to be substituted + * @param instance Instance of {@link PerforceSCM} + * @param build A build to be substituted + * @param env Additional environment variables. + * @return Substituted string. May be null if the input string is null * @throws ParameterSubstitutionException Format error (unresolved variable, etc.) */ - public static String substituteParameters(String string, AbstractBuild build, Map env) - throws ParameterSubstitutionException - { - String result = substituteParametersNoCheck(string, build, env); + public static String substituteParameters( + @CheckForNull String string, + @Nonnull PerforceSCM instance, + @Nonnull AbstractBuild build, + @CheckForNull Map env) + throws ParameterSubstitutionException, InterruptedException { + if (string == null) return null; + String result = substituteParametersNoCheck(string, instance, build, env); checkString(result); return result; } /** * Checks string from unsubstituted variable references. - * @param string Input string (should be substituted before call) + * @param string Input string (should be substituted before call). + * Null string will be interpreted as OK * @throws ParameterSubstitutionException Substitution error */ - public static void checkString(String string) throws ParameterSubstitutionException - { - if (string == null) { - return; - } + public static void checkString(@CheckForNull String string) throws ParameterSubstitutionException { // Conditional fail on substitution error - if ( true && string.matches(".*\\$\\{.*\\}.*")) { - throw new ParameterSubstitutionException(string, "Found unresolved macro at '"+string+"'"); + if (containsMacro(string)) { + throw new ParameterSubstitutionException(string, "Found unresolved macro at '" + string + "'"); } - + //TODO: manage validation by global params? //TODO: Check single brackets //TODO: Add checks for '$' without brackets @@ -107,9 +173,10 @@ public static void checkString(String string) throws ParameterSubstitutionExcept * @param string Input string * @param subst Variables Map * @return Substituted string - * @deprecated Use checked methods instead */ - public static String substituteParametersNoCheck(String string, Map subst) { + private static String substituteParametersNoCheck( + @CheckForNull String string, + @Nonnull Map subst) { if (string == null) { return null; } @@ -120,52 +187,102 @@ public static String substituteParametersNoCheck(String string, Map env) { - if (!containsMacro(inputString)) { + private static String substituteParametersNoCheck ( + @Nonnull String inputString, + @Nonnull PerforceSCM instance, + @CheckForNull AbstractProject project, + @CheckForNull Node node, + @CheckForNull Map env) throws InterruptedException { + + if (!containsMacro(inputString)) { // do nothing for the missing macro return inputString; } + String outputString = inputString; - // Substitute environment if possible - String string = inputString; + // Substitute additional environment vars if possible if (env != null && !env.isEmpty()) { - string = substituteParametersNoCheck(string, env); - - //exit if no macros left - if (!containsMacro(inputString)) { - return string; + outputString = substituteParametersNoCheck(outputString, env); + if (!containsMacro(outputString)) { //exit if no macros left + return outputString; } } - - // Try to substitute via node and global environment - for (NodeProperty nodeProperty : Hudson.getInstance().getGlobalNodeProperties()) { - if (nodeProperty instanceof EnvironmentVariablesNodeProperty) { - string = ((EnvironmentVariablesNodeProperty) nodeProperty).getEnvVars().expand(string); - } + + // Prepare the substitution container and substitute vars + Map substitutions = new HashMap(); + getDefaultCoreSubstitutions(substitutions); + NodeSubstitutionHelper.getDefaultNodeSubstitutions(instance, node, substitutions); + if (project != null) { + getDefaultSubstitutions(project, substitutions); } - for (NodeProperty nodeProperty : build.getBuiltOn().getNodeProperties()) { - if (nodeProperty instanceof EnvironmentVariablesNodeProperty) { - string = ((EnvironmentVariablesNodeProperty) nodeProperty).getEnvVars().expand(string); - } + getDefaultSubstitutions(instance, substitutions); + outputString = substituteParametersNoCheck(outputString, substitutions); + + return outputString; + } + + /** + * Substitute parameters and validate contents of the resulting string + * @param inputString Input string + * @param instance Instance of {@link PerforceSCM} + * @param build Related build + * @param env Additional environment variables + * @return Substituted string + */ + private static String substituteParametersNoCheck( + @Nonnull String inputString, + @Nonnull PerforceSCM instance, + @Nonnull AbstractBuild build, + @CheckForNull Map env) throws InterruptedException { + + if (!containsMacro(inputString)) { + return inputString; } + + String string = substituteParametersNoCheck(inputString, instance, + build.getProject(), build.getBuiltOn(), env); + + // Substitute default build variables + Map substitutions = new HashMap(); + getDefaultBuildSubstitutions(build, substitutions); + String result = MacroStringHelper.substituteParametersNoCheck(string, substitutions); + result = MacroStringHelper.substituteParametersNoCheck(result, build.getBuildVariables()); if (!containsMacro(string)) { return string; } - // The last attempts: Try to build the full environment - Map subst = new TreeMap(); + // The last attempts: Try to build the full environment + Map environmentVarsFromExtensions = new TreeMap(); boolean useEnvironment = true; - for (StackTraceElement ste : (new Throwable()).getStackTrace()) { + for (StackTraceElement ste : (new Throwable()).getStackTrace()) { // Inspect the stacktrace to avoid the infinite recursion if (ste.getMethodName().equals("buildEnvVars") && ste.getClassName().equals(PerforceSCM.class.getName())) { useEnvironment = false; } @@ -173,34 +290,108 @@ public static String substituteParametersNoCheck(String inputString, AbstractBui if (useEnvironment) { try { EnvVars vars = build.getEnvironment(TaskListener.NULL); - subst.putAll(vars); + environmentVarsFromExtensions.putAll(vars); } catch (IOException ex) { Logger.getLogger(PerforceSCM.class.getName()).log(Level.SEVERE, null, ex); } catch (InterruptedException ex) { Logger.getLogger(PerforceSCM.class.getName()).log(Level.SEVERE, null, ex); } } - if (!containsMacro(string)) { - return string; - } - - //TODO: remove? - subst.put("JOB_NAME", getSafeJobName(build)); - String hudsonName = Hudson.getInstance().getDisplayName().toLowerCase(); - subst.put("BUILD_TAG", hudsonName + "-" + build.getProject().getName() + "-" + String.valueOf(build.getNumber())); - subst.put("BUILD_ID", build.getId()); - subst.put("BUILD_NUMBER", String.valueOf(build.getNumber())); + result = MacroStringHelper.substituteParametersNoCheck(result, environmentVarsFromExtensions); - String result = MacroStringHelper.substituteParametersNoCheck(string, subst); - result = MacroStringHelper.substituteParametersNoCheck(result, build.getBuildVariables()); return result; } - public static String getSafeJobName(AbstractBuild build) { + private static String getSafeJobName(@Nonnull AbstractBuild build) { return getSafeJobName(build.getProject()); } - public static String getSafeJobName(AbstractProject project) { + private static String getSafeJobName(@Nonnull AbstractProject project) { return project.getFullName().replace('/', '-').replace('=', '-').replace(',', '-'); } + + /** + * Gets variables of {@link Hudson} instance. + */ + private static void getDefaultCoreSubstitutions(@Nonnull Map env) { + String rootUrl = Hudson.getInstance().getRootUrl(); + if (rootUrl != null) { + env.put("JENKINS_URL", rootUrl); + env.put("HUDSON_URL", rootUrl); // Legacy compatibility + } + env.put("JENKINS_HOME", Hudson.getInstance().getRootDir().getPath()); + env.put("HUDSON_HOME", Hudson.getInstance().getRootDir().getPath()); // legacy compatibility + } + + /** + * Substitutes {@link PerforceSCM}-specific variables. + * In order to retain the backward compatibility, the input subst map's + * should contain {@link AbstractProject} variables from + * {@link #getDefaultSubstitutions(hudson.model.AbstractProject, java.util.Map)}. + * @param instance {@link PerforceSCM} instance + * @param subst Input substitutions. + */ + private static void getDefaultSubstitutions( + @Nonnull PerforceSCM instance, + @Nonnull Map subst) { + subst.put("P4USER", MacroStringHelper.substituteParametersNoCheck(instance.getP4User(), subst)); + } + + private static void getDefaultBuildSubstitutions( + @Nonnull AbstractBuild build, + @Nonnull Map subst) { + String hudsonName = Hudson.getInstance().getDisplayName().toLowerCase(); + subst.put("BUILD_TAG", hudsonName + "-" + build.getProject().getName() + "-" + String.valueOf(build.getNumber())); + subst.put("BUILD_ID", build.getId()); + subst.put("BUILD_NUMBER", String.valueOf(build.getNumber())); + String rootUrl = Hudson.getInstance().getRootUrl(); + if (rootUrl != null) { + subst.put("BUILD_URL", rootUrl + build.getUrl()); + } + } + + + private static void getDefaultSubstitutions( + @Nonnull AbstractProject project, + @Nonnull Map subst) { + + subst.put("JOB_NAME", MacroStringHelper.getSafeJobName(project)); + String rootUrl = Hudson.getInstance().getRootUrl(); + if (rootUrl != null) { + subst.put("JOB_URL", rootUrl + project.getUrl()); + } + + for (NodeProperty nodeProperty : Hudson.getInstance().getGlobalNodeProperties()) { + if (nodeProperty instanceof EnvironmentVariablesNodeProperty) { + subst.putAll(((EnvironmentVariablesNodeProperty) nodeProperty).getEnvVars()); + } + } + ParametersDefinitionProperty pdp = (ParametersDefinitionProperty) project.getProperty(hudson.model.ParametersDefinitionProperty.class); + if (pdp != null) { + for (ParameterDefinition pd : pdp.getParameterDefinitions()) { + try { + ParameterValue defaultValue = pd.getDefaultParameterValue(); + if (defaultValue != null) { + String name = defaultValue.getName(); + String value = defaultValue.createVariableResolver(null).resolve(name); + subst.put(name, value); + } + } catch (Exception e) { + // Do nothing + } + } + } + + // Handle Matrix Axes + if (project instanceof MatrixConfiguration) { + MatrixConfiguration matrixConfiguration = (MatrixConfiguration) project; + subst.putAll(matrixConfiguration.getCombination()); + } + if (project instanceof MatrixProject) { + MatrixProject matrixProject = (MatrixProject) project; + for (Axis axis : matrixProject.getAxes()) { + subst.put(axis.name, axis.size() >0 ? axis.value(0) : ""); + } + } + } } diff --git a/src/main/java/hudson/plugins/perforce/utils/NodeSubstitutionHelper.java b/src/main/java/hudson/plugins/perforce/utils/NodeSubstitutionHelper.java new file mode 100644 index 0000000..a1d6a2f --- /dev/null +++ b/src/main/java/hudson/plugins/perforce/utils/NodeSubstitutionHelper.java @@ -0,0 +1,134 @@ +/* + * The MIT License + * + * Copyright 2014 Oleg Nenashev . + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package hudson.plugins.perforce.utils; + +import hudson.EnvVars; +import hudson.Util; +import hudson.model.Computer; +import hudson.model.Executor; +import hudson.model.Hudson; +import hudson.model.Node; +import hudson.plugins.perforce.PerforceSCM; +import hudson.slaves.EnvironmentVariablesNodeProperty; +import hudson.slaves.NodeProperty; +import java.io.IOException; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; + +/** + * + * @author Oleg Nenashev + */ +public class NodeSubstitutionHelper { + + private static final Logger LOGGER = Logger.getLogger(PerforceSCM.class.getName()); + + /** + * Gets default variable substitutions for the {@link Node}. + * The method injects global and node-specific {@link EnvironmentVariablesNodeProperty} + * instances. + * @param instance Instance of {@link PerforceSCM} + * @param node Target node. Can be null + * @param target Output collection + */ + /*package*/ static void getDefaultNodeSubstitutions( + @Nonnull PerforceSCM instance, + @CheckForNull Node node, + @Nonnull Map target) throws InterruptedException { + // Global node properties + for (NodeProperty globalNodeProperty: Hudson.getInstance().getGlobalNodeProperties()) { + if (globalNodeProperty instanceof EnvironmentVariablesNodeProperty) { + target.putAll(((EnvironmentVariablesNodeProperty)globalNodeProperty).getEnvVars()); + } + } + + // Local node properties + if (node != null) { + for (NodeProperty nodeProperty : node.getNodeProperties()) { + if (nodeProperty instanceof EnvironmentVariablesNodeProperty) { + target.putAll(((EnvironmentVariablesNodeProperty) nodeProperty).getEnvVars()); + } + } + + final String nodeName = node.getNodeName(); + + // Push legacy variables + target.put("nodename", nodeName); + target.put("hostname", getHostName(node)); + target.put("hash", getNodeHash(node)); + + // Push modern variables + target.put("NODE_NAME", nodeName.isEmpty() ? "master" : nodeName); + target.put("NODE_LABELS", Util.join(node.getAssignedLabels(), " ")); + + // Get environment + Computer c = node.toComputer(); + if (c != null) { + try { + EnvVars env = c.getEnvironment().overrideAll(target); + target.putAll(env); + } catch (IOException ex) { + // Ignore exception + } + } + } + } + + /** + * Get a unique {@link Node} hashcode. + * Use hashcode of the nodename to get a unique, slave-specific client name + * @param node Node + * @return + */ + @Nonnull + private static String getNodeHash(@Nonnull Node node) { + return String.valueOf(node.getNodeName().hashCode()); + } + + @Nonnull + private static String getHostName(@Nonnull Node node) { + String host = null; + try { + Computer c = node.toComputer(); + if (c != null) { + host = c.getHostName(); + } + } catch (Exception ex) { + // fallback to finally + } finally { + if (host == null) { + LOGGER.log(Level.WARNING, "Could not get hostname for slave " + node.getDisplayName()); + host = "UNKNOWNHOST"; + } + } + if (host.contains(".")) { + host = String.valueOf(host.subSequence(0, host.indexOf('.'))); + } + return host; + } +} diff --git a/src/main/resources/hudson/plugins/perforce/Messages.properties b/src/main/resources/hudson/plugins/perforce/Messages.properties index 65dfe3c..223f213 100644 --- a/src/main/resources/hudson/plugins/perforce/Messages.properties +++ b/src/main/resources/hudson/plugins/perforce/Messages.properties @@ -1 +1,6 @@ -PerforceToolInstallation.onLoaded=Checking p4 executable migration \ No newline at end of file +PerforceToolInstallation.onLoaded=Checking p4 executable migration + +PerforceSCM.doValidateP4Client.forbiddenVariableError= \ + The {0} variable is forbidden for the client (see the plugin's Wiki page). \ + An improper usage of the variable may corrupt Perforce workspaces in your build. \ + Use the variable on your own risk. \ No newline at end of file