/******************************************************************************* * * Copyright (c) 2004-2010 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi * * *******************************************************************************/ package hudson.tasks; import com.google.common.collect.ImmutableMap; import hudson.Extension; import hudson.FilePath; import hudson.FilePath.FileCallable; import hudson.Launcher; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.Build; import hudson.model.BuildListener; import hudson.model.Fingerprint; import hudson.model.Fingerprint.BuildPtr; import hudson.model.FingerprintMap; import hudson.model.Hudson; import hudson.model.Result; import hudson.model.Run; import hudson.model.RunAction; import hudson.remoting.VirtualChannel; import hudson.util.FormValidation; import hudson.util.IOException2; import hudson.util.PackedMap; import net.sf.json.JSONObject; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.types.FileSet; 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.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import static com.google.common.base.Preconditions.checkNotNull; /** * Records fingerprints of the specified files. * * @author Kohsuke Kawaguchi */ public class Fingerprinter extends Recorder implements Serializable { /** * Comma-separated list of files/directories to be fingerprinted. */ private final String targets; /** * Also record all the finger prints of the build artifacts. */ private final boolean recordBuildArtifacts; @DataBoundConstructor public Fingerprinter(String targets, boolean recordBuildArtifacts) { this.targets = targets; this.recordBuildArtifacts = recordBuildArtifacts; } public String getTargets() { return targets; } public boolean getRecordBuildArtifacts() { return recordBuildArtifacts; } @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException { try { listener.getLogger().println(Messages.Fingerprinter_Recording()); Map<String, String> record = new HashMap<String, String>(); if (targets.length() != 0) { record(build, listener, record, targets); } if (recordBuildArtifacts) { ArtifactArchiver aa = build.getProject().getPublishersList().get(ArtifactArchiver.class); if (aa == null) { // configuration error listener.error(Messages.Fingerprinter_NoArchiving()); build.setResult(Result.FAILURE); return true; } record(build, listener, record, aa.getArtifacts()); } FingerprintAction.add(build, record); } catch (IOException e) { e.printStackTrace(listener.error(Messages.Fingerprinter_Failed())); build.setResult(Result.FAILURE); } // failing to record fingerprints is an error but not fatal return true; } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.NONE; } private void record(AbstractBuild<?, ?> build, BuildListener 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(AbstractBuild build) throws IOException { FingerprintMap map = Hudson.getInstance().getFingerprintMap(); return map.getOrCreate(produced ? build : null, fileName, md5sum); } private static final long serialVersionUID = 1L; } final long buildTimestamp = build.getTimeInMillis(); FilePath ws = build.getWorkspace(); if (ws == null) { listener.error(Messages.Fingerprinter_NoWorkspace()); build.setResult(Result.FAILURE); return; } List<Record> records = ws.act(new FileCallable<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 IOException2(Messages.Fingerprinter_DigestFailed(file), e); } catch (InterruptedException e) { throw new IOException2(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.add(build); record.put(r.relativePath, fp.getHashString()); } } @Extension public static class DescriptorImpl extends BuildStepDescriptor<Publisher> { public String getDisplayName() { return Messages.Fingerprinter_DisplayName(); } @Override public String getHelpFile() { return "/help/project-config/fingerprint.html"; } /** * Performs on-the-fly validation on the file mask wildcard. */ public FormValidation doCheck(@AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException { 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. * * To ensure there is only one per build use * {@link FingerprintAction#add(AbstractBuild, Map)}. This allows for * additional fingerprint contributions outside of the * {@link Fingerprinter}. */ public static final class FingerprintAction implements RunAction { private final AbstractBuild build; /** * From file name to the digest. */ private /*almost final*/ Map<String, String> record; private transient WeakReference<Map<String, Fingerprint>> ref; public FingerprintAction(AbstractBuild build, Map<String, String> record) { this.build = checkNotNull(build); this.record = PackedMap.of(checkNotNull(record)); } /** * Add fingerprint records to this Action. Assumes the records came from * the same build that initially created the {@link FingerprintAction}. */ public void add(Map<String, String> moreRecords) { Map<String, String> r = new HashMap<String, String>(record); r.putAll(moreRecords); record = PackedMap.of(checkNotNull(r)); ref = null; } /** * Adds the record to a {@link FingerprintAction} corresponding to the * build. * * Safely consolidates multiple sources of records (e.g. from different * post build actions) into a single {@link FingerprintAction}. * * @param build to add the FingerprintAction and records to * @param record to add * * @since 2.1.0 */ public static void add(final AbstractBuild build, final Map<String, String> record) { checkNotNull(build); checkNotNull(record); FingerprintAction action = build.getAction(FingerprintAction.class); if (action != null) { action.add(record); } else { build.addAction(new FingerprintAction(build, record)); } } public String getIconFileName() { return "fingerprint.png"; } public String getDisplayName() { return Messages.Fingerprinter_Action_DisplayName(); } public String getUrlName() { return "fingerprints"; } public AbstractBuild getBuild() { return build; } /** * Obtains the raw data. */ public Map<String, String> getRecords() { return record; } public void onLoad() { // This causes unnecessary loading of previous build. // The compacting to save memory may not be a big saving anymore // after lazy loading improvements. // Run pb = build.getPreviousBuild(); // if (pb != null) { // FingerprintAction a = pb.getAction(FingerprintAction.class); // if (a != null) { // compact(a); // } // } } public void onAttached(Run r) { } public void onBuildComplete() { onLoad(); // make compact } /** * Reuse string instances from another {@link FingerprintAction} to * reduce memory footprint. */ protected void compact(FingerprintAction a) { Map<String, String> intern = new HashMap<String, String>(); // string intern map for (Entry<String, String> e : a.record.entrySet()) { intern.put(e.getKey(), e.getKey()); intern.put(e.getValue(), e.getValue()); } Map<String, String> b = new HashMap<String, String>(); for (Entry<String, String> e : record.entrySet()) { String k = intern.get(e.getKey()); if (k == null) { k = e.getKey(); } String v = intern.get(e.getValue()); if (v == null) { v = e.getValue(); } b.put(k, v); } record = 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; } } Hudson h = Hudson.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 builds in a map. Returns build numbers * instead of {@link Build}, since log records may be gone. */ public Map<AbstractProject, Integer> getDependencies() { 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 } AbstractProject job = bp.getJob(); if (job == null) { continue; // no longer exists } if (job.getParent() == build.getParent()) { continue; // we are the parent of the build owner, that is almost like we are the owner } Integer existing = r.get(job); if (existing != null && existing > bp.getNumber()) { continue; // the record in the map is already up to date } r.put(job, bp.getNumber()); } return r; } } private static final Logger logger = Logger.getLogger(Fingerprinter.class.getName()); private static final long serialVersionUID = 1L; }