/* * The MIT License * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi * * 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.tasks; import com.google.common.collect.ImmutableMap; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import jenkins.MasterToSlaveFileCallable; import hudson.Launcher; import jenkins.util.SystemProperties; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Action; import jenkins.model.DependencyDeclarer; import hudson.model.DependencyGraph; import hudson.model.DependencyGraph.Dependency; import hudson.model.Fingerprint; import hudson.model.Fingerprint.BuildPtr; import hudson.model.FingerprintMap; import hudson.model.Job; import jenkins.model.Jenkins; import hudson.model.Result; import hudson.model.Run; import hudson.model.TaskListener; import hudson.remoting.VirtualChannel; import hudson.util.FormValidation; import hudson.util.PackedMap; import hudson.util.RunList; import net.sf.json.JSONObject; import org.acegisecurity.AccessDeniedException; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.types.FileSet; import org.jenkinsci.Symbol; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.Set; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import jenkins.model.RunAction2; import jenkins.tasks.SimpleBuildStep; /** * Records fingerprints of the specified files. * * @author Kohsuke Kawaguchi */ public class Fingerprinter extends Recorder implements Serializable, DependencyDeclarer, SimpleBuildStep { public static boolean enableFingerprintsInDependencyGraph = SystemProperties.getBoolean(Fingerprinter.class.getName() + ".enableFingerprintsInDependencyGraph"); /** * Comma-separated list of files/directories to be fingerprinted. */ private final String targets; @Deprecated Boolean recordBuildArtifacts; @DataBoundConstructor public Fingerprinter(String targets) { this.targets = targets; } @Deprecated public Fingerprinter(String targets, boolean recordBuildArtifacts) { this(targets); this.recordBuildArtifacts = recordBuildArtifacts; } public String getTargets() { return targets; } @Deprecated public boolean getRecordBuildArtifacts() { return recordBuildArtifacts != null && recordBuildArtifacts; } @Override public void perform(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException { try { listener.getLogger().println(Messages.Fingerprinter_Recording()); Map<String,String> record = new HashMap<String,String>(); EnvVars environment = build.getEnvironment(listener); if(targets.length()!=0) { String expandedTargets = environment.expand(targets); record(build, workspace, listener, record, expandedTargets); } FingerprintAction fingerprintAction = build.getAction(FingerprintAction.class); if (fingerprintAction != null) { fingerprintAction.add(record); } else { build.addAction(new FingerprintAction(build,record)); } if (enableFingerprintsInDependencyGraph) { Jenkins.getInstance().rebuildDependencyGraphAsync(); } } catch (IOException e) { e.printStackTrace(listener.error(Messages.Fingerprinter_Failed())); build.setResult(Result.FAILURE); } // failing to record fingerprints is an error but not fatal } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } public void buildDependencyGraph(AbstractProject owner, DependencyGraph graph) { if (enableFingerprintsInDependencyGraph) { RunList builds = owner.getBuilds(); Set<String> seenUpstreamProjects = new HashSet<String>(); for ( ListIterator iter = builds.listIterator(); iter.hasNext(); ) { Run build = (Run) iter.next(); for (FingerprintAction action : build.getActions(FingerprintAction.class)) { for (AbstractProject key : action.getDependencies().keySet()) { if (key == owner) { continue; // Avoid self references } AbstractProject p = key; // TODO is this harmful to call unconditionally, so it would apply also to MavenModule for example? if (key.getClass().getName().equals("hudson.matrix.MatrixConfiguration")) { p = key.getRootProject(); } if (seenUpstreamProjects.contains(p.getName())) { continue; } seenUpstreamProjects.add(p.getName()); graph.addDependency(new Dependency(p, owner) { @Override public boolean shouldTriggerBuild(AbstractBuild build, TaskListener listener, List<Action> actions) { // Fingerprints should not trigger builds. return false; } }); } } } } } private void record(Run<?,?> build, FilePath ws, TaskListener listener, Map<String,String> record, final String targets) throws IOException, InterruptedException { final class Record implements Serializable { final boolean produced; final String relativePath; final String fileName; final String md5sum; public Record(boolean produced, String relativePath, String fileName, String md5sum) { this.produced = produced; this.relativePath = relativePath; this.fileName = fileName; this.md5sum = md5sum; } Fingerprint addRecord(Run build) throws IOException { FingerprintMap map = Jenkins.getInstance().getFingerprintMap(); return map.getOrCreate(produced?build:null, fileName, md5sum); } private static final long serialVersionUID = 1L; } final long buildTimestamp = build.getTimeInMillis(); List<Record> records = ws.act(new MasterToSlaveFileCallable<List<Record>>() { public List<Record> invoke(File baseDir, VirtualChannel channel) throws IOException { List<Record> results = new ArrayList<Record>(); FileSet src = Util.createFileSet(baseDir,targets); DirectoryScanner ds = src.getDirectoryScanner(); for( String f : ds.getIncludedFiles() ) { File file = new File(baseDir,f); // consider the file to be produced by this build only if the timestamp // is newer than when the build has started. // 2000ms is an error margin since since VFAT only retains timestamp at 2sec precision boolean produced = buildTimestamp <= file.lastModified()+2000; try { results.add(new Record(produced,f,file.getName(),new FilePath(file).digest())); } catch (IOException e) { throw new IOException(Messages.Fingerprinter_DigestFailed(file),e); } catch (InterruptedException e) { throw new IOException(Messages.Fingerprinter_Aborted(),e); } } return results; } }); for (Record r : records) { Fingerprint fp = r.addRecord(build); if(fp==null) { listener.error(Messages.Fingerprinter_FailedFor(r.relativePath)); continue; } fp.addFor(build); record.put(r.relativePath,fp.getHashString()); } } @Extension @Symbol("fingerprint") public static class DescriptorImpl extends BuildStepDescriptor<Publisher> { public String getDisplayName() { return Messages.Fingerprinter_DisplayName(); } @Deprecated public FormValidation doCheck(@AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException { return doCheckTargets(project, value); } public FormValidation doCheckTargets(@AncestorInPath AbstractProject<?,?> project, @QueryParameter String value) throws IOException { if (project == null) { return FormValidation.ok(); } return FilePath.validateFileMask(project.getSomeWorkspace(),value); } @Override public Publisher newInstance(StaplerRequest req, JSONObject formData) { return req.bindJSON(Fingerprinter.class, formData); } public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } } /** * Action for displaying fingerprints. */ public static final class FingerprintAction implements RunAction2 { private transient Run build; private static final Random rand = new Random(); /** * From file name to the digest. */ private /*almost final*/ PackedMap<String,String> record; private transient WeakReference<Map<String,Fingerprint>> ref; public FingerprintAction(Run build, Map<String, String> record) { this.build = build; this.record = compact(record); } @Deprecated public FingerprintAction(AbstractBuild build, Map<String, String> record) { this((Run) build, record); } public void add(Map<String,String> moreRecords) { Map<String,String> r = new HashMap<String, String>(record); r.putAll(moreRecords); record = compact(r); ref = null; } public String getIconFileName() { return "fingerprint.png"; } public String getDisplayName() { return Messages.Fingerprinter_Action_DisplayName(); } public String getUrlName() { return "fingerprints"; } public Run getRun() { return build; } @Deprecated public AbstractBuild getBuild() { return build instanceof AbstractBuild ? (AbstractBuild) build : null; } /** * Obtains the raw data. */ public Map<String,String> getRecords() { return record; } @Override public void onLoad(Run<?,?> r) { build = r; record = compact(record); } @Override public void onAttached(Run<?,?> r) { // for historical reasons this setup is done in the constructor instead } /** Share data structure with other builds, mainly those of the same job. */ private PackedMap<String,String> compact(Map<String,String> record) { Map<String,String> b = new HashMap<String,String>(); for (Entry<String,String> e : record.entrySet()) { b.put(e.getKey().intern(), e.getValue().intern()); } return PackedMap.of(b); } /** * Map from file names of the fingerprinted file to its fingerprint record. */ public synchronized Map<String,Fingerprint> getFingerprints() { if(ref!=null) { Map<String,Fingerprint> m = ref.get(); if(m!=null) return m; } Jenkins h = Jenkins.getInstance(); Map<String,Fingerprint> m = new TreeMap<String,Fingerprint>(); for (Entry<String, String> r : record.entrySet()) { try { Fingerprint fp = h._getFingerprint(r.getValue()); if(fp!=null) m.put(r.getKey(), fp); } catch (IOException e) { logger.log(Level.WARNING,e.getMessage(),e); } } m = ImmutableMap.copyOf(m); ref = new WeakReference<Map<String,Fingerprint>>(m); return m; } /** * Gets the dependency to other existing builds in a map. */ public Map<AbstractProject,Integer> getDependencies() { return getDependencies(false); } /** * Gets the dependency to other builds in a map. * * @param includeMissing true if the original build should be included in * the result, even if it doesn't exist * @since 1.430 */ public Map<AbstractProject,Integer> getDependencies(boolean includeMissing) { Map<AbstractProject,Integer> r = new HashMap<AbstractProject,Integer>(); for (Fingerprint fp : getFingerprints().values()) { BuildPtr bp = fp.getOriginal(); if(bp==null) continue; // outside Hudson if(bp.is(build)) continue; // we are the owner try { Job job = bp.getJob(); if (job==null) continue; // project no longer exists if (!(job instanceof AbstractProject)) { // Ignoring this for now. In the future we may want a dependency map function not limited to AbstractProject. // (Could be used by getDependencyChanges if pulled up from AbstractBuild into Run, for example.) continue; } if (job.getParent()==build.getParent()) continue; // we are the parent of the build owner, that is almost like we are the owner if(!includeMissing && job.getBuildByNumber(bp.getNumber())==null) continue; // build no longer exists Integer existing = r.get(job); if(existing!=null && existing>bp.getNumber()) continue; // the record in the map is already up to date r.put((AbstractProject) job, bp.getNumber()); } catch (AccessDeniedException e) { // Need to log in to access this job, so ignore continue; } } return r; } } private static final Logger logger = Logger.getLogger(Fingerprinter.class.getName()); private static final long serialVersionUID = 1L; }