- 一、历史
- 二、相关组件解析
- 1、平台基石
- ①、Jenkins
- 2、任务执行
- ①、Item&ItemGroup
- ②、Job
- ③、Run
- ④、Executor&Executable
- ⑤、WorkUnit
- ⑥、AbstractProject和AbstractBuild
- ⑦、Action
- 3、资源设备
- ①、Node
- ②、Label
- ③、Computer
- 4、调度策略
- ①、Queue
- ②、NodeProvisioner
- 三、流程调试
- 1、创建一个FreeStyleProject
- 2、构建调度调试
- 3、构建开始调试
- 总结
转载请注明 一、历史
二、相关组件解析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等
2、任务执行 ①、Item&ItemGroup系统的根对象,传承于Hudson,维持了某一个Job,Project,Executor,User,Buildable Item等等的数据和状态,可以对Jenkins整个生命周期进行管理
它是Jenkins中基本的配置单元,每一个Item都会被托管在一个ItemGroup中,而每一个Item本身也可以成为一个ItemGroup,这便形成了一个树状结构,而这个树状结构的根节点便是Jenkins
- Item提供获取父Item即ItemGroup的方法
public interface Item extends PersistenceRoot, SearchableModelObject, AccessControlled, OnMaster {
ItemGroup extends Item> getParent();
(...)
- 一个ItemGroup也提供获取所有子Item的方法
public interface ItemGroupextends PersistenceRoot, ModelObject { default Collection getItems(Predicate pred) { return getItemsStream(pred) .collect(Collectors.toList()); } (...)
有点像文件系统中的文件夹的感觉,但是不同的是,在文件系统中,文件可以从一个目录移动到另一个目录,Item本身只能属于单独的ItemGroup,这个关系不能改变。
- 在Windows设备管理器中,一个硬盘总是显示在磁盘驱动器下方,它永远不可能移动到处理器,显示器等等其他位置,同样的,ItemGroup也不是一个通用的容器,ItemGroup的每一个子类通常只能承载某种有限类型的Item,比如老熟人FreeStyleProject便是其子类
②、Job每一个Item都有一个唯一的名称用来区分其他Item,名称可以用“/”组合起来,从而形成一个完整的项目名称,并在Jenkins中来唯一标识。
Job便是Jenkins下面可追踪的可运行实体,它是静态的概念,包含一次构建的所有配置信息,比如构建的脚本,被构建的源代码,构建所需时间,源代码仓库等等等信息
如果需要自定义一个Job类型,需要继承TopLevelItemDescriptor顶级Item描述器的对象,并为其加上Extension注解
③、Run因为Job是一个可运行的实体,是一个静态的概念,那么在其运行起来后,对于这个动态的过程便是由Run来管理,比如在pre build stage任务执行构建前常做的拉取源代码,post build stage执行构建完成后执行归档产出物等,这些熟悉的操作都是由Run来管理,特别地,Run应该便是对应于Jenkins API中的Build
同样它也支持自定义,常与自定义Job来配合使用,毕竟自定义的Job理所应当需要特定的Run来执行才行
④、Executor&ExecutableExecutor是执行构建的线程,它可以按需执行,继承自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() {
@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
@Override
public void start() {
throw new UnsupportedOperationException();
}
void start(WorkUnit task) {
lock.writeLock().lock();
try {
this.workUnit = task;
super.start();
started = true;
} finally {
lock.writeLock().unlock();
}
}
⑤、WorkUnit
⑥、AbstractProject和AbstractBuild表示从队列Queue中拿到Task任务交给给Executor的基本单位,类结构较为简单,有用于执行的任务SubTask,有执行线程Executor和其对应的Executable,最后还有一个工作单元的共享上下文
⑦、Action前者表示软件构建的基础抽象类,其主要实现类有熟悉的Project和MavenModule,后者代表软件运行的基础抽象类,其主要的实现类有熟悉的Build和MavenBuild
3、资源设备 ①、Node盲猜应该是构建步骤,可以在任务配置中定义
②、LabelJenkins agent的基本类型,在实际操作中,也可以继承它来扩展定义新的agent类型,主要负责的是最基础的配置,比如ExecutorNum,NodeName,Description
③、Computer负责一组Node,因为在某些场景中,一个Job可能由多种语言实现,那么可以用Label来管理一组Node,如果遇到算力不足的情况,可以利用其内部属性NodeProvisioner来初始化新的Node
4、调度策略 ①、Queue它和Node多少有点关系,但是也有一些显著的差异,Computer作为一系列Executor的拥有者,如果一个Node中没有配置Executor(可能是暂时的),那么也将不会拥有Computer对象,如果一个Node本身是存在的,且其已经执行过若干构建,当把它移除后,相应的Computer也需要一定时间被删除,在更改节点配置的时候,没有被影响的Computer依然会保存完整直到所有的Node都被删除
这个对象实现了核心调度逻辑,其中的内部接口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
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.txt2、构建调度调试
对这个任务执行构建,断点走到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, Listactions) { (...) scheduleMaintenance(); // let an executor know that a new item is in the queue. (...) }
public Future> scheduleMaintenance() {
return maintainerThread.submit();
}
submit方法也还没有结束
public synchronized Futuresubmit() { 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() {
@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;
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() {
@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 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 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命令了,还可以继续再深入,感兴趣就继续跟下去吧
Jenkins确实是一个很不错的CI/DI引擎,但是毕竟年代久远,历史复杂,代码太重了,阅读起来有一定难度,接下来,我会尝试着将Jenkins的任务调度代码简化抽离出来,争取能够运行出一个helloworld



