Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-72653] fix timezone handling #301

Merged
merged 15 commits into from
Mar 22, 2024
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Add to your yaml file:
```yaml
unclassified:
scheduleBuild:
defaultScheduleTime: "11:00:00 PM"
defaultStartTime: "11:00:00 PM"
timeZone: "Europe/Paris"
```

Expand Down
Binary file modified docs/images/Schedule_Build_Queue.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/Schedule_Page.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/Schedule_Timezone.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,21 @@
import hudson.model.Job;
import hudson.model.ParametersDefinitionProperty;
import hudson.util.FormValidation;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.Locale;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.jenkins.ui.icon.IconSpec;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.interceptor.RequirePOST;
Expand All @@ -31,9 +30,10 @@
private static final Logger LOGGER = Logger.getLogger(ScheduleBuildAction.class.getName());

private final Job<?, ?> target;
private static final long SECURITY_MARGIN = 120 * 1000;
private static final long ONE_DAY = 24 * 3600 * 1000;
private static final long SECURITY_MARGIN = 120;

private static final String DATE_TIME_PATTERN = "dd-MM-yyyy HH:mm:ss";
private static final String PARSE_DATE_TIME_PATTERN = "d-M-y H:m[:s]";
private long quietperiod;

public ScheduleBuildAction(final Job<?, ?> target) {
Expand Down Expand Up @@ -78,41 +78,17 @@
return this;
}

public String getIconPath() {
Jenkins instance = Jenkins.getInstanceOrNull();
if (instance != null) {
String rootUrl = instance.getRootUrl();

if (rootUrl != null) {
return rootUrl + "plugin/schedule-build/";
}
}
throw new IllegalStateException("couldn't load rootUrl");
}

public String getDefaultDate() {
return dateFormat().format(getDefaultDateObject());
return getDefaultDateObject().format(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN));
}

public Date getDefaultDateObject() {
Date buildtime = new Date(),
now = new Date(),
defaultScheduleTime = new ScheduleBuildGlobalConfiguration().getDefaultScheduleTimeObject();
DateFormat dateFormat = dateFormat();
try {
now = dateFormat.parse(dateFormat.format(now));
} catch (ParseException e) {
LOGGER.log(Level.WARNING, "Error while parsing date", e);
public ZonedDateTime getDefaultDateObject() {
ZonedDateTime zdt = new ScheduleBuildGlobalConfiguration().getDefaultScheduleTimeObject();
ZonedDateTime now = ZonedDateTime.now();
if (now.isAfter(zdt)) {

Check warning on line 88 in src/main/java/org/jenkinsci/plugins/schedulebuild/ScheduleBuildAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 88 is only partially covered, one branch is missing
zdt = zdt.plusDays(1);
}
buildtime.setHours(defaultScheduleTime.getHours());
buildtime.setMinutes(defaultScheduleTime.getMinutes());
buildtime.setSeconds(defaultScheduleTime.getSeconds());

if (now.getTime() > buildtime.getTime()) {
buildtime.setTime(buildtime.getTime() + ScheduleBuildAction.ONE_DAY);
}

return buildtime;
return zdt;
}

@RequirePOST
Expand All @@ -122,55 +98,47 @@
}
// User requesting a build needs permission to start the build
item.checkPermission(Item.BUILD);
Date ddate, now = new Date();
DateFormat dateFormat = dateFormat();
ZonedDateTime now = ZonedDateTime.now();
ZonedDateTime ddate;
try {
ddate = dateFormat.parse(value);
now = dateFormat.parse(dateFormat.format(now));
} catch (ParseException ex) {
ddate = LocalDateTime.parse(value.trim(), getDateTimeFormatter())
.atZone(new ScheduleBuildGlobalConfiguration().getZoneId())
.plusSeconds(SECURITY_MARGIN);
} catch (DateTimeParseException ex) {
return FormValidation.error(Messages.ScheduleBuildAction_ParsingError());
}

if (now.getTime() > ddate.getTime() + ScheduleBuildAction.SECURITY_MARGIN) {
if (now.isAfter(ddate)) {
return FormValidation.error(Messages.ScheduleBuildAction_DateInPastError());
}

return FormValidation.ok();
}

public long getQuietPeriodInSeconds() {
return quietperiod / 1000;
return quietperiod;
}

@RequirePOST
public HttpResponse doNext(StaplerRequest req, @AncestorInPath Item item)
throws FormException, ServletException, IOException {
public HttpResponse doNext(@QueryParameter String date, @AncestorInPath Item item) {
if (item == null) {
return FormValidation.ok();
}
// User requesting a build needs permission to start the build
item.checkPermission(Item.BUILD);
// Deprecated function StructureForm.get()
// JSONObject param = StructuredForm.get(req);
JSONObject param = req.getSubmittedForm();
Date ddate = getDefaultDateObject(), now = new Date();
DateFormat dateFormat = dateFormat();
try {
now = dateFormat.parse(dateFormat.format(now));
} catch (ParseException e) {
LOGGER.log(Level.WARNING, "Error while parsing date", e);
}
ZonedDateTime ddate, now = ZonedDateTime.now();

if (param.containsKey("date")) {
try {
ddate = dateFormat().parse(param.getString("date"));
} catch (ParseException ex) {
return HttpResponses.redirectTo("error");
}
final String time = date.trim();
try {
ddate = LocalDateTime.parse(time, getDateTimeFormatter())
.atZone(new ScheduleBuildGlobalConfiguration().getZoneId());
} catch (DateTimeParseException ex) {
LOGGER.log(Level.INFO, ex, () -> "Error parsing " + time);
return HttpResponses.redirectTo("error");
}

quietperiod = ddate.getTime() - now.getTime();
quietperiod = ChronoUnit.SECONDS.between(now, ddate);
LOGGER.log(Level.FINER, () -> "Quietperiod: " + quietperiod);
if (quietperiod + ScheduleBuildAction.SECURITY_MARGIN < 0) { // 120 sec security margin
LOGGER.log(Level.INFO, () -> "Error security margin" + quietperiod);
return HttpResponses.redirectTo("error");
}
return HttpResponses.forwardToView(this, "redirect");
Expand All @@ -183,12 +151,17 @@
&& paramDefinitions.getParameterDefinitions().size() > 0;
}

private DateFormat dateFormat() {
Locale locale = Stapler.getCurrentRequest() != null
? Stapler.getCurrentRequest().getLocale()
: Locale.getDefault();
DateFormat df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, locale);
df.setTimeZone(new ScheduleBuildGlobalConfiguration().getTimeZoneObject());
return df;
private DateTimeFormatter getDateTimeFormatter() {
return DateTimeFormatter.ofPattern(PARSE_DATE_TIME_PATTERN);
}

@Restricted(NoExternalUse.class)
public String getDateTimeFormatting() {
return DATE_TIME_PATTERN;
}

@Restricted(NoExternalUse.class)
public String getTimeZone() {
return new ScheduleBuildGlobalConfiguration().getTimeZone();

Check warning on line 165 in src/main/java/org/jenkinsci/plugins/schedulebuild/ScheduleBuildAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 160-165 are not covered by tests
}
}