Fingerprinter.java 16.3 KB
Newer Older
K
kohsuke 已提交
1 2 3
/*
 * The MIT License
 * 
4
 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi
K
kohsuke 已提交
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 * 
 * 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.
 */
K
kohsuke 已提交
24 25
package hudson.tasks;

26
import com.google.common.collect.ImmutableMap;
27
import hudson.EnvVars;
28
import hudson.Extension;
K
kohsuke 已提交
29
import hudson.FilePath;
30
import hudson.Functions;
31
import jenkins.MasterToSlaveFileCallable;
K
kohsuke 已提交
32
import hudson.Launcher;
33
import jenkins.util.SystemProperties;
34
import hudson.Util;
K
kohsuke 已提交
35 36
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
37
import hudson.model.Action;
38
import jenkins.model.DependencyDeclarer;
39 40
import hudson.model.DependencyGraph;
import hudson.model.DependencyGraph.Dependency;
K
kohsuke 已提交
41
import hudson.model.Fingerprint;
K
kohsuke 已提交
42
import hudson.model.Fingerprint.BuildPtr;
K
kohsuke 已提交
43
import hudson.model.FingerprintMap;
44
import hudson.model.Job;
45
import jenkins.model.Jenkins;
K
kohsuke 已提交
46
import hudson.model.Result;
47
import hudson.model.Run;
48
import hudson.model.TaskListener;
K
kohsuke 已提交
49
import hudson.remoting.VirtualChannel;
K
kohsuke 已提交
50
import hudson.util.FormValidation;
51
import hudson.util.PackedMap;
52
import hudson.util.RunList;
53
import net.sf.json.JSONObject;
54
import org.acegisecurity.AccessDeniedException;
K
kohsuke 已提交
55 56
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.types.FileSet;
K
Kohsuke Kawaguchi 已提交
57
import org.jenkinsci.Symbol;
K
kohsuke 已提交
58
import org.kohsuke.stapler.AncestorInPath;
59
import org.kohsuke.stapler.DataBoundConstructor;
K
kohsuke 已提交
60
import org.kohsuke.stapler.QueryParameter;
61
import org.kohsuke.stapler.StaplerRequest;
K
kohsuke 已提交
62 63 64

import java.io.File;
import java.io.IOException;
K
kohsuke 已提交
65
import java.io.Serializable;
K
kohsuke 已提交
66
import java.lang.ref.WeakReference;
K
kohsuke 已提交
67
import java.util.ArrayList;
K
kohsuke 已提交
68
import java.util.HashMap;
69
import java.util.HashSet;
K
kohsuke 已提交
70
import java.util.List;
71
import java.util.ListIterator;
K
kohsuke 已提交
72 73
import java.util.Map;
import java.util.Map.Entry;
74
import java.util.Random;
75
import java.util.Set;
K
kohsuke 已提交
76
import java.util.TreeMap;
K
kohsuke 已提交
77 78
import java.util.logging.Level;
import java.util.logging.Logger;
79
import jenkins.model.RunAction2;
80
import jenkins.tasks.SimpleBuildStep;
K
kohsuke 已提交
81 82 83 84 85 86

/**
 * Records fingerprints of the specified files.
 *
 * @author Kohsuke Kawaguchi
 */
87
public class Fingerprinter extends Recorder implements Serializable, DependencyDeclarer, SimpleBuildStep {
88
    public static boolean enableFingerprintsInDependencyGraph = SystemProperties.getBoolean(Fingerprinter.class.getName() + ".enableFingerprintsInDependencyGraph");
89
    
K
kohsuke 已提交
90 91 92 93 94
    /**
     * Comma-separated list of files/directories to be fingerprinted.
     */
    private final String targets;

95 96
    @Deprecated
    Boolean recordBuildArtifacts;
K
kohsuke 已提交
97

98
    @DataBoundConstructor public Fingerprinter(String targets) {
K
kohsuke 已提交
99
        this.targets = targets;
100 101 102 103 104
    }

    @Deprecated
    public Fingerprinter(String targets, boolean recordBuildArtifacts) {
        this(targets);
K
kohsuke 已提交
105 106 107 108 109 110 111
        this.recordBuildArtifacts = recordBuildArtifacts;
    }

    public String getTargets() {
        return targets;
    }

112
    @Deprecated
K
kohsuke 已提交
113
    public boolean getRecordBuildArtifacts() {
114
        return recordBuildArtifacts != null && recordBuildArtifacts;
K
kohsuke 已提交
115 116
    }

117
    @Override
118
    public void perform(Run<?,?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws InterruptedException {
K
kohsuke 已提交
119
        try {
K
i18n  
kohsuke 已提交
120
            listener.getLogger().println(Messages.Fingerprinter_Recording());
K
kohsuke 已提交
121

K
kohsuke 已提交
122
            Map<String,String> record = new HashMap<String,String>();
123 124 125 126
            
            EnvVars environment = build.getEnvironment(listener);
            if(targets.length()!=0) {
                String expandedTargets = environment.expand(targets);
127
                record(build, workspace, listener, record, expandedTargets);
128
            }
K
kohsuke 已提交
129

130 131 132 133 134 135
            FingerprintAction fingerprintAction = build.getAction(FingerprintAction.class);
            if (fingerprintAction != null) {
                fingerprintAction.add(record);
            } else {
                build.addAction(new FingerprintAction(build,record));
            }
K
kohsuke 已提交
136

137
            if (enableFingerprintsInDependencyGraph) {
138
                Jenkins.getInstance().rebuildDependencyGraphAsync();
139
            }
K
kohsuke 已提交
140
        } catch (IOException e) {
141
            Functions.printStackTrace(e, listener.error(Messages.Fingerprinter_Failed()));
K
kohsuke 已提交
142 143
            build.setResult(Result.FAILURE);
        }
K
kohsuke 已提交
144

K
kohsuke 已提交
145
        // failing to record fingerprints is an error but not fatal
K
kohsuke 已提交
146 147
    }

K
kohsuke 已提交
148 149 150 151
    public BuildStepMonitor getRequiredMonitorService() {
        return BuildStepMonitor.NONE;
    }

152
    public void buildDependencyGraph(AbstractProject owner, DependencyGraph graph) {
153 154 155 156 157 158
        if (enableFingerprintsInDependencyGraph) {
            RunList builds = owner.getBuilds();
            Set<String> seenUpstreamProjects = new HashSet<String>();

            for ( ListIterator iter = builds.listIterator(); iter.hasNext(); ) {
                Run build = (Run) iter.next();
K
Kohsuke Kawaguchi 已提交
159
                for (FingerprintAction action : build.getActions(FingerprintAction.class)) {
160
                    for (AbstractProject key : action.getDependencies().keySet()) {
161 162 163 164
                        if (key == owner) {
                            continue;   // Avoid self references
                        }

165
                        AbstractProject p = key;
166 167
                        // TODO is this harmful to call unconditionally, so it would apply also to MavenModule for example?
                        if (key.getClass().getName().equals("hudson.matrix.MatrixConfiguration")) {
168 169 170 171 172 173 174 175 176 177
                            p = key.getRootProject();
                        }

                        if (seenUpstreamProjects.contains(p.getName())) {
                            continue;
                        }

                        seenUpstreamProjects.add(p.getName());
                        graph.addDependency(new Dependency(p, owner) {
                            @Override
178
                            public boolean shouldTriggerBuild(AbstractBuild build,
179 180 181 182 183 184 185 186 187 188 189 190
                                                              TaskListener listener,
                                                              List<Action> actions) {
                                // Fingerprints should not trigger builds.
                                return false;
                            }
                        });
                    }
                }
            }
        }
    }

191
    private void record(Run<?,?> build, FilePath ws, TaskListener listener, Map<String,String> record, final String targets) throws IOException, InterruptedException {
K
kohsuke 已提交
192 193 194 195
        final class Record implements Serializable {
            final boolean produced;
            final String relativePath;
            final String fileName;
K
kohsuke 已提交
196
            final String md5sum;
K
kohsuke 已提交
197

K
kohsuke 已提交
198
            public Record(boolean produced, String relativePath, String fileName, String md5sum) {
K
kohsuke 已提交
199 200 201 202 203
                this.produced = produced;
                this.relativePath = relativePath;
                this.fileName = fileName;
                this.md5sum = md5sum;
            }
K
kohsuke 已提交
204

205
            Fingerprint addRecord(Run build) throws IOException {
206
                FingerprintMap map = Jenkins.getInstance().getFingerprintMap();
K
kohsuke 已提交
207 208
                return map.getOrCreate(produced?build:null, fileName, md5sum);
            }
K
kohsuke 已提交
209

K
kohsuke 已提交
210 211
            private static final long serialVersionUID = 1L;
        }
K
kohsuke 已提交
212

213
        final long buildTimestamp = build.getTimeInMillis();
K
kohsuke 已提交
214

215
        List<Record> records = ws.act(new MasterToSlaveFileCallable<List<Record>>() {
K
kohsuke 已提交
216 217 218
            public List<Record> invoke(File baseDir, VirtualChannel channel) throws IOException {
                List<Record> results = new ArrayList<Record>();

219
                FileSet src = Util.createFileSet(baseDir,targets);
K
kohsuke 已提交
220

221
                DirectoryScanner ds = src.getDirectoryScanner();
K
kohsuke 已提交
222 223 224 225 226
                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.
K
kohsuke 已提交
227 228
                    // 2000ms is an error margin since since VFAT only retains timestamp at 2sec precision
                    boolean produced = buildTimestamp <= file.lastModified()+2000;
K
kohsuke 已提交
229 230

                    try {
K
kohsuke 已提交
231
                        results.add(new Record(produced,f,file.getName(),new FilePath(file).digest()));
K
kohsuke 已提交
232
                    } catch (IOException e) {
233
                        throw new IOException(Messages.Fingerprinter_DigestFailed(file),e);
K
kohsuke 已提交
234
                    } catch (InterruptedException e) {
235
                        throw new IOException(Messages.Fingerprinter_Aborted(),e);
K
kohsuke 已提交
236
                    }
K
kohsuke 已提交
237 238
                }

K
kohsuke 已提交
239 240 241 242 243 244 245
                return results;
            }
        });

        for (Record r : records) {
            Fingerprint fp = r.addRecord(build);
            if(fp==null) {
K
i18n  
kohsuke 已提交
246
                listener.error(Messages.Fingerprinter_FailedFor(r.relativePath));
K
kohsuke 已提交
247
                continue;
K
kohsuke 已提交
248
            }
249
            fp.addFor(build);
K
kohsuke 已提交
250
            record.put(r.relativePath,fp.getHashString());
K
kohsuke 已提交
251 252 253
        }
    }

K
Kohsuke Kawaguchi 已提交
254
    @Extension @Symbol("fingerprint")
255
    public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
K
kohsuke 已提交
256
        public String getDisplayName() {
K
i18n  
kohsuke 已提交
257
            return Messages.Fingerprinter_DisplayName();
K
kohsuke 已提交
258 259
        }

260 261 262
        @Deprecated
        public FormValidation doCheck(@AncestorInPath AbstractProject project, @QueryParameter String value) throws IOException {
            return doCheckTargets(project, value);
K
kohsuke 已提交
263 264
        }

265
        public FormValidation doCheckTargets(@AncestorInPath AbstractProject<?,?> project, @QueryParameter String value) throws IOException {
266 267 268
            if (project == null) {
                return FormValidation.ok();
            }
K
kohsuke 已提交
269
            return FilePath.validateFileMask(project.getSomeWorkspace(),value);
270 271
        }

272
        @Override
273
        public Publisher newInstance(StaplerRequest req, JSONObject formData) {
274
            return req.bindJSON(Fingerprinter.class, formData);
K
kohsuke 已提交
275
        }
276 277

        public boolean isApplicable(Class<? extends AbstractProject> jobType) {
278
            return true;
279
        }
280
    }
K
kohsuke 已提交
281 282 283 284

    /**
     * Action for displaying fingerprints.
     */
285
    public static final class FingerprintAction implements RunAction2 {
K
kohsuke 已提交
286

287
        private transient Run build;
288
        
289 290
        private static final Random rand = new Random();

K
kohsuke 已提交
291 292 293
        /**
         * From file name to the digest.
         */
294
        private /*almost final*/ PackedMap<String,String> record;
K
kohsuke 已提交
295 296 297

        private transient WeakReference<Map<String,Fingerprint>> ref;

298
        public FingerprintAction(Run build, Map<String, String> record) {
K
kohsuke 已提交
299
            this.build = build;
300
            this.record = compact(record);
K
kohsuke 已提交
301 302
        }

303 304 305 306 307
        @Deprecated
        public FingerprintAction(AbstractBuild build, Map<String, String> record) {
            this((Run) build, record);
        }

308 309 310
        public void add(Map<String,String> moreRecords) {
            Map<String,String> r = new HashMap<String, String>(record);
            r.putAll(moreRecords);
311
            record = compact(r);
312 313 314
            ref = null;
        }

K
kohsuke 已提交
315
        public String getIconFileName() {
316
            return "fingerprint.png";
K
kohsuke 已提交
317 318 319
        }

        public String getDisplayName() {
K
i18n  
kohsuke 已提交
320
            return Messages.Fingerprinter_Action_DisplayName();
K
kohsuke 已提交
321 322 323 324 325 326
        }

        public String getUrlName() {
            return "fingerprints";
        }

327
        public Run getRun() {
K
kohsuke 已提交
328 329 330
            return build;
        }

331 332 333 334 335
        @Deprecated
        public AbstractBuild getBuild() {
            return build instanceof AbstractBuild ? (AbstractBuild) build : null;
        }

336 337 338 339
        /**
         * Obtains the raw data.
         */
        public Map<String,String> getRecords() {
340
            return record;
341 342
        }

343
        @Override public void onLoad(Run<?,?> r) {
344
            build = r;
345
            record = compact(record);
346 347 348 349 350 351
        }

        @Override public void onAttached(Run<?,?> r) {
            // for historical reasons this setup is done in the constructor instead
        }

352 353 354
        /** 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>();
355
            for (Entry<String,String> e : record.entrySet()) {
356
                b.put(e.getKey().intern(), e.getValue().intern());
357
            }
358
            return PackedMap.of(b);
359 360
        }

K
kohsuke 已提交
361
        /**
K
kohsuke 已提交
362
         * Map from file names of the fingerprinted file to its fingerprint record.
K
kohsuke 已提交
363 364 365 366 367 368 369 370
         */
        public synchronized Map<String,Fingerprint> getFingerprints() {
            if(ref!=null) {
                Map<String,Fingerprint> m = ref.get();
                if(m!=null)
                    return m;
            }

371
            Jenkins h = Jenkins.getInstance();
K
kohsuke 已提交
372 373 374 375

            Map<String,Fingerprint> m = new TreeMap<String,Fingerprint>();
            for (Entry<String, String> r : record.entrySet()) {
                try {
376 377 378
                    Fingerprint fp = h._getFingerprint(r.getValue());
                    if(fp!=null)
                        m.put(r.getKey(), fp);
K
kohsuke 已提交
379 380 381 382 383
                } catch (IOException e) {
                    logger.log(Level.WARNING,e.getMessage(),e);
                }
            }

384
            m = ImmutableMap.copyOf(m);
K
kohsuke 已提交
385 386 387 388 389
            ref = new WeakReference<Map<String,Fingerprint>>(m);
            return m;
        }

        /**
390
         * Gets the dependency to other existing builds in a map.
K
kohsuke 已提交
391
         */
392
        public Map<AbstractProject,Integer> getDependencies() {
393 394 395 396 397 398 399 400
            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
401
         * @since 1.430
402
         */
403 404
        public Map<AbstractProject,Integer> getDependencies(boolean includeMissing) {
            Map<AbstractProject,Integer> r = new HashMap<AbstractProject,Integer>();
K
kohsuke 已提交
405 406 407 408 409

            for (Fingerprint fp : getFingerprints().values()) {
                BuildPtr bp = fp.getOriginal();
                if(bp==null)    continue;       // outside Hudson
                if(bp.is(build))    continue;   // we are the owner
410 411

                try {
412
                    Job job = bp.getJob();
413
                    if (job==null)  continue;   // project no longer exists
414 415 416 417 418
                    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;
                    }
419 420 421 422 423 424 425 426
                    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
427
                    r.put((AbstractProject) job, bp.getNumber());
428 429 430 431 432
                } catch (AccessDeniedException e) {
                    // Need to log in to access this job, so ignore
                    continue;
                }

K
kohsuke 已提交
433 434 435 436 437 438 439
            }
            
            return r;
        }
    }

    private static final Logger logger = Logger.getLogger(Fingerprinter.class.getName());
K
kohsuke 已提交
440 441

    private static final long serialVersionUID = 1L;
K
kohsuke 已提交
442
}