diff --git a/artifacts/arm64/libCmpAtomModbus.so b/artifacts/arm64/libCmpAtomModbus.so index 9ea57d1..b7a17ea 100755 Binary files a/artifacts/arm64/libCmpAtomModbus.so and b/artifacts/arm64/libCmpAtomModbus.so differ diff --git a/artifacts/armv7hf/libCmpAtomModbus.so b/artifacts/armv7hf/libCmpAtomModbus.so index 2336fd5..7b72a30 100755 Binary files a/artifacts/armv7hf/libCmpAtomModbus.so and b/artifacts/armv7hf/libCmpAtomModbus.so differ diff --git a/artifacts/x86_64/libCmpAtomModbus.so b/artifacts/x86_64/libCmpAtomModbus.so index b702332..ea5291a 100755 Binary files a/artifacts/x86_64/libCmpAtomModbus.so and b/artifacts/x86_64/libCmpAtomModbus.so differ diff --git a/component-source/component/CmpFrame.c b/component-source/component/CmpFrame.c index 7c1f10f..f3a0431 100644 --- a/component-source/component/CmpFrame.c +++ b/component-source/component/CmpFrame.c @@ -1,3 +1,7 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif +#include "atmb_boot_link.h" #include "../src/atom_bridge.h" /***************************************************************************** * Copyright: Copyright CODESYS Development GmbH @@ -38,6 +42,9 @@ DLL_DECL int CDECL ComponentEntry(INIT_STRUCT *pInitStruct) s_pfCMCallHook = pInitStruct->pfCMCallHook; s_pfCMRegisterClass = pInitStruct->pfCMRegisterClass; s_pfCMCreateInstance = pInitStruct->pfCMCreateInstance; + + /* Best effort only: never make component startup depend on this. */ + atmb_ensure_boot_link(); return ERR_OK; } diff --git a/component-source/component/atmb_boot_link.h b/component-source/component/atmb_boot_link.h new file mode 100644 index 0000000..ec2e7e5 --- /dev/null +++ b/component-source/component/atmb_boot_link.h @@ -0,0 +1,109 @@ +#ifndef ATMB_BOOT_LINK_H +#define ATMB_BOOT_LINK_H + +/* Linux-only boot-project pathname workaround. Include in CmpFrame.c only, + * with _GNU_SOURCE defined before all system headers. + * No PLC memory, communication state, worker thread or cyclic work is involved. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void atmb_ensure_boot_link(void) +{ + /* ComponentEntry is called at module initialization, not on each scan. + * Guard against repeated entry; failure is deliberately not retried. + */ + static int attempted; + const char *name = "libCmpAtomModbus.so"; + const char *problem = NULL; + Dl_info info; + struct stat source, existing; + char *module_path = NULL; + int directory = -1; + int error = 0; + int saved_errno = errno; + + if (attempted) + return; + attempted = 1; + + /* The component function address identifies this loaded ELF object. Using + * code rather than a local data symbol also works with runtimes that do not + * expose local data addresses through dladdr(). + */ + if (!dladdr((void *)(uintptr_t)&atmb_ensure_boot_link, &info) || !info.dli_fname) { + problem = "cannot locate loaded module"; + goto finish; + } + module_path = realpath(info.dli_fname, NULL); + if (!module_path) { + error = errno; + problem = "cannot resolve loaded module pathname"; + goto finish; + } + if (stat(module_path, &source) != 0) { + error = errno; + problem = "cannot inspect loaded module file"; + goto finish; + } + if (!S_ISREG(source.st_mode)) { + problem = "loaded module pathname is not a regular file"; + goto finish; + } + + /* Boot creation was observed looking up the bare filename relative to + * runtime cwd. Anchor this directory with an FD; never change cwd. + */ + directory = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (directory < 0) { + error = errno; + problem = "cannot open runtime working directory"; + goto finish; + } + if (fstatat(directory, name, &existing, AT_SYMLINK_NOFOLLOW) == 0) { + struct stat resolved; + if (fstatat(directory, name, &resolved, 0) == 0 && + resolved.st_dev == source.st_dev && + resolved.st_ino == source.st_ino) + goto finish; /* Correct link, hard link, or the source itself. */ + problem = "existing file or link left untouched; check boot-project path"; + goto finish; + } + if (errno != ENOENT) { + error = errno; + problem = "cannot inspect boot-project pathname"; + goto finish; + } + + /* symlinkat fails on EEXIST: even a concurrent creator cannot be replaced. + * Keep this link on shutdown; boot-project creation may still need it. + */ + if (symlinkat(module_path, directory, name) != 0) { + error = errno; + problem = "cannot create boot-project link"; + goto finish; + } + syslog(LOG_INFO, "CmpAtomModbus: created boot link %s -> %s in runtime cwd", + name, module_path); + +finish: + if (problem) { + if (error) + syslog(LOG_WARNING, "CmpAtomModbus: %s: %s (errno=%d); Modbus startup continues", + problem, strerror(error), error); + else + syslog(LOG_WARNING, "CmpAtomModbus: %s; Modbus startup continues", problem); + } + if (directory >= 0) + close(directory); + free(module_path); + errno = saved_errno; +} +#endif diff --git a/component-source/component/makefile b/component-source/component/makefile index 81c1db1..4167ec1 100644 --- a/component-source/component/makefile +++ b/component-source/component/makefile @@ -1,8 +1,8 @@ # Generated from supplied ExtensionSDK; build with target Linux toolchain. SDKDIR ?= ../../ExtensionSDK MAJORVER=0 -MINORVER=4 -CMPVERSION=0x00050000 +MINORVER=5 +CMPVERSION=0x00050001 # Local development ID; check no other component uses this ID. CMPID=0x2001 DOS2UNIX=dos2unix @@ -10,4 +10,9 @@ M4=m4 CC ?= gcc # The SDK otherwise includes CmpFrame in its component identity wildcard. override CSRC := CmpAtomModbus.c +# dladdr locates this module; -ldl also supports glibc older than 2.34. +LDLIBS += -ldl include $(SDKDIR)/makefile + +# Recompile the startup hook when the link helper changes. +$(OUTDIR)/CmpFrame.o: atmb_boot_link.h diff --git a/library/CmpAtomModbus.compiled-library b/library/CmpAtomModbus.compiled-library new file mode 100644 index 0000000..7eb4de2 Binary files /dev/null and b/library/CmpAtomModbus.compiled-library differ diff --git a/library/CmpAtomModbus.library b/library/CmpAtomModbus.library index ff5fcb7..9b155f7 100644 Binary files a/library/CmpAtomModbus.library and b/library/CmpAtomModbus.library differ diff --git a/library/CmpAtomModbus.~u b/library/CmpAtomModbus.~u new file mode 100644 index 0000000..662b4fd --- /dev/null +++ b/library/CmpAtomModbus.~u @@ -0,0 +1,4 @@ +atom +ADAM-CODESYS +4648 +639270284382256389 diff --git a/tests/test_boot_link.py b/tests/test_boot_link.py new file mode 100644 index 0000000..e4e9298 --- /dev/null +++ b/tests/test_boot_link.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Linux host integration tests for the real dladdr/symlinkat helper; needs cc.""" +import ctypes +import os +from pathlib import Path +import subprocess +import sys +import tempfile + +HEADER = Path(__file__).resolve().parents[1] / 'component-source/component' +CASES = ['missing', 'correct', 'file', 'wrong_link', 'dangling', 'same_directory', 'unwritable', 'missing_source'] + +def child(case, base): + base = Path(base) + module = base / 'PlcLogic/libCmpAtomModbus.so' + lib = ctypes.CDLL(str(module), use_errno=True) + lib.warning_count.restype = ctypes.c_int + lib.info_count.restype = ctypes.c_int + os.chdir(base) + link = base / 'libCmpAtomModbus.so' + if case == 'correct': + link.symlink_to('PlcLogic/libCmpAtomModbus.so') + elif case == 'file': + link.write_text('do not replace me') + elif case in ('wrong_link', 'dangling'): + if case == 'wrong_link': + (base / 'other').write_text('other file') + link.symlink_to('other') + elif case == 'same_directory': + os.chdir(module.parent) + elif case == 'missing_source': + module.unlink() + elif case == 'unwritable': + # sysfs cannot accept arbitrary regular symlinks, even as root. + os.chdir('/sys') + link = Path('/sys/libCmpAtomModbus.so') + assert not os.path.lexists(link) + ctypes.set_errno(123) + lib.run_check() + assert ctypes.get_errno() == 123, 'caller errno changed' + warnings = lib.warning_count() + infos = lib.info_count() + lib.run_check() + assert (warnings, infos) == (lib.warning_count(), lib.info_count()), 'retried or logged twice' + if case == 'missing': + assert link.is_symlink() and link.resolve() == module + assert (warnings, infos) == (0, 1) + elif case == 'correct': + assert os.readlink(link) == 'PlcLogic/libCmpAtomModbus.so' + assert (warnings, infos) == (0, 0) + elif case == 'same_directory': + assert module.is_file() and not module.is_symlink() + assert (warnings, infos) == (0, 0) + else: + assert (warnings, infos) == (1, 0) + if case == 'file': + assert link.read_text() == 'do not replace me' + elif case in ('wrong_link', 'dangling'): + assert os.readlink(link) == 'other' + else: + assert not os.path.lexists(link) + print('PASS', case) + +if len(sys.argv) == 3: + child(sys.argv[1], sys.argv[2]) +else: + with tempfile.TemporaryDirectory(prefix='atmb-link-test-') as temp: + root = Path(temp) + root.chmod(0o755) + harness = root / 'harness.c' + harness.write_text('''#define _GNU_SOURCE +#define syslog test_syslog +#include "atmb_boot_link.h" +static int warnings, infos; +void test_syslog(int priority, const char *format, ...) { + (void)format; + if (priority == LOG_WARNING) ++warnings; + if (priority == LOG_INFO) ++infos; +} +void run_check(void) { atmb_ensure_boot_link(); } +int warning_count(void) { return warnings; } +int info_count(void) { return infos; } +''') + output = root / 'test.so' + subprocess.run([os.environ.get('CC', 'cc'), '-std=c99', '-Wall', '-Wextra', '-Werror', + '-fPIC', '-shared', '-I', str(HEADER), str(harness), '-ldl', '-o', str(output)], check=True) + for case in CASES: + base = root / case + (base / 'PlcLogic').mkdir(parents=True) + (base / 'PlcLogic/libCmpAtomModbus.so').write_bytes(output.read_bytes()) + subprocess.run([sys.executable, __file__, case, str(base)], check=True) + base.chmod(0o755)