From 7808d8786c0dff627df88af98a9268a7a8fd56b7 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:17:13 +1100 Subject: [PATCH 1/4] task: release iocontrol.0 when milltask exits Since the IO handling moved into task, milltask owns the iocontrol.0 component but never called hal_exit() on it, so the component stayed registered after milltask was gone. The Task destructor now releases it and the shutdown deletes the Task object. --- src/emc/task/emctaskmain.cc | 4 ++++ src/emc/task/taskclass.cc | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/emc/task/emctaskmain.cc b/src/emc/task/emctaskmain.cc index 049c640f1af..47e19a32ae9 100644 --- a/src/emc/task/emctaskmain.cc +++ b/src/emc/task/emctaskmain.cc @@ -3389,6 +3389,10 @@ static int emctask_shutdown(void) delete emcStatus; emcStatus = NULL; } + + // releases iocontrol.0 + delete task_methods; + task_methods = NULL; return 0; } diff --git a/src/emc/task/taskclass.cc b/src/emc/task/taskclass.cc index 2de1c714641..5f05e638334 100644 --- a/src/emc/task/taskclass.cc +++ b/src/emc/task/taskclass.cc @@ -227,7 +227,10 @@ Task::Task(EMC_IO_STAT & emcioStatus_in) : } }; -Task::~Task() {}; +Task::~Task() { + if(comp_id >= 0) + hal_exit(comp_id); +} // set the have_tool_change_position global static int readToolChange(const IniFile &toolInifile) From c037b7c88e2ddc2b9dfc54fbd1284567f189e669 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:17:13 +1100 Subject: [PATCH 2/4] linuxcnc: do not wait on the thread pseudo components at shutdown Cleanup waits for every component but halcmd to go before it stops realtime. A thread is listed as a __ pseudo component, which unload all leaves in place, so together with the stale iocontrol.0 the wait always ran its full 2 seconds. Each linuxcnc -r in the tests paid it, and mdi-while-queuebuster-waitflag starts linuxcnc 20 times. --- scripts/linuxcnc.in | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/linuxcnc.in b/scripts/linuxcnc.in index 9e6de5efe1a..d0dbbe87bae 100644 --- a/scripts/linuxcnc.in +++ b/scripts/linuxcnc.in @@ -694,8 +694,9 @@ function Cleanup() { # Unused variable 'i', just a counter # shellcheck disable=SC2034 for i in $(seq 10); do - # (the one component is the halcmd itself) - if [ "$($HALCMD list comp | wc -w)" = 1 ]; then break; fi + # (the one component is the halcmd itself; a thread is listed as + # a __ pseudo component, which stays until realtime stops) + if [ "$($HALCMD list comp | tr -s ' ' '\n' | grep -c -v -e '^__' -e '^$')" = 1 ]; then break; fi sleep .2 done From 6ac68c78eea55dc793e1e77a72ceb55150ed784c Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:17:13 +1100 Subject: [PATCH 3/4] tests: abort/g64 and workplane-retain abort without the 5 s wait Both called wait_complete() right after AUTO_RUN. For a program run that waits for the end of the program, which these never reach before the abort, so every case sat out the 5 second timeout. Wait for the point to abort at instead: the Y position in abort/g64, which then also waits for halsampler to log it, and in workplane-retain the slow move with the read ahead stopped at the queue buster or M2, in place of the fixed sleeps. The other command waits get 30 seconds, so a loaded machine is not cut off at the 5 second default. --- tests/abort/g64/test-ui.py | 39 +++++++++++++++++------ tests/workplane-retain/test-ui.py | 51 ++++++++++++++++++++----------- 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/tests/abort/g64/test-ui.py b/tests/abort/g64/test-ui.py index 7aa4b6dc231..17eabf8b6e6 100755 --- a/tests/abort/g64/test-ui.py +++ b/tests/abort/g64/test-ui.py @@ -38,6 +38,21 @@ def process_samples(z_lev, expected_max_x): res += 1 return res +def wait_for_samples(z_lev, y): + # halsampler writes behind the motion; wait until the log reaches the + # point the zig-zags were aborted at + end = time.time() + 10 + while time.time() < end: + with open('motion-samples.log','r') as f: + for line in f: + try: + y_s, z_s = [float(n) for n in line.split(' ')[2:4]] + except ValueError: + break + if z_s == z_lev and y_s >= y: + return + time.sleep(0.1) + def check_status(msg, expected_mode, expected_p, expected_q): res = 0 # Read status and print control mode and tolerances @@ -77,37 +92,41 @@ def run_and_abort(msg, z_lev, expected_max_x, # Run init commands c.mode(linuxcnc.MODE_MDI) + c.wait_complete(30) c.mdi('F1200 G0X0Y0Z%.1f' % z_lev) - c.wait_complete() + c.wait_complete(30) for cmd in init_cmds: print("Running command '%s'" % cmd) c.mdi(cmd) - c.wait_complete() + c.wait_complete(30) res += check_status( '%s post-command' % msg, expected_mode, expected_p, expected_q) # Run program print("Running program 'test.ngc'") c.mode(linuxcnc.MODE_AUTO) + c.wait_complete(30) c.program_open("test.ngc") + c.wait_complete(30) c.auto(linuxcnc.AUTO_RUN, 1) - # Wait for things to get underway, then abort - c.wait_complete() - # Let zig-zags run to a certain point - while s.position[1] < 1.0: - time.sleep(0.1) + # Let zig-zags run to a certain point, then abort + end = time.time() + 60 + s.poll() + while s.position[1] < 1.0 and time.time() < end: + time.sleep(0.05) s.poll() res += check_status( '%s pre-abort' % msg, expected_mode, expected_p, expected_q) c.abort() - c.wait_complete() + c.wait_complete(30) # Check settings res += check_status( '%s post-abort' % msg, expected_mode, expected_p, expected_q) # Print max + wait_for_samples(z_lev, 1.0) res += process_samples(z_lev, expected_max_x) print() @@ -121,9 +140,11 @@ def run_and_abort(msg, z_lev, expected_max_x, e = linuxcnc.error_channel() c.state(linuxcnc.STATE_ESTOP_RESET) +c.wait_complete(30) c.state(linuxcnc.STATE_ON) +c.wait_complete(30) c.home(-1) -c.wait_complete() +c.wait_complete(30) # # TESTS diff --git a/tests/workplane-retain/test-ui.py b/tests/workplane-retain/test-ui.py index c9797a99cce..93f3c418bd8 100755 --- a/tests/workplane-retain/test-ui.py +++ b/tests/workplane-retain/test-ui.py @@ -36,24 +36,39 @@ def errors(): def mdi(*cmds): c.mode(linuxcnc.MODE_MDI) - c.wait_complete() + c.wait_complete(30) for cmd in cmds: c.mdi(cmd) - c.wait_complete() + c.wait_complete(30) l.wait_for_interp_state(linuxcnc.INTERP_IDLE, timeout=30) -def run(prog, abort_after=None): +def wait_for(done, timeout=30): + end = time.time() + timeout + while True: + s.poll() + if done(): + return True + if time.time() > end: + return False + time.sleep(0.05) + +def run(prog, abort=False): c.mode(linuxcnc.MODE_AUTO) - c.wait_complete() + c.wait_complete(30) c.program_open(prog) - c.wait_complete() + c.wait_complete(30) c.auto(linuxcnc.AUTO_RUN, 0) - c.wait_complete() - if abort_after is not None: - time.sleep(abort_after) + if abort: + # abort in the slow F10 move, once the read ahead has stopped at + # the queue buster or M2 + lines = open(prog).readlines() + slow = 1 + next(n for n, t in enumerate(lines) if "F10" in t) + stop = 1 + next(n for n, t in enumerate(lines) if t.startswith(("M66", "M2"))) + if not wait_for(lambda: s.motion_line == slow and s.read_line >= stop): + say("%s: never in the slow move with the read ahead at line %d" % (prog, stop)) c.abort() - c.wait_complete() - time.sleep(0.3) + # the end of the program, or of the abort + c.wait_complete(30) l.wait_for_interp_state(linuxcnc.INTERP_IDLE, timeout=30) def at(x, y, z): @@ -63,11 +78,11 @@ def at(x, y, z): return "at %g %g %g" % (x, y, z) if d < 1e-6 else \ "at %.4f %.4f %.4f" % tuple(s.position[i] * 25.4 for i in range(3)) -def case(name, prog, setup, abort_after, origin): +def case(name, prog, setup, abort, origin): mdi("G69", "G21", "G54", "G52 X0 Y0 Z0", "G10 L2 P2 X5 Y0 Z0", "G0 X0 Y0 Z0", *setup) errors() if prog: - run(prog, abort_after) + run(prog, abort) s.poll() active = s.g68_active s.poll() @@ -81,11 +96,11 @@ def case(name, prog, setup, abort_after, origin): o, z = (10, 20, 5), (0, 0, 0) retain = inifile.find("RS274NGC", "RETAIN_WORK_PLANE") == "1" # no program: a change of coordinate system in the plane takes it along -case("G55 in the plane", None, ["G68.2 X10 Y20 Z5 I10 J20 K30", "G55"], None, (15, 20, 5)) -case("M2", "end.ngc", ["G68.2 X10 Y20 Z5 I10 J20 K30"], None, o if retain else z) -case("abort, read ahead in the next plane", "readahead.ngc", [], 1.0, o if retain else z) -case("abort, read ahead past G55", "g55.ngc", [], 1.0, o if retain else z) -case("abort, read ahead in a plane on G55", "g55plane.ngc", [], 1.0, z) -case("abort, G52 in the plane", "slow.ngc", ["G68.2 X10 Y20 Z5 I10 J20 K30", "G52 X1"], 1.0, o if retain else z) +case("G55 in the plane", None, ["G68.2 X10 Y20 Z5 I10 J20 K30", "G55"], False, (15, 20, 5)) +case("M2", "end.ngc", ["G68.2 X10 Y20 Z5 I10 J20 K30"], False, o if retain else z) +case("abort, read ahead in the next plane", "readahead.ngc", [], True, o if retain else z) +case("abort, read ahead past G55", "g55.ngc", [], True, o if retain else z) +case("abort, read ahead in a plane on G55", "g55plane.ngc", [], True, z) +case("abort, G52 in the plane", "slow.ngc", ["G68.2 X10 Y20 Z5 I10 J20 K30", "G52 X1"], True, o if retain else z) mdi("G69") c.state(linuxcnc.STATE_OFF) From a8fe2175819676554c3653cc80cf342e405c484b Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:11:18 +1100 Subject: [PATCH 4/4] tests: split abort/g64 into its five cases The test ran five mode settings back to back in one linuxcnc session, about 45 seconds, which made it the floor of a parallel run. Each setting with its redo is now a test of its own in a directory named after it; the one-line test.ini includes the shared settings, now in g64.inc, and test-ui.py picks the case by the directory. The checks are the same. Measured by hdiethelm in #4770: with this and the slow tests started first, -j 32 goes from 48 to 32 seconds. --- tests/abort/g64/README | 6 +++++ tests/abort/g64/{ => g61.1}/checkresult | 0 tests/abort/g64/g61.1/test.ini | 1 + tests/abort/g64/{ => g61.1}/test.sh | 0 tests/abort/g64/g61/checkresult | 2 ++ tests/abort/g64/g61/test.ini | 1 + tests/abort/g64/g61/test.sh | 4 +++ tests/abort/g64/{test.ini => g64.inc} | 6 +++-- tests/abort/g64/g64/checkresult | 2 ++ tests/abort/g64/g64/test.ini | 1 + tests/abort/g64/g64/test.sh | 4 +++ tests/abort/g64/g64p0.5/checkresult | 2 ++ tests/abort/g64/g64p0.5/test.ini | 1 + tests/abort/g64/g64p0.5/test.sh | 4 +++ tests/abort/g64/g64q6/checkresult | 2 ++ tests/abort/g64/g64q6/test.ini | 1 + tests/abort/g64/g64q6/test.sh | 4 +++ tests/abort/g64/test-ui.py | 35 ++++++++++++++----------- 18 files changed, 58 insertions(+), 18 deletions(-) rename tests/abort/g64/{ => g61.1}/checkresult (100%) create mode 100644 tests/abort/g64/g61.1/test.ini rename tests/abort/g64/{ => g61.1}/test.sh (100%) create mode 100755 tests/abort/g64/g61/checkresult create mode 100644 tests/abort/g64/g61/test.ini create mode 100755 tests/abort/g64/g61/test.sh rename tests/abort/g64/{test.ini => g64.inc} (91%) create mode 100755 tests/abort/g64/g64/checkresult create mode 100644 tests/abort/g64/g64/test.ini create mode 100755 tests/abort/g64/g64/test.sh create mode 100755 tests/abort/g64/g64p0.5/checkresult create mode 100644 tests/abort/g64/g64p0.5/test.ini create mode 100755 tests/abort/g64/g64p0.5/test.sh create mode 100755 tests/abort/g64/g64q6/checkresult create mode 100644 tests/abort/g64/g64q6/test.ini create mode 100755 tests/abort/g64/g64q6/test.sh diff --git a/tests/abort/g64/README b/tests/abort/g64/README index 94743540374..2b956380ba6 100644 --- a/tests/abort/g64/README +++ b/tests/abort/g64/README @@ -5,6 +5,12 @@ This test runs and aborts a test .ngc program several times with various motion mode and blend/naive CAM tolerance settings. Checks are performed at various times relative to program execution: +Each setting is a test of its own in the directory named after it +(g61, g61.1, g64p0.5, g64, g64q6), so they run in parallel: it runs the +program twice, once after the mode command and once without it, which +must keep the mode across the first abort. Their test.ini includes the +shared settings in g64.inc; test-ui.py picks the case by the directory. + 1) Motion mode: Does stat buffer agree with programmed mode? 2) G64 tolerance settings: Does stat buffer agree with programmed P/Q diff --git a/tests/abort/g64/checkresult b/tests/abort/g64/g61.1/checkresult similarity index 100% rename from tests/abort/g64/checkresult rename to tests/abort/g64/g61.1/checkresult diff --git a/tests/abort/g64/g61.1/test.ini b/tests/abort/g64/g61.1/test.ini new file mode 100644 index 00000000000..f169635705c --- /dev/null +++ b/tests/abort/g64/g61.1/test.ini @@ -0,0 +1 @@ +#INCLUDE ../g64.inc diff --git a/tests/abort/g64/test.sh b/tests/abort/g64/g61.1/test.sh similarity index 100% rename from tests/abort/g64/test.sh rename to tests/abort/g64/g61.1/test.sh diff --git a/tests/abort/g64/g61/checkresult b/tests/abort/g64/g61/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/abort/g64/g61/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/abort/g64/g61/test.ini b/tests/abort/g64/g61/test.ini new file mode 100644 index 00000000000..f169635705c --- /dev/null +++ b/tests/abort/g64/g61/test.ini @@ -0,0 +1 @@ +#INCLUDE ../g64.inc diff --git a/tests/abort/g64/g61/test.sh b/tests/abort/g64/g61/test.sh new file mode 100755 index 00000000000..abc2d8df75b --- /dev/null +++ b/tests/abort/g64/g61/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e +rm -f motion-samples.log + +linuxcnc -r test.ini diff --git a/tests/abort/g64/test.ini b/tests/abort/g64/g64.inc similarity index 91% rename from tests/abort/g64/test.ini rename to tests/abort/g64/g64.inc index f8dee4fe1ad..db5fa295006 100644 --- a/tests/abort/g64/test.ini +++ b/tests/abort/g64/g64.inc @@ -1,3 +1,5 @@ +# Shared by the five cases, each in its own directory with a test.ini +# that includes this file; relative paths are from that directory. [EMC] # The version string for this INI file. VERSION = 1.1 @@ -6,7 +8,7 @@ DEBUG = 0x0 #DEBUG = 2147483647 [DISPLAY] -DISPLAY = ./test-ui.py +DISPLAY = ../test-ui.py [FILTER] #No Content @@ -30,7 +32,7 @@ CYCLE_TIME = 0.001 [HAL] HALUI = halui HALFILE = LIB:core_sim.hal -HALFILE = motion-test.hal +HALFILE = ../motion-test.hal [HALUI] #No Content diff --git a/tests/abort/g64/g64/checkresult b/tests/abort/g64/g64/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/abort/g64/g64/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/abort/g64/g64/test.ini b/tests/abort/g64/g64/test.ini new file mode 100644 index 00000000000..f169635705c --- /dev/null +++ b/tests/abort/g64/g64/test.ini @@ -0,0 +1 @@ +#INCLUDE ../g64.inc diff --git a/tests/abort/g64/g64/test.sh b/tests/abort/g64/g64/test.sh new file mode 100755 index 00000000000..abc2d8df75b --- /dev/null +++ b/tests/abort/g64/g64/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e +rm -f motion-samples.log + +linuxcnc -r test.ini diff --git a/tests/abort/g64/g64p0.5/checkresult b/tests/abort/g64/g64p0.5/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/abort/g64/g64p0.5/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/abort/g64/g64p0.5/test.ini b/tests/abort/g64/g64p0.5/test.ini new file mode 100644 index 00000000000..f169635705c --- /dev/null +++ b/tests/abort/g64/g64p0.5/test.ini @@ -0,0 +1 @@ +#INCLUDE ../g64.inc diff --git a/tests/abort/g64/g64p0.5/test.sh b/tests/abort/g64/g64p0.5/test.sh new file mode 100755 index 00000000000..abc2d8df75b --- /dev/null +++ b/tests/abort/g64/g64p0.5/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e +rm -f motion-samples.log + +linuxcnc -r test.ini diff --git a/tests/abort/g64/g64q6/checkresult b/tests/abort/g64/g64q6/checkresult new file mode 100755 index 00000000000..24dc9aa53e3 --- /dev/null +++ b/tests/abort/g64/g64q6/checkresult @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 # test failure is indicated by test.sh exit value diff --git a/tests/abort/g64/g64q6/test.ini b/tests/abort/g64/g64q6/test.ini new file mode 100644 index 00000000000..f169635705c --- /dev/null +++ b/tests/abort/g64/g64q6/test.ini @@ -0,0 +1 @@ +#INCLUDE ../g64.inc diff --git a/tests/abort/g64/g64q6/test.sh b/tests/abort/g64/g64q6/test.sh new file mode 100755 index 00000000000..abc2d8df75b --- /dev/null +++ b/tests/abort/g64/g64q6/test.sh @@ -0,0 +1,4 @@ +#!/bin/bash -e +rm -f motion-samples.log + +linuxcnc -r test.ini diff --git a/tests/abort/g64/test-ui.py b/tests/abort/g64/test-ui.py index 17eabf8b6e6..0b0ac45409f 100755 --- a/tests/abort/g64/test-ui.py +++ b/tests/abort/g64/test-ui.py @@ -106,7 +106,7 @@ def run_and_abort(msg, z_lev, expected_max_x, print("Running program 'test.ngc'") c.mode(linuxcnc.MODE_AUTO) c.wait_complete(30) - c.program_open("test.ngc") + c.program_open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "test.ngc")) c.wait_complete(30) c.auto(linuxcnc.AUTO_RUN, 1) @@ -153,22 +153,25 @@ def run_and_abort(msg, z_lev, expected_max_x, # 1) whether stat buffer shows correct motion mode; # 2) whether tolerance settings produce expected motion; # 3) for G64, whether tolerance settings are correct in stat buffer +# +# One case per directory, named after it: a mode command, then a redo +# without it, which must keep the mode across the first abort +cases = { + # exact path mode: X up to 5" + 'g61': ('G61', 0.0, 5.0, 610, None, None, 'G61'), + # exact stop mode: X up to 5" + 'g61.1': ('G61.1', 1.0, 5.0, 611, None, None, 'G61.1'), + # path blending, 0.5" blend tolerance: X up to 4.5" + 'g64p0.5': ('G64P0.5', 2.0, 4.5, 640, 0.5, 0.0, 'G64P0.5Q0'), + # path blending, best possible speed: X up to 3.725" + 'g64': ('G64', 3.0, 3.7, 640, 0.0, 0.0, 'G64'), + # path blending, 6" naive cam tolerance: X always 0" (zig-zags collapse) + 'g64q6': ('G64P0Q6', 4.0, 0.0, 640, 0.0, 6.0, 'G64Q6'), +} +msg, z_lev, max_x, mode, p, q, cmd = cases[os.path.basename(os.getcwd())] res = 0 -# Z=0: exact path mode: X up to 5" -res += run_and_abort('G61', 0.0, 5.0, 610, None, None, 'G61') -res += run_and_abort('G61 redo', 0.5, 5.0, 610, None, None) -# Z=1: exact stop mode: X up to 5" -res += run_and_abort('G61.1', 1.0, 5.0, 611, None, None, 'G61.1') -res += run_and_abort('G61.1 redo', 1.5, 5.0, 611, None, None) -# Z=2: path blending, 0.5" blend tolerance: X up to 4.5" -res += run_and_abort('G64P0.5', 2.0, 4.5, 640, 0.5, 0.0, 'G64P0.5Q0') -res += run_and_abort('G64P0.5 redo', 2.5, 4.5, 640, 0.5, 0.0) -# Z=3: path blending, best possible speed: X up to 3.725" -res += run_and_abort('G64', 3.0, 3.7, 640, 0.0, 0.0, 'G64') -res += run_and_abort('G64 redo', 3.5, 3.7, 640, 0.0, 0.0) -# Z=4: path blending, 6" naive cam tolerance: X always 0" (zig-zags collapse) -res += run_and_abort('G64P0Q6', 4.0, 0.0, 640, 0.0, 6.0, 'G64Q6') -res += run_and_abort('G64P0Q6 redo', 4.5, 0.0, 640, 0.0, 6.0) +res += run_and_abort(msg, z_lev, max_x, mode, p, q, cmd) +res += run_and_abort(msg + ' redo', z_lev + 0.5, max_x, mode, p, q) # # CLEANUP