Package org.jenkinsci.plugins.workflow.cps

Source Code of org.jenkinsci.plugins.workflow.cps.DSL$ThreadTaskImpl$HeadCollector

/*
* The MIT License
*
* Copyright (c) 2013-2014, CloudBees, Inc.
*
* 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 org.jenkinsci.plugins.workflow.cps;

import com.cloudbees.groovy.cps.Continuable;
import com.cloudbees.groovy.cps.Outcome;
import com.google.common.util.concurrent.FutureCallback;
import groovy.lang.Closure;
import groovy.lang.GString;
import groovy.lang.GroovyObject;
import groovy.lang.GroovyObjectSupport;
import hudson.model.TaskListener;
import org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode;
import org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode;
import org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode;
import org.jenkinsci.plugins.workflow.cps.persistence.PersistIn;
import org.jenkinsci.plugins.workflow.cps.steps.ParallelStep;
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.steps.MissingContextVariableException;
import org.jenkinsci.plugins.workflow.steps.Step;
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
import org.jenkinsci.plugins.workflow.steps.StepExecution;

import java.io.IOException;
import java.io.PrintStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import static org.jenkinsci.plugins.workflow.cps.ThreadTaskResult.*;
import static org.jenkinsci.plugins.workflow.cps.persistence.PersistenceContext.*;
import org.kohsuke.stapler.ClassDescriptor;

/**
* Scaffolding to experiment with the call into {@link Step}.
*
* @author Kohsuke Kawaguchi
*/
@PersistIn(PROGRAM)
public class DSL extends GroovyObjectSupport implements Serializable {
    private final FlowExecutionOwner handle;
    private transient CpsFlowExecution exec;
    private transient Map<String,StepDescriptor> functions;

    public DSL(FlowExecutionOwner handle) {
        this.handle = handle;
    }

    protected Object readResolve() throws IOException {
        return this;
    }

    /**
     * Executes the {@link Step} implementation specified by the name argument.
     *
     * @return
     *      If the step completes execution synchronously, the result will be
     *      returned. Otherwise this method {@linkplain Continuable#suspend(Object) suspends}.
     */
    @Override
    @CpsVmThreadOnly
    public Object invokeMethod(String name, Object args) {
        try {
            if (exec==null)
                exec = (CpsFlowExecution) handle.get();
        } catch (IOException e) {
            throw new Error(e); // TODO
        }

        if (functions == null) {
            functions = new TreeMap<String,StepDescriptor>();
            for (StepDescriptor d : StepDescriptor.all()) {
                functions.put(d.getFunctionName(), d);
            }
        }
        final StepDescriptor d = functions.get(name);
        if (d == null) {
            throw new NoSuchMethodError("No such DSL method " + name + " found among " + functions.keySet());
        }

        final NamedArgsAndClosure ps = parseArgs(d,args);

        CpsThread thread = CpsThread.current();

        FlowNode an;

        // TODO: generalize the notion of Step taking over the FlowNode creation.
        // see https://trello.com/c/v6Pbwqxj/13-allowing-steps-to-build-flownodes
        boolean hack = d instanceof ParallelStep.DescriptorImpl;

        if (ps.body == null && !hack) {
            an = new StepAtomNode(exec, d, thread.head.get());
            // TODO: use CPS call stack to obtain the current call site source location. See JENKINS-23013
            thread.head.setNewHead(an);
        } else {
            an = new StepStartNode(exec, d, thread.head.get());
            thread.head.setNewHead(an);
        }

        final CpsStepContext context = new CpsStepContext(d,thread,handle,an,ps.body);
        Step s;
        boolean sync;
        try {
            d.checkContextAvailability(context);
            s = d.newInstance(ps.namedArgs);
            StepExecution e = s.start(context);
            thread.setStep(e);
            sync = e.start();
        } catch (Exception e) {
            if (e instanceof MissingContextVariableException)
                reportMissingContextVariableException(context, (MissingContextVariableException)e);
            context.onFailure(e);
            s = null;
            sync = true;
        }

        if (sync) {
            assert context.bodyInvokers.isEmpty() : "If a step claims synchronous completion, it shouldn't invoke body";

            if (context.getOutcome()==null) {
                context.onFailure(new AssertionError("Step "+s+" claimed to have ended synchronously, but didn't set the result via StepContext.onSuccess/onFailure"));
            }

            thread.setStep(null);

            // if the execution has finished synchronously inside the start method
            // we just move on accordingly
            if (an instanceof StepStartNode) {
                // no body invoked, so EndNode follows StartNode immediately.
                thread.head.setNewHead(new StepEndNode(exec, (StepStartNode)an, an));
            }

            thread.head.markIfFail(context.getOutcome());

            return context.replay();
        } else {
            // if it's in progress, suspend it until we get invoked later.
            // when it resumes, the CPS caller behaves as if this method returned with the resume value
            Continuable.suspend(new ThreadTaskImpl(context));

            // the above method throws an exception to unwind the call stack, and
            // the control then goes back to CpsFlowExecution.runNextChunk
            // so the execution will never reach here.
            throw new AssertionError();
        }
    }

    /**
     * Reports a user-friendly error message for {@link MissingContextVariableException}.
     */
    private void reportMissingContextVariableException(CpsStepContext context, MissingContextVariableException e) {
        TaskListener tl;
        try {
            tl = context.get(TaskListener.class);
            if (tl==null)       return; // if we can't report an error, give up
        } catch (IOException _) {
            return;
        } catch (InterruptedException _) {
            return;
        }

        StringBuilder names = new StringBuilder();
        for (StepDescriptor p : e.getProviders()) {
            if (names.length()>0)   names.append(',');
            names.append(p.getFunctionName());
        }

        PrintStream logger = tl.getLogger();
        logger.println(e.getMessage());
        if (names.length()>0)
            logger.println("Perhaps you forgot to surround the code with a step that provides this, such as: "+names);
    }

    static class NamedArgsAndClosure {
        final Map<String,Object> namedArgs;
        final Closure body;

        private NamedArgsAndClosure(Map<?,?> namedArgs, Closure body) {
            this.namedArgs = new LinkedHashMap<String,Object>();
            this.body = body;

            for (Map.Entry<?,?> entry : namedArgs.entrySet()) {
                String k = entry.getKey().toString(); // coerces GString and more
                Object v = entry.getValue();
                // coerce GString, to save StepDescriptor.newInstance() from being made aware of that
                // this isn't the only type coercion that Groovy does, so this is not very kosher, but
                // doing a proper coercion like Groovy does require us to know the type that the receiver
                // expects.
                //
                // For the reference, Groovy does:
                //   ReflectionCache.getCachedClass(types[i]).coerceArgument(a)
                if (v instanceof GString) {
                    v = v.toString();
                }
                this.namedArgs.put(k, v);
            }
        }
    }

    /**
     * Given the Groovy style argument packing used in the sole object parameter of {@link GroovyObject#invokeMethod(String, Object)},
     * compute the named argument map and an optional closure that represents the body.
     *
     * <p>
     * Positional arguments are not allowed, unless it has a single argument, in which case
     * it is passed as an argument named "value", that is:
     *
     * <pre>
     * foo(x)  => foo(value:x)
     * </pre>
     *
     * <p>
     * This handling is designed after how Java defines literal syntax for {@link Annotation}.
     */
    static NamedArgsAndClosure parseArgs(StepDescriptor d, Object arg) {
        boolean expectsBlock = d.takesImplicitBlockArgument();

        if (arg instanceof Map) // TODO is this clause actually used?
            return new NamedArgsAndClosure((Map) arg, null);
        if (arg instanceof Closure && expectsBlock)
            return new NamedArgsAndClosure(Collections.<String,Object>emptyMap(),(Closure)arg);

        if (arg instanceof Object[]) {// this is how Groovy appears to pack argument list into one Object for invokeMethod
            List a = Arrays.asList((Object[])arg);
            if (a.size()==0)
                return new NamedArgsAndClosure(Collections.<String,Object>emptyMap(),null);

            Closure c=null;

            Object last = a.get(a.size()-1);
            if (last instanceof Closure && expectsBlock) {
                c = (Closure)last;
                a = a.subList(0,a.size()-1);
            }

            if (a.size()==1 && a.get(0) instanceof Map && !((Map) a.get(0)).containsKey("$class")) {
                // this is how Groovy passes in Map
                return new NamedArgsAndClosure((Map)a.get(0),c);
            }

            switch (a.size()) {
            case 0:
                return new NamedArgsAndClosure(Collections.<String,Object>emptyMap(),c);
            case 1:
                return new NamedArgsAndClosure(singleParam(d, a.get(0)), c);
            default:
                throw new IllegalArgumentException("Expected named arguments but got "+a);
            }
        }

        return new NamedArgsAndClosure(singleParam(d, arg), null);
    }
    private static Map<String,Object> singleParam(StepDescriptor d, Object arg) {
        String[] names = new ClassDescriptor(d.clazz).loadConstructorParamNames();
        if (names.length == 1) {
            return Collections.singletonMap(names[0], arg);
        } else {
            throw new IllegalArgumentException("Expected named arguments but got " + arg);
        }
    }

    /**
     * If the step starts executing asynchronously, this task
     * executes at the safe point to switch {@link CpsStepContext} into the async mode.
     */
    private static class ThreadTaskImpl extends ThreadTask implements Serializable {
        private final CpsStepContext context;

        public ThreadTaskImpl(CpsStepContext context) {
            this.context = context;
        }

        @Override
        protected ThreadTaskResult eval(CpsThread cur) {
            invokeBody(cur);

            if (!context.switchToAsyncMode()) {
                // we have a result now, so just keep executing
                // TODO: if this fails with an exception, we need ability to resume by throwing an exception
                return resumeWith(context.getOutcome());
            } else {
                // beyond this point, StepContext can receive a result at any time and
                // that would result in a call to scheduleNextChunk(). So we the call to
                // switchToAsyncMode to happen inside 'synchronized(lock)', so that
                // the 'executing' variable gets set to null before the scheduleNextChunk call starts going.

                return suspendWith(new Outcome(context,null));
            }
        }

        private void invokeBody(CpsThread cur) {
            // prepare enough heads for all the bodies
            // the first one can reuse the current thread, but other ones need to create new heads
            // we want to do this first before starting body so that the order of heads preserve
            // natural ordering.
            FlowHead[] heads = new FlowHead[context.bodyInvokers.size()];
            for (int i = 0; i < heads.length; i++) {
                heads[i] = i==0 ? cur.head : cur.head.fork();
            }

            int idx=0;
            for (BodyInvoker b : context.bodyInvokers) {
                // don't collect the first head, which is what we borrowed from our parent.
                FlowHead h = heads[idx];
                if (idx>0)
                    b.bodyExecution.prependCallback(new HeadCollector(context, h));
                b.start(cur, h);
                idx++;
            }

            context.bodyInvokers.clear();
        }

        /**
         * When a new {@link CpsThread} that runs the body completes, record
         * its new head.
         */
        private static class HeadCollector implements FutureCallback, Serializable {
            private final CpsStepContext context;
            private final FlowHead head;

            public HeadCollector(CpsStepContext context, FlowHead head) {
                this.context = context;
                this.head = head;
            }

            private void onEnd() {
                head.getExecution().removeHead(head);
                context.bodyInvHeads.put(head.getId(),head.get().getId());
            }

            @Override
            public void onSuccess(Object result) {
                onEnd();
            }

            @Override
            public void onFailure(Throwable t) {
                onEnd();
            }
        }


        private static final long serialVersionUID = 1L;
    }

    private static final long serialVersionUID = 1L;
}
TOP

Related Classes of org.jenkinsci.plugins.workflow.cps.DSL$ThreadTaskImpl$HeadCollector

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.