From fda749ac67ffb2007593a3ebc539fe563f292f88 Mon Sep 17 00:00:00 2001
From: Rob Swindell <rob@synchro.net>
Date: Sun, 20 Dec 2020 22:24:53 -0800
Subject: [PATCH] Fix next-forced-exclusive event time calculation

Jump the time forward (in 24-hour chunks) to find the next date/time the event will run rather than just adding 24-hours and assuming it's an event that runs every day (of the week or month) at a specific time.

Also, expose the next-run-date/time for an event as a new `next_run` property for `xtrn_area.event[]` (in `time_t` format) for easier debugging of these kinds of issues.
Also expose the error log level as a new property: `error_level` while we're here.
---
 src/sbbs3/data.cpp       |  64 +++++++++-----
 src/sbbs3/js_xtrn_area.c | 178 +++++++++++++++++++++++++--------------
 src/sbbs3/sbbs.h         |   1 +
 src/sbbs3/scfgdefs.h     |  17 +---
 4 files changed, 160 insertions(+), 100 deletions(-)

diff --git a/src/sbbs3/data.cpp b/src/sbbs3/data.cpp
index 4e4e587a79..96f52ace4e 100644
--- a/src/sbbs3/data.cpp
+++ b/src/sbbs3/data.cpp
@@ -133,6 +133,45 @@ int sbbs_t::getuserxfers(int fromuser, int destuser, char *fname)
 	return(found);
 }
 
+/****************************************************************************/
+/* Return date/time that the specified event should run next				*/
+/****************************************************************************/
+extern "C" time_t DLLCALL getnexteventtime(event_t* event)
+{
+	struct tm tm;
+	time_t	t = time(NULL);
+
+	if(event->misc & EVENT_DISABLED)
+		return 0;
+
+	if(event->days == 0 || event->freq != 0)
+		return 0;
+
+	if(localtime_r(&t, &tm) == NULL)
+		return 0;
+
+	tm.tm_hour = event->time / 60;
+	tm.tm_min = event->time % 60;
+	tm.tm_sec = 0;
+	tm.tm_isdst = -1;	/* Do not adjust for DST */
+	t = mktime(&tm);
+
+	if(event->last >= t)
+		t += 24 * 60 * 60; /* already ran today, so add 24hrs */
+
+	do {
+		if(localtime_r(&t, &tm) == NULL)
+			return 0;
+		if((event->days & (1 << tm.tm_wday))
+			&& (event->mdays == 0 || (event->mdays & (1 << tm.tm_mday)))
+			&& (event->months == 0 || (event->months & (1 << tm.tm_mon))))
+			break;
+		t += 24 * 60 * 60;
+	} while(t > 0);
+
+	return t;
+}
+
 /****************************************************************************/
 /* Return time of next forced timed event									*/
 /* 'event' may be NULL														*/
@@ -140,40 +179,21 @@ int sbbs_t::getuserxfers(int fromuser, int destuser, char *fname)
 extern "C" time_t DLLCALL getnextevent(scfg_t* cfg, event_t* event)
 {
     int     i;
-	time_t	now=time(NULL);
 	time_t	event_time=0;
 	time_t	thisevent;
-	time_t	tmptime;
-    struct  tm tm, last_tm;
-
-	if(localtime_r(&now,&tm) == NULL)
-		return 0;
 
 	for(i=0;i<cfg->total_events;i++) {
 		if(!cfg->event[i]->node || cfg->event[i]->node>cfg->sys_nodes
 			|| cfg->event[i]->misc&EVENT_DISABLED)
 			continue;
 		if(!(cfg->event[i]->misc&EVENT_FORCE)
-			|| (!(cfg->event[i]->misc&EVENT_EXCL) && cfg->event[i]->node!=cfg->node_num)
-			|| !(cfg->event[i]->days&(1<<tm.tm_wday))
-			|| (cfg->event[i]->mdays!=0 && !(cfg->event[i]->mdays&(1<<tm.tm_mday)))
-			|| (cfg->event[i]->months!=0 && !(cfg->event[i]->months&(1<<tm.tm_mon)))) 
+			|| (!(cfg->event[i]->misc&EVENT_EXCL) && cfg->event[i]->node!=cfg->node_num))
 			continue;
 
-		tm.tm_hour=cfg->event[i]->time/60;
-		tm.tm_min=cfg->event[i]->time%60;
-		tm.tm_sec=0;
-		tm.tm_isdst=-1;	/* Do not adjust for DST */
-		thisevent=mktime(&tm);
-		if(thisevent == -1)
+		thisevent = getnexteventtime(cfg->event[i]);
+		if(thisevent <= 0)
 			continue;
 
-		tmptime=cfg->event[i]->last;
-		if(localtime_r(&tmptime,&last_tm) == NULL)
-			memset(&last_tm,0,sizeof(last_tm));
-
-		if(tm.tm_mday==last_tm.tm_mday && tm.tm_mon==last_tm.tm_mon)
-			thisevent+=24L*60L*60L;     /* already ran today, so add 24hrs */
 		if(!event_time || thisevent<event_time) {
 			event_time=thisevent;
 			if(event!=NULL)
diff --git a/src/sbbs3/js_xtrn_area.c b/src/sbbs3/js_xtrn_area.c
index 804a77e2b5..590400ac07 100644
--- a/src/sbbs3/js_xtrn_area.c
+++ b/src/sbbs3/js_xtrn_area.c
@@ -1,9 +1,5 @@
-/* js_xtrn_area.c */
-
 /* Synchronet JavaScript "External Program Area" Object */
 
-/* $Id: js_xtrn_area.c,v 1.31 2019/01/05 06:33:39 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -17,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -83,12 +67,14 @@ static char* event_prop_desc[] = {
 	 "command-line"
 	,"startup directory"
 	,"node number"
-	,"time to execute"
+	,"time to execute (minutes since midnight)"
 	,"frequency to execute"
 	,"days of week to execute (bitfield)"
 	,"days of month to execute (bitfield)"
 	,"months of year to execute (bitfield)"
-	,"date/time last run (in time_t format)"
+	,"date/time of last run (in time_t format)"
+	,"date/time of next run (in time_t format)"
+	,"error log level"
 	,"toggle options (bitfield)"
 	,NULL
 };
@@ -105,6 +91,40 @@ static char* xedit_prop_desc[] = {
 
 #endif
 
+/* Event Object Properties */
+enum {
+	 EVENT_PROP_CMD,
+	 EVENT_PROP_STARTUP_DIR,
+	 EVENT_PROP_NODE_NUM,
+	 EVENT_PROP_TIME,
+	 EVENT_PROP_FREQ,
+	 EVENT_PROP_DAYS,
+	 EVENT_PROP_MDAYS,
+	 EVENT_PROP_MONTHS,
+	 EVENT_PROP_LAST_RUN,
+	 EVENT_PROP_NEXT_RUN,
+	 EVENT_PROP_ERRLEVEL,
+	 EVENT_PROP_MISC
+};
+
+static jsSyncPropertySpec js_event_properties[] = {
+/*		 name				,tinyid					,flags								,ver	*/
+
+	{	"cmd"				,EVENT_PROP_CMD			,JSPROP_ENUMERATE|JSPROP_READONLY	,311},
+	{	"startup_dir"		,EVENT_PROP_STARTUP_DIR	,JSPROP_ENUMERATE|JSPROP_READONLY	,311},
+	{	"node_num"			,EVENT_PROP_NODE_NUM	,JSPROP_ENUMERATE|JSPROP_READONLY	,311},
+	{	"time"				,EVENT_PROP_TIME		,JSPROP_ENUMERATE|JSPROP_READONLY	,311},
+	{	"freq"				,EVENT_PROP_FREQ		,JSPROP_ENUMERATE|JSPROP_READONLY	,311},
+	{	"days"				,EVENT_PROP_DAYS		,JSPROP_ENUMERATE|JSPROP_READONLY	,311},
+	{	"mdays"				,EVENT_PROP_MDAYS		,JSPROP_ENUMERATE|JSPROP_READONLY	,311},
+	{	"months"			,EVENT_PROP_MONTHS		,JSPROP_ENUMERATE|JSPROP_READONLY	,311},
+	{	"last_run"			,EVENT_PROP_LAST_RUN	,JSPROP_ENUMERATE|JSPROP_READONLY	,311},
+	{	"next_run"			,EVENT_PROP_NEXT_RUN	,JSPROP_ENUMERATE|JSPROP_READONLY	,31802},
+	{	"error_level"		,EVENT_PROP_ERRLEVEL	,JSPROP_ENUMERATE|JSPROP_READONLY	,31802},
+	{	"settings"			,EVENT_PROP_MISC		,JSPROP_ENUMERATE|JSPROP_READONLY	,311},
+	{ NULL }
+};
+
 BOOL DLLCALL js_CreateXtrnProgProperties(JSContext* cx, JSObject* obj, xtrn_t* xtrn)
 {
 	JSString* js_str;
@@ -182,6 +202,79 @@ BOOL DLLCALL js_CreateXtrnProgProperties(JSContext* cx, JSObject* obj, xtrn_t* x
 	return(TRUE);
 }
 
+static JSBool js_event_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
+{
+	const char* p = NULL;
+	JSString*	js_str;
+	jsval		idval;
+    jsint       tiny;
+	event_t*	event;
+
+	if((event = JS_GetPrivate(cx, obj)) == NULL)
+		return JS_FALSE;
+
+    JS_IdToValue(cx, id, &idval);
+    tiny = JSVAL_TO_INT(idval);
+
+	switch(tiny) {
+		case EVENT_PROP_CMD:
+			p = event->cmd;
+			break;
+		case EVENT_PROP_STARTUP_DIR:
+			p = event->dir;
+			break;
+		case EVENT_PROP_NODE_NUM:
+			*vp = UINT_TO_JSVAL(event->node);
+			break;
+		case EVENT_PROP_TIME:
+			*vp = UINT_TO_JSVAL(event->time);
+			break;
+		case EVENT_PROP_FREQ:
+			*vp = UINT_TO_JSVAL(event->freq);
+			break;
+		case EVENT_PROP_DAYS:
+			*vp = UINT_TO_JSVAL(event->days);
+			break;
+		case EVENT_PROP_MDAYS:
+			*vp = UINT_TO_JSVAL(event->mdays);
+			break;
+		case EVENT_PROP_MONTHS:
+			*vp = UINT_TO_JSVAL(event->months);
+			break;
+		case EVENT_PROP_LAST_RUN:
+			*vp = UINT_TO_JSVAL(event->last);
+			break;
+		case EVENT_PROP_NEXT_RUN:
+			*vp = UINT_TO_JSVAL((uint32)getnexteventtime(event));
+			break;
+		case EVENT_PROP_ERRLEVEL:
+			*vp = UINT_TO_JSVAL(event->errlevel);
+			break;
+		case EVENT_PROP_MISC:
+			*vp = UINT_TO_JSVAL(event->misc);
+			break;
+	}
+
+	if(p != NULL) {	/* string property */
+		if((js_str = JS_NewStringCopyZ(cx, p)) == NULL)
+			return JS_FALSE;
+		*vp = STRING_TO_JSVAL(js_str);
+	}
+	return JS_TRUE;
+}
+
+static JSClass js_event_class = {
+     "Event"				/* name			*/
+    ,JSCLASS_HAS_PRIVATE	/* flags		*/
+	,JS_PropertyStub		/* addProperty	*/
+	,JS_PropertyStub		/* delProperty	*/
+	,js_event_get			/* getProperty	*/
+	,JS_StrictPropertyStub	/* setProperty	*/
+	,JS_EnumerateStub		/* enumerate	*/
+	,JS_ResolveStub			/* resolve		*/
+	,JS_ConvertStub			/* convert		*/
+	,JS_FinalizeStub		/* finalize		*/
+};
 
 struct js_xtrn_area_priv {
 	scfg_t		*cfg;
@@ -407,55 +500,16 @@ JSBool DLLCALL js_xtrn_area_resolve(JSContext* cx, JSObject* areaobj, jsid id)
 
 		for(l=0;l<p->cfg->total_events;l++) {
 
-			if((eventobj=JS_NewObject(cx, NULL, NULL, NULL))==NULL)
+			if((eventobj=JS_NewObject(cx, &js_event_class, NULL, NULL))==NULL)
 				return JS_FALSE;
 
-			if(!JS_DefineProperty(cx, event_array, p->cfg->event[l]->code, OBJECT_TO_JSVAL(eventobj)
-				,NULL,NULL,JSPROP_READONLY|JSPROP_ENUMERATE))
-				return JS_FALSE;
+			JS_SetPrivate(cx, eventobj, p->cfg->event[l]);
 
-			if((js_str=JS_NewStringCopyZ(cx, p->cfg->event[l]->cmd))==NULL)
-				return JS_FALSE;
-			if(!JS_DefineProperty(cx, eventobj, "cmd", STRING_TO_JSVAL(js_str)
-				,NULL,NULL,JSPROP_ENUMERATE|JSPROP_READONLY))
-				return JS_FALSE;
-
-			if((js_str=JS_NewStringCopyZ(cx, p->cfg->event[l]->dir))==NULL)
-				return JS_FALSE;
-			if(!JS_DefineProperty(cx, eventobj, "startup_dir", STRING_TO_JSVAL(js_str)
-				,NULL,NULL,JSPROP_ENUMERATE|JSPROP_READONLY))
-				return JS_FALSE;
-
-			if(!JS_DefineProperty(cx, eventobj, "node_num", INT_TO_JSVAL(p->cfg->event[l]->node)
-				,NULL,NULL,JSPROP_ENUMERATE|JSPROP_READONLY))
+			if(!js_DefineSyncProperties(cx, eventobj, js_event_properties))
 				return JS_FALSE;
 
-			if(!JS_DefineProperty(cx, eventobj, "time", INT_TO_JSVAL(p->cfg->event[l]->time)
-				,NULL,NULL,JSPROP_ENUMERATE|JSPROP_READONLY))
-				return JS_FALSE;
-
-			if(!JS_DefineProperty(cx, eventobj, "freq", INT_TO_JSVAL(p->cfg->event[l]->freq)
-				,NULL,NULL,JSPROP_ENUMERATE|JSPROP_READONLY))
-				return JS_FALSE;
-
-			if(!JS_DefineProperty(cx, eventobj, "days", INT_TO_JSVAL(p->cfg->event[l]->days)
-				,NULL,NULL,JSPROP_ENUMERATE|JSPROP_READONLY))
-				return JS_FALSE;
-
-			if(!JS_DefineProperty(cx, eventobj, "mdays", INT_TO_JSVAL(p->cfg->event[l]->mdays)
-				,NULL,NULL,JSPROP_ENUMERATE|JSPROP_READONLY))
-				return JS_FALSE;
-
-			if(!JS_DefineProperty(cx, eventobj, "months", INT_TO_JSVAL(p->cfg->event[l]->months)
-				,NULL,NULL,JSPROP_ENUMERATE|JSPROP_READONLY))
-				return JS_FALSE;
-
-			if(!JS_DefineProperty(cx, eventobj, "last_run", INT_TO_JSVAL(p->cfg->event[l]->last)
-				,NULL,NULL,JSPROP_ENUMERATE|JSPROP_READONLY))
-				return JS_FALSE;
-
-			if(!JS_DefineProperty(cx, eventobj, "settings", INT_TO_JSVAL(p->cfg->event[l]->misc)
-				,NULL,NULL,JSPROP_ENUMERATE|JSPROP_READONLY))
+			if(!JS_DefineProperty(cx, event_array, p->cfg->event[l]->code, OBJECT_TO_JSVAL(eventobj)
+				,NULL,NULL,JSPROP_READONLY|JSPROP_ENUMERATE))
 				return JS_FALSE;
 
 #ifdef BUILD_JSDOCS
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index 13dd610031..b5abef99c3 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -1179,6 +1179,7 @@ extern "C" {
 
 	/* data.cpp */
 	DLLEXPORT time_t	DLLCALL getnextevent(scfg_t* cfg, event_t* event);
+	DLLEXPORT time_t	DLLCALL getnexteventtime(event_t* event);
 
 	/* sockopt.c */
 	DLLEXPORT int		DLLCALL set_socket_options(scfg_t* cfg, SOCKET sock, const char* section
diff --git a/src/sbbs3/scfgdefs.h b/src/sbbs3/scfgdefs.h
index 04da305be6..9ce17a5c82 100644
--- a/src/sbbs3/scfgdefs.h
+++ b/src/sbbs3/scfgdefs.h
@@ -1,7 +1,4 @@
 /* Synchronet configuration structure (scfg_t) definition */
-// vi: tabstop=4
-
-/* $Id: scfgdefs.h,v 1.62 2020/08/08 20:17:03 rswindell Exp $ */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -16,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -305,9 +290,9 @@ typedef struct {							/* External Editors */
 
 typedef struct {							/* Generic Timed Event */
 	char			code[LEN_CODE+1],		/* Internal code */
-					days,					/* Week days to run event */
 					dir[LEN_DIR+1], 		/* Start-up directory */
 					cmd[LEN_CMD+1]; 		/* Command line */
+	uint8_t			days;					/* Week days to run event */
 	uint16_t		node,					/* Node to execute event */
 					time,					/* Time to run event */
 					freq;					/* Frequency to run event */
-- 
GitLab