文章目录
转载请注明
一、历史
Jenkins
是由Java
语言编写的一款开源的持续集成的工具,前身为Sun
公司的Hudson
,众所周知,sun
于2009
年被Oracle
收购,2010
年末其主要贡献者与Oracle
之间发生了内部争执,导致Hudson
于2011
年年初,通过社区投票的方式,将项目名称由Hudson
变更为Jenkins
,但与此同时Oracle
表示也会继续开发原来的Hudson
,导致往后的一段时间,双方均认为对方是自己的复刻版本,到了第二年也就是2012
的一月,Eclipse
基金会将Hudson
纳入,代表Oracle
对Hudson
项目已经失去了兴趣,截止2013
年11
月,Jenkins
已经拥有了远超Hudson
的项目成员以及公共库
二、相关组件解析
Jenkins
作为一款老牌CI/CD
开源产品,历史悠久,也带来的代码量巨大的问题,这里尝试分析Jenkins
源码中的几个核心组件
-
平台基石:
jenkins.model.Jenkins
,它是Jenkins
的主要执行对象,所有的调度,运算,任务都是绑定在它上面。 -
任务执行:在
Jenkins
中,所有的动作都包装为一个一个可执行的任务作业Job
,底层采用线程来实现,其间分的层级也比较多,有:Item->Job->Run->Executor->Thread
- 资源设备:提供
CPU
,内存等资源的设备,相关组件有Label
,Node
,Computer
- 调度策略:如何分配执行任务与资源设备的关系,相关的组件有
Queue
,NodeProvisioner
,MultiStageTimeSeries
,TimeSeries
等
1、平台基石
①、Jenkins
系统的根对象,传承于
Hudson
,维持了某一个Job
,Project
,Executor
,User
,Buildable Item
等等的数据和状态,可以对Jenkins
整个生命周期进行管理
2、任务执行
①、Item&ItemGroup
它是
Jenkins
中基本的配置单元,每一个Item
都会被托管在一个ItemGroup
中,而每一个Item
本身也可以成为一个ItemGroup
,这便形成了一个树状结构,而这个树状结构的根节点便是Jenkins
Item
提供获取父Item
即ItemGroup
的方法
public interface Item extends PersistenceRoot, SearchableModelObject, AccessControlled, OnMaster {
/**
* Gets the parent that contains this item.
*/
ItemGroup<? extends Item> getParent();
(...)
- 一个
ItemGroup
也提供获取所有子Item
的方法
public interface ItemGroup<T extends Item> extends PersistenceRoot, ModelObject {
/**
* Gets all the items in this collection in a read-only view
* that matches supplied Predicate
* @since 2.221
*/
default Collection<T> getItems(Predicate<T> pred) {
return getItemsStream(pred)
.collect(Collectors.toList());
}
(...)
有点像文件系统中的文件夹的感觉,但是不同的是,在文件系统中,文件可以从一个目录移动到另一个目录,
Item
本身只能属于单独的ItemGroup
,这个关系不能改变。
- 在
Windows
设备管理器中,一个硬盘总是显示在磁盘驱动器下方,它永远不可能移动到处理器,显示器等等其他位置,同样的,ItemGroup
也不是一个通用的容器,ItemGroup
的每一个子类通常只能承载某种有限类型的Item
,比如老熟人FreeStyleProject
便是其子类
每一个
Item
都有一个唯一的名称用来区分其他Item
,名称可以用“/”
组合起来,从而形成一个完整的项目名称,并在Jenkins
中来唯一标识。
②、Job
Job
便是Jenkins
下面可追踪的可运行实体,它是静态的概念,包含一次构建的所有配置信息,比如构建的脚本,被构建的源代码,构建所需时间,源代码仓库等等等信息
如果需要自定义一个Job
类型,需要继承TopLevelItemDescriptor
顶级Item
描述器的对象,并为其加上Extension
注解
③、Run
因为
Job
是一个可运行的实体,是一个静态的概念,那么在其运行起来后,对于这个动态的过程便是由Run
来管理,比如在pre build stage
任务执行构建前常做的拉取源代码,post build stage
执行构建完成后执行归档产出物等,这些熟悉的操作都是由Run
来管理,特别地,Run
应该便是对应于Jenkins API
中的Build
同样它也支持自定义,常与自定义Job
来配合使用,毕竟自定义的Job
理所应当需要特定的Run
来执行才行
④、Executor&Executable
Executor
是执行构建的线程,它可以按需执行,继承自Thread
类,Executable
为真正代替Executor
执行计算的对象
Thread
类使用run
开启线程的时候,实际上调用的也是Runnable
接口的run
方法,所以既然Executor
继承自Thread
类,那么就应该还有一个类继承Runnable
,这个类就是Executable
@Override
public void run() {
//agent是否在线
if (!owner.isOnline()) {
(...)
}
//node节点是否为空
if (owner.getNode() == null) {
(...)
}
final WorkUnit workUnit;
//将当前Executor写锁锁住
lock.writeLock().lock();
try {
//记录执行开始时间
startTime = System.currentTimeMillis();
workUnit = this.workUnit;
} finally {
//释放当前Executor写锁
lock.writeLock().unlock();
}
try (ACLContext ctx = ACL.as2(ACL.SYSTEM2)) {
//这个类是一个任务的一部分,代表了一个Executor的一次计算,一个Task包含多个SubTask
SubTask task;
task = Queue.withLock(new Callable<SubTask>() {
@Override
public SubTask call() throws Exception {
if (!owner.isOnline()) {
(...)
}
if (owner.getNode() == null) {
(...)
}
workUnit.setExecutor(Executor.this);
queue.onStartExecuting(Executor.this);
(...)
lock.writeLock().lock();
try {
Executor.this.executable = executable;
} finally {
lock.writeLock().unlock();
}
workUnit.setExecutable(executable);
return task;
}
});
Executable executable;
//将当前Executor读锁锁住
lock.readLock().lock();
try {
if (this.workUnit == null) {
return;
}
executable = this.executable;
} finally {
//读锁解除
lock.readLock().unlock();
}
if (LOGGER.isLoggable(FINE))
LOGGER.log(FINE, getName()+" is going to execute "+executable);
Throwable problems = null;
try {
//同步开始
workUnit.context.synchronizeStart();
if (executable == null) {
return;
}
//估计时间
executableEstimatedDuration = executable.getEstimatedDuration();
(...)
try (ACLContext context = ACL.as2(auth)) {
//真正开始执行
queue.execute(executable, task);
}
} catch (AsynchronousExecution x) {
(...)
} catch (Throwable e) {
problems = e;
} finally {
(...)
}
} catch (InterruptedException e) {
LOGGER.log(FINE, getName()+" interrupted",e);
// die peacefully
} catch(Exception | Error e) {
LOGGER.log(SEVERE, getName()+": Unexpected executor death", e);
} finally {
if (asynchronousExecution == null) {
finish2();
}
}
}
- 而
Thread
类的start
方法,自然也被Executor
所继承下来,但是前者调用的是start
是没有参数的,后者却并不支持直接调用,必须传入一个WorkUnit
对象,否则会抛出UnsupportedOperationException
/**
* Can't start executor like you normally start a thread.
*
* @see #start(WorkUnit)
*/
@Override
public void start() {
throw new UnsupportedOperationException();
}
/*protected*/ void start(WorkUnit task) {
lock.writeLock().lock();
try {
this.workUnit = task;
super.start();
started = true;
} finally {
lock.writeLock().unlock();
}
}
⑤、WorkUnit
表示从队列
Queue
中拿到Task
任务交给给Executor
的基本单位,类结构较为简单,有用于执行的任务SubTask
,有执行线程Executor
和其对应的Executable
,最后还有一个工作单元的共享上下文
⑥、AbstractProject和AbstractBuild
前者表示软件构建的基础抽象类,其主要实现类有熟悉的
Project
和MavenModule
,后者代表软件运行的基础抽象类,其主要的实现类有熟悉的Build
和MavenBuild
⑦、Action
盲猜应该是构建步骤,可以在任务配置中定义
3、资源设备
①、Node
Jenkins agent
的基本类型,在实际操作中,也可以继承它来扩展定义新的agent
类型,主要负责的是最基础的配置,比如ExecutorNum
,NodeName
,Description
②、Label
负责一组
Node
,因为在某些场景中,一个Job
可能由多种语言实现,那么可以用Label
来管理一组Node
,如果遇到算力不足的情况,可以利用其内部属性NodeProvisioner
来初始化新的Node
③、Computer
它和
Node
多少有点关系,但是也有一些显著的差异,Computer
作为一系列Executor
的拥有者,如果一个Node
中没有配置Executor
(可能是暂时的),那么也将不会拥有Computer
对象,如果一个Node
本身是存在的,且其已经执行过若干构建,当把它移除后,相应的Computer
也需要一定时间被删除,在更改节点配置的时候,没有被影响的Computer
依然会保存完整直到所有的Node
都被删除
4、调度策略
①、Queue
这个对象实现了核心调度逻辑,其中的内部接口
Task
表示放置在此队列中的可执行任务,在队列中,Task
可以被包装进另一个内部类Item
中,所以我们可以跟踪额外的用于决定何时执行的其他数据,队列中的任务将经过如下几个阶段
(进入) --> 等待任务队列 --+--> 阻塞任务队列
| ^
| |
| v
+--> 可执行的任务 ---> 挂起 ---> 离开
^ |
| |
+--极少数情况--+
通常,一个任务的执行只能向出队方向移动,但是出现下面这种情况,任务的执行流程也可以回退
- 被分配执行任务的
Jenkins
节点在执行器线程Executable
启动前(或者实例化前)消失了,也就是这一个Jenkins
节点被移除了,那么这个任务就可以回退到一个可执行的状态
换句话说,当执行器线程已经实例化的情况下,唯一能让让执行器线程Executable
停止或销毁的情况是在让其执行的Jenkins
节点不存在
另外,在任何阶段,队列中的节点都可以被移除出去,比如在构建的时候点击取消
②、NodeProvisioner
用于负载统计和决定何时需要增加新的
Node
,Jenkins
需要合理分配Executor
和运算资源,保证运算充足的情况下,不启动冗余的Node
,即使运算需求陡然升高,也会有条不紊的去开启Node
,而NodeProvisioner
便是通过调用Queue
来管理下面的Node
资源,而如何管理资源调度则是交给其内部类StandardStrategyImpl
三、流程调试
Jenkins
所使用的Web
框架,名为Stapler
,这款框架国内很少见,网上的文档也极其稀少,所以这里仅仅做一个大致介绍
Stapler
可以将程序对象Model
绑定到一个URL
上面,它能够自动帮我们的对象Model
分配一个专属URL
,创建一种非常直观的URL
层级结构,这里的Model
我们可以简单理解为对应于一个SpringMVC
中的Controller
1、创建一个FreeStyleProject
Jenkins
的创建Web
页面为http://localhost:8080/jenkins/view/all/newJob
,这个方法绑定到了View
这个Model
上面,但是View
是一个抽象类,具体执行交由其实现类,对应的newJob.jelly
页面查询到这个新建请求会发送给View.createItem
方法
@RequirePOST
@Override
public Item doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
ItemGroup<? extends TopLevelItem> ig = getOwner().getItemGroup();
if (ig instanceof ModifiableItemGroup)
return ((ModifiableItemGroup<? extends TopLevelItem>)ig).doCreateItem(req, rsp);
return null;
}
方法会继续走到
Jenkins
对象的doCreateItem
方法,然后继续走到ItemGroupMixIn
的createTopLevelItem
方法,这个方法会对这个工程进行若干合法性校验,校验通过会走到createProject
方法
public synchronized TopLevelItem createTopLevelItem( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
(...)
if(name==null)
throw new Failure("Query parameter 'name' is required");
{// check if the name looks good
(...)
}
if(mode!=null && mode.equals("copy")) {
(...)
} else {
if(isXmlSubmission) {
(...)
} else {
(...)
// 这一步真正开始创建
result = createProject(descriptor, name, true);
}
}
rsp.sendRedirect2(redirectAfterCreateItem(req, result));
return result;
}
createProject
方法会对创建的工程进一步做校验,如果合法,则调用newInstance
进行创建
public synchronized TopLevelItem createProject( TopLevelItemDescriptor type, String name, boolean notify )
throws IOException {
(...)
TopLevelItem item = type.newInstance(parent, name);
item.onCreatedFromScratch();
item.save();
add(item);
Jenkins.get().rebuildDependencyGraphAsync();
if (notify)
ItemListener.fireOnCreated(item);
return item;
}
创建工程完毕后,会进入
http://localhost:8080/jenkins/job/Test/configure
对工程进行若干配置,这一步对应的页面为configure.jelly
页面,可以看到,当配置完成点击保存后,会把请求发送到configSubmit
方法
根据
Stapler
的特性,这个请求肯定会发送到Job
这个对象中,实际上,断点进入的方法为AbstractProject
类,重写自Job
的doConfigSubmit
方法
@Override
@POST
public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException {
super.doConfigSubmit(req,rsp);
updateTransientActions();
// notify the queue as the project might be now tied to different node
Jenkins.get().getQueue().scheduleMaintenance();
// this is to reflect the upstream build adjustments done above
Jenkins.get().rebuildDependencyGraphAsync();
}
给这个任务配置一个构建脚本
echo helloworld >> hello.txt
2、构建调度调试
对这个任务执行构建,断点走到
ParameterizedJobMixIn
对象的doBuild
方法
default void doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException {
getParameterizedJobMixIn().doBuild(req, rsp, delay);
}
这个方法会将当前构建任务从
Queue
中拿出来然后使用schedule2
方法将这个构建任务放入一个WaitingItem
的等待队列中,随时可以交给Jenkins
来执行
public final void doBuild(StaplerRequest req, StaplerResponse rsp, @QueryParameter TimeDuration delay) throws IOException, ServletException {
(...)
Queue.Item item = Jenkins.get().getQueue().schedule2(asJob(), delay.getTimeInSeconds(), getBuildCause(asJob(), req)).getItem();
if (item != null) {
rsp.sendRedirect(SC_CREATED, req.getContextPath() + '/' + item.getUrl());
} else {
rsp.sendRedirect(".");
}
}
schedule2
方法嵌套了好几层,不过始终都是在当前Queue
对象下,真正提交构建任务的核心代码在scheduleInternal
方法上,然后内部调用scheduleMaintenance
方法提交当前构建任务
private @NonNull ScheduleResult scheduleInternal(Task p, int quietPeriod, List<Action> actions) {
(...)
scheduleMaintenance(); // let an executor know that a new item is in the queue.
(...)
}
public Future<?> scheduleMaintenance() {
return maintainerThread.submit();
}
submit方法也还没有结束
public synchronized Future<V> submit() {
if (pending==null) {
pending = new CompletableFuture<>();
maybeRun();
}
return pending;
}
还要执行一个
maybeRun
方法,这里的submit方法会将当前任务正式提交进入等待队列,其中的参数是一个带回调的异步的方法,在这个方法中会继续直接调用task.call()
创建一个新的FutureTask
来将刚才提交的任务进行构建
private synchronized void maybeRun() {
if (inprogress==null && pending!=null) {
base.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
synchronized (AtmostOneTaskExecutor.this) {
// everyone who submits after this should form a next batch
inprogress = pending;
pending = null;
}
try {
(断点) inprogress.complete(task.call());
} catch (Throwable t) {
LOGGER.log(Level.WARNING, null, t);
inprogress.completeExceptionally(t);
} finally {
synchronized (AtmostOneTaskExecutor.this) {
// if next one is pending, get that scheduled
inprogress = null;
maybeRun();
}
}
return null;
}
});
}
}
正常情况下,当前
agent
的executor
还有空闲的时候,这里是可以直接创建出我们想要的FutureTask
来执行这个任务,执行构建的过程下面会聊,这里讲一下调度的特殊情况,当系统资源不足的情况下,即executor
没有空闲,这段代码显然并不会立马执行,那么我们将断点打到下面代码的低13
行,让程序停在这里,模拟了一下系统延迟执行的情况
- 在聊特殊情况之前,首先了解一下
Jenkins
架构中的另一个抽象类SafeTimerTask
,这个类是一个全局共享的定时器,他会每隔若干时间执行一个方法,在Queue
类中有一个内部类为MaintainTask
,它继承了这个抽象类,实现了doRun
方法,并且在periodic
方法中定义好了循环周期为五秒,也就是说MaintainTask对象每隔五秒就会执行一下doRun
方法
private static class MaintainTask extends SafeTimerTask {
private final WeakReference<Queue> queue;
MaintainTask(Queue queue) {
this.queue = new WeakReference<>(queue);
}
private void periodic() {
long interval = 5000;
Timer.get().scheduleWithFixedDelay(this, interval, interval, TimeUnit.MILLISECONDS);
}
@Override
protected void doRun() {
Queue q = queue.get();
if (q != null)
q.maintain();
else
cancel();
}
}
当程序停下来后,我们就会发现,程序就会执行到这个
doRun
方法,那么这个方法的核心便是maintain
方法,这个方法非常长,在整个Queue
类中都应该算是一个很核心的方法,你长任你长,将这个方法简化,我们关注这一段代码:
public void maintain() {
for (BuildableItem p : new ArrayList<>(
buildables)) {
(...)
WorkUnitContext wuc = new WorkUnitContext(p);
MappingWorksheet ws = new MappingWorksheet(p, candidates);
Mapping m = loadBalancer.map(p.task, ws);
m.execute(wuc);
(...)
}
}
Jenkins
会将当前等待队列中一个一个拿出可构建的任务出来,然后调用execute
方法执行
private void execute(WorkChunk wc, WorkUnitContext wuc) {
assert capacity() >= wc.size();
int e = 0;
for (SubTask s : wc) {
while (!get(e).isAvailable())
e++;
get(e++).set(wuc.createWorkUnit(s));
}
}
这段代码需要关注这里的
set
方法,这里就可以看到调用了excutor.start()方
法开始构建,还记得介绍Executor
的时候有提到,要使用它开启一个线程,需要调用的是start
的单参方法,无参的start
会报UnsupportedOperationException
,所以这里便传入了一个WorkUnit
对象来开启线程执行任务
@Override
protected void set(WorkUnit p) {
assert this.workUnit == null;
this.workUnit = p;
assert executor.isParking();
executor.start(workUnit);
// LOGGER.info("Starting "+executor.getName());
}
3、构建开始调试
前面讲到的两种方式最终都会调用
Executor
来执行构建任务,Executor
开始执行的时候执行的是run
方法,这段代码也非常长,所以还是将其简化,关注这段代码即可
@Override
public void run() {
(...)
startTime = System.currentTimeMillis();
workUnit = this.workUnit;
SubTask task;
task = Queue.withLock(new Callable<SubTask>() {
@Override
public SubTask call() throws Exception {
workUnit.setExecutor(Executor.this);
queue.onStartExecuting(Executor.this);
SubTask task = workUnit.work;
Executable executable = task.createExecutable();
lock.writeLock().lock();
try {
Executor.this.executable = executable;
} finally {
lock.writeLock().unlock();
}
workUnit.setExecutable(executable);
return task;
}
});
Executable executable;
executable = this.executable;
Throwable problems = null;
executableEstimatedDuration = executable.getEstimatedDuration();
try (ACLContext context = ACL.as2(auth)) {
queue.execute(executable, task);
}
(...)
}
将当前
Executor
中的Executable
对象拿到,然后就进入execute
方法执行构建
public void execute(@NonNull Runnable task, final ResourceActivity activity ) throws InterruptedException {
(...)
try {
task.run();
} finally {
(...)
}
}
task
是传入的一个executable
对象,这个对象实现了Runnable
接口,然后调用其run
方法,run
方法还会执行一个execute
方法,如下
protected final void execute(@NonNull RunExecution job) {
if(result!=null)
return; // already built.
OutputStream logger = null;
StreamBuildListener listener=null;
runner = job;
onStartBuilding();
try {
long start = System.currentTimeMillis();
try {
try {
Computer computer = Computer.currentComputer();
Charset charset = null;
if (computer != null) {
charset = computer.getDefaultCharset();
this.charset = charset.name();
}
logger = createLogger();
listener = createBuildListener(job, logger, charset);
listener.started(getCauses());
Authentication auth = Jenkins.getAuthentication2();
if (auth.equals(ACL.SYSTEM2)) {
listener.getLogger().println(Messages.Run_running_as_SYSTEM());
} else {
String id = auth.getName();
if (!auth.equals(Jenkins.ANONYMOUS2)) {
final User usr = User.getById(id, false);
if (usr != null) { // Encode user hyperlink for existing users
id = ModelHyperlinkNote.encodeTo(usr);
}
}
listener.getLogger().println(Messages.Run_running_as_(id));
}
RunListener.fireStarted(this,listener);
//执行入口
setResult(job.run(listener));
(...)
} catch (ThreadDeath t) {
(...)
}
} catch (ThreadDeath t) {
throw t;
} catch( Throwable e ) {
(...)
} finally {
long end = System.currentTimeMillis();
(...)
}
try {
getParent().logRotate();
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Failed to rotate log",e);
}
} finally {
(...)
}
}
在这个
execute
方法中,会将当前执行的Computer
对象拿到,然后还会创建若干的Listenner
,如StreamBuildListener
,RunListener
等,用于构建期间发生的一些事件的监听,完成这些前置工作后,调用job
对象run
方法
@Override
public Result run(@NonNull BuildListener listener) throws Exception {
final Node node = getCurrentNode();
assert builtOn==null;
builtOn = node.getNodeName();
hudsonVersion = Jenkins.VERSION;
this.listener = listener;
Result result = null;
buildEnvironments = new ArrayList<>();
TearDownCheckEnvironment tearDownMarker = new TearDownCheckEnvironment();
buildEnvironments.add(tearDownMarker);
try {
launcher = createLauncher(listener);
if (!Jenkins.get().getNodes().isEmpty()) {
if (node instanceof Jenkins) {
listener.getLogger().print(Messages.AbstractBuild_BuildingOnMaster());
} else {
listener.getLogger().print(Messages.AbstractBuild_BuildingRemotely(ModelHyperlinkNote.encodeTo("/computer/" + builtOn, node.getDisplayName())));
Set<LabelAtom> assignedLabels = new HashSet<>(node.getAssignedLabels());
assignedLabels.remove(node.getSelfLabel());
if (!assignedLabels.isEmpty()) {
boolean first = true;
for (LabelAtom label : assignedLabels) {
if (first) {
listener.getLogger().print(" (");
first = false;
} else {
listener.getLogger().print(' ');
}
listener.getLogger().print(label.getName());
}
listener.getLogger().print(')');
}
}
} else {
listener.getLogger().print(Messages.AbstractBuild_Building());
}
lease = decideWorkspace(node, Computer.currentComputer().getWorkspaceList());
workspace = lease.path.getRemote();
listener.getLogger().println(Messages.AbstractBuild_BuildingInWorkspace(workspace));
for (WorkspaceListener wl : WorkspaceListener.all()) {
wl.beforeUse(AbstractBuild.this, lease.path, listener);
}
getProject().getScmCheckoutStrategy().preCheckout(AbstractBuild.this, launcher, this.listener);
getProject().getScmCheckoutStrategy().checkout(this);
if (!preBuild(listener,project.getProperties()))
return Result.FAILURE;
result = doRun(listener);
} finally {
if (!tearDownMarker.tornDown) {
result = Result.combine(result, tearDownBuildEnvironments(listener));
}
}
if (node.getChannel() != null) {
launcher.kill(getCharacteristicEnvVars());
}
if (result==null) result = getResult();
if (result==null) result = Result.SUCCESS;
return result;
}
这段代码会获取到构建的时候所需要的一些环境资源
Environment
,还会拿到当前工作空间workspace
目录,如果配置了相关的构建前检出步骤,还会在这里执行,最后再调用doRun
方法,返回值为当前任务是否执行成功,接下来的调用嵌套便很繁琐了,关系是:doRun()->build()->perform()->…->perform()
public boolean perform(AbstractBuild<?,?> build, Launcher launcher, TaskListener listener) throws InterruptedException {
FilePath ws = build.getWorkspace();
if (ws == null) {
Node node = build.getBuiltOn();
if (node == null) {
throw new NullPointerException("no such build node: " + build.getBuiltOnStr());
}
throw new NullPointerException("no workspace from node " + node + " which is computer " + node.toComputer() + " and has channel " + node.getChannel());
}
FilePath script=null;
int r = -1;
try {
try {
script = createScriptFile(ws);
} catch (IOException e) {
Util.displayIOException(e,listener);
Functions.printStackTrace(e, listener.fatalError(Messages.CommandInterpreter_UnableToProduceScript()));
return false;
}
try {
EnvVars envVars = build.getEnvironment(listener);
for(Map.Entry<String,String> e : build.getBuildVariables().entrySet())
envVars.put(e.getKey(),e.getValue());
launcher.prepareFilterRules(build, this);
Launcher.ProcStarter procStarter = launcher.launch();
procStarter.cmds(buildCommandLine(script))
.envs(envVars)
.stdout(listener)
.pwd(ws);
try {
Proc proc = procStarter.start();
r = join(proc);
} catch (EnvVarsFilterException se) {
LOGGER.log(Level.FINE, "Environment variable filtering failed", se);
return false;
}
if(isErrorlevelForUnstableBuild(r)) {
build.setResult(Result.UNSTABLE);
r = 0;
}
} catch (IOException e) {
(...)
}
return r==0;
} finally {
(...)
}
}
到这一步就已经开始执行我们配置的
shell
命令了,还可以继续再深入,感兴趣就继续跟下去吧