package com.laytonsmith.core.functions;
import com.laytonsmith.PureUtilities.Common.MutableObject;
import com.laytonsmith.PureUtilities.Common.Range;
import com.laytonsmith.PureUtilities.Common.StringUtils;
import com.laytonsmith.PureUtilities.DaemonManager;
import com.laytonsmith.PureUtilities.Version;
import com.laytonsmith.abstraction.Implementation;
import com.laytonsmith.abstraction.StaticLayer;
import com.laytonsmith.annotations.api;
import com.laytonsmith.annotations.core;
import com.laytonsmith.annotations.hide;
import com.laytonsmith.annotations.noboilerplate;
import com.laytonsmith.annotations.seealso;
import com.laytonsmith.core.CHVersion;
import com.laytonsmith.core.LogLevel;
import com.laytonsmith.core.Optimizable;
import com.laytonsmith.core.ParseTree;
import com.laytonsmith.core.Static;
import com.laytonsmith.core.compiler.FileOptions;
import com.laytonsmith.core.constructs.CArray;
import com.laytonsmith.core.constructs.CClosure;
import com.laytonsmith.core.constructs.CInt;
import com.laytonsmith.core.constructs.CNull;
import com.laytonsmith.core.constructs.CString;
import com.laytonsmith.core.constructs.CVoid;
import com.laytonsmith.core.constructs.Construct;
import com.laytonsmith.core.constructs.Target;
import com.laytonsmith.core.environments.Environment;
import com.laytonsmith.core.environments.GlobalEnv;
import com.laytonsmith.core.exceptions.CRE.CRECastException;
import com.laytonsmith.core.exceptions.CRE.CREFormatException;
import com.laytonsmith.core.exceptions.CRE.CREInsufficientArgumentsException;
import com.laytonsmith.core.exceptions.CRE.CRERangeException;
import com.laytonsmith.core.exceptions.CRE.CREThrowable;
import com.laytonsmith.core.exceptions.CancelCommandException;
import com.laytonsmith.core.exceptions.ConfigCompileException;
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
import com.laytonsmith.core.exceptions.ProgramFlowManipulationException;
import com.laytonsmith.core.profiler.ProfilePoint;
import com.laytonsmith.core.taskmanager.CoreTaskType;
import com.laytonsmith.core.taskmanager.TaskManager;
import com.laytonsmith.core.taskmanager.TaskState;
import com.laytonsmith.core.taskmanager.TimeoutTaskHandler;
import com.laytonsmith.tools.docgen.DocGenTemplates;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
*/
@core
public class Scheduling {
public static void ClearScheduledRunners() {
StaticLayer.ClearAllRunnables();
}
public static String docs() {
return "This class contains methods for dealing with time and server scheduling.";
}
@api
public static class time extends AbstractFunction {
@Override
public String getName() {
return "time";
}
@Override
public Integer[] numArgs() {
return new Integer[]{0};
}
@Override
public String docs() {
return "int {} Returns the current unix time stamp, in milliseconds. The resolution of this is not guaranteed to be extremely accurate. If "
+ "you need extreme accuracy, use nano_time()";
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{};
}
@Override
public boolean isRestricted() {
return false;
}
@Override
public CHVersion since() {
return CHVersion.V3_1_0;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException {
return new CInt(System.currentTimeMillis(), t);
}
}
@api
public static class nano_time extends AbstractFunction {
@Override
public String getName() {
return "nano_time";
}
@Override
public Integer[] numArgs() {
return new Integer[]{0};
}
@Override
public String docs() {
return "int {} Returns an arbitrary number based on the most accurate clock available on this system. Only useful when compared to other calls"
+ " to nano_time(). The return is in nano seconds. See the Java API on System.nanoTime() for more information on the usage of this function.";
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{};
}
@Override
public boolean isRestricted() {
return false;
}
@Override
public CHVersion since() {
return CHVersion.V3_1_0;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException {
return new CInt(System.nanoTime(), t);
}
}
@api
@hide("Only meant for cmdline/testing")
@noboilerplate
public static class sleep extends AbstractFunction {
@Override
public String getName() {
return "sleep";
}
@Override
public Integer[] numArgs() {
return new Integer[]{1};
}
@Override
public String docs() {
return "void {seconds} Sleeps the script for the specified number of seconds, up to the maximum time limit defined in the preferences file."
+ " Seconds may be a double value, so 0.5 would be half a second."
+ " PLEASE NOTE: Sleep times are NOT very accurate, and should not be relied on for preciseness.";
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CRECastException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public CHVersion since() {
return CHVersion.V3_1_0;
}
@Override
public Construct exec(Target t, Environment env, Construct... args) throws CancelCommandException, ConfigRuntimeException {
Construct x = args[0];
double time = Static.getNumber(x, t);
try {
Thread.sleep((int) (time * 1000));
} catch (InterruptedException ex) {
}
return CVoid.VOID;
}
@Override
public Boolean runAsync() {
//Because we stop the thread
return true;
}
}
@api(environments = {GlobalEnv.class})
public static class set_interval extends AbstractFunction {
@Override
public String getName() {
return "set_interval";
}
@Override
public Integer[] numArgs() {
return new Integer[]{2, 3};
}
@Override
public String docs() {
return "int {timeInMS, [initialDelayInMS,] closure} Sets a task to run every so often. This works similarly to set_timeout,"
+ " except the task will automatically re-register itself to run again. Note that the resolution"
+ " of the time is in ms, however, the server will only have a resolution of up to 50 ms, meaning"
+ " that a time of 1-50ms is essentially the same as 50ms. The inital delay defaults to the same"
+ " thing as timeInMS, that is, there will be a pause between registration and initial firing. However,"
+ " this can be set to 0 (or some other number) to adjust how long of a delay there is before it begins.";
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CRECastException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return false;
}
@Override
public Construct exec(final Target t, final Environment environment, Construct... args) throws ConfigRuntimeException {
long time = Static.getInt(args[0], t);
int offset = 0;
long delay = time;
if (args.length == 3) {
offset = 1;
delay = Static.getInt(args[1], t);
}
if (!(args[1 + offset] instanceof CClosure)) {
throw new CRECastException(getName() + " expects a closure to be sent as the second argument", t);
}
final CClosure c = (CClosure) args[1 + offset];
final AtomicInteger ret = new AtomicInteger(-1);
ret.set(StaticLayer.SetFutureRepeater(environment.getEnv(GlobalEnv.class).GetDaemonManager(), time, delay, new Runnable() {
@Override
public void run() {
c.getEnv().getEnv(GlobalEnv.class).SetCustom("timeout-id", ret.get());
try {
ProfilePoint p = environment.getEnv(GlobalEnv.class).GetProfiler().start("Executing timeout with id " + ret.get() + " (defined at " + t.toString() + ")", LogLevel.ERROR);
try {
c.execute();
} finally {
p.stop();
}
} catch (ConfigRuntimeException e) {
ConfigRuntimeException.HandleUncaughtException(e, environment);
} catch (CancelCommandException e) {
//Ok
} catch (ProgramFlowManipulationException e) {
ConfigRuntimeException.DoWarning("Using a program flow manipulation construct improperly! " + e.getClass().getSimpleName());
}
}
}));
return new CInt(ret.get(), t);
}
@Override
public CHVersion since() {
return CHVersion.V3_3_1;
}
@Override
public ExampleScript[] examples() throws ConfigCompileException {
return new ExampleScript[]{
new ExampleScript("Basic usage", "set_interval(1000, closure(){\n"
+ "\tmsg('Hello World!');\n"
+ "});", "<Would message the user \"Hello World!\" every second>"),
new ExampleScript("Usage with initial delay", "set_interval(1000, 5000, closure(){\n"
+ "\tmsg('Hello World!');\n"
+ "});", "<Would message the user \"Hello World!\" every second, however there would be an initial delay of 5 seconds>")
};
}
}
@api(environments = {GlobalEnv.class})
public static class set_timeout extends AbstractFunction {
@Override
public String getName() {
return "set_timeout";
}
@Override
public Integer[] numArgs() {
return new Integer[]{2};
}
@Override
public String docs() {
return "int {timeInMS, closure} Sets a task to run in the specified number of ms in the future."
+ " The task will only run once. Note that the resolution"
+ " of the time is in ms, however, the server will only have a resolution of up to 50 ms, meaning"
+ " that a time of 1-50ms is essentially the same as 50ms.";
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CRECastException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return false;
}
@Override
public Construct exec(final Target t, final Environment environment, Construct... args) throws ConfigRuntimeException {
final TaskManager taskManager = environment.getEnv(GlobalEnv.class).GetTaskManager();
long time = Static.getInt(args[0], t);
if (!(args[1] instanceof CClosure)) {
throw new CRECastException(getName() + " expects a closure to be sent as the second argument", t);
}
final CClosure c = (CClosure) args[1];
final AtomicInteger ret = new AtomicInteger(-1);
final AtomicBoolean isRunning = new AtomicBoolean(false);
ret.set(StaticLayer.SetFutureRunnable(environment.getEnv(GlobalEnv.class).GetDaemonManager(), time, new Runnable() {
@Override
public void run() {
isRunning.set(true);
c.getEnv().getEnv(GlobalEnv.class).SetCustom("timeout-id", ret.get());
taskManager.getTask(CoreTaskType.TIMEOUT, ret.get()).changeState(TaskState.RUNNING);
try {
ProfilePoint p = environment.getEnv(GlobalEnv.class).GetProfiler().start("Executing timeout with id " + ret.get() + " (defined at " + t.toString() + ")", LogLevel.ERROR);
try {
c.execute();
} finally {
p.stop();
}
} catch (ConfigRuntimeException e) {
ConfigRuntimeException.HandleUncaughtException(e, environment);
} catch (CancelCommandException e) {
//Ok
} catch (ProgramFlowManipulationException e) {
ConfigRuntimeException.DoWarning("Using a program flow manipulation construct improperly! " + e.getClass().getSimpleName());
} finally {
taskManager.getTask(CoreTaskType.TIMEOUT, ret.get()).changeState(TaskState.FINISHED);
environment.getEnv(GlobalEnv.class).SetInterrupt(false);
}
}
}));
taskManager.addTask(new TimeoutTaskHandler(ret.get(), t, new Runnable() {
@Override
public void run() {
if (isRunning.get()) {
new clear_task().exec(t, environment, new CInt(ret.get(), t));
environment.getEnv(GlobalEnv.class).SetInterrupt(true);
taskManager.getTask(CoreTaskType.TIMEOUT, ret.get()).changeState(TaskState.KILLED);
}
}
}));
taskManager.getTask(CoreTaskType.TIMEOUT, ret.get()).changeState(TaskState.IDLE);
return new CInt(ret.get(), t);
}
@Override
public CHVersion since() {
return CHVersion.V3_3_1;
}
@Override
public ExampleScript[] examples() throws ConfigCompileException {
return new ExampleScript[]{
new ExampleScript("Basic usage", "set_timeout(10000, closure(){\n"
+ "\tmsg('Hello World!');\n"
+ "});", "<Would wait 5 seconds, then message the user \"Hello World!\">")
};
}
}
@api(environments = {GlobalEnv.class})
public static class clear_task extends AbstractFunction {
@Override
public String getName() {
return "clear_task";
}
@Override
public Integer[] numArgs() {
return new Integer[]{0, 1};
}
@Override
public String docs() {
return "void {[id]} Stops the interval or timeout that is specified. The id can be gotten by"
+ " storing the integer returned from either set_timeout or set_interval."
+ " An invalid id is simply ignored. The clear_task function is more useful for set_timeout, where"
+ " you may queue up some task to happen in the far future, yet have some trigger to"
+ " prevent it from happening. ID is optional, but only if called from within a set_interval or set_timeout"
+ " closure, in which case it defaults to the id of that particular task.";
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CRECastException.class, CREInsufficientArgumentsException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException {
if (args.length == 0 && environment.getEnv(GlobalEnv.class).GetCustom("timeout-id") != null) {
StaticLayer.ClearFutureRunnable((Integer) environment.getEnv(GlobalEnv.class).GetCustom("timeout-id"));
} else if (args.length == 1) {
StaticLayer.ClearFutureRunnable(Static.getInt32(args[0], t));
} else {
throw new CREInsufficientArgumentsException("No id was passed to clear_task, and it's not running inside a task either.", t);
}
return CVoid.VOID;
}
@Override
public CHVersion since() {
return CHVersion.V3_3_1;
}
@Override
public ExampleScript[] examples() throws ConfigCompileException {
return new ExampleScript[]{
new ExampleScript("Use from within an interval", "set_interval(1000, closure(){\n"
+ "\tif(rand(0, 10) == 9){\n"
+ "\t\tclear_task();\n"
+ "\t}\n"
+ "\tmsg('Hello World!');\n"
+ "});", "<Messages the user until the random number generator produces a 9, at which point the interval is stopped>"),
new ExampleScript("Using the id returned from set_timeout", "@id = set_timeout(5000, closure(){\n"
+ "\tmsg('Hello World!');\n"
+ "});\n"
+ "clear_task(@id);", "<Nothing happens, as the timeout is cancelled before it runs>")
};
}
}
@api
@seealso(Meta.get_locales.class)
public static class simple_date extends AbstractFunction {
@Override
public String getName() {
return "simple_date";
}
@Override
public Integer[] numArgs() {
return new Integer[]{1, 2, 3, 4};
}
@Override
public String docs() {
Map<String, DocGenTemplates.Generator> map = new HashMap<String, DocGenTemplates.Generator>();
map.put("timezoneValues", new DocGenTemplates.Generator() {
@Override
public String generate(String... args) {
String[] timezones = new String[0];
try {
timezones = TimeZone.getAvailableIDs();
} catch (NullPointerException e) {
//This is due to a JDK bug. As you can see, the code above
//should never NPE due to our mistake, so it would only occur
//during an internal error. The solution that worked for me is here:
//https://bugs.launchpad.net/ubuntu/+source/tzdata/+bug/1053160
//however, this appears to be an issue in Open JDK, so performance on
//other systems may vary. We will handle this error by reporting that
//list could not be retrieved, using the Join method's empty parameter.
}
//Let's sort the timezones
List<String> tz = new ArrayList<String>(Arrays.asList(timezones));
Collections.sort(tz);
return StringUtils.Join(tz, ", ", " or ", " or ", "Couldn't retrieve the list of timezones!");
}
});
return getBundledDocs(map);
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CRECastException.class, CREFormatException.class};
}
@Override
public boolean isRestricted() {
return false;
}
@Override
public CHVersion since() {
return CHVersion.V3_3_1;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public CString exec(Target t, Environment env, Construct... args) {
Date now = new Date();
if (args.length >= 2 && !(args[1] instanceof CNull)) {
now = new Date(Static.getInt(args[1], t));
}
TimeZone timezone = TimeZone.getDefault();
if (args.length >= 3 && args[2].nval() != null) {
timezone = TimeZone.getTimeZone(args[2].val());
}
Locale locale = Locale.getDefault();
if (args.length >= 4) {
String countryCode = args[3].nval();
if (countryCode == null) {
locale = Locale.getDefault();
} else {
locale = Static.GetLocale(countryCode);
}
if (locale == null) {
throw new CREFormatException("The given locale was not found on your system: "
+ countryCode, t);
}
}
SimpleDateFormat dateFormat;
try {
dateFormat = new SimpleDateFormat(args[0].toString(), locale);
} catch (IllegalArgumentException ex) {
throw new CREFormatException(ex.getMessage(), t);
}
dateFormat.setTimeZone(timezone);
return new CString(dateFormat.format(now), t);
}
@Override
public ExampleScript[] examples() throws ConfigCompileException {
return new ExampleScript[]{
/* 1 */new ExampleScript("Basic usage", "simple_date('h:mm a')", ":11:36 AM"),
/* 2 */ new ExampleScript("Usage with quoted letters", "simple_date('yyyy.MM.dd G \\'at\\' HH:mm:ss z')", ":2013.06.13 AD at 11:36:46 CDT"),
/* 3 */ new ExampleScript("Adding a single quote", "simple_date('EEE, MMM d, \\'\\'yy')", ":Wed, Jun 5, '13"),
/* 4 */ new ExampleScript("Specifying alternate time", "simple_date('EEE, MMM d, \\'\\'yy', 0)"),
/* 5 */ new ExampleScript("With timezone", "simple_date('hh \\'o\\'\\'clock\\' a, zzzz')", ":11 o'clock AM, Central Daylight Time"),
/* 6 */ new ExampleScript("With timezone", "simple_date('hh \\'o\\'\\'clock\\' a, zzzz')", ":11 o'clock AM, Central Daylight Time"),
/* 7 */ new ExampleScript("With simple timezone", "simple_date('K:mm a, z')", ":11:42 AM, CDT"),
/* 8 */ new ExampleScript("With alternate timezone", "simple_date('K:mm a, z', time(), 'GMT')", ":4:42 PM, GMT"),
/* 9 */ new ExampleScript("With 5 digit year", "simple_date('yyyyy.MMMMM.dd GGG hh:mm aaa')", ":02013.June.05 AD 11:42 AM"),
/* 10 */ new ExampleScript("Long format", "simple_date('EEE, d MMM yyyy HH:mm:ss Z')", ":Wed, 5 Jun 2013 11:42:56 -0500"),
/* 10 */ new ExampleScript("Long format with alternate locale", "simple_date('EEE, d MMM yyyy HH:mm:ss Z', 1444418254496, 'CET', 'no_NO')"),
/* 11 */ new ExampleScript("Computer readable format", "simple_date('yyMMddHHmmssZ')", ":130605114256-0500"),
/* 12 */ new ExampleScript("With milliseconds", "simple_date('yyyy-MM-dd\\'T\\'HH:mm:ss.SSSZ')", ":2013-06-05T11:42:56.799-0500"),};
}
}
@api
@seealso(simple_date.class)
public static class parse_date extends AbstractFunction {
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CREFormatException.class};
}
@Override
public boolean isRestricted() {
return false;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException {
SimpleDateFormat dateFormat;
try {
dateFormat = new SimpleDateFormat(args[0].toString());
Date d = dateFormat.parse(args[1].val());
return new CInt(d.getTime(), t);
} catch (IllegalArgumentException | ParseException ex) {
throw new CREFormatException(ex.getMessage(), t);
}
}
@Override
public String getName() {
return "parse_date";
}
@Override
public Integer[] numArgs() {
return new Integer[]{2};
}
@Override
public String docs() {
return "int {dateFormat, dateString} Parses a date string, and returns an integer timestamp representing that time. This essentially"
+ " works in reverse of {{function|simple_date}}. The dateFormat string is the same as simple_date, see the documentation for"
+ " that function to see full details on that. The dateString is the actual date to be parsed. The dateFormat should be the"
+ " equivalent format that was used to generate the dateString. In general, this function is fairly lenient, and will still"
+ " try to parse a dateString that doesn't necessarily conform to the given format, but it shouldn't be relied on to work"
+ " with malformed data. Various portions of the date may be left off, in which case the missing portions will be assumed,"
+ " for instance, if the time is left off completely, it is assumed to be midnight, and if the minutes are left off, "
+ " it is assumed to be on the hour, if the date is left off, it is assumed to be today, etc.";
}
@Override
public Version since() {
return CHVersion.V3_3_1;
}
@Override
public ExampleScript[] examples() throws ConfigCompileException {
return new ExampleScript[]{
new ExampleScript("Simple example", "parse_date('yyMMddHHmmssZ', '130605114256-0500')"),
new ExampleScript("Using the results of simple_date", "@format = 'EEE, d MMM yyyy HH:mm:ss Z';\n"
+ "msg(parse_date(@format, simple_date(@format, 1)));")
};
}
}
@api
public static class get_system_timezones extends AbstractFunction {
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException {
String[] timezones = new String[0];
try {
timezones = TimeZone.getAvailableIDs();
} catch (NullPointerException e) {
//This is due to a JDK bug. As you can see, the code above
//should never NPE due to our mistake, so it would only occur
//during an internal error. The solution that worked for me is here:
//https://bugs.launchpad.net/ubuntu/+source/tzdata/+bug/1053160
//however, this appears to be an issue in Open JDK, so performance on
//other systems may vary. We will handle this error by reporting that
//list could not be retrieved, using the Join method's empty parameter.
}
//Let's sort the timezones
List<String> tz = new ArrayList<>(Arrays.asList(timezones));
Collections.sort(tz);
CArray ret = new CArray(t);
for(String s : tz) {
ret.push(new CString(s, t), t);
}
return ret;
}
@Override
public String getName() {
return "get_system_timezones";
}
@Override
public Integer[] numArgs() {
return new Integer[]{0};
}
@Override
public String docs() {
return "array<string> {} Returns a list of time zones registered on this system.";
}
@Override
public Version since() {
return CHVersion.V3_3_2;
}
}
@api
public static class set_cron extends AbstractFunction implements Optimizable {
private static Thread cronThread = null;
private static final Object cronThreadLock = new Object();
private static final Map<Integer, CronFormat> cronJobs = new HashMap<Integer, CronFormat>();
private static final AtomicInteger jobIDs = new AtomicInteger(1);
/**
* Stops a job from running again, and returns true if the value was actually removed. False is returned
* otherwise.
*
* @return
* @param jobID The job ID
*/
public static boolean stopJob(int jobID) {
synchronized (cronJobs) {
if (cronJobs.containsKey(jobID)) {
cronJobs.remove(jobID);
return true;
} else {
return false;
}
}
}
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CRECastException.class, CREFormatException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException {
//First things first, check the format of the arguments.
if (!(args[0] instanceof CString)) {
throw new CRECastException("Expected string for argument 1 in " + getName(), t);
}
if (!(args[1] instanceof CClosure)) {
throw new CRECastException("Expected closure for argument 2 in " + getName(), t);
}
CronFormat format = validateFormat(args[0].val(), t);
format.job = ((CClosure) args[1]);
//At this point, the format is complete. We need to start up the cron thread if it's not running, and
//then register this job, as well as inform clear_task of this id.
synchronized (cronThreadLock) {
if (cronThread == null) {
final DaemonManager dm = environment.getEnv(GlobalEnv.class).GetDaemonManager();
final MutableObject<Boolean> stopCron = new MutableObject<>(false);
StaticLayer.GetConvertor().addShutdownHook(new Runnable() {
@Override
public void run() {
cronThread = null;
stopCron.setObject(true);
synchronized (cronJobs) {
cronJobs.clear();
}
synchronized (cronThreadLock) {
cronThreadLock.notifyAll();
}
}
});
cronThread = new Thread(new Runnable() {
@Override
public void run() {
long lastMinute = 0;
while (!stopCron.getObject()) {
//We want to check to make sure that we only run once per minute, even though the
//checks happen every second. This ensures that we have a fast enough sampling
//period, while ensuring that we don't repeat tasks within the same minute.
if ((System.currentTimeMillis() / 1000 / 60) > lastMinute) {
//Set the lastMinute value to now
lastMinute = System.currentTimeMillis() / 1000 / 60;
//Activate
synchronized (cronJobs) {
Calendar c = Calendar.getInstance();
for (final CronFormat f : cronJobs.values()) {
//Check to see if it is currently time to run each job
if (f.min.contains(c.get(Calendar.MINUTE))
&& f.hour.contains(c.get(Calendar.HOUR_OF_DAY))
&& f.day.contains(c.get(Calendar.DAY_OF_MONTH))
&& f.month.contains(c.get(Calendar.MONTH) + 1)
&& f.dayOfWeek.contains(c.get(Calendar.DAY_OF_WEEK) - 1)) {
//All the fields match, so let's trigger this job
StaticLayer.GetConvertor().runOnMainThreadLater(dm, new Runnable() {
@Override
public void run() {
try {
f.job.execute();
} catch (ConfigRuntimeException ex) {
ConfigRuntimeException.HandleUncaughtException(ex, f.job.getEnv());
}
}
});
}
}
}
} //else continue, we'll wait another second.
synchronized (cronThreadLock) {
try {
cronThreadLock.wait(1000);
} catch (InterruptedException ex) {
//Continue
}
}
}
dm.deactivateThread(cronThread);
}
}, Implementation.GetServerType().getBranding() + "-CronDaemon");
dm.activateThread(cronThread);
cronThread.start();
}
}
int jobID = jobIDs.getAndIncrement();
synchronized (cronJobs) {
cronJobs.put(jobID, format);
format.job.getEnv().getEnv(GlobalEnv.class).SetCustom("cron-task-id", jobID);
}
return new CInt(jobID, t);
}
private static final Map<String, Integer> MONTHS = new HashMap<String, Integer>();
private static final Map<String, Integer> DAYS = new HashMap<String, Integer>();
private static final Map<String, Integer> HOURS = new HashMap<String, Integer>();
private static final Pattern RANGE = Pattern.compile("(\\d+)-(\\d+)");
private static final Pattern EVERY = Pattern.compile("\\*/(\\d+)");
private static final List<Range> RANGES = Arrays.asList(new Range(0, 59), new Range(0, 23), new Range(1, 31), new Range(1, 12), new Range(0, 6));
static {
MONTHS.put("jan", 1);
MONTHS.put("feb", 2);
MONTHS.put("mar", 3);
MONTHS.put("apr", 4);
MONTHS.put("may", 5);
MONTHS.put("jun", 6);
MONTHS.put("jul", 7);
MONTHS.put("aug", 8);
MONTHS.put("sep", 9);
MONTHS.put("oct", 10);
MONTHS.put("nov", 11);
MONTHS.put("dec", 12);
MONTHS.put("january", 1);
MONTHS.put("february", 2);
MONTHS.put("febuary", 2); //common misspelling
MONTHS.put("march", 3);
MONTHS.put("april", 4);
MONTHS.put("may", 5);
MONTHS.put("june", 6);
MONTHS.put("july", 7);
MONTHS.put("august", 8);
MONTHS.put("september", 9);
MONTHS.put("october", 10);
MONTHS.put("november", 11);
MONTHS.put("december", 12);
DAYS.put("sun", 0);
DAYS.put("mon", 1);
DAYS.put("tue", 2);
DAYS.put("wed", 3);
DAYS.put("thu", 4);
DAYS.put("fri", 5);
DAYS.put("sat", 6);
DAYS.put("sunday", 0);
DAYS.put("monday", 1);
DAYS.put("tuesday", 2);
DAYS.put("wednesday", 3);
DAYS.put("thursday", 4);
DAYS.put("friday", 5);
DAYS.put("saturday", 6);
HOURS.put("midnight", 0);
HOURS.put("noon", 12);
}
private CronFormat validateFormat(String format, Target t) {
//Now we need to look at the format of the cron task, and convert it to a standardized format.
//Our goal here is to remove all ranges, predefined names, including @hourly and January.
format = format.trim();
//Changes tabs to spaces
format = format.replace("\t", " ");
//Change multiple spaces to one space
format = format.replaceAll("( )+", " ");
//Lowercase everything
format = format.toLowerCase();
if ("@yearly".equals(format) || "@annually".equals(format)) {
format = "0 0 1 1 *";
}
if ("@monthly".equals(format)) {
format = "0 0 1 * *";
}
if ("@weekly".equals(format)) {
format = "0 0 * * 0";
}
if ("@daily".equals(format)) {
format = "0 0 * * *";
}
if ("@hourly".equals(format)) {
format = "0 * * * *";
}
//Check for invalid characters
if (format.matches("[^a-z0-9\\*\\-,@/]")) {
throw new CREFormatException("Invalid characters found in format for " + getName() + ": \"" + format + "\". Check your format and try again.", t);
}
//Now split into the segments.
String[] sformat = format.split(" ");
if (sformat.length != 5) {
throw new CREFormatException("Expected 5 segments in " + getName() + ", but " + StringUtils.PluralTemplateHelper(sformat.length, "%d was", "%d were") + " found.", t);
}
String min = sformat[0];
String hour = sformat[1];
String day = sformat[2];
String month = sformat[3];
String dayOfWeek = sformat[4];
//Now replace the special shortcut names
for (String key : MONTHS.keySet()) {
month = month.replace(key, Integer.toString(MONTHS.get(key)));
}
for (String key : DAYS.keySet()) {
dayOfWeek = dayOfWeek.replace(key, Integer.toString(DAYS.get(key)));
}
for (String key : HOURS.keySet()) {
hour = hour.replace(key, Integer.toString(HOURS.get(key)));
}
//Split on commas
List<String> minList = new ArrayList<String>(Arrays.asList(min.split(",")));
List<String> hourList = new ArrayList<String>(Arrays.asList(hour.split(",")));
List<String> dayList = new ArrayList<String>(Arrays.asList(day.split(",")));
List<String> monthList = new ArrayList<String>(Arrays.asList(month.split(",")));
List<String> dayOfWeekList = new ArrayList<String>(Arrays.asList(dayOfWeek.split(",")));
List<List<String>> segments = Arrays.asList(minList, hourList, dayList, monthList, dayOfWeekList);
//Now go through each and pull out any ranges. At this point, everything
//is numbers or *
for (int i = 0; i < segments.size(); i++) {
List<String> segment = segments.get(i);
Iterator<String> it = segment.iterator();
List<String> addAll = new ArrayList<String>();
Range range = RANGES.get(i);
while (it.hasNext()) {
String part = it.next();
Matcher rangeMatcher = RANGE.matcher(part);
if (rangeMatcher.find()) {
it.remove();
Integer minRange = Integer.parseInt(rangeMatcher.group(1));
Integer maxRange = Integer.parseInt(rangeMatcher.group(2));
Range r = new Range(minRange, maxRange);
if (!r.isAscending()) {
throw new CREFormatException("Ranges must be min to max, and not the same value in format for " + getName(), t);
}
List<Integer> rr = r.getRange();
for (int j = 0; j < rr.size(); j++) {
addAll.add(Integer.toString(rr.get(j)));
}
continue;
}
Matcher everyMatcher = EVERY.matcher(part);
if (everyMatcher.find()) {
it.remove();
Integer every = Integer.parseInt(everyMatcher.group(1));
for (int j = range.getMin(); j <= range.getMax(); j += every) {
addAll.add(Integer.toString(j));
}
}
if ("*".equals(part)) {
it.remove();
for (int j = range.getMin(); j <= range.getMax(); j++) {
addAll.add(Integer.toString(j));
}
}
}
segment.addAll(addAll);
Collections.sort(segment);
}
//Everything is ints now, so parse it into a CronFormat object.
CronFormat f = new CronFormat();
for (int i = 0; i < 5; i++) {
List<Integer> list = new ArrayList<Integer>();
List<String> segment = segments.get(i);
Range range = RANGES.get(i);
for (String s : segment) {
try {
list.add(Integer.parseInt(s));
} catch (NumberFormatException ex) {
//Any unexpected strings would show up here. The expected string values would have already
//been replaced with a number, so this should work if there are no errors.
throw new CREFormatException("Unknown string passed in format for " + getName() + " \"" + s + "\"", t);
}
}
Collections.sort(list);
if (!range.contains(list.get(0))) {
throw new CREFormatException("Expecting value to be within the range " + range + " in format for " + getName() + ", but the value was " + list.get(0), t);
}
if (!range.contains(list.get(list.size() - 1))) {
throw new CREFormatException("Expecting value to be within the range " + range + " in format for " + getName() + ", but the value was " + list.get(list.size() - 1), t);
}
Set<Integer> set = new TreeSet<Integer>(list);
switch (i) {
case 0:
f.min = set;
break;
case 1:
f.hour = set;
break;
case 2:
f.day = set;
break;
case 3:
f.month = set;
break;
case 4:
f.dayOfWeek = set;
break;
}
}
return f;
}
private static class CronFormat {
public Set<Integer> min = new HashSet<Integer>();
public Set<Integer> hour = new HashSet<Integer>();
public Set<Integer> day = new HashSet<Integer>();
public Set<Integer> month = new HashSet<Integer>();
public Set<Integer> dayOfWeek = new HashSet<Integer>();
public CClosure job;
@Override
public String toString() {
return min + "\n" + hour + "\n" + day + "\n" + month + "\n" + dayOfWeek;
}
}
@Override
public String getName() {
return "set_cron";
}
@Override
public Integer[] numArgs() {
return new Integer[]{2};
}
@Override
public String docs() {
return getBundledDocs();
}
@Override
public Version since() {
return CHVersion.V3_3_1;
}
@Override
public Set<OptimizationOption> optimizationOptions() {
return EnumSet.of(OptimizationOption.OPTIMIZE_DYNAMIC);
}
@Override
public ParseTree optimizeDynamic(Target t, List<ParseTree> children, FileOptions fileOptions) throws ConfigCompileException, ConfigRuntimeException {
if (children.get(0).isConst()) {
if (children.get(0).getData() instanceof CString) {
validateFormat(children.get(0).getData().val(), t);
}
}
return null;
}
}
@api
public static class clear_cron extends AbstractFunction {
@Override
public Class<? extends CREThrowable>[] thrown() {
return new Class[]{CRERangeException.class, CRECastException.class};
}
@Override
public boolean isRestricted() {
return true;
}
@Override
public Boolean runAsync() {
return null;
}
@Override
public Construct exec(Target t, Environment environment, Construct... args) throws ConfigRuntimeException {
Integer id = (Integer) environment.getEnv(GlobalEnv.class).GetCustom("cron-task-id");
if (args.length == 1) {
id = (int) Static.getInt(args[0], t);
}
if (id == null) {
throw new CRERangeException("No task ID provided, and not running from within a cron task.", t);
}
if (!set_cron.stopJob(id)) {
throw new CRERangeException("Task ID invalid", t);
}
return CVoid.VOID;
}
@Override
public String getName() {
return "clear_cron";
}
@Override
public Integer[] numArgs() {
return new Integer[]{0, 1};
}
@Override
public String docs() {
return "void {[cronID]} Clears the previously registered cron job from the registered list."
+ " This will prevent the task from running again in the future. If run from within"
+ " a cron task, the id is optional, and the current task will be prevented from running"
+ " again in the future. If the ID provided is invalid, a RangeException is thrown.";
}
@Override
public Version since() {
return CHVersion.V3_3_1;
}
}
}