Repository: MDSplus/mdsplus
Pull request #2835: Fix: dispatching any action causes actmon/actlog to crash with SIGSEGV
Author: login: merlea
Labels: name: tool/actions
description: Relates to the action tools (actions, actmon, actlog)
Created: 2024-11-25T13:24:45Z | Merged: 2025-05-20T16:55:48Z

* Rename Svr -> Srv
* Always use 2 arguments for callback_done signature

A possible fix for #2829 basically setting all the `job_done`, `action_done` callbacks to have a signature ` void (*ast)(void *astprm, char *msg)` and setting `msg` to `NULL` when applicable. Using this fix with the case described in #2829 leads to no error.


Diff:
diff --git a/.gitignore b/.gitignore
index a236ed6f03..02de39fba5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -128,6 +128,7 @@ trigger.opts
 
 # bootstrap output files
 LabView/Makefile.in
+actions/testing/Makefile.in
 configure
 ChangeLog
 MANIFEST.MF
diff --git a/actions/actlogp.h b/actions/actlogp.h
index 9f4df7a100..ec0f474abe 100644
--- a/actions/actlogp.h
+++ b/actions/actlogp.h
@@ -203,7 +203,6 @@ static void MessageAst(void *dummy __attribute__((unused)), char *reply)
   }
   free(event->msg);
   free(event);
-  CheckIn(0);
 }
 
 inline static void _EventUpdate(LinkedEvent *event)
diff --git a/actions/actmon.c b/actions/actmon.c
index 9a05a3043d..c585ca35ed 100644
--- a/actions/actmon.c
+++ b/actions/actmon.c
@@ -66,7 +66,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #include <ctype.h>
 #include <mdsplus/mdsplus.h>
 static void Exit(Widget w, int *tag, XtPointer callback_data);
-static void MessageAst();
 static void EventUpdate();
 static void Phase(LinkedEvent *event);
 static void Dispatched(LinkedEvent *event);
diff --git a/actions/testing/Makefile.am b/actions/testing/Makefile.am
new file mode 100644
index 0000000000..e15cb9cdc6
--- /dev/null
+++ b/actions/testing/Makefile.am
@@ -0,0 +1,28 @@
+
+include @top_builddir@/Makefile.inc
+include ../../testing/testing.am
+
+
+## ////////////////////////////////////////////////////////////////////////// ##
+## // TESTS  //////////////////////////////////////////////////////////////// ##
+## ////////////////////////////////////////////////////////////////////////// ##
+
+TEST_EXTENSIONS = .sh
+
+TESTS = \
+  ServerShrTest.sh
+
+# Files produced by tests that must be purged
+#
+MOSTLYCLEANFILES = \
+	./*.log ./*.err\
+	*_*.characteristics* *_*.datafile* *_*.tree*
+## ////////////////////////////////////////////////////////////////////////// ##
+## // TARGETS  ////////////////////////////////////////////////////////////// ##
+## ////////////////////////////////////////////////////////////////////////// ##
+
+
+#clean-local: clean-local-tests
+
+check_PROGRAMS =
+check_SCRIPTS  = $(TESTS)
diff --git a/actions/testing/ServerShrTest.sh b/actions/testing/ServerShrTest.sh
new file mode 100755
index 0000000000..7c995720f3
--- /dev/null
+++ b/actions/testing/ServerShrTest.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+srcdir=$(readlink -f $(dirname ${0}))
+
+# See testing/ports.csv
+mon_port=8021
+act_port=8022
+dis_port=8023
+if [ ! -z $TEST_PORT_OFFET ]; then
+  mon_port=$((mon_port + TEST_PORT_OFFSET))
+  act_port=$((act_port + TEST_PORT_OFFSET))
+  dis_port=$((dis_port + TEST_PORT_OFFSET))
+fi
+
+export MONITOR_SERVER=localhost:$mon_port
+export ACTION_SERVER=localhost:$act_port
+export DISPATCH_SERVER=localhost:$dis_port
+
+
+if [ "$OS" == "windows" ]; then
+  MDSIP="wine mdsip"
+  MDSTCL="wine mdstcl"
+  ACTLOG="wine actlog"
+  DIFF_Z="-Z"
+else
+  MDSIP="mdsip"
+  MDSTCL="mdstcl"
+  ACTLOG="actlog"
+  DIFF_Z=""
+fi
+
+export distest_path=$PWD
+
+mkdir -p $distest_path
+
+$MDSIP -p ${mon_port} -s -h $MDSPLUS_DIR/testing/mdsip.hosts > server_mon.log 2> server_mon.err & # Monitor server
+mon_pid=$!
+$MDSIP -p ${act_port} -s -h $MDSPLUS_DIR/testing/mdsip.hosts > server_act.log 2> server_act.err & # Action server
+act_pid=$!
+$MDSIP -p ${dis_port} -s -h $MDSPLUS_DIR/testing/mdsip.hosts > server_dis.log 2> server_dis.err & # Dispatch server
+dis_pid=$!
+$ACTLOG -monitor ${MONITOR_SERVER} > actlog.log 2> actlog.err &
+log_pid=$!
+
+$MDSTCL @${srcdir}/build_tree.tcl
+
+$MDSTCL @${srcdir}/test_action.tcl
+
+kill -SIGKILL $mon_pid $act_pid $dis_pid $log_pid
+
+# Cut time info
+cat actlog.log | cut -d' ' -f2- > actlog_cut.log
+
+# Check output of actlog against reference
+if diff ${DIFF_Z} actlog_cut.log ${srcdir}/actlog.ans; then
+  echo "PASS: actlog"
+  exit 0
+else
+  echo "FAIL: actlog"
+  exit 1
+fi
diff --git a/actions/testing/actlog.ans b/actions/testing/actlog.ans
new file mode 100644
index 0000000000..6ab20a4e6a
--- /dev/null
+++ b/actions/testing/actlog.ans
@@ -0,0 +1,5 @@
+           1 NEW SHOT   ******************************************** ******************** DISTEST
+           1 PHASE      ******************************************** ******************** INITIALIZATION            
+           1 DISPATCHED                                              ACTION_SERVER        \DISTEST::TOP:ACT01
+           1 DOING                                                   ACTION_SERVER        \DISTEST::TOP:ACT01
+           1 DONE                                                    ACTION_SERVER        \DISTEST::TOP:ACT01
diff --git a/actions/testing/build_tree.tcl b/actions/testing/build_tree.tcl
new file mode 100644
index 0000000000..9088ee5184
--- /dev/null
+++ b/actions/testing/build_tree.tcl
@@ -0,0 +1,7 @@
+edit distest /shot=1 /new
+add node act01 /usage=action
+write
+put/extend ACT01
+Build_Action(Build_Dispatch(2,"ACTION_SERVER","INIT",10,*),BUILD_FUNCTION(BUILTIN_OPCODE("COMMA"),BUILD_FUNCTION(BUILTIN_OPCODE("WRITE"),*,"Test action"),1))
+
+close
diff --git a/actions/testing/test_action.tcl b/actions/testing/test_action.tcl
new file mode 100644
index 0000000000..f92bf8f371
--- /dev/null
+++ b/actions/testing/test_action.tcl
@@ -0,0 +1,6 @@
+dispatch /command /server=DISPATCH_SERVER set tree distest /shot=1
+wait 1
+dispatch /command /server=DISPATCH_SERVER dispatch/build/monitor=MONITOR_SERVER
+wait 1
+dispatch /command /server=DISPATCH_SERVER dispatch/phase/monitor=MONITOR_SERVER init
+wait 1
diff --git a/configure.ac b/configure.ac
index 188d5bbcfb..88f779cc8c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1375,6 +1375,7 @@ AC_OUTPUT(
   Makefile.inc
   _include/_mdsversion.h
   actions/Makefile
+  actions/testing/Makefile
   camshr/Makefile
   ccl/Makefile
   d3dshr/Makefile
diff --git a/include/servershr.h b/include/servershr.h
index 2e4ba4b3b9..7677304973 100644
--- a/include/servershr.h
+++ b/include/servershr.h
@@ -28,17 +28,17 @@ EXPORT extern int ServerBuildDispatchTable(char *wildcard, char *monitor_name,
                                            void **table);
 EXPORT extern int ServerCloseTrees(char *server);
 EXPORT extern int ServerCreatePulse(int *id, char *server, char *tree, int shot,
-                                    void (*ast)(), void *astprm, int *retstatus,
+                                    void (*ast)(void *, char *), void *astprm, int *retstatus,
                                     pthread_rwlock_t *lock,
                                     void (*before_ast)());
 EXPORT extern int ServerDispatchAction(int *id, char *server, char *tree,
-                                       int shot, int nid, void (*ast)(),
+                                       int shot, int nid, void (*ast)(void *, char *),
                                        void *astprm, int *retstatus,
                                        pthread_rwlock_t *lock, int *socket,
                                        void (*before_ast)());
 EXPORT extern int ServerDispatchClose(void *vtable);
 EXPORT extern int ServerDispatchCommand(int *id, char *server, char *cli,
-                                        char *command, void (*ast)(),
+                                        char *command, void (*ast)(void *, char *),
                                         void *astprm, int *retstatus,
                                         pthread_rwlock_t *lock,
                                         void (*before_ast)());
@@ -51,7 +51,7 @@ EXPORT extern int ServerDispatchPhase(int *id, void *vtable, char *phasenam,
                                       const char *monitor);
 EXPORT extern int ServerFailedEssential(void *vtable, int reset);
 EXPORT extern char *ServerFindServers(void **ctx, char *wild_match);
-EXPORT extern int ServerMonitorCheckin(char *server, void (*ast)(),
+EXPORT extern int ServerMonitorCheckin(char *server, void (*ast)(void *, char *),
                                        void *astparam);
 EXPORT extern int ServerSetLogging(char *server, char logging_mode);
 EXPORT extern int ServerStartServer(char *server);
diff --git a/servershr/Client.h b/servershr/Client.h
index f213582749..0f7c45a8b7 100644
--- a/servershr/Client.h
+++ b/servershr/Client.h
@@ -108,7 +108,7 @@ static void Client_cleanup_jobs(Client *c, fd_set *fdactive)
     Job *j = Job_pop_by_conid(conid);
     if (j)
     {
-      Job_callback_done(j, ServerPATH_DOWN, FALSE);
+      Job_callback_done(j, ServerPATH_DOWN, NULL, FALSE);
       free(j);
     }
     else
@@ -198,7 +198,7 @@ static void Client_do_message(Client *c, fd_set *fdactive)
       j = Job_get_by_jobid(MonJob);
     if (j)
     {
-      Job_callback_done(j, status, TRUE);
+      Job_callback_done(j, status, msg, TRUE);
     }
     else
     {
diff --git a/servershr/Job.h b/servershr/Job.h
index dc1aa79990..ccdaae7377 100644
--- a/servershr/Job.h
+++ b/servershr/Job.h
@@ -11,7 +11,7 @@ typedef struct job
   int conid;
   int *retstatus;
   pthread_rwlock_t *lock;
-  void (*callback_done)();
+  void (*callback_done)(void *, char*);
   void (*callback_before)();
   void *callback_param;
   pthread_cond_t *cond;
@@ -30,7 +30,7 @@ static Job *Jobs = NULL;
 static int MonJob = -1;
 
 static Job *newJob(int conid, int *retstatus, pthread_rwlock_t *lock,
-                   void (*callback_done)(void *),
+                   void (*callback_done)(void *, char *),
                    void *callback_param,
                    void (*callback_before)(void *))
 {
@@ -71,7 +71,7 @@ static void Job_pop(Job *job)
 static int Job_register(int *msgid,
                         int conid, int *retstatus, pthread_rwlock_t *lock,
                         void *callback_param,
-                        void (*callback_done)(void *),
+                        void (*callback_done)(void *, char *),
                         void (*callback_before)(void *))
 {
   Job *j = newJob(conid, retstatus, lock, callback_param, callback_done, callback_before);
@@ -107,10 +107,10 @@ static void Job_callback_before(Job *job)
 }
 
 /// returns  true if job was popped
-static int Job_callback_done(Job *j, int status, int remove)
+static int Job_callback_done(Job *j, int status, char *msg, int remove)
 {
   MDSDBG(JOB_PRI " status=%d, remove=%d", JOB_VAR(j), status, remove);
-  void (*callback_done)(void *);
+  void (*callback_done)(void *, char *);
   const int is_mon = j->jobid == MonJob;
   if (j->lock)
     pthread_rwlock_wrlock(j->lock);
@@ -122,7 +122,7 @@ static int Job_callback_done(Job *j, int status, int remove)
   if (j->lock)
     pthread_rwlock_unlock(j->lock);
   if (callback_done)
-    callback_done(j->callback_param);
+    callback_done(j->callback_param, msg);
   /**** If job has a condition, RemoveJob will not remove it. ***/
   if (remove && !is_mon)
   {
@@ -258,7 +258,7 @@ static void Job_cleanup(int status, int jobid)
   do
   {
     MDSDBG(JOB_PRI " done", JOB_VAR(j));
-    Job_callback_done(j, status, FALSE);
+    Job_callback_done(j, status, NULL, FALSE);
     free(j);
     j = Job_pop_by_conid(conid);
   } while (j);
diff --git a/servershr/ServerCreatePulse.c b/servershr/ServerCreatePulse.c
index 3e9b8b53e6..6dd0777365 100644
--- a/servershr/ServerCreatePulse.c
+++ b/servershr/ServerCreatePulse.c
@@ -59,7 +59,7 @@ dsc$descriptor *tree, int *shot, void (*ast)(), int astprm, int *netid, void
 #include "servershrp.h"
 
 EXPORT int ServerCreatePulse(int *id, char *server, char *tree, int shot,
-                             void (*ast)(), void *astprm, int *retstatus,
+                             void (*ast)(void *, char *), void *astprm, int *retstatus,
                              pthread_rwlock_t *lock, void (*before_ast)())
 {
   struct descrip p1, p2;
diff --git a/servershr/ServerDispatchAction.c b/servershr/ServerDispatchAction.c
index cdba034100..96a8f8e21e 100644
--- a/servershr/ServerDispatchAction.c
+++ b/servershr/ServerDispatchAction.c
@@ -59,7 +59,7 @@ dsc$descriptor *tree, int *shot, int *nid, void (*ast)(), int astprm, int
 #include "servershrp.h"
 
 EXPORT int ServerDispatchAction(int *id, char *server, char *tree, int shot,
-                                int nid, void (*ast)(), void *astprm,
+                                int nid, void (*ast)(void *, char*), void *astprm,
                                 int *retstatus, pthread_rwlock_t *lock,
                                 int *socket, void (*before_ast)())
 {
diff --git a/servershr/ServerDispatchCommand.c b/servershr/ServerDispatchCommand.c
index 7151ea0b3b..35bbec702c 100644
--- a/servershr/ServerDispatchCommand.c
+++ b/servershr/ServerDispatchCommand.c
@@ -59,7 +59,7 @@ int *netid, void (*link_down)(), void (*before_ast)())
 #include "servershrp.h"
 
 EXPORT int ServerDispatchCommand(int *id, char *server, char *cli,
-                                 char *command, void (*ast)(), void *astprm,
+                                 char *command, void (*ast)(void *, char *), void *astprm,
                                  int *retstatus, pthread_rwlock_t *lock,
                                  void (*before_ast)())
 {
diff --git a/servershr/ServerDispatchPhase.c b/servershr/ServerDispatchPhase.c
index b6ee15302e..612e70c2f5 100644
--- a/servershr/ServerDispatchPhase.c
+++ b/servershr/ServerDispatchPhase.c
@@ -86,7 +86,7 @@ extern int ProgLoc;
 
 static void dispatch(int idx);
 static void send_monitor(int mode, int idx);
-static void action_done(intptr_t idx);
+static void action_done(void *arg, char *msg);
 static void action_done_action_locked(int idx);
 static void action_done_action_unlocked(int idx);
 static void before(int idx);
@@ -723,7 +723,7 @@ static void action_done_action_unlocked(int idx)
         UNLOCK_ACTION(cidx, ad_fte);
         WRLOCK_ACTION(cidx, ad_fte);
         actions[cidx].status = ServerINVALID_DEPENDENCY;
-        action_done(cidx);
+        action_done((void *)(intptr_t)cidx, NULL);
         UNLOCK_ACTION(cidx, ad_fte);
         action_done_action_unlocked(cidx);
       }
@@ -836,18 +836,20 @@ static void action_done_thread()
   pthread_cleanup_pop(1);
 }
 
-static void action_done(intptr_t i)
+static void action_done(void *arg, char *dummy __attribute__((unused)))
 {
   INIT_STATUS;
   static pthread_t thread;
+  intptr_t i = (intptr_t) arg;
   action_done_push(i); /***** must be done before starting thread ****/
   CONDITION_START_THREAD(&ActionDoneRunningC, thread, , action_done_thread, NULL);
   if (STATUS_NOT_OK)
     perror("action_done: pthread creation failed");
 }
 #else
-static inline void action_done(intptr_t i)
+static inline void action_done(void *arg, char *dummy __attribute__((unused)))
 {
+  intptr_t i = (intptr_t) arg;
   return action_done_do(i);
 }
 #endif
diff --git a/servershr/ServerMonitorCheckin.c b/servershr/ServerMonitorCheckin.c
index fa1971c9a9..2e1414f2ee 100644
--- a/servershr/ServerMonitorCheckin.c
+++ b/servershr/ServerMonitorCheckin.c
@@ -101,7 +101,7 @@ static inline int using_events(char *server, void (*ast)(), void *astprm)
   return yesno;
 }
 
-EXPORT int ServerMonitorCheckin(char *server, void (*ast)(), void *astprm)
+EXPORT int ServerMonitorCheckin(char *server, void (*ast)(void *, char *), void *astprm)
 {
   if (using_events(server, ast, astprm))
     return MDSplusSUCCESS;
diff --git a/servershr/ServerQAction.c b/servershr/ServerQAction.c
index 3fe442633c..3bbf343322 100644
--- a/servershr/ServerQAction.c
+++ b/servershr/ServerQAction.c
@@ -196,12 +196,12 @@ EXPORT int ServerQAction(uint32_t *addrp, uint16_t *portp, int *op, int *flags,
     job.nid = *(int *)p3;
     if (job.h.addr)
     {
-      MDSDBG(SVRACTIONJOB_PRI, SVRACTIONJOB_VAR(&job));
+      MDSDBG(SRVACTIONJOB_PRI, SRVACTIONJOB_VAR(&job));
       status = QJob((SrvJob *)&job);
     }
     else
     {
-      MDSWRN(SVRACTIONJOB_PRI " No Addr", SVRACTIONJOB_VAR(&job));
+      MDSWRN(SRVACTIONJOB_PRI " No Addr", SRVACTIONJOB_VAR(&job));
       status = DoSrvAction((SrvJob *)&job);
     }
     break;
@@ -217,12 +217,12 @@ EXPORT int ServerQAction(uint32_t *addrp, uint16_t *portp, int *op, int *flags,
     job.h.jobid = *jobid;
     if (job.h.addr)
     {
-      MDSDBG(SVRCLOSEJOB_PRI, SVRCLOSEJOB_VAR(&job));
+      MDSDBG(SRVCLOSEJOB_PRI, SRVCLOSEJOB_VAR(&job));
       status = QJob((SrvJob *)&job);
     }
     else
     {
-      MDSWRN(SVRCLOSEJOB_PRI " No Addr", SVRCLOSEJOB_VAR(&job));
+      MDSWRN(SRVCLOSEJOB_PRI " No Addr", SRVCLOSEJOB_VAR(&job));
       status = DoSrvClose((SrvJob *)&job);
     }
     break;
@@ -240,12 +240,12 @@ EXPORT int ServerQAction(uint32_t *addrp, uint16_t *portp, int *op, int *flags,
     job.shot = *(int *)p2;
     if (job.h.addr)
     {
-      MDSDBG(SVRCREATEPULSEJOB_PRI, SVRCREATEPULSEJOB_VAR(&job));
+      MDSDBG(SRVCREATEPULSEJOB_PRI, SRVCREATEPULSEJOB_VAR(&job));
       status = QJob((SrvJob *)&job);
     }
     else
     {
-      MDSWRN(SVRCREATEPULSEJOB_PRI " No Addr", SVRCREATEPULSEJOB_VAR(&job));
+      MDSWRN(SRVCREATEPULSEJOB_PRI " No Addr", SRVCREATEPULSEJOB_VAR(&job));
       status = DoSrvCreatePulse((SrvJob *)&job);
     }
     break;
@@ -271,12 +271,12 @@ EXPORT int ServerQAction(uint32_t *addrp, uint16_t *portp, int *op, int *flags,
     job.command = strdup((char *)p2);
     if (job.h.addr)
     {
-      MDSDBG(SVRCOMMANDJOB_PRI, SVRCOMMANDJOB_VAR(&job));
+      MDSDBG(SRVCOMMANDJOB_PRI, SRVCOMMANDJOB_VAR(&job));
       status = QJob((SrvJob *)&job);
     }
     else
     {
-      MDSWRN(SVRCOMMANDJOB_PRI " No Addr", SVRCOMMANDJOB_VAR(&job));
+      MDSWRN(SRVCOMMANDJOB_PRI " No Addr", SRVCOMMANDJOB_VAR(&job));
       status = DoSrvCommand((SrvJob *)&job);
     }
     break;
@@ -300,12 +300,12 @@ EXPORT int ServerQAction(uint32_t *addrp, uint16_t *portp, int *op, int *flags,
     job.status = *(int *)p8;
     if (job.h.addr)
     {
-      MDSDBG(SVRMONITORJOB_PRI, SVRMONITORJOB_VAR(&job));
+      MDSDBG(SRVMONITORJOB_PRI, SRVMONITORJOB_VAR(&job));
       status = QJob((SrvJob *)&job);
     }
     else
     {
-      MDSWRN(SVRMONITORJOB_PRI " No Addr", SVRMONITORJOB_VAR(&job));
+      MDSWRN(SRVMONITORJOB_PRI " No Addr", SRVMONITORJOB_VAR(&job));
       status = MDSplusERROR;
     }
     break;
@@ -429,7 +429,7 @@ static int RemoveLast()
       JobQueueNext->h.next = 0;
     else
       JobQueue = 0;
-    MDSMSG(SVRJOB_PRI "Removed pending action", SVRJOB_VAR(job));
+    MDSMSG(SRVJOB_PRI "Removed pending action", SRVJOB_VAR(job));
     FreeJob(job);
     status = MDSplusSUCCESS;
   }
@@ -948,7 +948,7 @@ static SOCKET setup_client(SrvJob *job)
     if (sock != INVALID_SOCKET)
     {
       add_client(addr, port, sock);
-      MDSMSG("setup connection %" PRI_SOCKET " " SVRJOB_PRI, sock, SVRJOB_VAR(job));
+      MDSMSG("setup connection %" PRI_SOCKET " " SRVJOB_PRI, sock, SRVJOB_VAR(job));
     }
   }
   return sock;
@@ -961,11 +961,11 @@ static void cleanup_client(SrvJob *job)
     close_socket(sock);
   if (STATIC_Debug)
   {
-    MDSMSG("cleanup connection %" PRI_SOCKET " " SVRJOB_PRI, sock, SVRJOB_VAR(job));
+    MDSMSG("cleanup connection %" PRI_SOCKET " " SRVJOB_PRI, sock, SRVJOB_VAR(job));
   }
   else
   {
-    MDSDBG("cleanup connection %" PRI_SOCKET " " SVRJOB_PRI, sock, SVRJOB_VAR(job));
+    MDSDBG("cleanup connection %" PRI_SOCKET " " SRVJOB_PRI, sock, SRVJOB_VAR(job));
   }
 }
 
@@ -992,7 +992,7 @@ static int send_all(SOCKET sock, char *msg, int len)
 
 static int send_reply(SrvJob *job, int replyType, int status_in, int length, char *msg)
 {
-  MDSDBG(SVRJOB_PRI " %d", SVRJOB_VAR(job), replyType);
+  MDSDBG(SRVJOB_PRI " %d", SRVJOB_VAR(job), replyType);
   int status = MDSplusERROR;
   long msg_len = msg ? (long)strlen(msg) : 0;
   int try_again = FALSE;
@@ -1005,7 +1005,7 @@ static int send_reply(SrvJob *job, int replyType, int status_in, int length, cha
     SOCKET sock = setup_client(job);
     if (sock == INVALID_SOCKET)
     {
-      MDSMSG(SVRJOB_PRI " break connection", SVRJOB_VAR(job));
+      MDSMSG(SRVJOB_PRI " break connection", SRVJOB_VAR(job));
       cleanup_client(job);
       break;
     }
diff --git a/servershr/ServerSendMessage.c b/servershr/ServerSendMessage.c
index 4dcaa17c64..bd7d6fcd51 100644
--- a/servershr/ServerSendMessage.c
+++ b/servershr/ServerSendMessage.c
@@ -106,7 +106,7 @@ static SOCKET get_socket_by_conid(int conid)
 // Each thread uses a different port, thus a different network connection.
 // Connections persist and thus handle all traffic between the endpoints.
 int ServerSendMessage(int *msgid, char *server, int op, int *retstatus,
-                      pthread_rwlock_t *lock, int *conid_out, void (*callback_done)(),
+                      pthread_rwlock_t *lock, int *conid_out, void (*callback_done)(void *, char *),
                       void *callback_param, void (*callback_before)(), int numargs_in,
                       ...)
 {
@@ -125,7 +125,7 @@ int ServerSendMessage(int *msgid, char *server, int op, int *retstatus,
       MDSWRN("failed to connect");
     }
     if (callback_done)
-      callback_done(callback_param);
+      callback_done(callback_param, NULL);
     return ServerPATH_DOWN;
   }
   INIT_STATUS;
@@ -155,7 +155,7 @@ int ServerSendMessage(int *msgid, char *server, int op, int *retstatus,
   {
     MDSWRN("could not resolve address socket %" PRI_SOCKET " is bound to", sock);
     if (callback_done)
-      callback_done(callback_param);
+      callback_done(callback_param, NULL);
     return ServerSOCKET_ADDR_ERROR;
   }
   jobid = Job_register(msgid, conid, retstatus, lock, callback_done, callback_param, callback_before);
diff --git a/servershr/servershrp.h b/servershr/servershrp.h
index 8d6b84bbe1..070185fa7a 100644
--- a/servershr/servershrp.h
+++ b/servershr/servershrp.h
@@ -83,8 +83,8 @@ typedef struct _SrvJob
 {
   JHeader h;
 } SrvJob;
-#define SVRJOB_PRI "SvrJob" JHEADER_PRI
-#define SVRJOB_VAR(j) JHEADER_VAR(&(j)->h)
+#define SRVJOB_PRI "SrvJob" JHEADER_PRI
+#define SRVJOB_VAR(j) JHEADER_VAR(&(j)->h)
 
 typedef struct
 {
@@ -93,15 +93,15 @@ typedef struct
   int shot;
   int nid;
 } SrvActionJob;
-#define SVRACTIONJOB_PRI "SrvActionJob(" JHEADER_PRI ", tree='%s', shot=%d, nid=%d)"
-#define SVRACTIONJOB_VAR(j) JHEADER_VAR(&(j)->h), (j)->tree, (j)->shot, (j)->nid
+#define SRVACTIONJOB_PRI "SrvActionJob(" JHEADER_PRI ", tree='%s', shot=%d, nid=%d)"
+#define SRVACTIONJOB_VAR(j) JHEADER_VAR(&(j)->h), (j)->tree, (j)->shot, (j)->nid
 
 typedef struct
 {
   JHeader h;
 } SrvCloseJob;
-#define SVRCLOSEJOB_PRI "SrvCloseJob(" JHEADER_PRI ")"
-#define SVRCLOSEJOB_VAR(j) JHEADER_VAR(&(j)->h)
+#define SRVCLOSEJOB_PRI "SrvCloseJob(" JHEADER_PRI ")"
+#define SRVCLOSEJOB_VAR(j) JHEADER_VAR(&(j)->h)
 
 typedef struct
 {
@@ -109,8 +109,8 @@ typedef struct
   char *tree;
   int shot;
 } SrvCreatePulseJob;
-#define SVRCREATEPULSEJOB_PRI "SrvCreatePulseJob(" JHEADER_PRI ", tree='%s', shot=%d)"
-#define SVRCREATEPULSEJOB_VAR(j) JHEADER_VAR(&(j)->h), (j)->tree, (j)->shot
+#define SRVCREATEPULSEJOB_PRI "SrvCreatePulseJob(" JHEADER_PRI ", tree='%s', shot=%d)"
+#define SRVCREATEPULSEJOB_VAR(j) JHEADER_VAR(&(j)->h), (j)->tree, (j)->shot
 
 typedef struct
 {
@@ -118,8 +118,8 @@ typedef struct
   char *table;
   char *command;
 } SrvCommandJob;
-#define SVRCOMMANDJOB_PRI "SrvCommandJob(" JHEADER_PRI ", table='%s', command='%s')"
-#define SVRCOMMANDJOB_VAR(j) JHEADER_VAR(&(j)->h), (j)->table, (j)->command
+#define SRVCOMMANDJOB_PRI "SrvCommandJob(" JHEADER_PRI ", table='%s', command='%s')"
+#define SRVCOMMANDJOB_VAR(j) JHEADER_VAR(&(j)->h), (j)->table, (j)->command
 
 typedef struct
 {
@@ -133,8 +133,8 @@ typedef struct
   char *server;
   int status;
 } SrvMonitorJob;
-#define SVRMONITORJOB_PRI "SrvMonitorJob(" JHEADER_PRI ", tree='%s', shot=%d, phase=%d, nid=%d, on=%d, mode=%d, server='%s', status=%d)"
-#define SVRMONITORJOB_VAR(j) JHEADER_VAR(&(j)->h), (j)->tree, (j)->shot, (j)->phase, (j)->nid, (j)->on, (j)->mode, (j)->server, (j)->status
+#define SRVMONITORJOB_PRI "SrvMonitorJob(" JHEADER_PRI ", tree='%s', shot=%d, phase=%d, nid=%d, on=%d, mode=%d, server='%s', status=%d)"
+#define SRVMONITORJOB_VAR(j) JHEADER_VAR(&(j)->h), (j)->tree, (j)->shot, (j)->phase, (j)->nid, (j)->on, (j)->mode, (j)->server, (j)->status
 
 typedef struct
 {
@@ -179,7 +179,7 @@ typedef struct
 
 #ifndef _NO_SERVER_SEND_MESSAGE_PROTO
 extern int ServerSendMessage(int *msgid, char *server, int op, int *retstatus,
-                             pthread_rwlock_t *lock, int *socket, void (*ast)(),
+                             pthread_rwlock_t *lock, int *socket, void (*ast)(void *, char *),
                              void *astparam, void (*before_ast)(),
                              int numargs_in, ...);
 #endif
diff --git a/tcl/tcl_dispatch.c b/tcl/tcl_dispatch.c
index d01771525c..167f9b868d 100644
--- a/tcl/tcl_dispatch.c
+++ b/tcl/tcl_dispatch.c
@@ -436,8 +436,9 @@ typedef struct
   char *command;
 } DispatchedCommand;
 
-static void CommandDone(DispatchedCommand *command)
+static void CommandDone(void *arg, char *dummy __attribute__((unused)))
 {
+  DispatchedCommand *command = (DispatchedCommand *) arg;
   if (IS_NOT_OK(command->status))
   {
     char *msg = MdsGetMsg(command->status);
@@ -507,7 +508,7 @@ EXPORT int TclDispatch_command(void *ctx, char **error,
       iostatusp = &c.cmd->status;
     }
     status = ServerDispatchCommand(c.sid, c.svr, c.tab, c.cmd->command,
-                                   CommandDone, c.cmd, iostatusp, NULL, 0);
+                                   CommandDone, (void *)c.cmd, iostatusp, NULL, 0);
     if (STATUS_NOT_OK)
     {
       MDSMSG("ServerDispatchCommand failed.");
diff --git a/testing/Makefile.am b/testing/Makefile.am
index 7ff4500784..a79a604345 100644
--- a/testing/Makefile.am
+++ b/testing/Makefile.am
@@ -59,7 +59,8 @@ C_TEST_DIRS =\
 	mdslib/testing\
 	mdstcpip/testing\
 	tditest/testing\
-	mdsobjects/cpp/testing
+	mdsobjects/cpp/testing\
+	actions/testing
 #	testing/selftest
 
 TEST_DIRS ?=\
diff --git a/testing/ports.csv b/testing/ports.csv
index 43e30f8ebf..e3a2b87b2c 100644
--- a/testing/ports.csv
+++ b/testing/ports.csv
@@ -6,4 +6,5 @@ python/MDSplus/tests/connection-thick, 8013
 python/MDSplus/tests/connection-tcp, 8014
 python/MDSplus/tests/connection-write, 8015
 python/MDSplus/tests/dcl-dispatcher, 8016-8017
-python/MDSplus/tests/dcl-timeout, 8018
\ No newline at end of file
+python/MDSplus/tests/dcl-timeout, 8018
+actions/testing/ServerShrTest.sh, 8021-8023


Reviews:
user: login: WhoBrokeTheBuild
body: Once these are done, and the tests pass, I think we should merge this
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-09T15:19:51Z
user: login: merlea
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T14:09:18Z
user: login: merlea
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T14:09:29Z
user: login: WhoBrokeTheBuild
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-20T16:55:37Z

Review comments:
diff_hunk: @@ -0,0 +1,30 @@
+#!/bin/bash
+
+srcdir=$(readlink -f $(dirname ${0}))
+
+export distest_path=$PWD
+
+mkdir -p $distest_path
+
+mdsip -p 9997 -s > server_9997.log 2> server_9997.err & # Monitor server
+mdsip -p 9998 -s > server_9998.log 2> server_9998.err & # Action server
+mdsip -p 9999 -s > server_9999.log 2> server_9999.err & # Dispatch server
+actlog -monitor localhost:9997 > actlog.log 2> actlog.err &
+
+mdstcl @${srcdir}/build_tree.tcl
+
+mdstcl @${srcdir}/test_action.tcl
+
+pkill -P $$
+
+# Cut time info
+cat actlog.log | cut -d' ' -f2- > actlog_cut.log
+
+# Check output of actlog against reference
+if diff actlog_cut.log ${srcdir}/actlog.log_ref; then
path: actions/testing/ServerShrTest.sh
user: login: WhoBrokeTheBuild
body: Just like the other PR, can we rename .log_ref to .ans
created_at: 2025-05-09T15:15:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,30 @@
+#!/bin/bash
+
+srcdir=$(readlink -f $(dirname ${0}))
+
+export distest_path=$PWD
+
+mkdir -p $distest_path
+
+mdsip -p 9997 -s > server_9997.log 2> server_9997.err & # Monitor server
path: actions/testing/ServerShrTest.sh
user: login: WhoBrokeTheBuild
body: Like you did in the other PR, can you allocate ports for this and use the $TEST_PORT_OFFSET? This is currently failing on all platforms due to conflicting ports.
created_at: 2025-05-09T15:17:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,30 @@
+#!/bin/bash
+
+srcdir=$(readlink -f $(dirname ${0}))
+
+export distest_path=$PWD
+
+mkdir -p $distest_path
+
+mdsip -p 9997 -s > server_9997.log 2> server_9997.err & # Monitor server
path: actions/testing/ServerShrTest.sh
user: login: merlea
body: Done.
created_at: 2025-05-19T14:09:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,30 @@
+#!/bin/bash
+
+srcdir=$(readlink -f $(dirname ${0}))
+
+export distest_path=$PWD
+
+mkdir -p $distest_path
+
+mdsip -p 9997 -s > server_9997.log 2> server_9997.err & # Monitor server
+mdsip -p 9998 -s > server_9998.log 2> server_9998.err & # Action server
+mdsip -p 9999 -s > server_9999.log 2> server_9999.err & # Dispatch server
+actlog -monitor localhost:9997 > actlog.log 2> actlog.err &
+
+mdstcl @${srcdir}/build_tree.tcl
+
+mdstcl @${srcdir}/test_action.tcl
+
+pkill -P $$
+
+# Cut time info
+cat actlog.log | cut -d' ' -f2- > actlog_cut.log
+
+# Check output of actlog against reference
+if diff actlog_cut.log ${srcdir}/actlog.log_ref; then
path: actions/testing/ServerShrTest.sh
user: login: merlea
body: Done.
created_at: 2025-05-19T14:09:29Z
author_association: CONTRIBUTOR

PR comments:
user: login: zack-vii
created_at: 2024-11-25T13:31:15Z
body: I think we need to add a test maybe using actlog (terminal version of actmon).
What i found is that actmon/actlog dont handle null either .. further they need to be protected in case the action monitor server (mdsip) becomes offline otherwise it may generate a stack overflow. 
author_association: CONTRIBUTOR
user: login: zack-vii
created_at: 2024-11-25T20:28:48Z
body: If the ACTION_MONITOR server becomes unreachabel/offline The CheckIN will eventually call MessageAst with a NULL for message. This will cause if (!parseMsg(reply, event)) to skip and call CheckIN recursively until the program stack overflows.

We need to be able to detect this from the callback msg somehow, possibly by returning a special message instead. alternatively the CheckIN must be called from a async outside of MessageAst with some wait time to prevent a runaway process at 100% as well as the stack overflow due to recursion. 
author_association: CONTRIBUTOR
user: login: merlea
created_at: 2024-11-26T11:39:03Z
body: OK. I removed the recursive calls to `CheckIn` since it already had a loop with pauses.

I also put some stubs for tests of `actlog` based on the original issue. We could compare the output of `actlog` with the expected one. I am not too familiar with the tests setup so let me know how best to integrate this.
author_association: CONTRIBUTOR
user: login: joshStillerman
created_at: 2024-12-02T19:26:48Z
body: retest this please
author_association: CONTRIBUTOR
user: login: WhoBrokeTheBuild
created_at: 2025-01-02T19:23:41Z
body: By changing the definition of the `ast` parameter to include arguments, you've triggered some compile errors. You can run a build/test with:

```
./deploy/build.sh --os=bootstrap
./deploy/build.sh --os=ubuntu24 --test
```

And here is the error from the Jenkins build:

```
gcc -Wall -Wextra -Werror   -g -DWITH_DEBUG_SYMBOLS -O0 -fvisibility=hidden  -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -fpic -shared-libgcc -fsigned-char -fno-strict-aliasing   -DLIBPREFIX=MDSplus -pthread -I/workspace/tests/64/_include -I/opt/jenkins/workspace/MDSplus_PR-2835/debian10-64/_include -I/workspace/tests/64/include -I/opt/jenkins/workspace/MDSplus_PR-2835/debian10-64/include  -m64 -c -o tcl_dispatch.o /opt/jenkins/workspace/MDSplus_PR-2835/debian10-64/tcl/tcl_dispatch.c

/opt/jenkins/workspace/MDSplus_PR-2835/debian10-64/tcl/tcl_dispatch.c: In function 'TclDispatch_command':

/opt/jenkins/workspace/MDSplus_PR-2835/debian10-64/tcl/tcl_dispatch.c:510:36: error: passing argument 5 of 'ServerDispatchCommand' from incompatible pointer type [-Werror=incompatible-pointer-types]

                                    CommandDone, c.cmd, iostatusp, NULL, 0);

                                    ^~~~~~~~~~~

In file included from /opt/jenkins/workspace/MDSplus_PR-2835/debian10-64/tcl/tcl_dispatch.c:35:

/opt/jenkins/workspace/MDSplus_PR-2835/debian10-64/include/servershr.h:41:63: note: expected 'void (*)(void *, char *)' but argument is of type 'void (*)(DispatchedCommand *, char *)' {aka 'void (*)(struct <anonymous> *, char *)'}

                                         char *command, void (*ast)(void *, char *),

                                                        ~~~~~~~^~~~~~~~~~~~~~~~~~~~
```

If you include a fix for that, we will rerun the Jenkins build. Otherwise, we will look into this when we have time.

Thank you for investigating this so thoroughly, we greatly appreciate your help :)
author_association: MEMBER
user: login: zack-vii
created_at: 2025-03-04T21:33:06Z
body: sorry i totally missed this. for your tests comparing output with expected output i think the tditest tests are a good candidate to copy from.
author_association: CONTRIBUTOR
user: login: merlea
created_at: 2025-03-28T15:14:27Z
body: @WhoBrokeTheBuild I should have fixed the compilation issue.
I still have to work on the integration of tests, but perhaps we can rerun the Jenkins build to check that the compilation indeed goes through.
author_association: CONTRIBUTOR
user: login: WhoBrokeTheBuild
created_at: 2025-03-28T15:23:29Z
body: Retest this please
author_association: MEMBER
user: login: merlea
created_at: 2025-04-03T09:32:35Z
body: OK. I have integrated the tests with the test suite.

I think this is now ready for a final review. @WhoBrokeTheBuild @zack-vii 
author_association: CONTRIBUTOR
user: login: merlea
created_at: 2025-04-04T15:49:43Z
body: > OK. I have integrated the tests with the test suite.
> 
> I think this is now ready for a final review. @WhoBrokeTheBuild @zack-vii

@WhoBrokeTheBuild can we trigger the tests again please?
author_association: CONTRIBUTOR
user: login: WhoBrokeTheBuild
created_at: 2025-04-04T20:35:51Z
body: I'm going to do some local verification early next week, and then I'll trigger the tests and we'll get this merged, thanks again for all your hard work!
author_association: MEMBER
user: login: merlea
created_at: 2025-05-05T18:17:34Z
body: Ping @WhoBrokeTheBuild , just making sure we don't forget this PR...
author_association: CONTRIBUTOR
user: login: heidthecamp
created_at: 2025-05-09T14:44:13Z
body: Retest this please
author_association: CONTRIBUTOR
user: login: heidthecamp
created_at: 2025-05-19T14:03:29Z
body: Retest this please
author_association: CONTRIBUTOR
user: login: merlea
created_at: 2025-05-19T14:10:50Z
body: I implemented the requested changes but I expect some similar problems to #2906 in the tests for windows for example.
author_association: CONTRIBUTOR
user: login: WhoBrokeTheBuild
created_at: 2025-05-19T18:06:31Z
body: Awesome, thank you! We really appreciate your patience while we work to get these merged :tada: 
I spent a bit of time on it today, and I have some changes ready for you, I'll attach a diff.

The big things were:
* Conditionally prefix commands with `wine`
* Use ${TYPE}_SERVER variables to hold the localhost:${port} for each of the mdsip services (this means you don't have to hardcode the port numbers, and they will work when $TEST_PORT_OFFSET is set)
* Switch from pkill to kill, recording each process PID, which means we can run in the docker images that don't have pkill
* Add `--strip-trailing-cr` to `diff` so that it works with the windows build

[merlea-bugfix-servershr-diff.txt](https://github.com/user-attachments/files/20301823/merlea-bugfix-servershr-diff.txt)

author_association: MEMBER
user: login: WhoBrokeTheBuild
created_at: 2025-05-19T18:34:01Z
body: Oh and I forgot to mention / include it in the diff, but please move the ports close to the range of other allocated ports in `ports.csv`, your other PR uses up through 8020 so I would use 8021-8023, cheers
author_association: MEMBER
user: login: merlea
created_at: 2025-05-20T08:38:08Z
body: > Oh and I forgot to mention / include it in the diff, but please move the ports close to the range of other allocated ports in `ports.csv`, your other PR uses up through 8020 so I would use 8021-8023, cheers

OK. Done. I think I covered everything, some parts I had worked on before you sent the diff, so they are slightly different.
author_association: CONTRIBUTOR
user: login: heidthecamp
created_at: 2025-05-20T14:48:13Z
body: Retest this please
author_association: CONTRIBUTOR